#(4)信號槽
信號槽是 Qt 框架引以為豪的機制之一。熟練使用和理解信號槽,能夠設計出解耦的非常漂亮的程序,有利于增強我們的技術設計能力。
所謂信號槽,實際就是觀察者模式。當某個事件發生之后,比如,按鈕檢測到自己被點擊了一下,它就會發出一個信號(signal)。這種發出是沒有目的的,類似廣播。如果有對象對這個信號感興趣,它就會使用連接(connect)函數,意思是,用自己的一個函數(成為槽(slot))來處理這個信號。也就是說,當信號發出時,被連接的槽函數會自動被回調。這就類似觀察者模式:當發生了感興趣的事件,某一個操作就會被自動觸發。*(這里提一句,Qt 的信號槽使用了額外的處理來實現,并不是 GoF 經典的觀察者模式的實現方式。)*
為了體驗一下信號槽的使用,我們以一段簡單的代碼說明:
~~~
// !!! Qt 5
#include <QApplication>
#include <QPushButton>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QPushButton button("Quit");
QObject::connect(&button, &QPushButton::clicked, &QApplication::quit);
button.show();
return app.exec();
}
~~~
這里再次強調,我們的代碼是以 Qt 5 為主線,這意味著,有的代碼放在 Qt 4 上是不能編譯的。因此,豆子會在每一段代碼的第一行添加注釋,用以表明該段代碼是使用 Qt 5 還是 Qt 4 進行編譯。讀者在測試代碼的時候,需要自行選擇相應的 Qt 版本。
我們按照前面文章中介紹的在 Qt Creator 中創建工程的方法創建好工程,然后將`main()`函數修改為上面的代碼。點擊運行,我們會看到一個按鈕,上面有“Quit”字樣。點擊按鈕,程序退出。
按鈕在 Qt 中被稱為`QPushButton`。對它的創建和顯示,同前文類似,這里不做過多的講解。我們這里要仔細分析`QObject::connect()`這個函數。
在 Qt 5 中,`QObject::connect()`有五個重載:
~~~
QMetaObject::Connection connect(const QObject *, const char *,
const QObject *, const char *,
Qt::ConnectionType);
QMetaObject::Connection connect(const QObject *, const QMetaMethod &,
const QObject *, const QMetaMethod &,
Qt::ConnectionType);
QMetaObject::Connection connect(const QObject *, const char *,
const char *,
Qt::ConnectionType) const;
QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
const QObject *, PointerToMemberFunction,
Qt::ConnectionType)
QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
Functor);
~~~
這五個重載的返回值都是`QMetaObject::Connection`,現在我們不去關心這個返回值。下面我們先來看看`connect()`函數最常用的一般形式:
~~~
// !!! Qt 5
connect(sender, signal,
receiver, slot);
~~~
這是我們最常用的形式。`connect()`一般會使用前面四個參數,第一個是發出信號的對象,第二個是發送對象發出的信號,第三個是接收信號的對象,第四個是接收對象在接收到信號之后所需要調用的函數。也就是說,當 sender 發出了 signal 信號之后,會自動調用 receiver 的 slot 函數。
這是最常用的形式,我們可以套用這個形式去分析上面給出的五個重載。第一個,sender 類型是`const QObject *`,signal 的類型是`const char *`,receiver 類型是`const QObject *`,slot 類型是`const char *`。這個函數將 signal 和 slot 作為字符串處理。第二個,sender 和 receiver 同樣是`const QObject *`,但是 signal 和 slot 都是`const QMetaMethod &`。我們可以將每個函數看做是`QMetaMethod`的子類。因此,這種寫法可以使用`QMetaMethod`進行類型比對。第三個,sender 同樣是`const QObject *`,signal 和 slot 同樣是`const char *`,但是卻缺少了 receiver。這個函數其實是將 this 指針作為 receiver。第四個,sender 和 receiver 也都存在,都是`const QObject *`,但是 signal 和 slot 類型則是`PointerToMemberFunction`。看這個名字就應該知道,這是指向成員函數的指針。第五個,前面兩個參數沒有什么不同,最后一個參數是`Functor`類型。這個類型可以接受 static 函數、全局函數以及 Lambda 表達式。
由此我們可以看出,`connect()`函數,sender 和 receiver 沒有什么區別,都是`QObject`指針;主要是 signal 和 slot 形式的區別。具體到我們的示例,我們的`connect()`函數顯然是使用的第五個重載,最后一個參數是`QApplication`的 static 函數`quit()`。也就是說,當我們的 button 發出了`clicked()`信號時,會調用`QApplication`的`quit()`函數,使程序退出。
信號槽要求信號和槽的參數一致,所謂一致,是參數類型一致。如果不一致,允許的情況是,槽函數的參數可以比信號的少,即便如此,槽函數存在的那些參數的順序也必須和信號的前面幾個一致起來。這是因為,你可以在槽函數中選擇忽略信號傳來的數據(也就是槽函數的參數比信號的少),但是不能說信號根本沒有這個數據,你就要在槽函數中使用(就是槽函數的參數比信號的多,這是不允許的)。
如果信號槽不符合,或者根本找不到這個信號或者槽函數的話,比如我們改成:
~~~
QObject::connect(&button, &QPushButton::clicked, &QApplication::quit2);
~~~
由于 QApplication 沒有 quit2 這樣的函數的,因此在編譯時,會有編譯錯誤:
~~~
'quit2' is not a member of QApplication
~~~
這樣,使用成員函數指針,我們就不會擔心在編寫信號槽的時候會出現函數錯誤。
借助 Qt 5 的信號槽語法,我們可以將一個對象的信號連接到 Lambda 表達式,例如:
~~~
// !!! Qt 5
#include <QApplication>
#include <QPushButton>
#include <QDebug>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QPushButton button("Quit");
QObject::connect(&button, &QPushButton::clicked, [](bool) {
qDebug() << "You clicked me!";
});
button.show();
return app.exec();
}
~~~
注意這里的 Lambda 表達式接收一個 bool 參數,這是因為`QPushButton`的`clicked()`信號實際上是有一個參數的。Lambda 表達式中的`qDebug()`類似于`cout`,將后面的字符串打印到標準輸出。如果要編譯上面的代碼,你需要在 pro 文件中添加這么一句:
~~~
QMAKE_CXXFLAGS += -std=c++0x
~~~
然后正常編譯即可。
Qt 4 的信號槽同 Qt 5 類似。在 Qt 4 的 QObject 中,有三個不同的`connect()`重載:
~~~
bool connect(const QObject *, const char *,
const QObject *, const char *,
Qt::ConnectionType);
bool connect(const QObject *, const QMetaMethod &,
const QObject *, const QMetaMethod &,
Qt::ConnectionType);
bool connect(const QObject *, const char *,
const char *,
Qt::ConnectionType) const
~~~
除了返回值,Qt 4 的`connect()`函數與 Qt 5 最大的區別在于,Qt 4 的 signal 和 slot 只有`const char *`這么一種形式。如果我們將上面的代碼修改為 Qt 4 的,則應該是這樣的:
~~~
// !!! Qt 4
#include <QApplication>
#include <QPushButton>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QPushButton button("Quit");
QObject::connect(&button, SIGNAL(clicked()),
&app, SLOT(quit()));
button.show();
return app.exec();
}
~~~
我們使用了`SIGNAL`和`SLOT`這兩個宏,將兩個函數名轉換成了字符串。注意,即使`quit()`是`QApplication`的 static 函數,也必須傳入一個對象指針。這也是 Qt 4 的信號槽語法的局限之處。另外,注意到`connect()`函數的 signal 和 slot 都是接受字符串,因此,不能將全局函數或者 Lambda 表達式傳入`connect()`。一旦出現連接不成功的情況,Qt 4 是沒有編譯錯誤的(因為一切都是字符串,編譯期是不檢查字符串是否匹配),而是在運行時給出錯誤。這無疑會增加程序的不穩定性。
信號槽機制是 Qt 的最大特性之一。這次我們只是初略了解了信號槽,知道了如何使用`connect()`函數進行信號槽的連接。在后面的內容中,我們將進一步介紹信號槽,了解如何設計自己的信號槽等等。
- (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(續)