Qt 源码剖析 - 信号槽自动链接机制

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文件中定义的某对象的信号链接起来, 须要如下几步:ui

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

3. 源码剖析

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

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

咱们知道 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" 中实现.ip

qobjectdefs.h文档

struct Q_CORE_EXPORT QMetaObject {
	...

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

	...
}

复制代码

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

  • 递归的搜索 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 就不受控制了).

相关文章
相关标签/搜索