如果我们想要监控一个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_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
= 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()