元程式設計(Meta-programming),也叫超程式設計,根據維基百科上面的介紹大概是指那種以某種程式設計語言、特性為資料、對象的程式設計方法。本身比較抽象,具體到Qt程式設計,就是利用Moc出來的各種相關元資訊類進行涉及到類型、接口等相關操作。其實Qt的信号與槽機制就是Qt上最核心的元程式設計,是以用過Qt的人都可以說做過Qt元程式設計。使用Qt元程式設計可以實作很多有用而意想不到的功能,筆者将會分幾次和大家分享這方面有趣的執行個體,這篇博文先從信号監聽開始。
什麼是信号監聽?有何作用?
這裡的信号當然是指Qt中的信号。所謂信号監聽就是接收某個QObject發射的所有信号。
常見的用途有:
- 調試開發時自動log某個類的所有信号以及參數。該log機制是通用的、類型透明的,可以針對任何
,不需要事先知道該對象有什麼信号、信号有什麼參數;QObject
- 自動将
的信号以及參數轉換成另外一種程序間通信資料。用過QtRO的人都知道,Source端的信号都會自動地傳到遠端Replica端,其底層原理就是将所有信号和參數序列化,然後在Replica端反序列化。QObject
需求分析
我們具體需求是:
- 能接受任意
,能自動枚舉所有信号;QObject
- 能通過
連接配接這些信号;connect
- 信号發射時,能自動運作我們的處理函數,并拿到裡面所有參數。
QtTest子產品中的QSignalSpy比較接近我們的需求,但是該類隻能監聽指定的某個信号。是以我們使用Qt的元程式設計自己實作一個
SignalSpy
。
筆者參考了Qt Graphics組的元老Eskil早年的一篇文章:Dynamic Signals and Slots。
枚舉、連接配接信号
我們使用
QMetaObject
來枚舉所有信号。每個
QObject
都有
metaObject
方法,傳回該對象的
QMetaObject
成員對象,包含幾乎所有Qt的元資訊:
void MySignalSpy::setupDynamicConnections(QObject* obj){
m_target = obj;
auto mo = obj->metaObject();
auto offset = mo->methodOffset();
// m_dynamicMappting 是一個<int,int>的Map
// 用于存放信号的index和我們的一個辨別動态槽函數的index的
// 映射關系
m_dynamicMapping.clear();
for(auto i = offset; i != mo->methodCount(); ++i){
auto m = mo->method(i);
// 我們隻關心信号
if(m.type() != QMetaMethod::Signal)
continue;
m_dynamicMapping[i - offset] = i;
QMetaObject::connect(obj, i, this, i, Qt::UniqueConnection);
}
}
注意,因為我們沒辦法事先寫好槽函數(為什麼?因為我們并不知道該準備幾個參數、每個參數都什麼類型),是以沒法用大家通常用的
connect
函數将枚舉到的信号接到我們的槽函數上。這裡使用了一個Qt官方并未寫在Qt文檔中的接口:
QMetaObject::connect(QObject* sender, int signalIndex, QObject* receiver, int slotIndex, Qt::ConnectionType type);
這裡的
signalIndex
是我們枚舉得到的,但
slotIndex
卻是我們“杜撰”的,因為此時我們并沒有槽函數。該index在之後信号發射後的處理代碼中我們用來辨識到底是哪個信号發的,是以我們需要用一個
QMap<int, int>
将這種信号與“槽”的對應關系存起來。
重寫 qt_metacall
函數
qt_metacall
動态連接配接、處理信号的精髓在于重新
QObject
的
qt_metacall
函數,該函數同樣是未在Qt官方文檔中介紹的。(這裡順便提一下,Qt元程式設計相關原理、函數很多都是Qt官方文檔上沒有的,需要大家檢視Qt源碼以及Moc生成的中間檔案去了解)。
要重寫
qt_metacall
函數,首先得保證我們的
QObject
子類中沒有
Q_OBJECT
宏:
// mysignalspy.h
class MySignalSpy : public QObject
{
public:
void setupDynamicConnections(QObject* obj);
int qt_metacall(QMetaObject::Call c, int id, void **arguments) override;
private:
QObject* m_target;
};
// mysignalspy.cpp
int MySignalSpy::qt_metacall(QMetaObject::Call c, int id, void **arguments){
// 這裡的參數 id 就是槽函數的Index
// 首先是調用父類的qt_metacall,
// 如果父類處理完畢,則會傳回-1,否則會将id減去父類的methodOffset
// 之後再傳回
id = QObject::qt_metacall(c, id, arguments);
if (id < 0 || c != QMetaObject::InvokeMetaMethod)
return id;
// 如果父類沒處理,那正是我們要處理的動态槽函數
auto signalId = m_dynamicSlotMapping[id];
// 獲得發送的信号元對象
auto signalMethod = m_target->metaObject()->method(signalId);
for (int i = 0; i != signalMethod.parameterCount(); ++i){
auto type = signalMethod.parameterType(i);
auto arg = arguments[i + 1];
// 這裡可以對這些參數做任何想要的處理
qDebug()<<"param"<<i<<"type:"<<type<<"value:"<<arg;
}
}
使用
使用
MySignalSpy
類很簡單:
auto btn = new QPushButton("Click!");
auto spy = new MySignalSpy;
spy->setupDynamicConnections(btn);
當點選按鈕時,大家就可以看到相關調試輸出了。