Qt的Signal和Slot機制(二)

混混諤諤的十年發表於2010-08-04

!-->![endif]-->!-->![endif]-->!-->

!-->![endif]-->!-->![endif]-->!-->

!-->![endif]-->!-->![endif]-->!-->

第二節 Signal和Slot的粘合劑

 

如果要連線一個SignalSlot我們會用connect函式,下面我們就看一下connect是如何把SignalSlot粘合在一起的。

以下是connect函式的宣告,

bool connect(const QObject *sender, const char *signal,

                      const QObject *receiver, const char *method,

                      Qt::ConnectionType type)

首先我們先看一下connect函式用到的SIGNAL()和SLOT()這兩個巨集,其實他們就是分別生成Signal函式字串和Slot函式字串。字串裡包含了函式型別(用SIGNAL()函式型別就是2,用SLOT()函式型別就是1),函式名,函式引數列表。比如:

SIGNALSignalA2int))生成了”2SignalA2(int)”的字串,2表示是Signal函式,SignalA2表示函式名,(int)表示函式列表。

SLOTSlotA2char*int))生成了”1SlotA2(char*,int)”,1表示是Slot函式,SlotA2是函式名,(char*int)表示引數列表。

看以下的例子

QTestA a;

QTestB b;

connect(&a,SIGNAL(SignalA2(int)),&b,SLOT(SlotB2(int)));

其實就是connect(&a, “2SignalA2(int)”,&b,”1SlotB2(int)”);

 

看到這裡,有看官會問,為啥要有在函式名前要放個標識阿。我們平常作的時候,const char *signal填入的總是Signal函式,const char *method填入的總是Slot函式。其實Qt還是很靈活的,const char *method引數裡你是可以填入一個Signal函式的,換句話說就是,你可以用一個Signal函式去觸發另一個Signal函式,Signal函式也可以作為被觸發函式。但是觸發函式只能是Signal函式。

接下來我們看一下,connect函式是怎樣一步步地把Signal函式和Slot(還有被觸發的Signal函式)連繫在一起的。

 

第一步,得到引數*signal字串裡Signal函式的id號(也就是觸發函式)

以下是相關程式碼:

QByteArray tmp_signal_name;

    if(!check_signal_macro(sender, signal, "connect", "bind"))

        returnfalse;

    constQMetaObject *smeta= sender->metaObject();

    constchar *signal_arg= signal;

    ++signal;//skip code

    intsignal_index = smeta->indexOfSignal(signal);

    if(signal_index < 0) {

        //check for normalized signatures

        tmp_signal_name= QMetaObject::normalizedSignature(signal - 1);

        signal= tmp_signal_name.constData()+ 1;

 

        signal_index= smeta->indexOfSignal(signal);

        if(signal_index < 0) {

            err_method_notfound(sender, signal_arg,"connect");

            err_info_about_objects("connect", sender,receiver);

            returnfalse;

        }

    }

 

 

 

首先呼叫check_signal_macro檢查*signal所指向的字串是不是Signal函式,怎麼判斷呢?就是看第一個字元(就是函式型別)是不是“2“,如果不是的話,則檢查失敗。

 

我們可以看一下check_signal_macro函式的實現部分

 

int sigcode= extract_code(signal);

    if(sigcode != QSIGNAL_CODE){

        if(sigcode == QSLOT_CODE)

            qWarning("Object::%s: Attempt to %s non-signal %s::%s",

                     func, op, sender->metaObject()->className(), signal+1);

        else

            qWarning("Object::%s: Use the SIGNAL macro to %s %s::%s",

                     func, op, sender->metaObject()->className(), signal);

        returnfalse;

    }

    return true;

 

通過extract_code函式得到此函式型別。

以下是extract_code的程式碼,

return (((int)(*member) - '0')& 0x3);

就是取第一個字元與0相減然後與3“且”。為什麼與“3”且呢?因為Qt中相關的函式型別就三種

分別是,

#define QMETHOD_CODE  0  // member type codes

#define QSLOT_CODE    1  //Slot型別

#define QSIGNAL_CODE  2 //Signal型別

 

