上一節我們詳細分析了
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 的信号槽機制,不需要觀察者的容器,不需要注冊對象,就實作了觀察者模式。
下面總結一下自定義信号槽需要注意的事項:
- 發送者和接收者都需要是
的子類(當然,槽函數是全局函數、Lambda 表達式等無需接收者的時候除外);QObject
- 使用 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 函數的指針是不能通過編譯的。