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++語法,因此可以順利編譯和鏈接。