1. 程式人生 > >QT 中 關鍵字講解(emit,signal,slot)

QT 中 關鍵字講解(emit,signal,slot)

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

原文:https://www.cnblogs.com/felix-wang/p/6212197.html