1、在Qt的内部设计中,通过信号/反应槽(signals/slot)的使用对回调进行了很好的封装。为了更好地了解该机制我们先看一下其他几种常用的信号相关程序。1.Win32 Win32的程序总是从WinMain开始执行。在WinMain的代码中,主要功能一般有三个:一是注册窗口类,二是在屏幕上显示窗口,三是实现消息环。消息环的作用就是从应用程序队列中取出操作系统放入的消息,从而实现用户和程序之间的交互(也包括象定时器之类的非用户输入的消息)。应用程序不定期地在消息环中等待消息的到来。如下所示:/ 消息环while(GetMessage(&msg, NULL, 0, 0)TranslateMessa
2、ge(&msg);DispatchMessage(&这一段程序包括了形成一个标准消息环的三个基本API:GetM essage()、TranslateMessage()和ispatchMessage(),采用加速键和非模式对话框时将相应改变消息环的结构。在Windows中,GetMessage()是多任务的核心。在应用程序的消息队列中出现一条消息之前,该函数并不返回任何东西。GetMessage()的等待阻塞了当前进程,因而为正在运行的其他应用程序提供了检查私有消息环的机会。出现一条消息后,GetMessage()将取出该消息,并将信息存储在一个MSG数据结构中。对于每一条迫使退出消息环、进程
3、终止的消息(WM_QUIT除外),GetMessage()返回TRUE。通常在消息环后面跟一个返回语句,迫使WinMain()返回系统。紧跟着GetMessage()的TranslateMessage()对msg进行处理并修改该数据块的内容。DispatchMessage()负责查找应调用哪一个窗口过程,这种选择是根据msg中hwnd所标识的窗口进行决策。窗口过程对消息进行处理, 完毕后即返回到消息环, 再次执行GetMessage()。如下图所示: 为了对所关心的消息做出处理,窗口在创建时一定要提供一个消息回调函数,不管该创建过程是显式调用还是其他API函数隐式生成。用户在该回调函数中要对每
4、一个关心的消息做出判断与处理,从C语言的观点来看,一个窗口过程(回调函数)就是这样一个函数:接受四个参数,返回一个LRESULT值,一个switch语句在过程内占用了大量的代码以完成各个行为动作。2.MFC 虽然直接用Win32 API开发的程序运行效率高、条理分明,但开发起来却较为复杂,维护工时也耗用较多,因此现在Windows环境中大部分用C+开发的应用程序使用了微软提供的MFC类库。它是面向对象设计的,虽然乍一看其编程风格与Win32迥然不同,但那是高度封装的结果,其内部的实现与Win32没有区别。MFC的一个主导设计思想就是程序框架下(CFrameWnd)的视图/文档模型,同时定义了许
5、多宏来简化编程,其消息的传递也与宏息息相关(有关MFC的解剖可看侯捷先生的深入浅出MFC第二版)。通过使用这些宏,应用程序自身将维护这一张可能为数不菲的消息映射表。对于程序员来说,只需要点击鼠标就可完成以上的工作,开发效率有了很大的提高。3.LinuxLinux(包括其他的Unix)和Windows的一个很大不同点在于其图形界面的管理是与内核分开的,负责图形操作(还包括键盘、鼠标等事件捕获)的模块是X Window。请注意,此处的“Window”与微软的Windows毫无亲戚关系。X Window包括三大部分:服务端(XServer)、客户端(X Client)和协议(X protocol),
6、示意图如下:我们平时在Linux下开发的有图形界面的程序一般就是X Window中的客户端程序,相对应的库就是X Lib。X Lib是X Window中最低层的接口库,相当于微软Windows中的 API。这个库封装了对X protocol的存取,提供了超过610个函数。由于X protocol可以在网络上传播,因此X Window中服务器端和客户端可以不在一台机器上,这一点和微软Windows有着很大的区别。对比X Lib与Win32 API的处理方式,可以发现虽然两者框架不一、风格不一,但在流程处理上都有异曲同工之妙。4.Qt Qt中的类库有接近一半是从基类QObject上继承下来,信号与
7、反应槽(signals/slot)机制就是用来在QObject类或其子类间通讯的方法。作为一种通用的处理机制,信号与反应槽非常灵活,可以携带任意数量的参数,参数的类型也由用户自定。同时其本身也是类型安全的,任何一个从QObject或其子类继承的用户类都可以使用信号与反应槽。信号的作用如同Windows系统中的消息。在Qt中,对于发出信号的对象来说,它并不知道是谁接收了这个信号。这样的设计可能在某些地方会有些不便,但却杜绝了紧耦合,于总体设计有利。反应槽是用来接收信号的, 但它实际上也是普通的函数,程序员可以象调用普通函数一样来调用反应槽。与信号类似的是,反应槽的拥有者也不知道是谁向它发出了信号
8、。在程序设计过程中,多个信号可以连接至一个反应槽,类似的,一个信号也可以连接至多个反应槽,甚至一个信号可以连接至另一个信号。在Windows中,如果我们需要多个菜单都激发一个函数,一般是先写一个共用函数,然后在每个菜单的事件中调用此函数。在Qt中如果要实现同样的功能,就可以把实现部分写在一个菜单中,然后把其他菜单与这个菜单级联起来。虽然信号/反应槽机制有很多优点,使用也很方便,但它也不是没有缺点。最大的缺点在于要稍微牺牲一点性能。根据Trolltech公司的自测,在CPU为Intel PentiumII 500 Mhz的PC机上,对于一个信号对应一个反应槽的连接来说,一秒钟可以调用两百万次;对
9、于一个信号对应两个反应槽的连接来说,一秒钟可以调用一百二十万次。这个速度是不经过连接而直接进行回调的速度的十分之一。请注意这里的十分之一速度比是调用速度的比较,而不是一个完整函数执行时间的比较。事实上一般情况下一个函数的总执行时间大部分是在执行部分,只有小部分是在调用部分,因些这个速度是可以接受的。这就象面向对象的编程和早些年的结构化编程相比一样:程序的执行效率并没有提高,反而是有所下降的,但现在大家都在用面向对象的方法编写程序。用一部分执行效率换回开发效率与维护效率是值得的,况且现在已是P4为主流的时代。我们先来看一个简单的样例:class Demo : public QObjectQ_OB
10、JECTpublic:Demo();int value() const return val; ;public slots:void setValue( int );signals:void valueChanged( int );private:int val; 由样例可看到,类的定义中有两个关键字slots和signals,还有一个宏Q_OBJECT。在Qt的程序中如果使用了信号与反应槽就必须在类的定义中声明这个宏,不过如果你声明了该宏但在程序中并没有信号与反应槽,对程序也不会有任何影响,所以建议大家在用Qt写程序时不妨都把这个宏加上。使用slots定义的就是信号的功能实现,即反应槽,例如
11、:void Demo:setValue( int v )if ( v != val ) val = v;emit valueChanged(v); 这段程序表明当setValue执行时它将释放出valueChanged这个信号。以下程序示范了不同对象间信号与反应槽的连接。Demo a, b;connect(&a, SIGNAL(valueChanged(int), &b, SLOT(setValue(int);b.setValue( 11 );a.setValue( 79 );b.value(); / b的值将是79而不是原先设的11 在以上程序中,一旦信号与反应槽连接,当执行a.setVal
12、ue(79)时就会释放出一个valueChanged(int)的信号,对象b将会收到这个信号并触发setValue(int)这个函数。当b在执行setValue(int)这个函数时,它也将释放valueChanged(int)这个信号,当然b 的信号无人接收,因此就什么也没干。示意图如下:请注意,在样例中我们仅当输入变量v不等于val时才释放信号,因此就算对象a与b进行了交叉连接也不会导致死循环的发生。由于在样例中使用了Qt特有的关键字和宏,而Qt本身并不包括C+的编译器,因此如果用流行的编译程序(如Windows下的Visual C+或Linux下的gcc)是不能直接编译这段代码的,必须用Q
13、t的中间编译工具moc.exe把该段代码转换为无专用关键字和宏的C+代码才能为这些编译程序所解析、编译与链接。 以上代码中信号与反应槽的定义是在类中实现的。那么,非类成员的函数,比如说一个全局函数可不可以也这样做呢?答案是不行,只有是自身定义了信号的类或其子类才可以发出该种信号。一个对象的不同信号可以连接至不同的对象。当一个信号被释放时,与之连接的反应槽将被立刻执行,就象是在程序中直接调用该函数一样。信号的释放过程是阻塞的,这意味着只有当反应槽执行完毕后该信号释放过程才返回。如果一个信号与多个反应槽连接,则这些反应槽将被顺序执行,排序过程则是任意的。因此如果程序中对这些反应槽的先后执行次序有严
14、格要求的话,应特别注意。使用信号时还应注意:信号的定义过程是在类的定义过程即头文件中实现的。为了中间编译工具moc的正常运行,不要在源文件(.cpp)中定义信号,同时信号本身不应返回任何数据类型,即是空值(void)。如果你要设计一个通用的类或控件,则在信号或反应槽的参数中应尽可能使用常规数据以增加通用性。如上例代码中valueChanged的参数为int型,如果它使用了特殊类型如QRangeControl:Range,那么这种信号只能与RangeControl中的反应槽连接。如前所述,反应槽也是常规函数,与未定义slots的用户函数在执行上没有任何区别。但在程序中不可把信号与常规函数连接在一
15、起,否则信号的释放不会引起对应函数的执行。要命的是中间编译程序moc并不会对此种情况报错,C+编译程序更不会报错。初学者比较容易忽略这一点,往往是程序编好了没有错误,逻辑上也正确,但运行时就是不按自己的意愿出现结果,这时候应检查一下是不是这方面的疏忽。Qt的设计者之所以要这样做估计是为了信号与反应槽之间匹配的严格性。既然反应槽与常规函数在执行时没有什么区别,因此它也可以定义成公共反应槽(public slots)、保护反应槽(protected slots)和私有反应槽(private slots)。如果需要,我们也可以把反应槽定义成虚函数以便子类进行不同的实现,这一点是非常有用的。只讨论一下
16、信号与反应槽的使用好象还不过瘾,既然Qt的X11 Free版提供了源代码,我们就进去看一下在QObject中connect的实现。由于Qt是一个跨平台的开发库,为了与不同平台上的编译器配合,它定义了一个中间类QMetaObject,该类的作用是存放有关信号/反应槽以及对象自身的信息。这个类是Qt内部使用的,用户不应去使用它。 以下是QMetaObject的定义(为了浏览方便,删除了一部分次要代码):class Q_EXPORT QMetaObjectQMetaObject( const char * const class_name, QMetaObject *superclass,const
17、 QMetaData * const slot_data, int n_slots,const QMetaData * const signal_data, int n_signals);virtual QMetaObject();int numSlots( bool super = FALSE ) const; /* 反应槽的数量 */int numSignals( bool super = FALSE ) const; /* 信号的数量 */int findSlot( const char *, bool super = FALSE ) const;/* 根据反应槽的名称找到其在列表中的索
18、引 */int findSignal( const char *, bool super = FALSE ) const;/* 根据信号的名称找到其在列表中的索引 */const QMetaData *slot( int index, bool super = FALSE ) const;/* 根据索引取得反应槽的数据 */const QMetaData *signal( int index, bool super = FALSE ) const;/* 根据索引取得信号的数据 */QStrList slotNames( bool super = FALSE ) const;/* 取得反应槽列表
19、 */QStrList signalNames( bool super = FALSE ) const;/* 取得信号列表 */int slotOffset() const;int signalOffset() const;static QMetaObject *metaObject( const char *class_name );QMemberDict *init( const QMetaData *, int );const QMetaData *slotData; /* 反应槽数据指针 */QMemberDict *slotDict; /* 反应槽数据字典指针 */const QMe
20、taData *signalData; /* 信号数据指针*/QMemberDict *signalDict; /* 信号数据字典指针*/int signaloffset;int slotoffset;再看一下QObject中connect的实现。剥去粗枝,函数中便露出一个更细化的函数:connectInternal,它又做了哪些工作呢?让我们看一下:void QObject:connectInternal( const QObject *sender, int signal_index,const QObject *receiver,int membcode, int member_inde
21、x )QObject *s = (QObject*)sender;QObject *r = (QObject*)receiver;if ( !s-connections ) /* 如果某个对象有信号或反应槽但没有建立相互连接是不会建立连接列表的,这样可减少一些无谓的资源消耗 */connections = new QSignalVec( 7 );connections-setAutoDelete( TRUE );/* 无连接时,连接列表将被自动删除 */QConnectionList *clist = s-at( signal_index );clist ) /* 建立与信号源对象中某一个信号
22、所对应的接收对象的列表 */clist = new QConnectionList;clist-insert( signal_index, clist );QMetaObject *rmeta = r-metaObject();switch ( membcode ) /* 取得信号或反应槽的数据指针 */case QSLOT_CODE:rm = rmeta-slot( member_index, TRUE );break;case QSIGNAL_CODE:signal( member_index, TRUE );QConnection *c = new QConnection( r, mem
23、ber_index,rm ? rm-name : qt_invoke, membcode );/* 创建一个新的信号/反应槽连接 */append( c ); /* 信号源端加入这一对连接 */r-senderObjects ) /* 类似于信号源端,反应槽端的连接列表也是动态创建的 */senderObjects = new QObjectList;senderObjects-append( s ); /* 反应槽端加入这一对连接 */到此,信号与反应槽的连接已建立完毕,那么信号产生时又是如何触发反应槽的呢?从QObject的定义中可以看出其有多个activate_signal的成员函数,这
24、些函数都是protected的,也即只有其自身或子类才可以使用。看一下它的实现:activate_signal( QConnectionList *clist, QUObject *o )clist ) /* 有效性检查 */return;QObject *object;QConnection *c;if ( clist-count() = 1 ) /* 对某一个对象的一个具体信号来说,一般只有一种反应槽与之相连,这样事先判断一下可以加快处理速度 */c = clist-first();object = c-object();sigSender = this;if ( c-memberType
25、() = QSIGNAL_CODE )object-qt_emit( c-member(), o ); /* 信号级连 */elseqt_invoke( c-/* 调用反应槽函数 */ else QConnectionListIt it(*clist);while ( (c=it.current() ) /* 有多个连接时,逐一扫描 */+it; /* 调用反应槽函数 */至此我们已经可以基本了解Qt中信号/反应槽的流程。我们再看一下Qt为此而新增的语法:三个关键字:slots、signals和emit,三个宏:SLOT()、SIGNAL()和Q_OBJECT。在头文件qobjectdefs.
26、h中,我们可以看到这些新增语法的定义如下:#define slots / slots: in class#define signals protected / signals:#define emit / emit signal#define SLOT(a) 1#a#define SIGNAL(a) 2由此可知其实三个关键字没有做什么事情,而SLOT()和SIGNAL()宏也只是在字符串前面简单地加上单个字符,以便程序仅从名称就可以分辨谁是信号、谁是反应槽。中间编译程序moc.exe则可以根据这些关键字和宏对相应的函数进行“翻译”,以便在C+编译器中编译。剩下一个宏Q_OBJECT比较复杂,它
27、的定义如下:#define Q_OBJECT publi virtual QMetaObject *metaObject() const return staticMetaObject(); virtual const char *className() const;virtual void* qt_cast( const char* );virtual bool qt_invoke( int, QUObject* );virtual bool qt_emit( int, QUObject* );QT_PROP_FUNCTIONSstatic QMetaObject* staticMetaObject();QObject* qObject() return (QObject*)this; QT_TR_FUNCTIONSstatic QMetaObje