#(5)自定義信號槽
上一節我們詳細分析了`connect()`函數。使用`connect()`可以讓我們連接系統提供的信號和槽。但是,Qt 的信號槽機制并不僅僅是使用系統提供的那部分,還會允許我們自己設計自己的信號和槽。這也是 Qt 框架的設計思路之一,用于我們設計解耦的程序。本節將講解如何在自己的程序中自定義信號槽。
信號槽不是 GUI 模塊提供的,而是 Qt 核心特性之一。因此,我們可以在普通的控制臺程序使用信號槽。
經典的觀察者模式在講解舉例的時候通常會舉報紙和訂閱者的例子。有一個報紙類`Newspaper`,有一個訂閱者類`Subscriber`。`Subscriber`可以訂閱`Newspaper`。這樣,當`Newspaper`有了新的內容的時候,`Subscriber`可以立即得到通知。在這個例子中,觀察者是`Subscriber`,被觀察者是`Newspaper`。在經典的實現代碼中,觀察者會將自身注冊到被觀察者的一個容器中(比如`subscriber.registerTo(newspaper)`)。被觀察者發生了任何變化的時候,會主動遍歷這個容器,依次通知各個觀察者(`newspaper.notifyAllSubscribers()`)。
下面我們看看使用 Qt 的信號槽,如何實現上述觀察者模式。注意,這里我們僅僅是使用這個案例,我們的代碼并不是去實現一個經典的觀察者模式。也就是說,我們使用 Qt 的信號槽機制來獲得同樣的效果。
~~~
//!!! 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 中相應的代碼:
~~~
//!!! 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 函數的指針是不能通過編譯的。
- (1)序
- (2)Qt 簡介
- (3)Hello, world!
- (4)信號槽
- (5)自定義信號槽
- (6)Qt 模塊簡介
- (7)MainWindow 簡介
- (8)添加動作
- (9)資源文件
- (10)對象模型
- (11)布局管理器
- (12)菜單欄、工具欄和狀態欄
- (13)對話框簡介
- (14)對話框數據傳遞
- (15)標準對話框 QMessageBox
- (16)深入 Qt5 信號槽新語法
- (17)文件對話框
- (18)事件
- (19)事件的接受與忽略
- (21)事件過濾器
- (22)事件總結
- (23)自定義事件
- (24)Qt 繪制系統簡介
- (25)畫刷和畫筆
- (26)反走樣
- (27)漸變
- (28)坐標系統
- (29)繪制設備
- (30)Graphics View Framework
- (31)貪吃蛇游戲(1)
- (32)貪吃蛇游戲(2)
- (33)貪吃蛇游戲(3)
- (34)貪吃蛇游戲(4)
- (35)文件
- (36)二進制文件讀寫
- (37)文本文件讀寫
- (38)存儲容器
- (39)遍歷容器
- (40)隱式數據共享
- (41)model/view 架構
- (42)QListWidget、QTreeWidget 和 QTableWidget
- (43)QStringListModel
- (44)QFileSystemModel
- (45)模型
- (46)視圖和委托
- (47)視圖選擇
- (48)QSortFilterProxyModel
- (49)自定義只讀模型
- (50)自定義可編輯模型
- (51)布爾表達式樹模型
- (52)使用拖放
- (53)自定義拖放數據
- (54)剪貼板
- (55)數據庫操作
- (56)使用模型操作數據庫
- (57)可視化顯示數據庫數據
- (58)編輯數據庫外鍵
- (59)使用流處理 XML
- (60)使用 DOM 處理 XML
- (61)使用 SAX 處理 XML
- (62)保存 XML
- (63)使用 QJson 處理 JSON
- (64)使用 QJsonDocument 處理 JSON
- (65)訪問網絡(1)
- (66)訪問網絡(2)
- (67)訪問網絡(3)
- (68)訪問網絡(4)
- (69)進程
- (70)進程間通信
- (71)線程簡介
- (72)線程和事件循環
- (73)Qt 線程相關類
- (74)線程和 QObject
- (75)線程總結
- (76)QML 和 QtQuick 2
- (77)QML 語法
- (78)QML 基本元素
- (79)QML 組件
- (80)定位器
- (81)元素布局
- (82)輸入元素
- (83)Qt Quick Controls
- (84)Repeater
- (85)動態視圖
- (86)視圖代理
- (87)模型-視圖高級技術
- (88)Canvas
- (89)Canvas(續)