QT信号和槽机制分析


QObject这个class是QT对象模型的核心,绝大部分的 QT 类都是从这个类继承而来。这个模型的中心特征就是一个叫做信号和槽(signal and slot)的机制来实现对象间的通讯,你可以把一个信号和另一个槽通过 connect(„) 方法连接起来,并可以使用disconnect(„) 方法来断开这种连接,你还可以通过调用blockSignal(„) 这个方法来临时的阻塞信号
    QObject 把它们自己组织在对象树中。当你创建一个 QObject 并使用其它对象作为父对象时,这个对象会自动添加到父对象的 
children() list 中。父对象拥有这个对象,比如,它将在它的析构函数中自动删除它所有的 child 对象。你可以通过 findChild() 或者 findChildren()函数来查找一个对象。 
    每个对象都有一个对象名称(objectName())和类名称(class name), 他们都可以通过相应的 metaObject 对象来获得。你还可以通过inherits()方法来判断一个对象的类是不是从另一个类继承而来。 
    当对象被删除时,它发出destroyed()信号。你可以捕获这个信号来避免对QObject的无效引用。 
    QObject可以通过event()接收事件并且过滤其它对象的事件。详细情况请参考installEventFilter()和 eventFilter()。     对于每一个实现了信号、槽和属性的对象来说,Q_OBJECT 宏都是必须要加上的。 

    QObject 实现了这么多功能,那么,它是如何做到的呢?让我们通过它的 Source Code 来解开这个秘密吧。 QObject类的实现文件一共有四个. 


1、qobject.h

QObject class 的基本定义,也是我们一般定义一个类的头文件. 


2、qobject.cpp

QObject class 的实现代码基本上都在这个文件. 


3、 qobjectdefs.h

这个文件中最重要的东西就是定义了 QMetaObject class,这个class是为了实现 signal、slot、properties,的核心部分。 

4、qobject_p.h

这个文件中的 code 是辅助实现 QObject class 的,这里面最重要的东西是定义了一个 QObjectPrivate 类来存储 QOjbect 对象的成员数据。 

理解这个 QObjectPrivate class 又是我们理解 QT kernel source code 的基础,这个对象包含了每一个 QT 对象中的数据成员,好了,让我们首先从理解 QObject 的数据存储代码开始我们的 QT Kernel Source Code 之旅。

 

二:元对象系统(Meta-Object System) 



   从本节开始,我们讲解 QT Meta-Object System 的功能,以及实现。 

   在使用 QT 开发的过程中,大量的使用了 signal 和 slot. 比如,响应一个 button 的 click 事件,我们一般都写如下的代码:

class MyWindow : public QWidget { 
  Q_OBJECT   
public: 
  MyWindow(QWidget* parent) : QWidget(parent)   { 
    QPushButton* btnStart = new QPushButton(“start”, this); 
    connect(btnStart, SIGNAL(clicked()), this, SLOT(slotStartClicked()));   }  
private slots: 
  void slotStartClicked(); };  

void MyWindow:: slotStartClicked() 
      { 
        // 省略 
       }




    在这段代码中,我们把 btnStart 这个 button 的clicked() 信号和 MyWindow 的 slotStartClicked() 这个槽相连接,当 btnStart 这个 button 被用户按下(click)的时候,就会发出一个 clicked() 的信号,然后,MyWindow:: slotStartClicked() 这个 slot 函数就会被调用用来响应 button 的 click 事件。  
    这段代码是最为典型的 signal/slot 的应用实例,在实际的工作过程中,signal/slot 还有更为广泛的应用。准确的说,signal/slot 是QT提供的一种在对象间进行通讯的技术,那么,这个技术在QT 中是如何实现的呢?  

    这就是 QT 中的元对象系统(Meta Object System)的作用,为了更好的理解它,让我先来对它的功能做一个回顾,让我们一起来揭开它神秘的面纱。


 
三、Meta-Object System 的基本功能  


