Qt的Signal和Slot机制(二)

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

是什么意思还未了解。

 

没有更多推荐了,返回首页