Qt的Signal和Slot機制(二)
!-->![endif]-->!-->![endif]-->!-->
!-->![endif]-->!-->![endif]-->!-->
!-->![endif]-->!-->![endif]-->!-->
第二節 Signal和Slot的粘合劑
如果要連線一個Signal和Slot我們會用connect函式,下面我們就看一下connect是如何把Signal和Slot粘合在一起的。
以下是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),函式名,函式引數列表。比如:
SIGNAL(SignalA2(int))生成了”2SignalA2(int)”的字串,2表示是Signal函式,SignalA2表示函式名,(int)表示函式列表。
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
接下來我們看一下,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_CODE0 // member type codes
#define QSLOT_CODE1//Slot型別
#define QSIGNAL_CODE2 //Signal型別
得到型別後,比較如果不是QSIGNAL_CODE(Signal型別),則返回false,反之返回true
經過check_signal_macro檢查後,如果是Signal函式,則取出傳送方物件的QMetaObject值。
const QMetaObject *smeta = sender->metaObject();
smeta就是傳送方的QMetaObject物件指標,在上一章裡我們知道一個QObject類的Slot和Signal函式相關資訊都放在這個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號,因為在父類中也會有和自己一樣的索引號(比如只要父類中也有Signal或Slot,它必然也有個Signal或Slot的索引值為0),為了不衝突所以要加一個偏移量。
第二步,得到引數*method字串裡被觸發函式的id號(Signal和Slot都有可能)
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::QueuedConnection,Qt::BlockingQueuedConnection或者自動模式,但是*send和*receive不屬於一個執行緒),我們需要校驗引數型別,如果不是Qt所認同的型別,就不能生成物件拷貝,來給被觸發函式使用。同理如果是指標的話,則不需要校驗,因為具體物件開發者自己維護。所以這就是為什麼有時候我們使用自定義的類或結構物件(不是指標),作為Signal和Slot的引數,會被提示“QObject::connect: Cannot queue arguments of type'%s'/n"
"(Make sure '%s' is registered usingqRegisterMetaType().”
解決的方法是呼叫qRegisterMetaType來註冊自定義類或結構,使之成為Qt認同的型別。
第五步,記錄Signal和Slot資訊
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::Connection”(id號,被觸發物件指標,連線型別(BlockingQueuedConnection,QueuedConnection,direct)),通過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);
先是看有沒有connectionLists,connectionLists是一個元素為QList<Connection >的vector
換句話說,它裡面的每一個元素“QList<Connection >”就是一個Signal函式要相應得被觸發函式資訊集合,比方說(*connectionLists)[0],所有id號為0的Signal,它所對應的被觸發函式資訊“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);
是什麼意思還未了解。