得到型別後,比較如果不是QSIGNAL_CODESignal型別),則返回false,反之返回true

 

經過check_signal_macro檢查後,如果是Signal函式,則取出傳送方物件的QMetaObject值。

const QMetaObject *smeta = sender->metaObject();

smeta就是傳送方的QMetaObject物件指標,在上一章裡我們知道一個QObject類的SlotSignal函式相關資訊都放在這個QMetaObject物件內。

然後我們會看到,觸發函式的id號是這樣被取得的,

const char *signal_arg = signal;

    ++signal; //skip code

int signal_index= smeta->indexOfSignal(signal);

注意++signal;,這主要是要查詢函式的id時,是用到函式名和引數列表,但signal的字串的第一個字元是函式型別,所以要忽略掉。

忽略掉函式型別後,呼叫indexOfSignal獲得函式的id號,但我們會發現這個id號不等於我們在QMetaObject裡儲存的id號,而是被加了偏移量。我們來看一下indexOfSignal函式的程式碼,看看為什麼要加偏移量以及偏移量是怎麼產生的?

int i =-1;

    constQMetaObject *m= this;

    while(m && i< 0) {

        for(i = priv(m->d.data)->methodCount-1;i >= 0; --i)

            if((m->d.data[priv(m->d.data)->methodData+ 5*i + 4] & MethodTypeMask)== MethodSignal

              && strcmp(signal, m->d.stringdata

                          + m->d.data[priv(m->d.data)->methodData+ 5*i]) == 0) {

                i += m->methodOffset();

                break;

            }

        m= m->d.superdata;

    }

 

在查詢索引號程式碼裡,首先先查詢在自己的QMetaObject裡有沒有此函式。

priv(m->d.data)->methodData表示Signal函式和Slot函式資訊的起始位置。

5*i是因為5個陣列元素為一條Signal函式或Slot函式資訊

priv(m->d.data)->methodData + 5*i+ 4就是表示函式型別的元素位置

m->d.data[priv(m->d.data)->methodData + 5*i+ 4]表示的就是函式型別

同理m->d.data[priv(m->d.data)->methodData+ 5*i]的是函式字串的在stringdata中的位置

 

 if ((m->d.data[priv(m->d.data)->methodData+ 5*i + 4] & MethodTypeMask)== MethodSignal

              && strcmp(signal, m->d.stringdata

                          + m->d.data[priv(m->d.data)->methodData+ 5*i]) == 0)

所以此語句的意思就是如果函式名一樣,且型別是Signal,那麼i就是索引值。如果找不到,去父類中查詢(因為我們可以使用父類的Signal來觸發)

而它的id號就是i+ m->methodOffset()m->methodOffset()就是偏移量。

我們可以來看一下這個offset是如何確定的

 

int offset= 0;

    constQMetaObject *m= d.superdata;

    while(m) {

        offset+= priv(m->d.data)->methodCount;

        m= m->d.superdata;

    }

return offset;