Meta Object System 的设计基于以下几个基础设施:

 
1、 QObject 类 
作为每一个需要利用元对象系统的类的基类 ,Q_OBJECT 宏, 定义在每一个类的私有数据段,用来启用元对象功能,比如,动态属性,信号和槽 。元对象编译器moc (the Meta Object Complier), moc 分析C++源文件,如果它发现在一个头文件(header file)中包含Q_OBJECT 宏定义,然后动态的生成另外一个C++源文件,这个新的源文件包含 Q_OBJECT 的实现代码,这个新的 C++ 源文件也会被编译、链接到这个类的二进制代码中去,因为它也是这个类的完整的一部分。通常,这个新的C++ 源文件会在以前的C++ 源文件名前面加上 moc_ 作为新文件的文件名。其具体过程如下图所示: 

 Moc分析时,需要判断源代码中的Q_OBJECT、Q_PROPERTY、Q_ENUMS、Q_CLASSINFO、slots、slot、emit等信息,并生成元对象;但这些关键字大部分C++编译器并不认识(Q_OBJECT除外),他们都被define为空。 


Qt的信号槽机制其实就是按照名称查表 
 

除了提供在对象间进行通讯的机制外,元对象系统还包含以下几种功能: 

QObject::metaObject() 方法 
它获得与一个类相关联的 meta-object 

QMetaObject::className() 方法 ,在运行期间返回一个对象的类名,它不需要本地C++编译器的RTTI(run- time type information)支持 

QObject::inherits() 方法 
它用来判断生成一个对象类是不是从一个特定的类继承出来,当然,这必须是在QObject 类的直接或者间接派生类当中 

QObject::tr() and QObject::trUtf8() 这两个方法为软件的国际化翻译字符串 


QObject::setProperty() and QObject::property() 这两个方法根据属性名动态的设置和获取属性值 
 
除了以上这些功能外,它还使用qobject_cast()方法在QObject类之间提供动态转换,qobject_cast()方法的功能类似于标准 C++的dynamic_cast(),但是qobject_cast()不需要RTTI的支持, 
 
    在一个QObject类或者它的派生类中,我们可以不定义Q_OBJECT宏。如果我们在一个类中没有定义Q_OBJECT宏,那么在这里所提到的相应的功能在这个类中也不能使用,从meta-object的观点来说,一个没有定义Q_OBJECT宏的类与它最接近的那个祖先类是相同的,那就是说,QMetaObject::className()方法所返回的名字并不是这个类的名字,而是与它最接近的那个祖先类的名字。所以,我们强烈建议,任何从QObject继承出来的类都定义Q_OBJECT 宏。 


三:元对象编译器 - Meta Object Compiler (moc) 


元对象编译器用来处理QT的C++扩展,moc 分析C++源文件,如果它发现在一个头文件(header file)中包含Q_OBJECT 宏定义,然后动态的生成另外一个C++源文件,这个新的源文件包含 Q_OBJECT 的实现代码,这个新的 C++源文件也会被编译、链接到这个类的二进制代码中去,因为它也是这个类的完整的一部分。通常,这个新的C++ 源文件会在以前的C++源文件名前面加上 moc_ 作为新文件的文件名。     如果使用qmake工具来生成Makefile文件,所有需要使用moc的编译规则都会给自动的包含到Makefile文件中,所以对程序员来说不需要直接的使用moc 


    除了处理信号和槽之外,moc还处理属性信息,Q_PROPERTY()宏定义类的属性信息,而Q_ENUMS()宏则定义在一个类中的枚举类型列表。 Q_FLAGS()宏定义在一个类中的flag枚举类型列表,Q_CLASSINFO()宏则允许你在一个类的meta信息中插入name/value 对。     由moc所生成的文件必须被编译和链接,就象你自己写的另外一个C++文件一样,否则,在链接的过程中就会失败。  



Code example: 

class MyClass : public QObject { 
    Q_OBJECT 
    Q_PROPERTY(Priority priority READ priority WRITE setPriority)     Q_ENUMS(Priority) 
    Q_CLASSINFO("Author", "Oscar Peterson")     Q_CLASSINFO("Status", "Active")  
public: 
    enum Priority { High, Low, VeryHigh, VeryLow };
 
    MyClass(QObject *parent = 0);     virtual ~MyClass();  
    void setPriority(Priority priority);     Priority priority() const; };


四:Signal; Slot 

信号和槽是用来在对象间通讯的方法,当一个特定事件发生的时候,signal会被 emit 出来,slot 调用是用来响应相应的 signal 的。     

