1、说明
使用Qt已经好几年了,一直以为自己懂Qt,熟悉Qt,使用起来很是熟练,无论什么项目,都喜欢用Qt编写。但真正去看Qt的源码,去理解Qt的思想也就近两年的事。
本次就着重介绍一下Qt的核心功能--信号槽机制,相信接触过Qt的人都能很熟悉地使用,甚至,大部分人还能轻松地说出信息槽的几种用法。但是信号槽的核心可不是简单说说就能说清楚的。
那么,本次,就从Qt的源码中讲解一下信号槽的机制。
其实,直到写这篇文章,我也没有完全看明白相关的源码,只是明白了其中的大部分以及使用机制,其中还有很多细节的,留待以后整理。
如果错误还请大家指正。
2、环境以及知识点
Qt版本:Qt 5.5.1
系统:windows 10
在阅读本文前,希望你能:
- 熟练使用C++,了解make的编译方法和过程;
- 熟练使用Qt的信号槽功能,对信号槽的写法以及4和5的区别了如指掌;
- QMetaObject元数据系统;
- 懂一些设计模式,能理解观察者模式;
3、信号槽源码分析
以下将按照SIGNAL/SLOT宏定义连接信号槽的方式做讲解
接下来将会从按照以下的步骤来进行分析:
- Qt元数据系统;
- moc预编译;
- Q_OBJECT宏;
- signals和slots关键字以及emit;
- SIGNAL()和SLOT()宏;
- connect 方法;
- 触发信号;
3.1、Qt的元数据系统
没看过Qt源码的同学可能会对QMetaObject有些陌生,我们打开Qt手册,查看此类的说明,介绍如下:
The QMetaObject class contains meta-information about Qt objects.
The Qt Meta-Object System in Qt is responsible for the signals and slots inter-object communication mechanism, runtime type information, and the Qt property system. A single QMetaObject instance is created for each QObject subclass that is used in an application, and this instance stores all the meta-information for the QObject subclass. This object is available as QObject::metaObject().
这里是说,QMetaObject包含了Qt的元对象信息。元对象机制类似Java的反射机制。通过继承QObject,并在定义类是添加一定Qt内置宏,能在运行时动态获取Qt的信号槽、类型信息以及相关属性。
一个简答的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void MainWindow::onClickButton() { qDebug()<< "on click button" ; const QMetaObject* metaObject = this ->metaObject(); qDebug()<<metaObject->className(); qDebug()<<metaObject->superClass()->className(); int methodIndex = metaObject->indexOfMethod( "testFunction()" ); qDebug()<<methodIndex; qDebug()<<metaObject->method(methodIndex).name(); metaObject->method(methodIndex).invoke( this ); QMetaObject::invokeMethod( this , "testFunction" ); } |
如上,一个简单的例子,通过QMetaObject,我们得到了该对象的类名、父类名、方法并调用了该方法
怎么样,熟悉Java的小伙伴已经发现了,这不就是Java的反射吗,谁说C++没有反射呢
那么,Qt是如何实现”反射“的呢?答案是使用moc预编译
3.2、moc编译
moc全称Meta-Object Compiler,即元对象编译器。我们可以在Qt的安装目录的bin文件下看到moc工具,moc.exe。Qt的构建的时候,会调用该工具生成moc文件,我们在编译目录下看到的moc_xxx.cpp文件就是该工具生成的。
Qt的MinGW版本使用的是qmake进行项目管理,它和cmake功能类似,但没有后者强大。使用qmake生成Makefile后,我们打开Makefile文件,我们可以狠清楚地看到有一个调用moc.exe工具的地方,代码太多,就不列出来了。
此外,我们还发现,并不是所有的代码都会生成moc_xxx.cpp文件的,只有使用了 Q_OBJECT 宏的类文件,才会生成。没有错,moc工具就是根据 Q_OBJECT 宏来生成moc_xxx.cpp文件的,而实现“反射”的元数据系统的也是依靠Q_OBJECT的。
到此,我们其实已经能够大概理清qmake项目的构建步骤了。步骤和常用的cmake项目类似,区别就是,qmake生成的Makefile文件种,会写有调用moc工具的指令,以达到moc_xxx.cpp文件的生成。
我们可以使用moc工具手动生成moc_xxx.cpp,使用指令 moc.exe mainwindow.h,即会在控制台打印moc文件信息,也可以使用 -o 参数来将生成的内容写入文件,其余参数可以使用 moc.exe -h 来查看
3.3、Q_OBJECT
我们可以从源代码中查看 Q_OBJECT 的内容,这里调整一个格式,使用 Q_OBJECT 宏之后,会在类定义的开头多出以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public : Q_OBJECT_CHECK QT_WARNING_PUSH Q_OBJECT_NO_OVERRIDE_WARNING static const QMetaObject staticMetaObject; virtual const QMetaObject *metaObject() const ; virtual void *qt_metacast( const char *); virtual int qt_metacall(QMetaObject::Call, int , void **); QT_WARNING_POP QT_TR_FUNCTIONS private : Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int , void **); struct QPrivateSignal {}; |
可以看到,这里多出几个方法和一些变量
- 属性staticMetaObject,元数据对象,可以从中获取当前类的元数据;
- 方法metaObject(),获取元数据对象指针,大多数情况下,返回staticMetaObject指针;
- 方法qt_metacast(),原数据对象类型转换,转换成指定的类型,使用时一般传入父类的名称字符串;
- 方法qt_metacall(),执行函数的回调,信号触发;
- 方法qt_static_metacall(),回调函数,被qt_metacall()调用,内部执行槽;
这里的几个方法都没有实现体,因为实现部分会有 moc 工具生成,在moc_xxx.cpp 文件中可以查看这些方法的实现体
3.4、signals和slots
signals 用于声明自定义信号,slots 用于声明槽函数,emit 用于发送信号,我们可以从源码中查看这三个宏定义
1
2
3
|
define slots define signals public define emit |
可以看出,这三个宏几乎什么都没有做,signals 就是声明所谓的信号是public方法,而slots和emit更是为空,标准C++在编译的时候,根本不受这三个宏的影响,那么它们的用处在哪里呢?在moc工具调用和connect连接的时候。
打开moc_xxx.cpp文件,对比查看信号
1
2
3
4
|
signals: void clickButton( int value); void clickButton2(); |
1
2
3
4
5
6
7
8
9
10
11
12
|
// SIGNAL 0 void MainWindow::clickButton( int _t1) { void *_a[] = { Q_NULLPTR, const_cast < void *>( reinterpret_cast < const void *>(&_t1)) }; QMetaObject::activate( this , &staticMetaObject, 0, _a); } // SIGNAL 1 void MainWindow::clickButton2() { QMetaObject::activate( this , &staticMetaObject, 1, Q_NULLPTR); } |
其实,信号就是方法,而 emit clickButton() 发送信号,就是调用 clickButton() 方法,换言之,触发信号,就算不要emit也无妨
3.5、SIGNAL()和SLOT()
查看源码
1
2
|
# define SLOT(a) qFlagLocation("1"#a QLOCATION) # define SIGNAL(a) qFlagLocation("2"#a QLOCATION) |
qFlagLocation() 源码如下:
1
2
3
4
5
6
7
|
const char *qFlagLocation( const char *method) { QThreadData *currentThreadData = QThreadData::current( false ); if (currentThreadData != 0) currentThreadData->flaggedSignatures.store(method); return method; } |
store() 方法
1
2
|
void store( const char * method) { locations[idx++ % Count] = method; } |
所以 SIGNAL(clickButton()) 宏展开为 qFlagLocation("2"clickButton(int) QLOCATION)
SLOT() 同理,这里的1和2,最后会添加到信号槽的前面,其实是为了区分信号和槽,源码中还有一个0在 METHOD() 宏
qFlagLocation 方法的作用是将信号槽转换成字符串保存起来,store 方法中,locations是个二维数组,而 idx 每次都加一,保证信号和槽的不同的方法存储在不同的数组中。
我们也可以在代码中打印出来看下:
1
2
|
qDebug()<<SIGNAL(clickButton( int )); //2clickButton(int) qDebug()<<SLOT(onClickButton()); //1onClickButton() |
3.6、connect方法
最后,就是最关键的connect方法,做了一些简单的注释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
|
QMetaObject::Connection QObject::connect( const QObject *sender, const char * signal , const QObject *receiver, const char *method, Qt::ConnectionType type) { if (sender == 0 || receiver == 0 || signal == 0 || method == 0) { qWarning( "QObject::connect: Cannot connect %s::%s to %s::%s" , sender ? sender->metaObject()->className() : "(null)" , ( signal && * signal ) ? signal +1 : "(null)" , receiver ? receiver->metaObject()->className() : "(null)" , (method && *method) ? method+1 : "(null)" ); return QMetaObject::Connection(0); } QByteArray tmp_signal_name; if (!check_signal_macro(sender, signal , "connect" , "bind" )) return QMetaObject::Connection(0); const QMetaObject *smeta = sender->metaObject(); //获取发送者的元数据对象 const char *signal_arg = signal ; //信号 ++ signal ; //skip code QArgumentTypeArray signalTypes; //信号参数类型数组 Q_ASSERT(QMetaObjectPrivate::get(smeta)->revision >= 7); //信号转换为签名,并得到信号参数类型数组 QByteArray signalName = QMetaObjectPrivate::decodeMethodSignature( signal , signalTypes); //找到信号索引 int signal_index = QMetaObjectPrivate::indexOfSignalRelative( &smeta, signalName, signalTypes.size(), signalTypes.constData()); //小于0表示,表示信号索引有问题 if (signal_index < 0) { // check for normalized signatures //将信号重新规范化,再进行上面的签名转换,并重新得到索引 tmp_signal_name = QMetaObject::normalizedSignature( signal - 1); signal = tmp_signal_name.constData() + 1; //重新进行签名转换,并得到参数类型列表 signalTypes.clear(); signalName = QMetaObjectPrivate::decodeMethodSignature( signal , signalTypes); smeta = sender->metaObject(); signal_index = QMetaObjectPrivate::indexOfSignalRelative( &smeta, signalName, signalTypes.size(), signalTypes.constData()); } //重新获取的信号索引还是无效,则是头文件中信号的定义出错,找不到信号,报错信号不存在 if (signal_index < 0) { err_method_notfound(sender, signal_arg, "connect" ); err_info_about_objects( "connect" , sender, receiver); return QMetaObject::Connection(0); } //根据当前信号的索引找到最原始的信号的索引,因为信号是可以被继承,这里找的祖先信号 signal_index = QMetaObjectPrivate::originalClone(smeta, signal_index); signal_index += QMetaObjectPrivate::signalOffset(smeta); //信号的索引再加上信号的偏移量 QByteArray tmp_method_name; //提取槽的编码,应该是QSLOT_CODE或者QSIGNAL_CODE,用于判断槽是信号还是方法 int membcode = extract_code(method); //检查槽编码,槽可以是槽函数或者信号,初次以为,都无效 if (!check_method_code(membcode, receiver, method, "connect" )) return QMetaObject::Connection(0); const char *method_arg = method; ++method; // skip code QArgumentTypeArray methodTypes; //转换槽签名,并获取槽的参数类型列表 QByteArray methodName = QMetaObjectPrivate::decodeMethodSignature(method, methodTypes); const QMetaObject *rmeta = receiver->metaObject(); //获取接受者的元数据对象 int method_index_relative = -1; Q_ASSERT(QMetaObjectPrivate::get(rmeta)->revision >= 7); switch (membcode) { case QSLOT_CODE: //接受者是槽函数 method_index_relative = QMetaObjectPrivate::indexOfSlotRelative( &rmeta, methodName, methodTypes.size(), methodTypes.constData()); break ; case QSIGNAL_CODE: //接受者是信号 method_index_relative = QMetaObjectPrivate::indexOfSignalRelative( &rmeta, methodName, methodTypes.size(), methodTypes.constData()); break ; } //槽的索引为-1,表示无效 if (method_index_relative < 0) { // check for normalized methods //将槽进行规范化处理,并重新转换槽签名 tmp_method_name = QMetaObject::normalizedSignature(method); method = tmp_method_name.constData(); methodTypes.clear(); methodName = QMetaObjectPrivate::decodeMethodSignature(method, methodTypes); // rmeta may have been modified above //接受者元数据对象前面可能被修改过,这里重新获取 rmeta = receiver->metaObject(); //重新获取槽的索引 switch (membcode) { case QSLOT_CODE: method_index_relative = QMetaObjectPrivate::indexOfSlotRelative( &rmeta, methodName, methodTypes.size(), methodTypes.constData()); break ; case QSIGNAL_CODE: method_index_relative = QMetaObjectPrivate::indexOfSignalRelative( &rmeta, methodName, methodTypes.size(), methodTypes.constData()); break ; } } //如果还找不到,则说明槽定义有误,报错 if (method_index_relative < 0) { err_method_notfound(receiver, method_arg, "connect" ); err_info_about_objects( "connect" , sender, receiver); return QMetaObject::Connection(0); } //检查信号和槽的参数 if (!QMetaObjectPrivate::checkConnectArgs(signalTypes.size(), signalTypes.constData(), methodTypes.size(), methodTypes.constData())) { qWarning( "QObject::connect: Incompatible sender/receiver arguments" "\n %s::%s --> %s::%s" , sender->metaObject()->className(), signal , receiver->metaObject()->className(), method); return QMetaObject::Connection(0); } int *types = 0; //队列连接检查,参数要是基本类型,或者使用元数据注册 if ((type == Qt::QueuedConnection) && !(types = queuedConnectionTypes(signalTypes.constData(), signalTypes.size()))) { return QMetaObject::Connection(0); } #ifndef QT_NO_DEBUG //打印调试信息 QMetaMethod smethod = QMetaObjectPrivate:: signal (smeta, signal_index); QMetaMethod rmethod = rmeta->method(method_index_relative + rmeta->methodOffset()); check_and_warn_compat(smeta, smethod, rmeta, rmethod); #endif QMetaObject::Connection handle = QMetaObject::Connection(QMetaObjectPrivate::connect( sender, signal_index, smeta, receiver, method_index_relative, rmeta ,type, types)); return handle; } 方法代码很多很杂,但无非就是检查信号槽的格式,获取参数列表, 最后保存起来 QScopedPointer<QObjectPrivate::Connection> c( new QObjectPrivate::Connection); c->sender = s; //发送者对象 c->signal_index = signal_index; //信号索引 c->receiver = r; //接受者对象 c->method_relative = method_index; //槽索引 c->method_offset = method_offset; //槽偏移 c->connectionType = type; //连接方式 c->isSlotObject = false ; c->argumentTypes.store(types); c->nextConnectionList = 0; c->callFunction = callFunction; //静态回调函数 //在发送者元数据内加上连接信息 //信号发送者的对象内存中保存了连接的信息,包括槽的对象,槽地址,连接方式等 QObjectPrivate::get(s)->addConnection(signal_index, c.data()); |
3.7、触发信号
这时候再回过头来看3.4中的信号触发,我们知道,emit信号就是调用moc文件中的方法,方法的核心就是 QMetaObject::activate()
直接看该方法中调用槽函数的一段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
//因为一个信号可能连接多个槽,这里循环遍历链表进行调用 do { QObjectPrivate::Connection *c = list->first; if (!c) continue ; // We need to check against last here to ensure that signals added // during the signal emission are not emitted in this emission. QObjectPrivate::Connection *last = list->last; do { if (!c->receiver) continue ; QObject * const receiver = c->receiver; const bool receiverInSameThread = currentThreadId == receiver->d_func()->threadData->threadId; // determine if this connection should be sent immediately or // put into the event queue //直接连接并且发送和接受不再一个线程中,或者队列连接,则放入事件队列中 //可知,直接连接并且发送和接受不在同一个线程,则效果和队列连接相同 if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread) || (c->connectionType == Qt::QueuedConnection)) { queued_activate(sender, signal_index, c, argv ? argv : empty_argv, locker); continue ; #ifndef QT_NO_THREAD //阻塞式队列连接 } else if (c->connectionType == Qt::BlockingQueuedConnection) { locker.unlock(); //在同一个线程,则报错 if (receiverInSameThread) { qWarning( "Qt: Dead lock detected while activating a BlockingQueuedConnection: " "Sender is %s(%p), receiver is %s(%p)" , sender->metaObject()->className(), sender, receiver->metaObject()->className(), receiver); } QSemaphore semaphore; //资源计数器,avail为0 QMetaCallEvent *ev = c->isSlotObject ? new QMetaCallEvent(c->slotObj, sender, signal_index, 0, 0, argv ? argv : empty_argv, &semaphore) : new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal_index, 0, 0, argv ? argv : empty_argv, &semaphore); //根据连接信息构造一个事件,并添加到接受者的 事件队列中 QCoreApplication::postEvent(receiver, ev); //信号发送者的线程阻塞,acquire资源数为1,>avail(0),这里阻塞 //当槽执行玩之后释放,这里的avail才会增加,阻塞结束 semaphore.acquire(); locker.relock(); continue ; #endif } QConnectionSenderSwitcher sw; if (receiverInSameThread) { sw.switchSender(receiver, sender, signal_index); } const QObjectPrivate::StaticMetaCallFunction callFunction = c->callFunction; const int method_relative = c->method_relative; if (c->isSlotObject) { c->slotObj->ref(); QScopedPointer<QtPrivate::QSlotObjectBase, QSlotObjectBaseDeleter> obj(c->slotObj); locker.unlock(); obj->call(receiver, argv ? argv : empty_argv); // Make sure the slot object gets destroyed before the mutex is locked again, as the // destructor of the slot object might also lock a mutex from the signalSlotLock() mutex pool, // and that would deadlock if the pool happens to return the same mutex. obj.reset(); locker.relock(); } else if (callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) { //we compare the vtable to make sure we are not in the destructor of the object. locker.unlock(); const int methodIndex = c->method(); if (qt_signal_spy_callback_set.slot_begin_callback != 0) qt_signal_spy_callback_set.slot_begin_callback(receiver, methodIndex, argv ? argv : empty_argv); callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv ? argv : empty_argv); if (qt_signal_spy_callback_set.slot_end_callback != 0) qt_signal_spy_callback_set.slot_end_callback(receiver, methodIndex); locker.relock(); } else { const int method = method_relative + c->method_offset; locker.unlock(); if (qt_signal_spy_callback_set.slot_begin_callback != 0) { qt_signal_spy_callback_set.slot_begin_callback(receiver, method, argv ? argv : empty_argv); } metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv); if (qt_signal_spy_callback_set.slot_end_callback != 0) qt_signal_spy_callback_set.slot_end_callback(receiver, method); locker.relock(); } if (connectionLists->orphaned) break ; } while (c != last && (c = c->nextConnectionList) != 0); if (connectionLists->orphaned) break ; } while (list != &connectionLists->allsignals && //start over for all signals; ((list = &connectionLists->allsignals), true )); |
由上面代码,我们大概可以理解信号槽的几种连接方式:
- 默认连接并且信号槽的对象不在同一个线程中,则效果和队列连接类似;
- 阻塞时队列连接,信号和槽对象不同处于同一个线程中;
- Qt使用QSemaphore来实现阻塞式的槽函数调用;
4、小结
本次的源码因为种种原因,看的不是很详细,但是理解Qt的信号槽机制绰绰有余了
- Qt自带的元数据系统利用C++的宏等特性实现反射机制;
- 利用元数据系统,在连接信号槽是将槽的信息(接收对象、槽方法、参数列表、连接方式等)保存在信号的元数据中;
- 信号也是方法,方法体有moc工具生成,方法内获取该信号连接的所有槽信息,并依序执行;
直到这里,信号槽的逻辑已经显而易见了,它就是一个变种的观察者模式,槽的信息保存在信号对象中也就是设置回调函数,触发信号也就是执行回调函数,只是Qt库将其中的各种操作细节封装起来了,所以,使用起来,不去关注设计模式的细节,也就容易很多了。不得不说,无论是从设计思路,还是开发技巧上看,Qt的开发者真的很牛叉。
5、第三方信号槽库
信号槽机制是Qt首创,但不是其独有,其他各类C++流行框架也都是互相借鉴,C++标准库的预备役的boost中也有信号槽机制的实现。如果平时开发中需要用到信号槽机制,但是又不想引入这些庞大的类库,可以使用轻量级别的信号槽库:http://sigslot.sourceforge.net,该库不详细介绍,有兴趣的小伙伴自己学习把。
以上就是c++ Qt信号槽原理的详细内容,更多关于c++ Qt信号槽的资料请关注服务器之家其它相关文章!
原文链接:https://www.cnblogs.com/sherlock-lin/p/13960936.html