我們發現這個偏移量就是自己的Method數量(Signal+Slot,再加上所有的父類的Method數量。這樣可以形成一個唯一的id號,因為在父類中也會有和自己一樣的索引號(比如只要父類中也有SignalSlot,它必然也有個SignalSlot的索引值為0),為了不衝突所以要加一個偏移量。

 

第二步,得到引數*method字串裡被觸發函式的id號(SignalSlot都有可能)

 

QByteArray tmp_method_name;

    intmembcode = extract_code(method);

 

    if(!check_method_code(membcode,receiver, method,"connect"))

        returnfalse;

    constchar *method_arg= method;

    ++method;// skip code

 

    constQMetaObject *rmeta= receiver->metaObject();

    intmethod_index = -1;

    switch(membcode) {

    caseQSLOT_CODE:

        method_index= rmeta->indexOfSlot(method);

        break;

    caseQSIGNAL_CODE:

        method_index= rmeta->indexOfSignal(method);

        break;

    }

    if(method_index < 0) {

        //check for normalized methods

        tmp_method_name= QMetaObject::normalizedSignature(method);

        method= tmp_method_name.constData();

        switch(membcode) {

        case QSLOT_CODE:

            method_index= rmeta->indexOfSlot(method);

            break;

        caseQSIGNAL_CODE:

            method_index= rmeta->indexOfSignal(method);

            break;

        }

    }

 

期過程和第一步非常相像。只是在這一步

switch (membcode) {

    caseQSLOT_CODE:

        method_index= rmeta->indexOfSlot(method);

        break;

    caseQSIGNAL_CODE:

        method_index= rmeta->indexOfSignal(method);

        break;

    }

被觸發的函式可以是Signal,也可以是Slot

 

第三步,校驗觸發函式和被觸發函式的引數列表是否一致

if (!QMetaObject::checkConnectArgs(signal,method)) {

        qWarning("QObject::connect: Incompatible sender/receiverarguments"

                 "/n       %s::%s --> %s::%s",

                 sender->metaObject()->className(), signal,

                 receiver->metaObject()->className(), method);

        returnfalse;

}

 

第四步,校驗引數型別是否合法

if ((type == Qt::QueuedConnection || type== Qt::BlockingQueuedConnection)

            && !(types = queuedConnectionTypes(smeta->method(signal_index).parameterTypes())))

        returnfalse;

 

當是非同步觸發時(我們的connect模式為Qt::QueuedConnectionQt::BlockingQueuedConnection或者自動模式,但是*send*receive不屬於一個執行緒),我們需要校驗引數型別,如果不是Qt所認同的型別,就不能生成物件拷貝,來給被觸發函式使用。同理如果是指標的話,則不需要校驗,因為具體物件開發者自己維護。所以這就是為什麼有時候我們使用自定義的類或結構物件(不是指標),作為SignalSlot的引數,會被提示“QObject::connect: Cannot queue arguments of type'%s'/n"

                     "(Make sure '%s' is registered usingqRegisterMetaType().

 

解決的方法是呼叫qRegisterMetaType來註冊自定義類或結構,使之成為Qt認同的型別。

 

第五步,記錄SignalSlot資訊

 

QMetaObject::connect(sender, signal_index,receiver, method_index,type, types);

 

QObject *s =const_cast<QObject*>(sender);

    QObject*r = const_cast<QObject *>(receiver);

 

    QOrderedMutexLockerlocker(&s->d_func()->threadData->mutex,

                               &r->d_func()->threadData->mutex);

 

    QObjectPrivate::Connectionc;

   c.receiver = r;

   c.method = method_index;

   c.connectionType = type;

c.argumentTypes= types;

 

    s->d_func()->addConnection(signal_index, &c);

r->d_func()->refSender(s, signal_index);

 

將被觸發函式資訊“QObjectPrivate::Connectionid號,被觸發物件指標,連線型別(BlockingQueuedConnectionQueuedConnectiondirect)),通過addConnection儲存到觸發物件(sender)的ConnectionList中,以後Signal函式就通過它直接呼叫被觸發函式,或者壓入到訊息佇列中。

 

我們看一下addConnection程式碼,

if (!connectionLists)

        connectionLists= new QObjectConnectionListVector();

    if(signal >= connectionLists->count())

        connectionLists->resize(signal +1);//保證陣列比他的id號大,否則它無法插入

 

    ConnectionList&connectionList = (*connectionLists)[signal];

connectionList.append(*c);

 

先是看有沒有connectionListsconnectionLists是一個元素為QList<Connection >vector

換句話說,它裡面的每一個元素“QList<Connection >”就是一個Signal函式要相應得被觸發函式資訊集合,比方說(*connectionLists)[0],所有id號為0Signal,它所對應的被觸發函式資訊“QObjectPrivate::Connection”(是Slot,也有可能是Signal)都放在這個list裡。因為一個Signal可以connect給不同的Slot函式或者Signal函式。所以這也是為什麼Signal函式id號要唯一的原因。否則會衝突(父類和子類有同樣的Signal id)。

 

另外,在讀程式碼中,始終無法明白為什要“儲存觸發函式資訊“到被觸發物件中,即以下這段程式碼

r->d_func()->refSender(s, signal_index);

還有就是tmp_method_name = QMetaObject::normalizedSignature(method);

是什麼意思還未了解。

 

相關文章