QT 对象已经包含了许多预定义的 signal,但我们总是可以在派生类中添加新的 signal。

 
QT 对象中也已经包含了许多预定义的 slog,但我们可以在派生类中添加新的 slot 来处理我们感兴趣的 signal     signal 和 slot 机制是类型安全的,signal 和 slot必须互相匹配(实际上,一个solt的参数可以比对应的signal的参数少,因为它可以忽略多余的参数)。signal 和 slot是松散的配对关系,发出signal的对象不关心是那个对象链接了这个signal,也不关心是那个或者有多少slot链接到了这个 signal。QT的signal 和 slot机制保证了,如果一个signal和slot相链接,slot会在正确的时机被调用,并且是使用正确的参数。Signal和slot都可以携带任何数量和类型的参数,他们都是类型安全的。 


    所有从QObject直接或者间接继承出来的类都能包含信号和槽,当一个对象的状态发生变化的时候,信号就可以被emit出来,这可能是某个其它的对象所关心的。这个对象并不关心有那个对象或者多少个对象链接到这个信号了,这是真实的信息封装,它保证了这个对象可以作为一个软件组件来被使用。

 

    槽(slot)是用来接收信号的,但同时他们也是一个普通的类成员函数,就象一个对象不关心有多少个槽链接到了它的某个信号,一个对象也不关心一个槽链接了多少个信号。这保证了用QT创建的对象是一个真实的独立的软件组件。 
    一个信号可以链接到多个槽,一个槽也可以链接多个信号。同时,一个信号也可以链接到另外一个信号。

 
    所有使用了信号和槽的类都必须包含 Q_OBJECT 宏,而且这个类必须从QObject类派生(直接或者间接派生)出来, 当一个signal被emit出来的时候,链接到这个signal的slot会立刻被调用,就好像是一个函数调用一样。当这件事情发生的时候,signal和slot机制与GUI的事件循环完全没有关系,当所有链接到这个signal的slot执行完成之后,在 emit 代码行之后的代码会立刻被执行。当有多个slot链接到一个signal的时候,这些slot会一个接着一个的、以随机的顺序被执行。 


    Signal代码会由 moc 自动生成,开发人员一定不能在自己的C++代码中实现它,并且,它永远都不能有返回值。     Slot其实就是一个普通的类函数,并且可以被直接调用,唯一特殊的地方是它可以与signal相链接。 


    C++的预处理器更改或者删除 signal, slot, emit 关键字,所以,对于C++编译器来说,它处理的是标准的C++源文件。  


五:Meta Object Class 
前面我们介绍了Meta Object的基本功能,和它支持的最重要的特性之一:Signal & Slot的基本功能。现在让我们来进入 Meta Object 的内部,看看它是如何支持这些能力的。  


    Meta Object 的所有数据和方法都封装在一个叫QMetaObject 的类中。它用来查询一个 QT 类的 meta 信息,meta信息包含以下几种,:     

1、信号表(signal table),其中有这个对应的 QT 类的所有Signal的名字      

2、 槽表(slot table),其中有这个对应的QT类中的所有Slot的名字。      

3、类信息表(class info table),包含这个QT类的类型信息 
4、属性表(property table),其中有这个对应的QT类中的所有属性的名字。      

5、指向parent meta object的指针(pointers to parent meta object)  



    QMetaObject 对象与 QT 类之间的关系: 

每一个QMetaObject对象包含了与之相对应的一个QT类的元信息 


每一个QT类(QObject 以及它的派生类) 都有一个与之相关联的静态的(static) QMetaObject 对象(注:class的定义中必须有 Q_OBJECT 宏,否则就没有这个Meta Object) 


每一个 QMetaObject 对象保存了与它相对应的 QT 类的父类的 QMetaObject 对象的指针。或者,我们可以这样说:“每一个QMetaObject对象都保存了一个其父亲(parent)的指针”.注意:严格来说,这种说法是不正确的,最起码是不严谨的。
 




Q_OBJECT宏  



Meta Object 的功能实现,这个宏立下了汗马功劳。首先,让我们来看看这个宏是如何定义的:  

#define Q_OBJECT \
public: \
    static const QMetaObject staticMetaObject; \
    virtual const QMetaObject *metaObject() const; \
    virtual void *qt_metacast(const char *); \
    virtual int qt_metacall(QMetaObject::Call, int, void **); \
    QT_TR_FUNCTIONS \
private: \
    static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
    struct QPrivateSignal {}; \


这里,我们先忽略Q_OBJECT_CHECK 和QT_TR_FUNCTIONS 这两个宏。

   

我们看到,首先定义了一个静态类型的类变量staticMetaObject,然后有一个获取这个对象指针的方法metaObject()。这里最重要的就是类变量staticMetaObject 的定义。这说明所有的 QObject 的对象都会共享这一个staticMetaObject 类变量,靠它来完成所有信号和槽的功能,所以我们就有必要来仔细的看看它是怎么回事了。 
六:QMetaObject class data members 
我们来看一下QMetaObject的定义,我们先看一下QMetaObject对象中包含的成员数据。 
    
