Qt中的类库有接近一半是从基类QObject上继承下来,信号与反应槽(signals/slot)机制就是用来在QObject类或其子类间通讯的方法。作为一种通用的处理机制,信号与反应槽非常灵活,可以携带任意数量的参数,参数的类型也由用户自定。同时其本身也是类型安全的,任何一个从QObject或其子类继承的用户类都可以使用信号与反应槽。
信号的作用如同Windows系统中的消息。在Qt中,对于发出信号的对象来说,它并不知道是谁接收了这个信号。这样的设计可能在某些地方会有些不便,但却杜绝了紧耦合,于总体设计有利。反应槽是用来接收信号的, 但它实际上也是普通的函数,程序员可以象调用普通函数一样来调用反应槽。与信号类似的是,反应槽的拥有者也不知道是谁向它发出了信号。在程序设计过程中,多个信号可以连接至一个反应槽,类似的,一个信号也可以连接至多个反应槽,甚至一个信号可以连接至另一个信号。
在Windows中,如果我们需要多个菜单都激发一个函数,一般是先写一个共用函数,然后在每个菜单的事件中调用此函数。在Qt中如果要实现同样的功能,就可以把实现部分写在一个菜单中,然后把其他菜单与这个菜单级联起来。
虽然信号/反应槽机制有很多优点,使用也很方便,但它也不是没有缺点。最大的缺点在于要稍微牺牲一点性能。根据Trolltech公司的自测,在CPU为Intel PentiumII 500 Mhz的PC机上,对于一个信号对应一个反应槽的连接来说,一秒钟可以调用两百万次;对于一个信号对应两个反应槽的连接来说,一秒钟可以调用一百二十万次。这个速度是不经过连接而直接进行回调的速度的十分之一。请注意这里的十分之一速度比是调用速度的比较,而不是一个完整函数执行时间的比较。事实上一般情况下一个函数的总执行时间大部分是在执行部分,只有小部分是在调用部分,因些这个速度是可以接受的。这就象面向对象的编程和早些年的结构化编程相比一样:
程序的执行效率并没有提高,反而是有所下降的,但现在大家都在用面向对象的方法编写程序。用一部分执行效率换回开发效率与维护效率是值得的,况且现在已是P4为主流的时代。
我们先来看一个简单的样例:
class Demo : public QObject
{
Q_OBJECT
public:
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定义的就是信号的功能实现,即反应槽,例如:
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.setValue(79)时就会释放出一个valueChanged(int)的信号,对象b将会收到这个信号并触发setValue(int)这个函数。当b在执行setValue(int)这个函数时,它也将释放valueChanged(int)这个信号,当然b的信号无人接收,因此就什么也没干。请注意,在样例中我们仅当输入变量v不等于val时才释放信号,因此就算对a与b进行了交叉连接也不会导致死循环的发生。由于在样例中使用了Qt特有的关键字和宏,而Qt本身并不包括C++的编译器,因此如果用流行的编译程序(如Windows下的Visual C++或Linux下的gcc)是不能直接编译这段代码的,必须用Qt的中间编译工具moc.exe把该段代码转换为无专用关键字和宏的C++代码才能为这些编译程序所解析、编译与链接。
以上代码中信号与反应槽的定义是在类中实现的。那么,非类成员的函数,比如说一个全局函数可不可以也这样做呢?答案是不行,只有是自身定义了信号的类或其子类才可以发出该种信号。一个对象的不同信号可以连接至不同的对象。当一个信号被释放时,与之连接的反应槽将被立刻执行,就象是在程序中直接调用该函数一样。信号的释放过程是阻塞的,这意味着只有当反应槽执行完毕后该信号释放过程才返回。如果一个信号与多个反应槽连接,则这些反应槽将被顺序执行,排序过程则是任意的。因此如果程序中对这些反应槽的先后执行次序有严格要求的,应特别注意。使用信号时还应注意:信号的定义过程是在类的定义过程即头文件中实现的。为了中间编译工具moc的正常运行,不要在源文件(.cpp)中定义信号,同时信号本身不应返回任何数据类型,即是空值(void)。如果你要设计一个通用的类或控件,则在信号或反应槽的参数中应尽可能使用常规数据以增加通用性。如上例代码中valueChanged的参数为int型,如果它使用了特殊类型如QRangeControl::Range,那么这种信号只能与RangeControl中的反应槽连接。如前所述,反应槽也是常规函数,与未定义slots的用户函数在执行上没有任何区别。
但在程序中不可把信号与常规函数连接在一起,否则信号的释放不会引起对应函数的执行。要命的是中间编译程序moc并不会对此种情况报错,C++编译程序更不会报错。初学者比较容易忽略这一点,往往是程序编好了没有错误,逻辑上也正确,但运行时就是不按自己的意愿出现结果,这时候应检查一下是不是这方面的疏忽。
Qt的设计者之所以要这样做估计是为了信号与反应槽之间匹配的严格性。既然反应槽与常规函数在执行时没有什么区别,因此它也可以定义成公共反应槽(public slots)、保护反应槽(protected slots)和私有反应槽(private slots)。如果需要,我们也可以把反应槽定义成虚函数以便子类进行不同的实现,这一点是非常有用的。
只讨论一下信号与反应槽的使用好象还不过瘾,既然Qt的X11 Free版提供了源代码,我们就进去看一下在QObject中connect的实现。由于Qt是一个跨平台的开发库,为了与不同平台上的编译器配合,它定义了一个中间类QMetaObject,该类的作用是存放有关信号/反应槽以及对象自身的信息。这个类是Qt内部使用的,用户不应去使用它。
以下是QMetaObject的定义(为了浏览方便,删除了一部分次要代码):
class Q_EXPORT QMetaObject
{
public:
QMetaObject( const char * const class_name, QMetaObject *superclass,
const 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;
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;
QStrList signalNames( bool super = FALSE ) const;
int slotOffset() const;
int signalOffset() const;
static QMetaObject *metaObject( const char *class_name );
private:
QMemberDict *init( const QMetaData *, int );
const QMetaData *slotData;
QMemberDict *slotDict;
const QMetaData *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_index )
{
QObject *s = (QObject*)sender;
QObject *r = (QObject*)receiver;
if ( !s->connections )
{
s->connections = new QSignalVec( 7 );
s->connections->setAutoDelete( TRUE );
}
QConnectionList *clist = s->connections->at( signal_index );
if ( !clist )
{
clist = new QConnectionList;
clist->setAutoDelete( TRUE );
s->connections->insert( signal_index, clist );
}
QMetaObject *rmeta = r->metaObject();
switch ( membcode ) {
case QSLOT_CODE:
rm = rmeta->slot( member_index, TRUE );
break;
case QSIGNAL_CODE:
rm = rmeta->signal( member_index, TRUE );
break;
}
QConnection *c = new QConnection( r, member_index,
rm ? rm->name : "qt_invoke", membcode );
clist->append( c );
if ( !r->senderObjects )
{
r->senderObjects = new QObjectList;
}
r->senderObjects->append( s );
}
到此,信号与反应槽的连接已建立完毕,那么信号产生时又是如何触发反应槽的呢?从QObject的定义中可以看出其有多个activate_signal的成员函数,这些函数都是protected的,也即只有其自身或子类才可以使用。看一下它的实现:
void QObject::activate_signal( QConnectionList *clist, QUObject *o )
{
if ( !clist )
return;
QObject *object;
QConnection *c;
if ( clist->count() == 1 ) {
c = clist->first();
object = c->object();
sigSender = this;
if ( c->memberType() == QSIGNAL_CODE )
object->qt_emit( c->member(), o );
else
object->qt_invoke( c->member(), o );
} else {
QConnectionListIt it(*clist);
while ( (c=it.current()) ) {
++it;
object = c->object();
sigSender = this;
if ( c->memberType() == QSIGNAL_CODE )
object->qt_emit( c->member(), o );
else
object->qt_invoke( c->member(), o );
}
}
}
至此我们已经可以基本了解Qt中信号/反应槽的流程。我们再看一下Qt为此而新增的语法:三个关键字:slots、signals和emit,三个宏:SLOT()、SIGNAL()和Q_OBJECT。在头文件qobjectdefs.h中,我们可以看到这些新增语法的定义如下:
#define slots // slots: in class
#define signals protected // signals: in class
#define emit // emit signal
#define SLOT(a) "1"#a
#define SIGNAL(a) "2"#a
由此可知其实三个关键字没有做什么事情,而SLOT()和SIGNAL()宏也只是在字符串前面简单地加上单个字符,以便程序仅从名称就可以分辨谁是信号、谁是反应槽。中间编译程序moc.exe则可以根据这些关键字和宏对相应的函数进行“翻译”,以便在C++编译器中编译。剩下一个宏Q_OBJECT比较复杂,它的定义如下:
#define Q_OBJECT \
public: \
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_FUNCTIONS
\
static QMetaObject* staticMetaObject(); \
QObject* qObject() { return (QObject*)this; } \
QT_TR_FUNCTIONS
\
private: \
static QMetaObject *metaObj;
从定义中可以看出该宏的作用有两个:一是对与自己相关的QMetaObject中间类操作进行声明,另一个是对信号的释放操作和反应槽的激活操作进行声明。当moc.exe对头文件进行预编译之后,将会产生一个可供C++编译器编译的源文件。以上述的Demo类为例,假设它的代码文件分别为demo.h和demo.cpp,预编译后将产生moc_demo.cpp,其主要内容如下:
QMetaObject *Demo::metaObj = 0;
void Demo::initMetaObject()
{
if ( metaObj )
return;
if ( strcmp(QObject::className(), "QObject") != 0 )
badSuperclassWarning("Demo","QObject");
(void) staticMetaObject();
}
QMetaObject* Demo::staticMetaObject()
{
if ( metaObj )
return metaObj;
(void) QObject::staticMetaObject();
typedef void(Demo::*m1_t0)(int);
m1_t0 v1_0 = Q_AMPERSAND Demo::setValue;
QMetaData *slot_tbl = QMetaObject::new_metadata(1);
QMetaData::Access *slot_tbl_access = QMetaObject::new_metaaccess(1);
slot_tbl[0].name = "setValue(int)";
slot_tbl[0].ptr = *((QMember*)&v1_0);
slot_tbl_access[0] = QMetaData::Public;
typedef void(Demo::*m2_t0)(int);
m2_t0 v2_0 = Q_AMPERSAND Demo::valueChanged;
QMetaData *signal_tbl = QMetaObject::new_metadata(1);
signal_tbl[0].name = "valueChanged(int)";
signal_tbl[0].ptr = *((QMember*)&v2_0);
metaObj = QMetaObject::new_metaobject(
"Demo", "QObject",
slot_tbl, 1,
signal_tbl, 1,
0, 0 );
metaObj->set_slot_access( slot_tbl_access );
return metaObj;
}
// 有信号时即激活对应的反应槽或另一个信号
void Demo::valueChanged( int t0 )
{
activate_signal( "valueChanged(int)", t0 );
}
该文件中既没有Qt特有的关键字,也没有特殊的宏定义,完全符合普通的C++语法,因此可以顺利编译和链接。