#(16):深入 Qt5 信號槽新語法
在前面的章節([信號槽](http://www.devbean.net/2012/08/qt-study-road-2-signal-slot/ "Qt 學習之路 2:信號槽")和[自定義信號槽](http://www.devbean.net/2012/08/qt-study-road-2-custom-signal-slot/ "Qt 學習之路 2:自定義信號槽"))中,我們詳細介紹了有關 Qt 5 的信號槽新語法。由于這次改動很大,許多以前看起來不是問題的問題接踵而來,因此,我們用單獨的一章重新介紹一些 Qt 5 的信號槽新語法。
## 基本用法
Qt 5 引入了信號槽的新語法:使用函數指針能夠獲得編譯期的類型檢查。使用我們在[自定義信號槽](http://www.devbean.net/2012/08/qt-study-road-2-custom-signal-slot/ "Qt 學習之路 2:自定義信號槽")中設計的`Newspaper`類,我們來看看其基本語法:
~~~
//!!! Qt5
#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() {}
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, &Newspaper::newPaper,
&reader, &Reader::receiveNewspaper);
newspaper.send();
return app.exec();
}
~~~
在`main()`函數中,我們使用`connect()`函數將`newspaper`對象的`newPaper()`信號與`reader`對象的`receiveNewspaper()`槽函數聯系起來。當`newspaper`發出這個信號時,`reader`相應的槽函數就會自動被調用。這里我們使用了取址操作符,取到`Newspaper::newPaper()`信號的地址,同樣類似的取到了`Reader::receiveNewspaper()`函數地址。編譯器能夠利用這兩個地址,在編譯期對這個連接操作進行檢查,如果有個任何錯誤(包括對象沒有這個信號,或者信號參數不匹配等),編譯時就會發現。
## 有重載的信號
如果信號有重載,比如我們向`Newspaper`類增加一個新的信號:
~~~
void newPaper(const QString &name, const QDate &date);
~~~
此時如果還是按照前面的寫法,編譯器會報出一個錯誤:由于這個函數(注意,信號實際也是一個普通的函數)有重載,因此不能用一個取址操作符獲取其地址。回想一下 Qt 4 中的處理。在 Qt 4 中,我們使用`SIGNAL`和`SLOT`兩個宏來連接信號槽。如果有一個帶有兩個參數的信號,像上面那種,那么,我們就可以使用下面的代碼:
~~~
QObject::connect(&newspaper, SIGNAL(newPaper(QString, QDate)),
&reader, SLOT(receiveNewspaper(QString, QDate)));
~~~
注意,我們臨時增加了一個`receiveNewspaper()`函數的重載,以便支持兩個參數的信號。在 Qt 4 中不存在我們所說的錯誤,因為 Qt 4 的信號槽連接是帶有參數的。因此,Qt 能夠自己判斷究竟是哪一個信號對應了哪一個槽。
對此,我們也給出了一個解決方案,使用一個函數指針來指明到底是哪一個信號:
~~~
void (Newspaper:: *newPaperNameDate)(const QString &, const QDate &) = &Newspaper::newPaper;
QObject::connect(&newspaper, newPaperNameDate,
&reader, &Reader::receiveNewspaper);
~~~
這樣,我們使用了函數指針`newspaperNameDate`聲明一個帶有`QString`和`QDate`兩個參數,返回值是 void 的函數,將該函數作為信號,與`Reader::receiveNewspaper()`槽連接起來。這樣,我們就回避了之前編譯器的錯誤。歸根結底,這個錯誤是因為函數重載,編譯器不知道要取哪一個函數的地址,而我們顯式指明一個函數就可以了。
如果你覺得這種寫法很難看,想像前面一樣寫成一行,當然也是由解決方法的:
~~~
QObject::connect(&newspaper,
(void (Newspaper:: *)(const QString &, const QDate &))&Newspaper::newPaper,
&reader,
&Reader::receiveNewspaper);
~~~
這是一種換湯不換藥的做法:我們只是聲明了一個匿名的函數指針,而之前我們的函數指針是有名字的。不過,我們并不推薦這樣寫,而是希望以下的寫法:
~~~
QObject::connect(&newspaper,
static_cast<void (Newspaper:: *)(const QString &, const QDate &)>(&Newspaper::newPaper),
&reader,
&Reader::receiveNewspaper);
~~~
對比上面兩種寫法。第一個使用的是 C 風格的強制類型轉換。此時,如果你改變了信號的類型,那么你就會有一個潛在的運行時錯誤。例如,如果我們把`(const QString &, const QDate &)`兩個參數修改成`(const QDate &, const QString &)`,C 風格的強制類型轉換就會失敗,并且這個錯誤只能在運行時發現。而第二種則是 C++ 推薦的風格,當參數類型改變時,編譯器會檢測到這個錯誤。
注意,這里我們只是強調了函數參數的問題。如果前面的對象都錯了呢?比如,我們寫的`newspaper`對象并不是一個`Newspaper`,而是`Newspaper2`?此時,編譯器會直接失敗,因為`connect()`函數會去尋找`sender->*signal`,如果這兩個參數不滿足,則會直接報錯。
## 帶有默認參數的槽函數
Qt 允許信號和槽的參數數目不一致:槽函數的參數數目可以比信號的參數少。這是因為,我們信號的參數實際是作為一種返回值。正如普通的函數調用一樣,我們可以選擇忽略函數返回值,但是不能使用一個并不存在的返回值。如果槽函數的參數數目比信號的多,在槽函數中就使用到這些參數的時候,實際這些參數并不存在(因為信號的參數比槽的少,因此并沒有傳過來),函數就會報錯。這種情況往往有兩個原因:一是槽的參數就是比信號的少,此時我們可以像前面那種寫法直接連接。另外一個原因是,信號的參數帶有默認值。比如
~~~
void QPushButton::clicked(bool checked = false)
~~~
就是這種情況。
然而,有一種情況,槽函數的參數可以比信號的多,那就是槽函數的參數帶有默認值。比如,我們的`Newspaper`和`Reader`有下面的代碼:
~~~
// Newspaper
signals:
void newPaper(const QString &name);
// Reader
void receiveNewspaper(const QString &name, const QDate &date = QDate::currentDate());
~~~
雖然`Reader::receiveNewspaper()`的參數數目比`Newspaper::newPaper()`多,但是由于`Reader::receiveNewspaper()`后面一個參數帶有默認值,所以該參數不是必須提供的。但是,如果你按照前面的寫法,比如如下的代碼:
~~~
QObject::connect(&newspaper,
static_cast<void (Newspaper:: *)(const QString &)>(&Newspaper::newPaper),
&reader,
static_cast<void (Reader:: *)(const QString &, const QDate & =QDate::currentDate())>(&Reader::receiveNewspaper));
~~~
你會得到一個斷言錯誤:
~~~
The slot requires more arguments than the signal provides.
~~~
我們不能在函數指針中使用函數參數的默認值。這是 C++ 語言的限制:**參數默認值只能使用在直接地函數調用中。當使用函數指針取其地址的時候,默認參數是不可見的!**
當然,此時你可以選擇 Qt 4 的連接語法。如果你還是想使用 Qt 5 的新語法,目前的辦法只有一個:Lambda 表達式。不要擔心你的編譯器不支持 Lambda 表達式,因為在你使用 Qt 5 的時候,能夠支持 Qt 5 的編譯器都是支持 Lambda 表達式的。于是,我們的代碼就變成了:
~~~
QObject::connect(&newspaper,
static_cast<void (Newspaper:: *)(const QString &)>(&Newspaper::newPaper),
[=](const QString &name) { /* Your code here. */ });
~~~
- (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(續)