第二节 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))生成了”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_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);
是什么意思还未了解。