QT信号槽实现原理-观察者设计模式架构-QT源码
一 、使用场景
我们使用QT的UI控件时,常用到触发控件的操作。
比如点击一个按钮,就进行一个什么操作。ui文件可以右击控件–转到槽来生成一个槽函数,很方便。
选择槽函数后,头文件里会多一个槽函数。
这是QT的 QObject函数的信号与槽功能,使得控件的点击为一个信号,点击后触发槽函数进行操作。
我们自己也可以在QObject类里写上Q_OBJECT,signals和slots来实现信号槽。
它实现的是
观察者模式:通知依赖关系,一个对象目标的状态发生改变,所有依赖对象( 观察者对象)都将得到通知。
观察者模式通过面向对象技术,弱化关系,形成稳定依赖,实现软件体系结构的松耦合。
QT的预处理-------MOC元对象编译器
信号槽需要使用QT的MOC实现,需要在类里写Q_OBJECT宏才能实现。
moc工具会解析具有Q_OBJECT宏的类,生成对应的moc_xx.cpp文件,该文件会随着项目一起编译。
Qt程序在交由标准编译器(例如MSVC)编译之前,先使用moc(MOC, 元对象编译器 the Meta Object Compiler)分析cpp头文件;如果它发现在一个头文件中包含了Q_OBJECT宏,则会生成另外一个cpp源文件(moc_文件名.cpp),该cpp源文件中包含了Q_OBJECT宏的实现、运行时信息(反射)等。因此Qt程序的完整编译过程为moc->预处理->编译->链接
QObject和QMetaObject:
顾名思义,QMetaObject包含了QObject的所谓的元数据,也就是QObject信息的一些描述信息:除了类型信息外,还包含QT中特 有的signal&slot信息。
QObject::metaObject ()方法返回一个QObject对象对应的metaobject对象,注意这个方法是virtual方法。如上文所说,如果一个类的声明中包含了 Q_OBJECT宏,编译器会生成代码来实现这个类对应的QMetaObject类,并重载QObject::metaObject()方法来返回这个 QMetaObject类的实例引用。这样当通过QObject类型的引用调用metaObejct方法时,返回的是这个引用的所指的真实对象的 metaobject。
如果一个类从QObject派生,确没有声明Q_OBJECT宏,那么这个类的metaobject对象不会被生成,这样这个类所声明的 signal slot都不能使用,而这个类实例调用metaObject()返回的就是其父类的metaobject对象,这样导致的后果就是你从这个类实例获得的元 数据其实都是父类的数据,这显然给你的代码埋下隐患。因此如果一个类从QOBject派生,它都应该声明Q_OBJECT宏,不管这个类有没有定义 signal&slot和Property。
这样每个QObject类都有一个对应的QMetaObject类,形成一个平行的类型层次。
信号和槽实现类似设计模式的观察者模式
观察者模式定义:
定义对象间的一种一对多变化的依赖关系,以便当一个对象(subject)的状态发送变化时,所有依赖于它的对象都得到通知并自动更新。
动机:
在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ----一个对象(目标对象)的状态发生改变,所以的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。
使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
目的都是去耦合。信号槽是将这种模式封装好了,方便我们调用。实现了 对接口编程而不是对实现编程.从抽象类QObject类继承的类都共享其接口(signals slots).
二 、信号与槽实现及源码
我们定义一个发送信号的sender发送者
发送信号operate(),
和一个接收槽函数的接收者receiver
信号 :当按钮(对象)改变状态时,信号就emit,对象只负责发送,不负责接收方的处理。
槽: 接收到信号,不关心信号。
信号与槽如何连接:
原来QT是通过QObject::connect静态函数建立连接;其中sender与receiver是指向对象的指针,分别代表了被观察者和 观察者。
signal与method分别通过SIGNAL()与SLOT()宏来进行转换 (类型是const char*)
他们通过 connect(this, &myclass::operate, worker, &Worker::doWork);绑定信号和槽函数。
宏定义 -moc文件
QObject类型有 signals slots Q_OBJECT emit SIGNAL SLOT 等信号槽相关的宏定义。
CTRL键+鼠标点击这些宏,进入qobjecdefs.h文件,这里定义了这些宏。
信号 moc预处理
首先 emit 信号 发送了什么?(注:emit是个空的宏)
moc_xxx.cpp文件中看到
发送者的信号函数里有 QMetaObject::activate
// SIGNAL 0void myclass::operate(const QString & _t1){ void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) }; QMetaObject::activate(this, &staticMetaObject, 0, _a);}
其定义在
qobjectdefs.h文件里
// internal index-based signal activation static void activate(QObject *sender, int signal_index, void **argv); static void activate(QObject *sender, const QMetaObject *, int local_signal_index, void **argv); static void activate(QObject *sender, int signal_offset, int local_signal_index, void **argv);
槽moc预处理
槽函数呢 在moc_xxx文件里 找到槽函数相关的函数 后面会讲如何和信号绑定 的如何触发 的。
void Worker::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a){ if (_c == QMetaObject::InvokeMetaMethod) { auto *_t = static_cast<Worker *>(_o); Q_UNUSED(_t) switch (_id) { case 0: _t->resultReady((*reinterpret_cast< const QString(*)>(_a[1]))); break; case 1: _t->doWork((*reinterpret_cast< const QString(*)>(_a[1]))); break; default: ; } } else if (_c == QMetaObject::IndexOfMethod) { int *result = reinterpret_cast<int *>(_a[0]); { using _t = void (Worker::*)(const QString & ); if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&Worker::resultReady)) { *result = 0; return; } } }}
发送者和接收者都继承QObject类,
重写了QObject::qt_metacall 调用了qt_static_metacall()
qt_static_metacall会将槽函数调用
int Worker::qt_metacall(QMetaObject::Call _c, int _id, void **_a){ _id = QObject::qt_metacall(_c, _id, _a); if (_id < 0) return _id; if (_c == QMetaObject::InvokeMetaMethod) { if (_id < 2) qt_static_metacall(this, _c, _id, _a); _id -= 2; } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) { if (_id < 2) *reinterpret_cast<int*>(_a[0]) = -1; _id -= 2; } return _id;}
QMetaObject 内容- -
QObject里有 静态成员变量
static const QMetaObject staticQtMetaObject;
moc文件里看出来预处理对应的worker 的QMetaObject 的成员变量是
QT_INIT_METAOBJECT const QMetaObject Worker::**staticMetaObject** = { { &QObject::staticMetaObject, qt_meta_stringdata_Worker.data, qt_meta_data_Worker, qt_static_metacall, nullptr, nullptr} };
其中成员 qt_meta_stringdata_Worker.data,为
static const qt_meta_stringdata_Worker_t qt_meta_stringdata_Worker = { {QT_MOC_LITERAL(0, 0, 6), // "Worker"QT_MOC_LITERAL(1, 7, 11), // "resultReady"QT_MOC_LITERAL(2, 19, 0), // ""QT_MOC_LITERAL(3, 20, 7), // "result1"QT_MOC_LITERAL(4, 28, 6), // "doWork"QT_MOC_LITERAL(5, 35, 9) // "parameter" }, "Worker\0resultReady\0\0result1\0doWork\0" "parameter"};
可以看出是Worker类的名称、信号名称、槽函数名称及参数等字符串。
connect有什么呢?
点击进去
connect函数是QObject类的成员函数 返回值是QMetaObject::Connection
static QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *member, Qt::ConnectionType = Qt::AutoConnection); static QMetaObject::Connection connect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method, Qt::ConnectionType type = Qt::AutoConnection); inline QMetaObject::Connection connect(const QObject *sender, const char *signal, const char *member, Qt::ConnectionType type = Qt::AutoConnection) const;
所以QObject 里实现connect函数
QMetaObject 实现Connection 类是connect的返回类型
class Q_CORE_EXPORT QMetaObject::Connection { void *d_ptr; //QObjectPrivate::Connection* explicit Connection(void *data) : d_ptr(data) { } friend class QObject; friend class QObjectPrivate; friend struct QMetaObject; bool isConnected_helper() const;public: ~Connection(); Connection(); Connection(const Connection &other); Connection &operator=(const Connection &other);#ifdef Q_QDOC operator bool() const;#else typedef void *Connection::*RestrictedBool; operator RestrictedBool() const { return d_ptr && isConnected_helper() ? &Connection::d_ptr : nullptr; }#endif Connection(Connection &&o) Q_DECL_NOTHROW : d_ptr(o.d_ptr) { o.d_ptr = nullptr; } Connection &operator=(Connection &&other) Q_DECL_NOTHROW { qSwap(d_ptr, other.d_ptr); return *this; }};
三 、接下来把这些函数连接起来
思路:
-
定义两个类: sender(被观察者)和recver(观察者) 均继承于QObject
-
recver(观察者),定义对某个处理函数比如按钮触发后处理函数。槽函数
-
sender(被观察者),定义一个数据结构,保持观察者对哪个事件id感兴趣,使用map建立对应关系。并调用(运行时)槽函数。
-
connect函数在QObject中可由 sender(被观察者)或recver(观察者) 调用
定义一个QObject类,有以下内容:
- qt_metacall调用观察者的槽函数用 虚函数重写形式实现 动态绑定实现 观察者模式的关键 在moc分析时将槽函数信号的信息写入 qt_static_metacall函数里 ,此函数在qt_metacall函数动态绑定时调用
- 成员变量 QMetaObject staticQtMetaObject;
QMetaObject 类实现moc, 有以下内容
关键数据信号和槽的信息都被分别存在此staticQtMetaObject变量里 ,
相关的宏定义 - 数据结构 存放观察者和被观察者信号绑定。std::multimap
- connect函数 将观察者和被观察者绑定 利用了上一步数据结构std::multimap进行存储。
对应代码struct Connection{ QObject * receiver; int methodID;};class QObject{public:... static void connect(Object*, const char*, Object*, const char*);...private: std::multimap<int, Connection> connections; protected:static const QMetaObject staticQtMetaObject;**int qt_metacall(QMetaObject::Call _c, int _id, void **_a);** //观察者会对此函数进行重写 }
QMetaObject类内容:
1、 QMetaObject 里有active(发送者的信号函数里有 QMetaObject::activate,active是被信号函数emit时调用的)
2、 信号和槽函数对应的名称 const char * sig_names;
class QObject;struct QMetaObject{ const char * sig_names; const char * slts_names; //信号、槽名称 简化处理 具体内容是 struct d 的 const QByteArrayData *stringdata; (qt_meta_stringdata_XXX.data) static void active(QObject * sender, int idx);};
因为sender(被观察者)需要实现2件事
1 、添加观察者和感兴趣的事件id到容器map 中
程序运行时,connect借助两个字符串,即可将信号与槽的关联建立起来,那么,它是如果做到的呢?
void QObject::connect(Object* sender, const char* sig, Object* 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};//将receiver sender和对应槽函数id 关联 **sender->connections.insert(std::pair<int, Connection>(sig_idx, c));** }}
2 、通知事件函数执行逻辑:首先遍历map容器,有没有感兴趣的id
若有,则代表一系列观察者,对这个事件感兴趣,再次遍历观察者列表,让其执行相应的槽函数。
Active实现如下:
void QMetaObject::active(Object* 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_metacall(-,c.methodID,-); //索引---槽函数--调用接收槽函数 }}
接下来到观察者这里 调用槽函数
直接调用槽函数我们都知道了,就一个普通函数
可现在通过索引调用了,那么我们必须定义一个接口函数
槽的索引和槽的调用关联起来
前面active通过receiver->qt_metacall调用,所以需要用到 C++的多态,动态多态 重写 虚函数QObject::qt_metacall,
int Worker::qt_metacall(QMetaObject::Call _c, int _id, void **_a)//重写了虚函数 QObject::qt_metacall 实现了运行时调用{ _id = QObject::qt_metacall(_c, _id, _a); if (_id < 0) return _id; if (_c == QMetaObject::InvokeMetaMethod) { if (_id < 2) qt_static_metacall(this, _c, _id, _a); _id -= 2; } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) { if (_id < 2) *reinterpret_cast<int*>(_a[0]) = -1; _id -= 2; } return _id;}
然后查找槽函数的索引找到槽函数,调用槽函数
void Worker::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a){ if (_c == QMetaObject::InvokeMetaMethod) { auto *_t = static_cast<Worker *>(_o); Q_UNUSED(_t) switch (_id) { case 0: _t->resultReady((*reinterpret_cast< const QString(*)>(_a[1]))); break; case 1: _t->doWork((*reinterpret_cast< const QString(*)>(_a[1]))); break;// 调用具体操作的槽函数 default: ; } } else if (_c == QMetaObject::IndexOfMethod) { int *result = reinterpret_cast<int *>(_a[0]); { using _t = void (Worker::*)(const QString & ); if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&Worker::resultReady)) { *result = 0; return; } } }}