Qt之信号槽原理
一.概述
所谓信号槽,实际就是观察者模式。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,将想要处理的信号和自己的一个函数(称为槽(slot))绑定来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。(这里提一句,Qt 的信号槽使用了额外的处理来实现 ,并不是 GoF 经典的观察者模式的实现方式。)
信号和槽是Qt特有的信息传输机制,是Qt设计程序的重要基础,它可以让互不干扰的对象建立一种联系 Qt信号槽有如下优点:
1.类型安全。需要关联的信号槽的签名必须是等同的。即信号的参数类型和参数个数同接受该信号的槽的参数类型和参数个数相同。若信号和槽签名不一致,编译器会报错。
2.松散耦合。信号和槽机制减弱了Qt对象的耦合度。激发信号的Qt对象无需知道是那个对象的那个信号槽接收它发出的信号,它只需在适当的时间发送适当的信号即可,而不需要关心是否被接受和那个对象接受了。Qt就保证了适当的槽得到了调用,即使关联的对象在运行时被删除。程序也不会奔溃。
3.灵活性。一个信号可以关联多个槽,或多个信号关联同一个槽
二.信号槽的实现
众所周知,C++语言的编译过程为预处理->编译->汇编->链接。
a.驱动程序首先运行C预处理器(cpp),将源程序翻译成一个ASCII码的中间文件main.i, b.驱动程序运行C编译器,将main.i翻译成一个ASCII汇编文件main.s c.驱动程序运行汇编器(as),将main.s翻译成一个可重定位目标文件main.o d.运行链接器,将main.o及其一些必要的系统目标文件组合在一起,创建一个可执行的目标文件
但是在qt中,首先会有一个Moc预处理器对源代码进行处理,经过Moc预处理器处理后的代码才是标准的C++代码,此后就可以执行正常的C++编译流程了。正是因为Moc预处理器,Qt才实现了信号槽的功能,下面我们通过用纯C++代码实现一个简洁的信号槽功能来对信号槽深入了解。
使用C++语言模拟信号槽实现:
首先看一下类图:
QObject有一个静态元对象QMetaObject,静态元对象存放着信号槽名称的字符信息以及一个根据信号发送者和信号index调用对应槽的函数;
QObject有一个容器connections,此容器是一个map,key是信号index,value是一个Connection,维护者信号槽的对应关系;
QObject有一个静态方法connect,此方法将信号的index作为key,创建一个value插入到对象维护的连接列表容器中;
-
程序运行时,connect借助两个字符串,即可将信号与槽的关联建立起来,那么,它是如果做到的呢?C++的经验可以告诉我们:
-
类中应该保存有信号和槽的字符串信息
-
字符串和信号槽函数要关联
-
引入元对象系统
定义信号和槽,为了和普通成员进行区分以使得预处理器可以提取信息,定义几个关键字。
#define slots #define signals protected #define emit
-
通过预处理器,将信息提取取来,放置到一个单独的文件中(比如moc_QObject.cpp):
-
规则很简单,将信号和槽的名字提取出来,放到字符串中。可以有多个信号或槽,按顺序"sig1/nsig2/n"
static const char sig_names[] = "sig1/nsing2/n"; static const char slts_names[] = "slot1/nslot2/n";
利用这些信号槽的信息,建立连接;
定义一个结构体,存放信息
struct QMetaObject { const char * sig_names; const char * slts_names; };
然后将它作为QObject的一个静态对象,这个就是Qt中的元对象。
class QObject { static QMetaObject staticMetaObject; ...
利用预处理器生成的moc_Object.cpp:
#include "object.h" static const char sig_names[] = "sig1/n"; static const char slts_names[] = "slot1/n"; QMetaObject QObject::staticMetaObject = {sig_names, slts_names};
建立信号槽连接
利用moc预处理器保存的信息,通过 connect 将信号和槽的对应关系保存到一个 mutlimap中。
struct Connection { Object * receiver; int method; }; class QObject { public: ... static void connect(QObject*, const char*, QObject*, const char*); ... private: std::multimap<int, Connection> connections;
connect函数:
void QObject::connect(QObject* sender, const char* sig, QObject* receiver, const char* slt) { int sig_idx = find_string(sender->meta.sig_names, sig); int slt_idx = find_string(receiver->meta.slts_names, slt); if (sig_idx == -1 || slt_idx == -1) { perror("signal or slot not found!"); } else { Connection c = {receiver, slt_idx}; sender->connections.insert(std::pair<int, Connection>(sig_idx, c)); } }
首先从元对象信息中查找信号和槽的名字是否存在,如果存在,则将信号的索引和接收者的信息存入信号发送者的一个map中。
信号的激活:
在Qt中我们都是使用emit来激活一个信号,这是emit的本来面目。
#define emit
在Qt中我们必须要在类里增加一个Q_OBJECT的宏,发送信号使用emit,定义槽和信号,使用signals,public slots等。其实这些都是一些宏替换,在qobjects.h文件里可以看到这些宏的本来面目。
我们这里使用emit来激活信号。我们在定义信号的时候只写了信号的声明,信号的实现并未给出。而槽函数的实现是开发者给出的。其实信号的实现是Moc编译器帮助我们实现的。
void QObject::sig1() { QMetaObject::active(this, 0); }
信号的调用工作由QMetaObject类来完成
class QObject; struct QMetaObject { const char * sig_names; const char * slts_names; static void active(QObject * sender, int idx); };
这个函数该怎么写呢:思路很简单
从前面的保存连接的map中,找出与该信号关联的对象和槽 调用该对象这个槽
typedef std::multimap<int, Connection> ConnectionMap; typedef std::multimap<int, Connection>::iterator ConnectionMapIt; void QMetaObject::active(QObject* sender, int idx) { ConnectionMapIt it; std::pair<ConnectionMapIt, ConnectionMapIt> ret; ret = sender->connections.equal_range(idx); for (it=ret.first; it!=ret.second; ++it) { Connection c = (*it).second; //c.receiver->metacall(c.method); } }
槽的调用:
这个最后一个关键问题了,槽函数如何根据一个索引值进行调用。
-
直接调用槽函数我们都知道了,就一个普通函数
-
可现在通过索引调用了,那么我们必须定义一个接口函数
class QObject { void metacall(int idx); ...
该函数如何实现呢?这个又回到我们的元对象预处理过程中了,因为在预处理的过程,我们能将槽的索引和槽的调用关联起来。
所以,在预处理生成的文件(moc_QObject.cpp)中,我们很容易生成其定义:
void QObject::qt_static_metacall(int idx) { switch (idx) { case 0: { slot1(); break; } case 1: { slot2(); break; } }; }
总结:moc通过元对象系统保存了信号和槽的字符信息,然后为每一个信号和槽分配编号。当我们调用connect函数时,把信号槽的对应关系保存在对象的一个map容器中。当发送信号时(也就是调用信号函数时)通过刚才保存在map容器中的信号槽对应关系找到对应的接收对象和槽函数。
完整代码:
#include <string.h> #include "QObject.h" #include <iostream> static int find_string(const char * str, const char * substr) { if (strlen(str) < strlen(substr)) return -1; int idx = 0; int len = strlen(substr); bool start = true; const char * pos = str; char cEnd = '\n'; while (*pos) { if (start && !strncmp(pos, substr, len) && pos[len] == '\n') return idx; start = false; if (*pos == cEnd) { idx++; start = true; } pos++; } return -1; } void QObject::connect(QObject* sender, const char* sig, QObject* receiver, const char* slt) { int sig_idx = find_string(sender->staticMetaObject.sig_names, sig); int slt_idx = find_string(receiver->staticMetaObject.slts_names, slt); if (sig_idx == -1 || slt_idx == -1) { perror("signal or slot not found!"); } else { Connection c = { receiver, slt_idx }; sender->connections.insert(std::pair<int, Connection>(sig_idx, c)); } } void QObject::slot1() { std::cout << "into slot1" << std::endl; } void QObject::slot2() { std::cout << "into slot2" << std::endl; } /* 从前面的保存连接的map中,找出与 该信号关联的对象和槽调用该对象这个槽 */ void QMetaObject::active(QObject* sender, int idx) { ConnectionMapIt it; std::pair<ConnectionMapIt, ConnectionMapIt> ret; ret = sender->connections.equal_range(idx); for (it = ret.first; it != ret.second; ++it) { Connection c = (*it).second; c.receiver->qt_static_metacall(c.method); } } void QObject::testSignal() { emit sig2(); emit sig1(); }
#pragma once #ifndef Q_OBJECT_H #define Q_OBJECT_H #include <map> # define slots # define signals protected # define emit # define Q_OBJECT static QMetaObject staticMetaObject;void qt_static_metacall(int idx); class QObject; /*元对象*/ struct QMetaObject { const char * sig_names; const char * slts_names; /*信号的发送者和信号的索引*/ static void active(QObject * sender, int idx); }; struct Connection { QObject * receiver; int method; }; typedef std::multimap<int, Connection> ConnectionMap; typedef std::multimap<int, Connection>::iterator ConnectionMapIt; class QObject { static QMetaObject staticMetaObject; void qt_static_metacall(int idx); public: static void connect(QObject*, const char*, QObject*, const char*); void testSignal(); signals: void sig1(); void sig2(); public slots: void slot1(); void slot2(); friend struct QMetaObject; private: ConnectionMap connections; }; #endif
/*此文件手动生成,其实应该是Moc预编译器生成的*/ #include "QObject.h" static const char sig_names[] = "sig1\nsig2\n"; static const char slts_names[] = "slot1\nslot2\n"; QMetaObject QObject::staticMetaObject = { sig_names, slts_names }; void QObject::sig1() { QMetaObject::active(this, 0); } void QObject::sig2() { QMetaObject::active(this, 1); } void QObject::qt_static_metacall(int idx) { switch (idx) { case 0: { slot1(); break; } case 1: { slot2(); break; } }; }
#include <iostream> #include <string> #include "QObject.h" int main() { QObject obj1, obj2; QObject::connect(&obj1, "sig1", &obj2, "slot1"); QObject::connect(&obj1, "sig2", &obj2, "slot2"); obj1.testSignal(); return 0;; }
下面是Qt中的宏替换
QObject::connect(countObj1, SIGNAL(valueChanged()), countObj2, SLOT(slotValueChanged()));
QObject::connect(countObj1, "2""valueChanged()", countObj2, "1""slotValueChanged()");
#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 **); private: \ static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \ struct QPrivateSignal {};
struct Q_CORE_EXPORT QArrayData { QtPrivate::RefCount ref; int size; uint alloc : 31; uint capacityReserved : 1; qptrdiff offset; // in bytes from beginning of header }