C++ ——Qt的訊號和槽的詳解
1、概述
訊號槽是 Qt 框架引以為豪的機制之一。所謂訊號槽,實際就是觀察者模式。當某個事件發生之後,比如,按鈕檢測到自己被點選了一下,它就會發出一個訊號(signal)。這種發出是沒有目的的,類似廣播。如果有物件對這個訊號感興趣,它就會使用連線(connect)函式,意思是,將想要處理的訊號和自己的一個函式(稱為槽(slot))繫結來處理這個訊號。也就是說,當訊號發出時,被連線的槽函式會自動被回撥。這就類似觀察者模式:當發生了感興趣的事件,某一個操作就會被自動觸發。(這裡提一句,Qt 的訊號槽使用了額外的處理來實現,並不是 GoF 經典的觀察者模式的實現方式。)
訊號和槽是Qt特有的資訊傳輸機制,是Qt設計程式的重要基礎,它可以讓互不干擾的物件建立一種聯絡。
槽的本質是類的成員函式,其引數可以是任意型別的。和普通C++成員函式幾乎沒有區別,它可以是虛擬函式;也可以被過載;可以是公有的、保護的、私有的、也可以被其他C++成員函式呼叫。唯一區別的是:槽可以與訊號連線在一起,每當和槽連線的訊號被髮射的時候,就會呼叫這個槽。
1.1物件樹(子物件動態分配空間不需要釋放)
參考連線:https://blog.csdn.net/fzu_dianzi/article/details/6949081
Qt提供了一種機制,能夠自動、有效的組織和管理繼承自QObject的Qt物件,這種機制就是物件樹。
Qt物件樹在使用者介面程式設計上是非常有用的。它能夠幫助程式設計師減輕記憶體洩露的壓力。
比如說當應用程式建立了一個具有父視窗部件的物件時,該物件將被加入父視窗部件的孩子列表。當應用程式銷燬父視窗部件時,其下的孩子列表中的物件將被一一刪除。這讓我們在程式設計時,能夠將主要精力放在系統的業務上,提高程式設計效率,同時也保證了系統的穩健性。
下面筆者將簡單分析物件樹。
程式碼驗證:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QDialog *dlg = new QDialog(0);
QPushButton *btn = new QPushButton(dlg);
qDebug() << "dlg = " << dlg;
qDebug() << "btn = " << btn;
dlg->exec(); delete btn;
qDebug() << "dlg = " << dlg; return 0;
}
dlg = QDialog(0x3ea1a0)
btn = QPushButton(0x3ea228)/*關閉視窗後,dlg = QDialog(0x3ea1a0)
這說明關閉視窗,不會銷燬該視窗部件,而是將其隱藏起來。
我們在qDebug() << "dlg = " << dlg;
之後加上
qDebug() << "btn = " << btn;
明顯的,我們之前已經delete btn,btn指標沒有被賦值為0,這是編譯器決定的。
執行程式後,必然出現段錯誤。
2、
將程式稍微修改下。*/int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QDialog *dlg = new QDialog(0);
QPushButton *btn = new QPushButton(dlg);
qDebug() << "dlg = " << dlg;
qDebug() << "btn = " << btn;
dlg->exec(); delete dlg;
qDebug() << "btn = " << btn;
return 0;
}
2、訊號和槽
為了體驗一下訊號槽的使用,我們以一段簡單的程式碼說明:
Qt5 的書寫方式:(推薦的使用)★★★★★
#include <QApplication>
#include <QPushButton>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QPushButton button("Quit");
QObject::connect(&button, &QPushButton::clicked,&app, &QApplication::quit);
button.show();
return app.exec();
}
我們按照前面文章中介紹的在 Qt Creator 中建立工程的方法建立好工程,然後將main()函式修改為上面的程式碼。點選執行,我們會看到一個按鈕,上面有“Quit”字樣。點選按鈕,程式退出。
connect()函式最常用的一般形式:
connect(sender, signal, receiver, slot);
引數:
sender:發出訊號的物件
signal:傳送物件發出的訊號
receiver:接收訊號的物件
slot:接收物件在接收到訊號之後所需要呼叫的函式
訊號槽要求訊號和槽的引數一致,所謂一致,是引數型別一致。如果不一致,允許的情況是,槽函式的引數可以比訊號的少,即便如此,槽函式存在的那些引數的順序也必須和訊號的前面幾個一致起來。這是因為,你可以在槽函式中選擇忽略訊號傳來的資料(也就是槽函式的引數比訊號的少),但是不能說訊號根本沒有這個資料,你就要在槽函式中使用(就是槽函式的引數比訊號的多,這是不允許的)。
如果訊號槽不符合,或者根本找不到這個訊號或者槽函式,比如我們改成:
connect(&button, &QPushButton::clicked, &QApplication::quit2);
由於 QApplication 沒有 quit2 這樣的函式,因此在編譯時會有編譯錯誤:
'quit2' is not a member of QApplication
這樣,使用成員函式指標我們就不會擔心在編寫訊號槽的時候出現函式錯誤。
Qt4 的書寫方式:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QPushButton *button = new QPushButton("Quit");
connect(button, SIGNAL(clicked()), &a, SLOT(quit()));
button->show();
return a.exec();
}
這裡使用了SIGNAL和SLOT這兩個巨集,將兩個函式名轉換成了字串。注意到connect()函式的 signal 和 slot 都是接受字串,一旦出現連線不成功的情況,Qt4是沒有編譯錯誤的(因為一切都是字串,編譯期是不檢查字串是否匹配),而是在執行時給出錯誤。這無疑會增加程式的不穩定性。
Qt5在語法上完全相容Qt4
小總結:
1>. 格式: connect(訊號發出者物件(指標), &className::clicked, 訊號接收者物件(指標), &classB::slot);
2>. 標準訊號槽的使用:
connect(sender, &Send::signal, receiver, &Receiver::slot)
3、自定義訊號槽
使用connect()可以讓我們連線系統提供的訊號和槽。但是,Qt 的訊號槽機制並不僅僅是使用系統提供的那部分,還會允許我們自己設計自己的訊號和槽。
下面我們看看使用 Qt 的訊號槽,實現一個報紙和訂閱者的例子:
有一個報紙類Newspaper,有一個訂閱者類Subscriber。Subscriber可以訂閱Newspaper。這樣,當Newspaper有了新的內容的時候,Subscriber可以立即得到通知。
#include <QObject>
////////// newspaper.h //////////
class Newspaper : public QObject
{
Q_OBJECTpublic:
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_OBJECTpublic:
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。不管是不是使用訊號槽,都應該新增這個巨集。這個巨集的展開將為我們的類提供訊號槽機制、國際化機制以及 Qt 提供的不基於 C++ RTTI 的反射能力。
● Newspaper類的 public 和 private 程式碼塊都比較簡單,只不過它新加了一個 signals。signals 塊所列出的,就是該類的訊號。訊號就是一個個的函式名,返回值是 void(因為無法獲得訊號的返回值,所以也就無需返回任何值),引數是該類需要讓外界知道的資料。訊號作為函式名,不需要在 cpp 函式中新增任何實現。
●Newspaper類的send()函式比較簡單,只有一個語句emit newPaper(m_name);。emit 是 Qt 對 C++ 的擴充套件,是一個關鍵字(其實也是一個巨集)。emit 的含義是發出,也就是發出newPaper()訊號。感興趣的接收者會關注這個訊號,可能還需要知道是哪份報紙發出的訊號?所以,我們將實際的報紙名字m_name當做引數傳給這個訊號。當接收者連線這個訊號時,就可以通過槽函式獲得實際值。這樣就完成了資料從發出者到接收者的一個轉移。
● Reader類更簡單。因為這個類需要接受訊號,所以我們將其繼承了QObject,並且添加了Q_OBJECT巨集。後面則是預設建構函式和一個普通的成員函式。Qt 5 中,任何成員函式、static 函式、全域性函式和 Lambda 表示式都可以作為槽函式。與訊號函式不同,槽函式必須自己完成實現程式碼。槽函式就是普通的成員函式,因此作為成員函式,也會受到 public、private 等訪問控制符的影響。(如果訊號是 private 的,這個訊號就不能在類的外面連線,也就沒有任何意義。)
3.1自定義訊號槽需要注意的事項
●傳送者和接收者都需要是QObject的子類(當然,槽函式是全域性函式、Lambda 表示式等無需接收者的時候除外);
●使用 signals 標記訊號函式,訊號是一個函式宣告,返回 void,不需要實現函式程式碼;
●槽函式是普通的成員函式,作為成員函式,會受到 public、private、protected 的影響;
●使用 emit 在恰當的位置傳送訊號;
●使用QObject::connect()函式連線訊號和槽。
●任何成員函式、static 函式、全域性函式和 Lambda 表示式都可以作為槽函式
3.2訊號槽的更多用法
●一個訊號可以和多個槽相連
如果是這種情況,這些槽會一個接一個的被呼叫,但是它們的呼叫順序是不確定的。
●多個訊號可以連線到一個槽
只要任意一個訊號發出,這個槽就會被呼叫。
●一個訊號可以連線到另外的一個訊號
當第一個訊號發出時,第二個訊號被髮出。除此之外,這種訊號-訊號的形式和訊號-槽的形式沒有什麼區別。
●槽可以被取消連結
這種情況並不經常出現,因為當一個物件delete之後,Qt自動取消所有連線到這個物件上面的槽。
●使用Lambda 表示式
在使用 Qt 5 的時候,能夠支援 Qt 5 的編譯器都是支援 Lambda 表示式的。
我們的程式碼可以寫成下面這樣:
QObject::connect(&newspaper, static_cast
(const QString &)>(&Newspaper::newPaper),
[=](const QString &name)
{ /* Your code here. */ }
);
在連線訊號和槽的時候,槽函式可以使用Lambda表示式的方式進行處理。
4、Lambda表示式
C++11中的Lambda表示式用於定義並建立匿名的函式物件,以簡化程式設計工作。首先看一下Lambda表示式的基本構成:
[函式物件引數](操作符過載函式引數)mutable或exception ->返回值{函式體}
①函式物件引數;
[],標識一個Lambda的開始,這部分必須存在,不能省略。函式物件引數是傳遞給編譯器自動生成的函式物件類的建構函式的。函式物件引數只能使用那些到定義Lambda為止時Lambda所在作用範圍內可見的區域性變數(包括Lambda所在類的this)。函式物件引數有以下形式:
▲空。沒有使用任何函式物件引數。
▲=。函式體內可以使用Lambda所在作用範圍內所有可見的區域性變數(包括Lambda所在類的this),並且是值傳遞方式(相當於編譯器自動為我們按值傳遞了所有區域性變數)。
▲&。函式體內可以使用Lambda所在作用範圍內所有可見的區域性變數(包括Lambda所在類的this),並且是引用傳遞方式(相當於編譯器自動為我們按引用傳遞了所有區域性變數)。
▲ this。函式體內可以使用Lambda所在類中的成員變數。
▲ a。將a按值進行傳遞。按值進行傳遞時,函式體內不能修改傳遞進來的a的拷貝,因為預設情況下函式是const的。要修改傳遞進來的a的拷貝,可以新增mutable修飾符。
▲ &a。將a按引用進行傳遞。
▲ a, &b。將a按值進行傳遞,b按引用進行傳遞。
▲ =,&a, &b。除a和b按引用進行傳遞外,其他引數都按值進行傳遞。
▲ &, a, b。除a和b按值進行傳遞外,其他引數都按引用進行傳遞。
int m = 0, n = 0;
[=] (int a) mutable { m = ++n + a; }(4);
[&] (int a) { m = ++n + a; }(4);
[=,&m] (int a) mutable { m = ++n + a; }(4);
[&,m] (int a) mutable { m = ++n + a; }(4);
[m,n] (int a) mutable { m = ++n + a; }(4);
[&m,&n] (int a) { m = ++n + a; }(4);
② 操作符過載函式引數;
標識過載的()操作符的引數,沒有引數時,這部分可以省略。引數可以通過按值(如:(a,b))和按引用(如:(&a,&b))兩種方式進行傳遞。
③ 可修改標示符;
mutable宣告,這部分可以省略。按值傳遞函式物件引數時,加上mutable修飾符後,可以修改按值傳遞進來的拷貝(注意是能修改拷貝,而不是值本身)。
④ 錯誤丟擲標示符;
exception宣告,這部分也可以省略。exception宣告用於指定函式丟擲的異常,如丟擲整數型別的異常,可以使用throw(int)
⑤ 函式返回值;
->返回值型別,標識函式返回值的型別,當返回值為void,或者函式體中只有一處return的地方(此時編譯器可以自動推斷出返回值型別)時,這部分可以省略。
⑥ 是函式體;
{},標識函式的實現,這部分不能省略,但函式體可以為空。
一個好的學習環境能營造出一種好的學習氛圍,大家互相討論,亦師亦友,為了同一個夢想前進,這是一件浪漫並且熱血的事,如果你是C/C++的愛好者,喜歡或者想要學習,那麼一個學習基地適合你,歡迎每一位想要學習、學好C/C++的朋友。(見評論區),大量資源、乾貨、大佬離你更近。