一、初識信号槽
所謂信号槽,實際就是觀察者模式。當某個事件發生之後,比如,按鈕檢測到自己被點選了一下,它就會發出一個信号(signal)。這種發出是沒有目的的,類似廣播。如果有對象對這個信号感興趣,它就會使用連接配接(connect)函數,意思是,用自己的一個函數(成為槽(slot))來處理這個信号。也就是說,當信号發出時,被連接配接的槽函數會自動被回調。這就類似觀察者模式:當發生了感興趣的事件,某一個操作就會被自動觸發。
#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();
}
以上面這段代碼為例體驗信号槽的使用

點選Quit即可退出。按鈕在 Qt 中被稱為
QPushButton
。
下面了解一下
QObject::connect()
這個函數。
connect()
函數最常用的一般形式:
connect(sender, signal,
receiver, slot);
connect()
一般會使用前面四個參數,第一個是發出信号的對象,第二個是發送對象發出的信号,第三個是接收信号的對象,第四個是接收對象在接收到信号之後所需要調用的函數。也就是說,當 sender 發出了 signal 信号之後,會自動調用 receiver 的 slot 函數。
在 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);
第一個,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()
函數,使程式退出。
信号槽要求信号和槽的參數類型一緻。如果不一緻,允許的情況是,槽函數的參數可以比信号的少,即便如此,槽函數存在的那些參數的順序也必須和信号的前面幾個一緻起來。
二、自定義信号槽
Qt 的信号槽機制并不僅僅是使用系統提供的那部分,還會允許我們自己設計自己的信号和槽。
下面是通過信号槽實作觀察者模式的一個例子:
經典的觀察者模式在講解舉例的時候通常會舉報紙和訂閱者的例子。有一個報紙類
Newspaper
,有一個訂閱者類
Subscriber
。
Subscriber
可以訂閱
Newspaper
。這樣,當
Newspaper
有了新的内容的時候,
Subscriber
可以立即得到通知。在這個例子中,觀察者是
Subscriber
,被觀察者是
Newspaper
。在經典的實作代碼中,觀察者會将自身注冊到被觀察者的一個容器中(比如
subscriber.registerTo(newspaper)
)。被觀察者發生了任何變化的時候,會主動周遊這個容器,依次通知各個觀察者(
newspaper.notifyAllSubscribers()
)。
#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();
}
首先看
Newspaper
這個類。這個類繼承了
QObject
類。隻有繼承了
QObject
類的類,才具有信号槽的能力。是以,為了使用信号槽,必須繼承
QObject
。凡是
QObject
類(不管是直接子類還是間接子類),都應該在第一行代碼寫上
Q_OBJECT
。不管是不是使用信号槽,都應該添加這個宏。注意,這個宏将由 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"
就可以了。不過,這是相當繁瑣的步驟,為了避免這樣修改,我們還是将其放在頭檔案中。
Newspaper
類的 public 和 private 代碼塊都比較簡單,隻不過它新加了一個 signals。signals 塊所列出的,就是該類的信号。信号就是一個個的函數名,傳回值是 void(因為無法獲得信号的傳回值,是以也就無需傳回任何值),參數是該類需要讓外界知道的資料。
Reader
類更簡單。因為這個類需要接受信号,是以我們将其繼承了
QObject
,并且添加了
Q_OBJECT
宏。後面則是預設構造函數和一個普通的成員函數。Qt 5 中,任何成員函數、static 函數、全局函數和 Lambda 表達式都可以作為槽函數。與信号函數不同,槽函數必須自己完成實作代碼。槽函數就是普通的成員函數,是以作為成員函數,也會受到 public、private 等通路控制符的影響。(我們沒有說信号也會受此影響,事實上,如果信号是 private 的,這個信号就不能在類的外面連接配接,也就沒有任何意義。)
main()
函數中,我們首先建立了
Newspaper
和
Reader
兩個對象,然後使用
QObject::connect()
函數。這個函數我們上一節已經詳細介紹過,這裡應該能夠看出這個連接配接的含義。然後我們調用
Newspaper
的
send()
函數。這個函數隻有一個語句:發出信号。由于我們的連接配接,當這個信号發出時,自動調用 reader 的槽函數,列印出語句。
下面總結一下自定義信号槽需要注意的事項:
- 發送者和接收者都需要是
的子類(當然,槽函數是全局函數、Lambda 表達式等無需接收者的時候除外);QObject
- 使用 signals 标記信号函數,信号是一個函數聲明,傳回 void,不需要實作函數代碼;
- 槽函數是普通的成員函數,作為成員函數,會受到 public、private、protected 的影響;
- 使用 emit 在恰當的位置發送信号;
- 使用
函數連接配接信号和槽。QObject::connect()