天天看點

QT 學習之路--自定義信号槽

上一節我們詳細分析了

connect()

函數。使用

connect()

可以讓我們連接配接系統提供的信号和槽。但是,Qt 的信号槽機制并不僅僅是使用系統提供的那部分,還會允許我們自己設計自己的信号和槽。這也是 Qt 架構的設計思路之一,用于我們設計解耦的程式。本節将講解如何在自己的程式中自定義信号槽。

信号槽不是 GUI 子產品提供的,而是 Qt 核心特性之一。是以,我們可以在普通的控制台程式使用信号槽。

經典的觀察者模式在講解舉例的時候通常會舉報紙和訂閱者的例子。有一個報紙類

Newspaper

,有一個訂閱者類

Subscriber

Subscriber

可以訂閱

Newspaper

。這樣,當

Newspaper

有了新的内容的時候,

Subscriber

可以立即得到通知。在這個例子中,觀察者是

Subscriber

,被觀察者是

Newspaper

。在經典的實作代碼中,觀察者會将自身注冊到被觀察者的一個容器中(比如

subscriber.registerTo(newspaper)

)。被觀察者發生了任何變化的時候,會主動周遊這個容器,依次通知各個觀察者(

newspaper.notifyAllSubscribers()

)。

下面我們看看使用 Qt 的信号槽,如何實作上述觀察者模式。注意,這裡我們僅僅是使用這個案例,我們的代碼并不是去實作一個經典的觀察者模式。也就是說,我們使用 Qt 的信号槽機制來獲得同樣的效果。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 //!!! Qt5 #include <QObject>   // newspaper.h class Newspaper : public QObject {      Q_OBJECT public :      Newspaper ( const QString & name ) :          m_name ( name )      {      }        void send ( )      {          emit newPaper ( m_name ) ;      }   signals :      void newPaper ( const QString & name ) ;   private :      QString m_name ; } ;   // reader.h #include <QObject> #include <QDebug>   class Reader : public QObject {      Q_OBJECT public :      Reader ( ) { }        void receiveNewspaper ( const QString & name )      {          qDebug ( ) << "Receives Newspaper: " << name ;      } } ;   // main.cpp #include <QCoreApplication>   #include "newspaper.h" #include "reader.h"   int main ( int argc , char * argv [ ] ) {      QCoreApplication app ( argc , argv ) ;        Newspaper newspaper ( "Newspaper A" ) ;      Reader reader ;      QObject :: connect ( & newspaper , & Newspaper :: newPaper ,                      & reader ,      & Reader :: receiveNewspaper ) ;      newspaper . send ( ) ;        return app . exec ( ) ; }

當我們運作上面的程式時,會看到終端輸出 Receives Newspaper: Newspaper A 這樣的字樣。

下面我們來分析下自定義信号槽的代碼。

這段代碼放在了三個檔案,分别是 newspaper.h,reader.h 和 main.cpp。為了減少檔案數量,可以把 newspaper.h 和 reader.h 都放在 main.cpp 的

main()

函數之前嗎?答案是,可以,但是需要有額外的操作。具體問題,我們在下面會詳細說明。

首先看

Newspaper

這個類。這個類繼承了

QObject

類。隻有繼承了

QObject

類的類,才具有信号槽的能力。是以,為了使用信号槽,必須繼承

QObject

。凡是

QObject

類(不管是直接子類還是間接子類),都應該在第一行代碼寫上

Q_OBJECT

。不管是不是使用信号槽,都應該添加這個宏。這個宏的展開将為我們的類提供信号槽機制、國際化機制以及 Qt 提供的不基于 C++ RTTI 的反射能力。是以,如果你覺得你的類不需要使用信号槽,就不添加這個宏,就是錯誤的。其它很多操作都會依賴于這個宏。注意,這個宏将由 moc(我們會在後面章節中介紹 moc。這裡你可以将其了解為一種預處理器,是比 C++ 預處理器更早執行的預處理器。) 做特殊處理,不僅僅是宏展開這麼簡單。moc 會讀取标記了 Q_OBJECT 的頭檔案,生成以 moc_ 為字首的檔案,比如 newspaper.h 将生成 moc_newspaper.cpp。你可以到建構目錄檢視這個檔案,看看到底增加了什麼内容。注意,由于 moc 隻處理頭檔案中的标記了

Q_OBJECT

的類聲明,不會處理 cpp 檔案中的類似聲明。是以,如果我們的

Newspaper

Reader

類位于 main.cpp 中,是無法得到 moc 的處理的。解決方法是,我們手動調用 moc 工具處理 main.cpp,并且将 main.cpp 中的

#include "newspaper.h"

改為

#include "moc_newspaper.h"

就可以了。不過,這是相當繁瑣的步驟,為了避免這樣修改,我們還是将其放在頭檔案中。許多初學者會遇到莫名其妙的錯誤,一加上

Q_OBJECT

就出錯,很大一部分是因為沒有注意到這個宏應該放在頭檔案中。

Newspaper

