Qt 源码剖析 - 信号槽自动连接

2 篇文章 0 订阅
1 篇文章 0 订阅

1. 概述

我们在使用 Qt 创建一个窗口 MyWidget 时, Qt Creator 会帮我们创建出 “MyWidget.h”, “MyWidget.cpp”, “MyWidget.ui” 这三个文件. 我们使用 Qt Designer 打开 MyWidget.ui 文件, 拖一个 QPushButton 上去, Qt Designer 默认给这个按钮设置一个对象名 “pushButton”. 在该按钮上右键选择转到槽, 选择clicked()信号, Qt Creator 就会在 MyWidget 类中生成一个槽函数 void on_pushButton_clicked(). 我们只需要在这个槽函数中添加自己的逻辑就行了.

有没有感觉到和平时自己写信号槽的时候不一样? 没有 connect? 但是程序跑起来, 按下按钮就自动执行该槽函数了呀. 从结果看, 肯定是 connect 过了. 所以我们不难想到, 一定是信号槽被自动 connect 了.

2. 猜测

如果让我们自己实现的话, 将一个对象的槽函数与其UI文件中定义的某对象的信号连接起来, 需要以下几步:

  1. 规定一个 可自动连接的槽函数 的命名格式, 其中需要包含这些信息: 哪个对象发送了信号, 发送了什么信号. 这样我们就知道需要自动连接哪些槽函数了.
  2. 遍历传入的这个对象的所有方法. 找到符合命名规范的槽函数.
  3. 遍历这个对象及其子对象, 找到信号的发送对象, 再找到该对象的相应信号. 完成连接.

3. 源码剖析

我们知道 “MyWidget.ui” 文件会被处理, 并生成 “ui_MyWidget.h” 文件. 这个文件会被包含在 MyWidget 类的实现中. 那我们就先来看看这个 “ui_MyWidget.h” 文件吧.

ui_MyWidget.h 中有一个 Ui_MyWidget 类. 我们在类成员变量中发现了QPushButton *pushButton;, 这个就是我们之前拖上去的按钮. 除此之外, 还有两个成员函数void setupUi(QWidget *MyWidget)void retranslateUi(QWidget *MyWidget).

我们知道 retranslateUi 函数是在当程序语言改变时, 用来刷新UI中显示语言的. 是国际化相关的内容, 在这里我们先忽略. 我们先看看 setupUi 函数中做了些什么.

void setupUi(QWidget *MyWidget)
{
	if (MyWidget->objectName().isEmpty())
		MyWidget->setObjectName(QStringLiteral("MyWidget"));
	MyWidget->resize(400, 300);
	pushButton = new QPushButton(MyWidget);
	pushButton->setObjectName(QStringLiteral("pushButton"));
	pushButton->setGeometry(QRect(220, 220, 75, 23));
	retranslateUi(MyWidget);

这半部分用来将控件创建出来, 进行一些设置并使其作为 MyWidget 的子对象. 我们接着往下看.

	QMetaObject::connectSlotsByName(MyWidget);
} // setupUi

在最后调用了 QMetaObject::connectSlotsByName() 函数, 从函数名我们就知道其功能是 “通过名字连接槽函数”. 这么看来, 自动连接信号槽就是它做的了. QMetaObject 在 “qobjectdefs.h” 中定义, 在 “qobject.cpp” 中实现.

qobjectdefs.h

struct Q_CORE_EXPORT QMetaObject
{
	...

	// internal slot-name based connect
	static void connectSlotsByName(QObject *o);

	...
}

我们先看一下该函数的文档说明:

  • 递归的搜索 object 及其子对象, 如果发现符合以下格式的槽函数, 则会自动连接

    void on_<object name>_<signal name>(<signal parameters>);
    
  • 举个例子: 如果有一个子对象, 其类型为 QPushButton, object name 为 button1, 要关联该按钮 clicked() 信号的槽函数签名应该为:

    void on_button1_clicked();
    

我们再看它是如何实现的:

void QMetaObject::connectSlotsByName(QObject *o)
{
    if (!o)
        return;
    const QMetaObject *mo = o->metaObject();
    Q_ASSERT(mo);
	// list of all objects to look for matching signals including...
    const QObjectList list = 
            o->findChildren<QObject *>(QString()) // all children of 'o'...
            << o; // and the object 'o' itself

	/* [1] */
}

