天天看點

qt connect函數_Qt元程式設計之動态監聽所有信号

元程式設計(Meta-programming),也叫超程式設計,根據維基百科上面的介紹大概是指那種以某種程式設計語言、特性為資料、對象的程式設計方法。本身比較抽象,具體到Qt程式設計,就是利用Moc出來的各種相關元資訊類進行涉及到類型、接口等相關操作。其實Qt的信号與槽機制就是Qt上最核心的元程式設計,是以用過Qt的人都可以說做過Qt元程式設計。使用Qt元程式設計可以實作很多有用而意想不到的功能,筆者将會分幾次和大家分享這方面有趣的執行個體,這篇博文先從信号監聽開始。

什麼是信号監聽?有何作用?

這裡的信号當然是指Qt中的信号。所謂信号監聽就是接收某個QObject發射的所有信号。

常見的用途有:

  1. 調試開發時自動log某個類的所有信号以及參數。該log機制是通用的、類型透明的,可以針對任何

    QObject

    ,不需要事先知道該對象有什麼信号、信号有什麼參數;
  2. 自動将

    QObject

    的信号以及參數轉換成另外一種程序間通信資料。用過QtRO的人都知道,Source端的信号都會自動地傳到遠端Replica端,其底層原理就是将所有信号和參數序列化,然後在Replica端反序列化。

需求分析

我們具體需求是:

  1. 能接受任意

    QObject

    ,能自動枚舉所有信号;
  2. 能通過

    connect

    連接配接這些信号;
  3. 信号發射時,能自動運作我們的處理函數,并拿到裡面所有參數。

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

函數

動态連接配接、處理信号的精髓在于重新

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);
           

當點選按鈕時,大家就可以看到相關調試輸出了。

繼續閱讀