struct Q_CORE_EXPORT QMetaObject
{
   //...................

struct { // private data
        const QMetaObject *superdata;
        const QByteArrayData *stringdata;
        const uint *data;
        typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);
        StaticMetacallFunction static_metacall;
        const QMetaObject * const *relatedMetaObjects;
        void *extradata; //reserved for future use
    } d;


}


上面的代码就是QMetaObject类所定义的全部数据成员。就是这些成员记录了所有signal,slot,property,class information这么多的信息。下面让我们来逐一解释这些成员变量:  
const QMetaObject *superdata: 
这个变量指向与之对应的QObject类的父类,或者是祖先类的QMetaObject对象。  如何理解这一句话呢?我们知道,每一个QMetaObject对象,一定有一个与之相对应的QObject类(或者由其直接或间接派生出的子类),注意:这里是类,不是对象。  

那么每一个QObject类(或其派生类)可能有一个父类,或者父类的父类,或者很多的继承层次之前的祖先类。或者没有父类(QObject)。那么 superdata 这个变量就是指向与其最接近的祖先类中的QMetaObject对象。对于QObject类QMetaObject对象来说,这是一个NULL指针,因为 QObject没有父类。  



下面,让我们来举例说明: 
class Animal : public QObject  
{  
   Q_OBJECT  
  //............. 
};   
class Cat : public Animal   
{  
     Q_OBJECT  
    //.............  
}  

那么,Cat::staticMetaObject.d.superdata 这个指针变量指向的对象是Animal::staticMetaObject,     

Animal::staticMetaObject.d.superdata个指针变量指向的对象是QObject::staticMetaObject

QObject::staticMetaObject.d.superdat 这个指针变量的值为 NULL



但如果我们把上面class的定义修改为下面的定义,就不一样了:  

class Animal : public QObject 
{
// Q_OBJECT,这个 class 不定义这个  
//............. 
}; 
class Cat : public Animal  
{   Q_OBJECT   
//............. 
}
那么,Cat::staticMetaObject.d.superdata 这个指针变量指向的对象是 QObject::staticMetaObject 因为 Animal::staticMetaObject 这个对象是不存在的


const char *stringdata:


顾名思义,这是一个指向string data的指针。但它和我们平时所使用的一般的字符串指针却很不一样,我们平时使用的字符串指针只是指向一个字符串的指针,而这个指针却指向的是很多个字符串。那么它不就是字符串数组吗?哈哈,也不是。因为C++的字符串数组要求数组中的每一个字符串拥有相同的长度,这样才能组成一个数组。那它是不是一个字符串指针数组呢?也不是,那它到底是什么呢?让我们来看一看它的具体值,还是让我们以QObject这个class的QMetaObject为例来说明 吧。   


下面是QObject::staticMetaObject.d.stringdata指针所指向的多个字符串数组,其实它就是指向一个连续的内存区,而这个连续的内存区中保存了若干个字符串。  


static const char qt_meta_stringdata_QObject[] =  
{  

"QObject\0\0destroyed(QObject*)\0destroyed()\0"  
"deleteLater()\0_q_reregisterTimers(void*)\0"  
"QString\0objectName\0parent\0QObject(QObject*)\0"
"QObject()\0"
};

这个字符串都是些什么内容呀?有,Class Name, Signal Name, Slot Name, Property Name。看到这些大家是不是觉得很熟悉呀,对啦,他们就是Meta System所支持的最核心的功能属性了。  


既然他们都是不等长的字符串,那么Qt是如何来索引这些字符串,以便于在需要的时候能正确的找到他们呢?第三个成员正式登场了

const uint *data;  


这个指针本质上就是指向一个正整数数组,只不过在不同的object中数组的长度都不尽相同,这取决于与之相对应的class中定义了多少 signal,slot,property。  
这个整数数组的的值,有一部分指出了前一个变量(stringdata)中不同字符串的索引值,但是这里有一点需要注意的是,这里面的数值并不是直接标明了每一个字符串的索引值,这个数值还需要通过一个相应的算法计算之后,才能获得正确的字符串的索引值。 

 
下面是QObject::staticMetaObject.d.data指针所指向的正整数数组的值。

