Qt的信号和槽是如何工作的
用Qt做过开发的朋友,不知道是否曾为下面这些问题疑惑过:
我们知道Qt是基于C++的,Qt写的代码最终还是要由C++编译器来编译,但是我们的Qt代码中有很多C++里没有的关键字,比如slots\signals\Q_OBJECT等,为什么C++编译器会放过他们呢?
Qt的槽函数跟普通的成员函数有何区别?一个信号发出后,与之关联的槽函数是什么时候由谁来调用的?
Qt的信号定义跟函数定义有相同的形式,那信号跟普通函数有何区别?一个信号被emit时我们的程序到底在干什么?
信号和槽的连接是怎么建立的?又是如何断开的?
本篇博文里对这些问题作了详尽的解释,初学Qt或者用了很长时间Qt却很少思考过这些问题的朋友,可以在下面的段落中找到答案。
下文出自- original:http://woboq.com/blog/how-qt-signals-slots-work.html
译:NewThinker_Jiwey NewThinker_wei @CSDN
很多人都知道Qt的信号和槽机制,但它究竟是如何工作的呢?在本篇博文中,我们会探索QObject 和 QMetaObject 内部的一些具体实现,揭开信号-槽的神秘面纱。
在本篇博文中,我会贴出部分Qt5的源码,有时候会根据需要改变一下格式并做一些简化。
首先,我们看一个官方例程,借此回顾一下信号和槽的使用方法。
头文件是这样的:
***********************************************
class Counter :publicQObject
{
Q_OBJECT
int m_value;
public:
int value()const {returnm_value; }
public slots:
void setValue(intvalue);
signals:
void valueChanged(intnewValue);
};
***********************************************
在源文件的某个地方,可以找到setValue()函数的实现:
***********************************************
voidCounter::setValue(intvalue)
{
if (value !=m_value)
{
m_value =value; emitvalueChanged(value);
}
}
***********************************************
然后开发者可以像下面这样使用Counter的对象:
***********************************************
Counter a, b;
QObject::connect(&a,SIGNAL(valueChanged(int)),
&b, SLOT(setValue(int)));
a.setValue(12); // a.value() == 12, b.value() == 12
***********************************************
这是从1992年Qt最初阶段开始就沿用下来而几乎没有变过的原始语法。
虽然基本的API并没有怎么变过,但它的实现方式却变了几次。很多新特性被添加了进来,底层的实现也发生了很多变化。不过这里面并没有什么神奇的难以理解的东西,本篇博文会向你展示这究竟是如何工作的,
MOC,元对象编译器
Qt的信号/槽和属性系统都基于其能在运行时刻对对象进行实时考察的功能。实时考察意味着即使在运行过程中也可以列出一个对象有哪些方法(成员函数)和属性,以及关于它们的各种信息(比如参数类型)。
如果没有实时考察这个功能,QtScript 和 QML 就基本不可能实现了。
C++本身不提供对实时考察的支持,所以Qt就推出了一个工具来提供这个支持。这个工具就是MOC。注意,它是一个代码生成器,而不是很多人说的“预编译器”。
MOC会解析头文件并为每一个含有Q_OBJECT宏的头文件生成一个对应的C++文件(这个文件会跟工程中的其他代码一块参与编译)。这个生成的C++文件包含了实时考察功能所需的全部信息(文件一般被命名为moc_HeaderName。cpp)。
因为这个额外的代码生成器,Qt有时对语言会有很严格的要求。 这里我就让这篇Qt文档来解释这个严格的要求。代码生成器没有什么错误,MOC起到了很大的作用。
几个神奇的宏
你能看出这几个关键字并不是标准C++的关键字吗?signals, slots, Q_OBJECT, emit, SIGNAL, SLOT
. 这些都是Qt对C++的扩展。这几个关键字其实都是很简单的宏定义而已,在qobjectdefs.h 头文件中可以找到他们的定义。
- #define signals public
- #define slots /* nothing */
没错,信号和槽都是很简单的功能:编译器会将他们与其他任何宏一样处理。不过这些宏还有一个特殊的作用:MOC会发现他们。
Signals在Qt4之前都是protected类型的,他们在Qt5中变为了public,这样就可以使用一些新的语法了。
- #define Q_OBJECT \
- public: \
- static const QMetaObject staticMetaObject; \
- virtual const QMetaObject *metaObject() const; \
- virtual void *qt_metacast(const char *); \
- virtual int qt_metacall(QMetaObject::Call, int, void **); \
- QT_TR_FUNCTIONS /* translations helper */ \
- private: \
- Q_DECL_HIDDEN static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **);
- Q_OBJECT defines a bunch of functions and a static QMetaObject Those functions are implemented in the file generated by MOC.
- #define emit /* nothing */
emit是个空的宏定义,而且MOC也不会对它进行解析。也就是,emit完全是可有可无的,他没有任何意义(除了对开发者有提示作用)。
- Q_CORE_EXPORT const char *qFlagLocation(const char *method);
- #ifndef QT_NO_DEBUG
- # define QLOCATION "\0" __FILE__ ":" QTOSTRING(__LINE__)
- # define SLOT(a) qFlagLocation("1"#a QLOCATION)
- # define SIGNAL(a) qFlagLocation("2"#a QLOCATION)
- #else
- # define SLOT(a) "1"#a
- # define SIGNAL(a) "2"#a
- #endif
(译注:对于#define的一些高级用法,参见我整理的一片文章:http://blog.csdn.net/newthinker_wei/article/details/8893407)
上面这些宏,会利用预编译器将一些参数转化成字符串,并且在前面添加上编码。
在调试模式中,如果singnal的连接出现问题,我们提示警告信息的时候还会注明对应的文件位置。这是在Qt4.5之后以兼容方式添加进来的功能。为了知道代码对应的行信息,我们可以用qFlagLocation
,它会将对应代码的地址信息注册到一个有两个入口的表里。
MOC生成的代码
我们现在就来看看Qt5的moc生成的部分代码。
The QMetaObject
***********************************************
const QMetaObject Counter::staticMetaObject = {
{ &QObject::staticMetaObject,qt_meta_stringdata_Counter.data,
qt_meta_data_Counter, qt_static_metacall,0,0}
};
const QMetaObject *Counter::metaObject()const
{
return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}
***********************************************
我们在这里可以看到Counter::metaObject()和
Counter::staticMetaObject 的实现。他们都是在Q_OBJECT宏中被声明的。
QObject::d_ptr->metaObject 只被用于动态元对象(QML对象),随意通常虚函数metaObject()只是返回类的staticMetaObject 。
staticMetaObject被创建为只读数据。qobjectdefs.h 文件中QMetaObject的定义如下:
- struct QMetaObject
- {
- /* ... Skiped all the public functions ... */
- enum Call { InvokeMetaMethod, ReadProperty, WriteProperty, /*...*/ };
- struct { // private data
- const QMetaObject *superdata;
- const QByteArrayData *stringdata;
- const uint *data;
- typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);
- StaticMetacallFunction static_metacall;
- const QMetaObject **relatedMetaObjects;
- void *extradata; //reserved for future use
- } d;
- };
代码中用的d是为了表明那些数据都本应为私有的。然而他们并没有成为私有的是为了保持它为POD和允许静态初始化。(译注:在C++中,我们把传统的C风格的struct叫做POD(Plain Old Data),字面意思古老的普通的结构体)。
这里会用父类对象的元对象(此处就是指QObject::staticMetaObject )来初始化QMetaObject的superdata,而它的stringdata 和 data
这两个成员则会用之后要讲到的数据初始化。static_metacall是一个被初始化为 Counter::qt_static_metacall的函数指针。
实时考察功能用到的数据表
首先,我们来分析 QMetaObject的这个整型数组:
***********************************************
static const uint qt_meta_data_Counter[] = {
// content:
7, // revision
0, // classname
0, 0, // classinfo
2, 14, // methods
0, 0, // properties
0, 0, // enums/sets
0, 0, // constructors
0, // flags
1, // signalCount
// signals: name, argc, parameters, tag, flags
1, 1, 24, 2, 0x05,
// slots: name, argc, parameters, tag, flags
4, 1, 27, 2, 0x0a,
// signals: parameters
QMetaType::Void,QMetaType::Int, 3,
// slots: parameters
QMetaType::Void,QMetaType::Int, 5,
0 // eod
};
***********************************************
开头的13个整型数组成了结构体的头信息。对于有两列的那些数据,第一列表示某一类项目的个数,第二列表示这一类项目的描述信息开始于这个数组中的哪个位置(索引值)。
这里,我们的Counter类有两个方法,并且关于方法的描述信息开始于第14个int数据。
每个方法的描述信息由5个int型数据组成。第一个整型数代表方法名,它的值是该方法名(译注:方法名就是个字符串)在字符串表中的索引位置(之后会介绍字符串表)。第二个整数表示该方法所需参数的个数,后面紧跟的第三个数就是关于参数的描述(译注:它表示与参数相关的描述信息开始于本数组中的哪个位置,也是个索引)。我们现在先忽略掉tag和flags。对于每个函数,Moc还会保存它的返回类型、每个参数的类型、以及参数的名称。
字符串表
***********************************************
struct qt_meta_stringdata_Counter_t {
QByteArrayData data[6];
char stringdata[47];
};
#define QT_MOC_LITERAL(idx, ofs, len) \
Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
offsetof(qt_meta_stringdata_Counter_t, stringdata) + ofs \
- idx * sizeof(QByteArrayData) \
)
static const qt_meta_stringdata_Counter_tqt_meta_stringdata_Counter = {
{
QT_MOC_LITERAL(0,0,7),
QT_MOC_LITERAL(1,8,12),
QT_MOC_LITERAL(2,21,0),
QT_MOC_LITERAL(3,22,8),
QT_MOC_LITERAL(4,31,8),
QT_MOC_LITERAL(5,40,5)
},
"Counter\0valueChanged\0\0newValue\0setValue\0"
"value\0"
};
#undef QT_MOC_LITERAL
***********************************************
这主要就是一个QByteArray的静态数组。QT_MOC_LITERAL 这个宏可以创建一个静态的QByteArray
,其数据就是参考的在它下面的对应索引处的字符串。
信号
void Counter::valueChanged(int_t1)
{
void *_a[] = { 0,const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
QMetaObject::activate(this, &staticMetaObject,0,_a);
}
{
if (_c == QMetaObject::InvokeMetaMethod) {
Counter *_t =static_cast<Counter *>(_o);
switch (_id) {
case 0: _t->valueChanged((*reinterpret_cast<int(*)>(_a[1])));break;
case 1: _t->setValue((*reinterpret_cast<int(*)>(_a[1])));break;
default: ;
}
QMetaObject::indexOf{Signal,Slot,Method}这样的函数返回的就是绝对索引。
另外,在信号槽的连接机制中还要用到一个关于信号的向量索引。这样的索引表中如果把槽也包含进来的话槽会造成向量的浪费,而一般槽的数量又要比信号多。所以从Qt4.6开始,Qt内部又多出了一个专门的信号索引signal index ,它是一个只包含了信号的索引表。
在用Qt开发的时候,我们只需要关心绝对索引就行。不过在浏览Qt源码的时候,要留意这三种索引的不同。
连接是如何进行的
在进行信号和槽的连接时Qt做的一件事就是找出要连接的信号和槽的索引。Qt会在meta object的字符串表中查找对应的索引。
然后会创建一个QObjectPrivate::Connection对象并添加到内部的链表中。
一个
connection 中需要存储哪些数据呢?我们需要一种能根据信号索引signal index快速访问到对应的connection的方法。因为可能会同时有不止一个槽连接到同一个信号上,所以每一个信号都要有一个槽列表;每个connection必须包含接收对象的指针以及被连接的槽的索引;我们还想让一个connection能在与之对应的接收者被销毁时自动取消连接,所以每个接收者对象也需要知道谁与他建立了连接这样才能在析构时将connection清理掉。
下面是定义在 qobject_p.h 中的QObjectPrivate::Connection :
(译注:只认识QObject不认识QObjectPrivate?看这里:http://blog.csdn.net/newthinker_wei/article/details/22789885)
***********************************************
struct QObjectPrivate::Connection
{
QObject *sender;
QObject *receiver;
union {
StaticMetaCallFunction callFunction;
QtPrivate::QSlotObjectBase *slotObj;
};
// The next pointer for the singly-linked ConnectionList
Connection *nextConnectionList;
//senders linked list
Connection *next;
Connection **prev;
QAtomicPointer<const int> argumentTypes;
QAtomicInt ref_;
ushort method_offset;
ushort method_relative;
uint signal_index : 27; // In signal range (see QObjectPrivate::signalIndex())
ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
ushort isSlotObject : 1;
ushort ownArgumentTypes : 1;
Connection() : nextConnectionList(0), ref_(2), ownArgumentTypes(true) {
//ref_ is 2 for the use in the internal lists, and for the use in QMetaObject::Connection
}
~Connection();
int method() const { return method_offset + method_relative; }
void ref() { ref_.ref(); }
void deref() {
if (!ref_.deref()) {
Q_ASSERT(!receiver);
delete this;
}
}
};
***********************************************
每一个对象有一个connection vector:每一个信号有一个 QObjectPrivate::Connection的链表,这个vector就是与这些链表相关联的。
每一个对象还有一个反向链表,它包含了这个对象被连接到的所有的 connection,这样可以实现连接的自动清除。而且这个反向链表是一个双重链表。
使用链表是因为它们可以快速地添加和移除对象,它们靠保存在QObjectPrivate::Connection中的next/previous前后节点的指针来实现这些操作。
注意senderList的prev指针是一个“指针的指针”。这是因为我们不是真的要指向前一个节点,而是要指向一个指向前节点的指针。这个“指针的指针”只有在销毁连接时才用到,而且不要用它重复往回迭代。这样设计可以不用对链表的首结点做特殊处理。
(译注:对连接的建立如果还有疑惑,请参考:http://blog.csdn.net/newthinker_wei/article/details/22791617)
信号的发送
我们已经知道当调用一个信号的时候,最终调用的是MOC生成的代码中的QMetaObject::activate函数。
这里是qobject.cpp中这个函数的实现代码,这里贴出来的只是一个注解版本。
***********************************************
void QMetaObject::activate(QObject *sender, const QMetaObject *m,intlocal_signal_index,
void **argv)
{
activate(sender,QMetaObjectPrivate::signalOffset(m),local_signal_index,argv);
/* We just forward to the next function here. We pass the signal offset of
* the meta object rather than the QMetaObject itself
* It is split into two functions because QML internals will call the later. */
}
void QMetaObject::activate(QObject *sender, intsignalOffset,intlocal_signal_index,void **argv)
{
int signal_index =signalOffset +local_signal_index;
/* The first thing we do is quickly check a bit-mask of 64 bits. If it is 0,
* we are sure there is nothing connected to this signal, and we can return
* quickly, which means emitting a signal connected to no slot is extremely
* fast. */
if (!sender->d_func()->isSignalConnected(signal_index))
return; // nothing connected to these signals, and no spy
......
......
/*译注:获得当前正在运行的线程的ID*/
Qt::HANDLE currentThreadId = QThread::currentThreadId();
/* ... Skipped some debugging and QML hooks, and some sanity check ... */
/*跳过一些调试代码和完整性检测*/
/* We lock a mutex because all operations in the connectionLists are thread safe */
/*这里用一个互斥量锁住,因为在ConnectionList里的所有操作都应是线程安全的*/
QMutexLocker locker(signalSlotLock(sender));
/* Get the ConnectionList for this signal. I simplified a bit here. The real code
* also refcount the list and do sanity checks */
/*得到这个信号的ConnectionList。这里我做了一些简化。原来的代码还有完整性检测等*/
QObjectConnectionListVector *connectionLists =sender->d_func()->connectionLists;
const QObjectPrivate::ConnectionList *list =
&connectionLists->at(signal_index);
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;
/* Now iterates, for each slot */
do {
if (!c->receiver)
continue;
QObject * const receiver = c->receiver;
/*译注:比较当前正在运行的线程的ID与receiver所属的线程的ID是否相同*/
const bool receiverInSameThread = QThread::currentThreadId() == receiver->d_func()->threadData->threadId;
// determine if this connection should be sent immediately or
// put into the event queue
//译注:注意下面这一段,从这里可以看出对于跨线程的连接,信号发出
//后槽函数不会立即在当前线程中执行。其执行要等到槽函数所在的线程被
//激活后。有时间了再研究一下queued_activate这个函数。
if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
|| (c->connectionType == Qt::QueuedConnection)) {
/* Will basically copy the argument and post an event */
queued_activate(sender,signal_index,c,argv);
continue;
} else if (c->connectionType == Qt::BlockingQueuedConnection) {
/* ... Skipped ... */
continue;
}
/* Helper struct that sets the sender() (and reset it backs when it
* goes out of scope */
QConnectionSenderSwitcher sw;
if (receiverInSameThread)
sw.switchSender(receiver,sender,signal_index);
const QObjectPrivate::StaticMetaCallFunctioncallFunction =c->callFunction;
const int method_relative = c->method_relative;
if (c->isSlotObject) {
/* ... Skipped.... Qt5-style connection to function pointer */
} else if (callFunction &&c->method_offset <=receiver->metaObject()->methodOffset()) {
/* If we have a callFunction (a pointer to the qt_static_metacall
* generated by moc) we will call it. We also need to check the
* saved metodOffset is still valid (we could be called from the
* destructor) */
locker.unlock();// We must not keep the lock while calling use code
callFunction(receiver,QMetaObject::InvokeMetaMethod,method_relative,argv);
locker.relock();
} else {
/* Fallback for dynamic objects */
const int method =method_relative +c->method_offset;
locker.unlock();
metacall(receiver,QMetaObject::InvokeMetaMethod,method,argv);
locker.relock();
}
// Check if the object was not deleted by the slot
if (connectionLists->orphaned)break;
} while (c != last && (c = c->nextConnectionList) !=0);
}
***********************************************
Qt的信号和槽是如何工作的的更多相关文章
- QT点击"X"按钮,调用closeEvent()函数来实现调用特定事件(附:粗略介绍QT的信号与槽的使用方法)
背景: QT在用户关闭窗口(直接点击"X"键)时,程序一般都需要做一些善后的事情,就我现在的程序来说,既关闭USB.如何实现? 正文: 首先,在对应窗体的".h" ...
- QT 的信号与槽
转载: QT 的信号与槽机制介绍 QT 是一个跨平台的 C++ GUI 应用构架,它提供了丰富的窗口部件集,具有面向对象.易于扩展.真正的组件编程等特点,更为引人注目的是目前 Linux 上最为流行的 ...
- QT的信号与槽机制介绍
信号与槽作为QT的核心机制在QT编程中有着广泛的应用,本文介绍了信号与槽的一些基本概念.元对象工具以及在实际使用过程中应注意的一些问题. QT是一个跨平台的C++ GUI应用构架,它提供了丰富的窗 ...
- Qt 的信号与槽机制介绍(10个要注意的问题)
QT 是一个跨平台的 C++ GUI 应用构架,它提供了丰富的窗口部件集,具有面向对象.易于扩展.真正的组件编程等特点,更为引人注目的是目前 Linux 上最为流行的 KDE 桌面环境就是建立在 QT ...
- C++框架_之Qt的信号和槽的详解
C++_之Qt的信号和槽的详解 1.概述 信号槽是 Qt 框架引以为豪的机制之一.所谓信号槽,实际就是观察者模式.当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal ...
- 【qt】QT 的信号与槽机制
QT 是一个跨平台的 C++ GUI 应用构架,它提供了丰富的窗口部件集,具有面向对象.易于扩展.真正的组件编程等特点,更为引人注目的是目前 Linux 上最为流行的 KDE 桌面环境就是建立在 QT ...
- QT的信号和槽机制简介
信号与槽作为QT的核心机制在QT编程中有着广泛的应用,本文介绍了信号与槽的一些基本概念.元对象工具以及在实际使用过程中应注意的一些问题. QT是一个跨平台的C++ GUI应用构架,它提供了丰富的窗口部 ...
- qt中信号与槽机制
一. 简介 就我个人来理解,信号槽机制与Windows下消息机制类似,消息机制是基于回调函数,Qt中用信号与槽来代替函数指针,使程序更安全简洁. 信号和槽机制是 Qt 的核心机制,可以让编程人员将互不 ...
- QT_5_ Qt中信号和槽 + 自定义信号和槽 + lambda 表达式
1.Qt中信号和槽 1.1 需求:点击按钮关闭窗口 1.2 利用connect进行链接 1.3 参数1 信号发送者(指针) 参数2 发送的信号(信号地址) 参数3 信号的接受者(指针) 参数4 处理槽 ...
随机推荐
- 96、facebook Fresco框架库源使用(转载)
各个属性详情:http://blog.csdn.net/y1scp/article/details/49245535 开源项目链接 facebook Fresco仓库:git clone https: ...
- 【算法】深度优先搜索(DFS)III
1. DFS生成排列 众所周知,1,2…n的排列一共有n!个,因此生成全排列至少需要n!的时间复杂度.如果用循环来生成排列,当n稍大时,内外循环会非常之多.可以用DFS模拟解决,生成0 … n-1的排 ...
- android 动画学习系列(一)
老师今天开始讲android的游戏,讲到了view和surfaceview 下面是view的演示部分 private Paint paint; //游戏里很少用double,都是用long priva ...
- JS-元素大小深入学习-offset、client、scroll等学习研究笔记
一些属性和方法,在dom中没有规定如何确定页面中元素大小的情况下诞生... 1.偏移量(offset dimension) 测试代码: <!DOCTYPE html> <html&g ...
- ubuntu android studio kvm
Android studio 启动模拟器失败: Cannot launch AVD in emulator.Output:emulator: ERROR: x86 emulation currentl ...
- Egret Wing4.0.3 动画编辑器
一 exml上摆放组件 切换动画编辑 创建动画组,命名test1. 选中一个对象,创建动画(必须选中一个对象后,+号才会亮.且一个对象只能创建一个动画) 之后和Flash差不多.在时间轴插入关键帧. ...
- java类的初始化的默认值
转自:http://blog.csdn.net/abc5382334/article/details/18254517 对于类的成员变量 不管程序有没有显示的初始化,Java 虚拟机都会先自动给 ...
- ionic+cordova开发!
这里是一些学习的过程中纪录的: 官方网站: http://www.ionic-china.com/ 参考文章: https://blog.csdn.net/xyzz609/article/detail ...
- 【巷子】---vue项目打包---基本使用---【vue】
一.基本设置 二.打包 npm run build 三.github创建一个仓库 1.将文件上传至仓库 2.点击仓库设置 3.选择代码上传到master 4.点击地址即可预览 5.常用技巧 a.如果在 ...
- OA之框架的搭建
1.使用框架可以有效的解决耦合性过高的问题,减少代码修改的程度,同时方便添加新的功能.首先创建出基本的几个类库.这个框架就是使用基本的逻辑分层三层架构,然后进一步再使用接口对每个逻辑中的类库调用进行解 ...