如果我们想要监控一个QObject对象发射的所有信号,同时又不追求手段的通用性的话,可以给目标对象的每个信号写一个槽函数,然后手动connect。这听起来就麻烦,有没有不那么麻烦的通用方法呢,自然是有的。如果能对Qt的信号槽原理进行一点深入的探索,我们就能以很简单的方法达到我们的目的。
以下代码均基于Qt5.12.7。
信号槽的连接保存在sender的metaObject中,其数据结构为QObjectPrivate::Connection:
struct 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(nullptr), 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 { Q_ASSERT(!isSlotObject); return method_offset + method_relative; } void ref() { ref_.ref(); } void deref() { if (!ref_.deref()) { Q_ASSERT(!receiver); delete this; } } };这些字段中,我们关心的有:sender,信号发送者;receiver,信号接收者;method_offset和method_relative,利用这两个字段我们可以在接收者的metaObject中索引到槽函数。
每一个成功的connect调用都会构造一个QObjectPrivate::Connection对象,保存在sender的metaObject。当发射信号时,遍历这个信号对应的Connection列表(为什么是列表呢?因为一个信号可以连接多个槽或信号),取出每个Connection的sender、method_offset和method_relative,调用sender的qt_metacall函数,传入计算得到的槽函数索引和信号参数,再由sender的qt_metacall进一步调用到槽函数,完成信号处理的流程。
请看下面这个堆栈,是在一个槽函数被调用时产生的:
1 MainWindow::on_inputTextEdit_textChanged mainwindow.cpp 28 0x7ff67cb12c42 2 MainWindow::qt_static_metacall moc_mainwindow.cpp 77 0x7ff67cb180a3 3 MainWindow::qt_metacall moc_mainwindow.cpp 110 0x7ff67cb18006 4 QMetaObject::metacall qmetaobject.cpp 317 0x7ff67db596a8 5 QMetaObject::activate qobject.cpp 3825 0x7ff67db3b620 6 QMetaObject::activate qobject.cpp 3658 0x7ff67db3abe8 7 QTextEdit::textChanged moc_qtextedit.cpp 544 0x7ff67cf919f0(这个信号槽是利用QMetaObject::connectSlotsByName自动连接的,如果手动connect的话,一般是没有MainWindow::qt_metacall的调用的)
可以看一下堆栈中大致的代码:
// 7 QTextEdit::textChanged moc_qtextedit.cpp 544 // SIGNAL 0 void QTextEdit::textChanged() { // 我要发射信号了!信号本地索引为0。 QMetaObject::activate(this, &staticMetaObject, 0, nullptr); } // 6 QMetaObject::activate qobject.cpp 3658 void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index, void **argv) { // 获得信号索引偏移量,利用偏移量和本地索引,计算出信号在metaObject中的索引 activate(sender, QMetaObjectPrivate::signalOffset(m), local_signal_index, argv); } // 5 QMetaObject::activate qobject.cpp 3825 void QMetaObject::activate(QObject *sender, int signalOffset, int local_signal_index, void **argv) { // 计算信号索引 int signal_index = signalOffset + local_signal_index; ... const QObjectPrivate::ConnectionList *list; ... // 得到Connection列表 list = &connectionLists->at(signal_index); ... // 得到Connection QObjectPrivate::Connection *c = list->first; ... // 得到receiver QObject * const receiver = c->receiver; ... // 得到槽函数索引 const int method = c->method_relative + c->method_offset; ... // 调用! metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv); ... } // 4 QMetaObject::metacall qmetaobject.cpp 317 int QMetaObject::metacall(QObject *object, Call cl, int idx, void **argv) { if (object->d_ptr->metaObject) return object->d_ptr->metaObject->metaCall(object, cl, idx, argv); else // 调用qt_metacall return object->qt_metacall(cl, idx, argv); } // 3 MainWindow::qt_metacall moc_mainwindow.cpp 110 int MainWindow::qt_metacall(QMetaObject::Call _c, int _id, void **_a) { _id = QMainWindow::qt_metacall(_c, _id, _a); // _id小于0说明这次调用由父类处理 if (_id < 0) return _id; // _id大于等于0,已经被修改为本地索引 ... // 调用qt_static_metacall qt_static_metacall(this, _c, _id, _a); ... } // 2 MainWindow::qt_static_metacall moc_mainwindow.cpp 77 void MainWindow::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a) { if (_c == QMetaObject::InvokeMetaMethod) { auto *_t = static_cast<MainWindow *>(_o); Q_UNUSED(_t) switch (_id) { // 调用槽函数 case 0: _t->on_inputTextEdit_textChanged(); break; default: ; } } Q_UNUSED(_a); }以上是一次槽函数触发的大致流程,我们的信号监控机制的核心就在qt_metacall上。qt_metacall正常情况下是在类中添加Q_OBJECT宏,然后由moc生成的,这次我们偏要自己写。以下是SignalSpy类代码,不能添加Q_OBJECT宏,否则会重定义:
class SignalSpy final : public QObject { enum { VirtualSlotBase = 10000 }; public: using MessageCallback = std::function<void(const QString &)>; public: SignalSpy(QObject *o) : QObject(o), m_obj(o), m_cb(defaultMessageCallback) { if (!m_sender || !m_sender->metaObject()) { return; } for (int i = 0; i < m_obj->metaObject()->methodCount(); ++i) { QMetaMethod m = m_obj->metaObject()->method(i); // 遍历所有信号 if (m.methodType() == QMetaMethod::Signal) { //qDebug() << i << QString::fromLatin1(m.methodSignature()); // 将信号连接到虚拟槽函数索引 QMetaObject::connect(m_obj, i, this, i + VirtualSlotBase); } } } int qt_metacall(QMetaObject::Call c, int id, void **a) override { if (id >= VirtualSlotBase) { // 计算信号id int signalId = id - VirtualSlotBase; // 得到信号 QMetaMethod m = m_obj->metaObject()->method(signalId); if (m.methodType() == QMetaMethod::Signal) { QString buf; QDebug dbg(&buf); int count = m.parameterCount(); // 打印sender和信号签名 dbg << m_obj << "emit:" << m.methodSignature().data(); for (int i = 0; i < count; i++) { int paramType = m.parameterType(i); QVariant v(paramType, a[i + 1]); // 打印参数 dbg << v; } m_cb(buf); } return -1; } else { return QObject::qt_metacall(c, id, a); } } void setMessageCallback(const MessageCallback &cb) { if (cb) { m_cb = cb; } else { m_cb = defaultMessageCallback; } } private: static void defaultMessageCallback(const QString &s) { qDebug() << s.toStdString().c_str(); } private: QObject *m_obj; MessageCallback m_cb; };在SignalSpy构造函数中,我们利用metaObject枚举出sender的所有信号及其索引,然后利用QMetaObject::Connection QMetaObject::connect(const QObject *sender, int signal_index, const QObject *receiver, int method_index, int type, int *types)这个(内部)接口,将其连接到一个虚拟的槽函数上,信号索引和槽函数索引对应关系为signalIndex + VirtualSlotBase = slotIndex,VirtualSlotBase取了一个很大的值,以防和真实的槽函数索引产生冲突。
槽函数索引是虚拟的并不要紧,因为我们在qt_metacall中不会真去调用槽函数。当我们在qt_metacall中收到一个函数调用请求后,如果发现请求的索引大于等于VirtualSlotBase,那我们就可以确定这是我们在构造函数中构造的连接被触发了,并且信号索引signalId等于id - VirtualSlotBase,这时我们就可以利用sender的metaObject和这个信号索引得到具体是哪个信号了。同时我们还可以利用Qt提供的类型信息和传入的数据指针构造出QVariant,然后利用QDebug类得到QVariant的字符串表示。
下面是使用SignalSpy监控一个QSpinBox对象时的输出:
QSpinBox(0x1d48bb5c100, name = "spinBox") emit: valueChanged(QString) QVariant(QString, "1") QSpinBox(0x1d48bb5c100, name = "spinBox") emit: valueChanged(int) QVariant(int, 1) QSpinBox(0x1d48bb5c100, name = "spinBox") emit: valueChanged(QString) QVariant(QString, "2") QSpinBox(0x1d48bb5c100, name = "spinBox") emit: valueChanged(int) QVariant(int, 2) QSpinBox(0x1d48bb5c100, name = "spinBox") emit: valueChanged(QString) QVariant(QString, "3") QSpinBox(0x1d48bb5c100, name = "spinBox") emit: valueChanged(int) QVariant(int, 3) QSpinBox(0x1d48bb5c100, name = "spinBox") emit: editingFinished()