static const uint qt_meta_data_QObject[] =  
{  
content:  

	2,       // revision
	0,       // classname
	0,    0, // classinfo
	4,   12, // methods
	1,   32, // properties
	0,    0, // enums/sets
	2,   35, // constructors  //signals: signature, parameters, type, tag, flags
	9,    8,    8,    8, 0x05,
	29,    8,    8,    8, 0x25,  
// slots: signature, parameters, type, tag, flags  
	41,    8,    8,    8, 0x0a,
	55,    8,    8,    8, 0x08, 
// properties: name, type, flags  
	90,   82, 0x0a095103,
// constructors: signature, parameters, type, tag, flags  
	108,  101,    8,    8, 0x0e, 
	126,    8,    8,    8, 0x2e,
	0        // eod


简单的说明一下,  第一个section,就是 //content 区域的整数值,这一块区域在每一个QMetaObject的实体对象中数量都是相同的,含义也相同,但具体的值就不同了。专门有一个struct定义了这 个section,其含义在上面的注释中已经说的很清楚了。


struct QMetaObjectPrivate
{
    int revision;
    int className;
    int classInfoCount, classInfoData;
    int methodCount, methodData;
    int propertyCount, propertyData;
    int enumeratorCount, enumeratorData;
    int constructorCount, constructorData; //since revision 2
    int flags; //since revision 3
    int signalCount; //since revision 4
    // revision 5 introduces changes in normalized signatures, no new members
    // revision 6 added qt_static_metacall as a member of each Q_OBJECT and inside QMetaObject itself
};


这个 struct 就是定义第一个secton的,和上面的数值对照一下,很清晰,是吧?  


第二个section,以 // signals 开头的这段。这个section中的数值指明了QObject这个class包含了两个signal,  
第三个section,以 // slots 开头的这段。这个section中的数值指明了QObject这个class包含了两个slot。  
第四个section,以 // properties 开头的这段。这个section中的数值指明了QObject这个class包含有一个属性定义。  
第五个section,以 // constructors 开头的这段,指明了QObject这个class有两个constructor。  



const void *extradata;  


这是一个指向QMetaObjectExtraData数据结构的指针,关于这个指针,这里先略过。  

对于每一个具体的整数值与其所指向的实体数据之间的对应算法,实在是有点儿麻烦,这里就不讲解细节了,有兴趣的朋友自己去

读一下源代码,一定会有很多发现。



 
七:connect, 幕后的故事 


我们都知道,把一个signal和slot连接起来,需要使用QObject类的connect方法,它的作用就是把一个object的 signal和

另外一个object的slot连接起来,以达到对象间通讯的目的。 connect 在幕后到底都做了些什么事情?为什么emit一个signal后,

相应的slot都会被调用?好了,让我们来逐一解开其中的谜团.

 

SIGNAL 和 SLOT 宏定义  
我们在调用connect方法的时候,一般都会这样写: 
obj.connect(&obj, SIGNAL(destroyed()), &app, SLOT(aboutQt())); 
我们看到,在这里signal和slot的名字都被包含在了两个大写的SIGNAL和SLOT中,这两个是什么呢?原来SIGNAL 和 SLOT

是Qt定义的两个宏。好了,让我们先来看看这两个宏都做了写什么事情,这里是这两个宏的定义: 


# define SLOT(a)      "1"#a #

#define SIGNAL(a)   "2"#a  
原来Qt把signal和slot都转化成了字符串,并且还在这个字符串的前面加上了附加的符号,signal前面加了’2’,slot前面加了’1’。 也就

是说,我们前面写了下面的connect调用,在经过编译器预处理之后,

就便成了: obj.connect(&obj, "2destroyed()", &app, "1aboutQt()”));



当connect函数被调用了之后,都会去检查这两个参数是否是使用这两个宏正确的转换而来的,它检查的根据就是这两个前置数

字,是否等于1或者是2,如果不是,connect函数当然就会失败啦!

 
然后,会去检查发送signal的对象是否有这个signal,方法就是查找这个对象的class所对应的staticMetaObject对象中所包含 的
d.stringdata所指向的字符串中是否包含这个signal的名字,在这个检查过程中,就会用到d.data所指向的那一串整数,通过这些整数

值来计算每一个具体字符串的起始地址。同理,还会使用同样的方法去检查slot,看响应这个signal的对象是否包含有相应的slot。

这两个检查的任何一个如果失败的话,connect函数就失败了,返回false. 
前面的步骤都是在做一些必要的检查工作,下一步,就是要把发送signal的对象和响应signal的对象关联起来。



  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值