Qt:监控一个QObject对象发射的所有信号

本文详细介绍了Qt信号槽的工作原理,通过分析QObjectPrivate::Connection结构展示了信号如何触发槽函数。借助Qt的元对象系统,创建了一个SignalSpy类,实现在不修改原有代码的情况下,监控任意QObject对象的所有信号,包括信号发送者、信号名称和参数信息。SignalSpy通过重写qt_metacall方法,在接收到信号时记录并打印相关信息,提供了一种通用的信号监控解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

如果我们想要监控一个QObject对象发射的所有信号,同时又不追求手段的通用性的话,可以给目标对象的每个信号写一个槽函数,然后手动connect。这听起来就麻烦,有没有不那么麻烦的通用方法呢,自然是有的。如果能对Qt的信号槽原理进行一点深入的探索,我们就能以很简单的方法达到我们的目的。

以下代码均基于Qt5.12.7。

信号槽的连接保存在sendermetaObject中,其数据结构为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_offsetmethod_relative,利用这两个字段我们可以在接收者的metaObject中索引到槽函数。

每一个成功的connect调用都会构造一个QObjectPrivate::Connection对象,保存在sendermetaObject。当发射信号时,遍历这个信号对应的Connection列表(为什么是列表呢?因为一个信号可以连接多个槽或信号),取出每个Connectionsendermethod_offsetmethod_relative,调用senderqt_metacall函数,传入计算得到的槽函数索引和信号参数,再由senderqt_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_obj || !m_obj->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 = slotIndexVirtualSlotBase取了一个很大的值,以防和真实的槽函数索引产生冲突。

槽函数索引是虚拟的并不要紧,因为我们在qt_metacall中不会真去调用槽函数。当我们在qt_metacall中收到一个函数调用请求后,如果发现请求的索引大于等于VirtualSlotBase,那我们就可以确定这是我们在构造函数中构造的连接被触发了,并且信号索引signalId等于id - VirtualSlotBase,这时我们就可以利用sendermetaObject和这个信号索引得到具体是哪个信号了。同时我们还可以利用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() 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值