類的 public 和 private 代碼塊都比較簡單,隻不過它新加了一個 signals。signals 塊所列出的,就是該類的信号。信号就是一個個的函數名,傳回值是 void(因為無法獲得信号的傳回值,是以也就無需傳回任何值),參數是該類需要讓外界知道的資料。信号作為函數名,不需要在 cpp 函數中添加任何實作(我們曾經說過,Qt 程式能夠使用普通的 make 進行編譯。沒有實作的函數名怎麼會通過編譯?原因還是在 moc,moc 會幫我們實作信号函數所需要的函數體,是以說,moc 并不是單純的将 Q_OBJECT 展開,而是做了很多額外的操作)。

Newspaper

類的

send()

函數比較簡單,隻有一個語句

emit newPaper(m_name);

。emit 是 Qt 對 C++ 的擴充,是一個關鍵字(其實也是一個宏)。emit 的含義是發出,也就是發出

newPaper()

信号。感興趣的接收者會關注這個信号,可能還需要知道是哪份報紙發出的信号?是以,我們将實際的報紙名字

m_name

當做參數傳給這個信号。當接收者連接配接這個信号時,就可以通過槽函數獲得實際值。這樣就完成了資料從發出者到接收者的一個轉移。

Reader

類更簡單。因為這個類需要接受信号,是以我們将其繼承了

QObject

,并且添加了

Q_OBJECT

宏。後面則是預設構造函數和一個普通的成員函數。Qt 5 中,任何成員函數、static 函數、全局函數和 Lambda 表達式都可以作為槽函數。與信号函數不同,槽函數必須自己完成實作代碼。槽函數就是普通的成員函數,是以作為成員函數,也會受到 public、private 等通路控制符的影響。(我們沒有說信号也會受此影響,事實上,如果信号是 private 的,這個信号就不能在類的外面連接配接,也就沒有任何意義。)

main()

函數中,我們首先建立了

Newspaper

Reader

兩個對象,然後使用

QObject::connect()

函數。這個函數我們上一節已經詳細介紹過,這裡應該能夠看出這個連接配接的含義。然後我們調用

Newspaper

send()

函數。這個函數隻有一個語句:發出信号。由于我們的連接配接,當這個信号發出時,自動調用 reader 的槽函數,列印出語句。

這樣我們的示例程式講解完畢。我們基于 Qt 的信号槽機制,不需要觀察者的容器,不需要注冊對象,就實作了觀察者模式。

下面總結一下自定義信号槽需要注意的事項:

  • 發送者和接收者都需要是

    QObject

    的子類(當然,槽函數是全局函數、Lambda 表達式等無需接收者的時候除外);
  • 使用 signals 标記信号函數,信号是一個函數聲明,傳回 void,不需要實作函數代碼;
  • 槽函數是普通的成員函數,作為成員函數,會受到 public、private、protected 的影響;
  • 使用 emit 在恰當的位置發送信号;
  • 使用

    QObject::connect()

    函數連接配接信号和槽。

Qt 4

下面給出 Qt 4 中相應的代碼:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 //!!! Qt4 #include <QObject>   // newspaper.h class Newspaper : public QObject {      Q_OBJECT public :      Newspaper ( const QString & name ) :          m_name ( name )      {      }        void send ( ) const      {          emit newPaper ( m_name ) ;      }   signals :      void newPaper ( const QString & name ) const ;   private :      QString m_name ; } ;   // reader.h #include <QObject> #include <QDebug>   class Reader : public QObject {      Q_OBJECT public :      Reader ( ) { }   public slots :      void receiveNewspaper ( const QString & name ) const      {          qDebug ( ) << "Receives Newspaper: " << name ;      } } ;   // main.cpp #include <QCoreApplication>   #include "newspaper.h" #include "reader.h"   int main ( int argc , char * argv [ ] ) {      QCoreApplication app ( argc , argv ) ;        Newspaper newspaper ( "Newspaper A" ) ;      Reader reader ;      QObject :: connect ( & newspaper , SIGNAL ( newPaper ( QString ) ) ,                      & reader ,      SLOT ( receiveNewspaper ( QString ) ) ) ;      newspaper . send ( ) ;        return app . exec ( ) ; }

注意下 Qt 4 與 Qt 5 的差別。

Newspaper

類沒有什麼差別。

Reader

類,

receiveNewspaper()

函數放在了 public slots 塊中。在 Qt 4 中,槽函數必須放在由 slots 修飾的代碼塊中,并且要使用通路控制符進行通路控制。其原則同其它函數一樣:預設是 private 的,如果要在外部通路,就應該是 public slots;如果隻需要在子類通路,就應該是 protected slots。

main()

函數中,

QObject::connect()

函數,第二、第四個參數需要使用

SIGNAL

SLOT

這兩個宏轉換成字元串(具體事宜我們在上一節介紹過)。注意

SIGNAL

SLOT

的宏參數并不是取函數指針,而是除去傳回值的函數聲明,并且 const 這種參數修飾符是忽略不計的。

下面說明另外一點,我們提到了“槽函數是普通的成員函數,作為成員函數,會受到 public、private、protected 的影響”,public、private 這些修飾符是供編譯器在編譯期檢查的,是以其影響在于編譯期。對于 Qt4 的信号槽連接配接文法,其連接配接是在運作時完成的,是以即便是 private 的槽函數也是可以作為槽進行連接配接的。但是,如果你使用了 Qt5 的新文法,新文法提供了編譯期檢查(取函數指針),是以取 private 函數的指針是不能通過編譯的。

繼續閱讀