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++的朋友。(見評論區),大量資源、干貨、大佬離你更近。