QObjectList list 中存储了 o 及其所有子对象. 接下来看 "[1]"代码.

// for each method/slot of o ...
for (int i = 0; i < mo->methodCount(); ++i) {
	const QByteArray slotSignature = mo->method(i).methodSignature();
	const char *slot = slotSignature.constData();
	Q_ASSERT(slot);

	// ...that starts with "on_", ...
	if (slot[0] != 'o' || slot[1] != 'n' || slot[2] != '_')
		continue;

	// ...we check each object in our list, ...
	bool foundIt = false;

	/* [2] 遍历对象列表, foundIt */

	if (foundIt) {
		// we found our slot, now skip all overloads
		while (mo->method(i + 1).attributes() & QMetaMethod::Cloned)
				++i;
	} else if (!(mo->method(i).attributes() & QMetaMethod::Cloned)) {
		// check if the slot has the following signature: "on_..._...(..."
		int iParen = slotSignature.indexOf('(');
		int iLastUnderscore = slotSignature.lastIndexOf('_', iParen-1);
		if (iLastUnderscore > 3)
			qWarning("QMetaObject::connectSlotsByName: No matching signal for %s", slot);
	}
}

开始遍历该对象的所有成员方法, 找到以on_开头的方法. 然后遍历存储所有对象的列表 list. (遍历过程后面再说). 如果找到匹配的了, 就跳过该方法的重载函数. 如果未找到, 确定该函数符合on_<objectName>_<signal>()格式, 然后打印 warning 信息.

  • 刚开始看到直接使用 slot[0], slot[1], slot[2] 的时候, 还很好奇, 如果函数名只有一个字母, 不就数组越界了吗? 后来自己把函数签名打出来, 发现其实不会的. slotSignature 获取到的为函数签名, 即使函数名为f, 其签名也为 “f()”. 所以函数签名至少是三个字符.

接下来就看 “[2]” 代码. 看看在遍历对象列表时都做了些什么.

for(int j = 0; j < list.count(); ++j) {
	const QObject *co = list.at(j);
	const QByteArray coName = co->objectName().toLatin1();

	// ...discarding those whose objectName is not fitting the pattern "on_<objectName>_...", ...
	if (coName.isEmpty() || qstrncmp(slot + 3, coName.constData(), coName.size()) || slot[coName.size()+3] != '_')
		continue;

	const char *signal = slot + coName.size() + 4; // the 'signal' part of the slot name

	// ...for the presence of a matching signal "on_<objectName>_<signal>".
	const QMetaObject *smeta;
	int sigIndex = co->d_func()->signalIndex(signal, &smeta);
	if (sigIndex < 0) {
		QList<QByteArray> compatibleSignals;
		const QMetaObject *smo = co->metaObject();
		int sigLen = qstrlen(signal) - 1; // ignore the trailing ')'
		for (int k = QMetaObjectPrivate::absoluteSignalCount(smo)-1; k >= 0; --k) {
			const QMetaMethod method = QMetaObjectPrivate::signal(smo, k);
			if (!qstrncmp(method.methodSignature().constData(), signal, sigLen)) {
				smeta = method.enclosingMetaObject();
				sigIndex = k;
				compatibleSignals.prepend(method.methodSignature());
			}
		}
		if (compatibleSignals.size() > 1)
			qWarning() << "QMetaObject::connectSlotsByName: Connecting slot" << slot
						<< "with the first of the following compatible signals:" << compatibleSignals;
	}

	if (sigIndex < 0)
		continue;

	// we connect it...
	if (Connection(QMetaObjectPrivate::connect(co, sigIndex, smeta, o, i))) {
		foundIt = true;
		break;
	}
}

遍历对象列表, 找到符合本槽函数 object name 的对象. 然后查找该对象的是否有完全符合 singnalName + 参数列表 的信号.

如果没有找到, 则继续查找至少满足该槽参数列表的信号. (因为信号的参数个数可以大于槽参数个数). 如果多个信号都符合要求, 就按照信号在源文件中的声明顺序, 选择第一个, 并打印一个 warning 信息.

之后, 将该信号与槽进行连接, 并退出该循环. 所以说, 如果存在相同 object name 的对象, 也只连接第一个. 其他的都会被忽略. (Qt Designer 会确保 object name 的唯一性, 但其他代码添加的 object name 就不受控制了).

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值