推薦參考
https://doc.qt.io/qt-5/signalsandslots.html 官方文檔-- 最權威的介紹
https://www.devbean.net/2012/08/qt-study-road-2-catelog/ 豆子大神的博客
https://www.cnblogs.com/lifexy/p/8876016.html 簡潔明了
0.概述
信號和槽機制是 QT 的核心機制,要精通 QT 編程就必須對信號和槽有所了解。信號和槽是一種高級接口,應用於對象之間的通信,它是 QT 的核心特性,也是 QT 區別於其它工具包的重要地方。信號和槽是 QT 自行定義的一種通信機制,它獨立於標准的 C/C++ 語言,因此要正確的處理信號和槽,必須借助一個稱為 moc(Meta Object Compiler)的 QT 工具,該工具是一個 C++ 預處理程序,它為高層次的事件處理自動生成所需要的附加代碼。
在我們所熟知的很多 GUI 工具包中,窗口小部件 (widget) 都有一個回調函數用於響應它們能觸發的每個動作,這個回調函數通常是一個指向某個函數的指針。但是,在 QT 中信號和槽取代了這些凌亂的函數指針,使得我們編寫這些通信程序更為簡潔明了。 信號和槽能攜帶任意數量和任意類型的參數,他們是類型完全安全的,不會像回調函數那樣產生 core dumps。
所有從 QObject 或其子類 ( 例如 Qwidget) 派生的類都能夠包含信號和槽。當對象改變其狀態時,信號就由該對象發射 (emit) 出去,這就是對象所要做的全部事情,它不知道另一端是誰在接收這個信號。這就是真正的信息封裝,它確保對象被當作一個真正的軟件組件來使用。槽用於接收信號,但它們是普通的對象成員函數。一個槽並不知道是否有任何信號與自己相連接。而且,對象並不了解具體的通信機制。
你可以將很多信號與單個的槽進行連接,也可以將單個的信號與很多的槽進行連接,甚至於將一個信號與另外一個信號相連接也是可能的,這時無論第一個信號什么時候發射系統都將立刻發射第二個信號。總之,信號與槽構造了一個強大的部件編程機制。
1.信號槽機制
信號槽是 Qt 框架引以為豪的機制之一。所謂信號槽,實際就是觀察者模式。當某個事件發生之后,比如,按鈕檢測到自己被點擊了一下,它就會發出一個信號(signal)。這種發出是沒有目的的,類似廣播。如果有對象對這個信號感興趣,它就會使用連接(connect)函數,意思是,將想要處理的信號和自己的一個函數(稱為槽(slot))綁定來處理這個信號。也就是說,當信號發出時,被連接的槽函數會自動被回調。這就類似觀察者模式:當發生了感興趣的事件,某一個操作就會被自動觸發。
2.系統自帶的信號和槽
下面我們完成一個小功能,我們已經學習了按鈕的創建,但是還沒有體現出按鈕的功能,按鈕最大的功能也就是點擊后觸發一些事情,比如我們點擊按鈕,就把當前的窗口給關閉掉,那么在Qt中,這樣的功能如何實現呢?
其實兩行代碼就可以搞定了,我們看下面的代碼:
QPushButton * quitBtn = new QPushButton("關閉窗口",this);
connect(quitBtn,&QPushButton::clicked,this,&MyWidget::close);
第一行是創建一個關閉按鈕,第二行就是核心了,也就是信號槽的使用方式
connect()函數最常用的一般形式:
connect(sender, signal, receiver, slot);
參數解釋:
- sender:發出信號的對象
- signal:發送對象發出的信號
- receiver:接收信號的對象
- slot:接收對象在接收到信號之后所需要調用的函數(槽函數)
那么系統自帶的信號和槽通常如何查找呢,這個就需要利用幫助文檔了,在幫助文檔中比如我們上面的按鈕的點擊信號,在幫助文檔中輸入QPushButton,首先我們可以在Contents中尋找關鍵字 signals,信號的意思,但是我們發現並沒有找到,這時候我們應該想到也許這個信號的被父類繼承下來的,因此我們去他的父類QAbstractButton中就可以找到該關鍵字
// 摘抄自文檔
Signals
void clicked(bool checked = false)
void pressed()
void released()
void toggled(bool checked)
- 3 signals inherited from QWidget
- 2 signals inherited from QObject
這里的clicked就是我們要找到,槽函數的尋找方式和信號一樣,只不過他的關鍵字是slot。
3.使用信號槽所需要的條件
很簡單:類需要繼承自QObject,並且在類的開頭聲明Q_OBJECT宏. 下面看一下官方文檔的例子
#include <QObject>
class Counter : public QObject
{
Q_OBJECT
public:
Counter() { m_value = 0; }
int value() const { return m_value; }
// slots
public slots:
void setValue(int value);
// signal
signals:
void valueChanged(int newValue);
private:
int m_value;
};
4.自定義信號和自定義槽
使用connect()可以讓我們連接系統提供的信號和槽。但是,Qt 的信號槽機制並不僅僅是使用系統提供的那部分,還會允許我們自己設計自己的信號和槽。
4.1自定義一個信號
signals:
void valueChanged(int newValue); // 參數可有可無,看自己需求
// 信號不需要實現,也不能實現
4.2發送信號
發送信號需要使用Qt 增加的 emit關鍵字
emit valueChanged(value);
// demo
void Counter::setValue(int value)
{
if (value != m_value) {
m_value = value;
// emit 信號
emit valueChanged(value);
}
}
4.3自定義一個槽
public slots:
void processValue(int value);
// 自定義槽函數 需要聲明也需要實現
void Counter::processValue(int value){
// do something
}
4.4 同名信號和同名槽函數(允許有重載的信號和槽函數哦)
重載的信號和槽函數 ,需要利用函數指針來指向函數地址來確定究竟是需要哪一個信號和槽函數。(見下文,8.Qt5推薦的信號槽寫法)
4.5自定義信號槽需要注意的事項
- 發送者和接收者都需要是QObject的子類;
- 信號函數和槽函數返回值是 void。 信號返回值是必須為void,槽是否必須為void雖然都都能編譯和運行,但是還是建議返回值為void
- 信號只需要聲明,不需要實現,只有Qt類才能定義信號,且必須在頭文件中聲明。返回值必須為void類型
- 發送信號時,只需要通過emit關鍵字調用信號函數即可
- 信號函數的屬性會被自動設置為protected類型
- 槽函數需要聲明也需要實現
- 槽函數是普通的成員函數,作為成員函數,會受到 public、private、protected 的影響;
- 使用connect()函數連接信號和槽。
- 任何成員函數、static 函數、全局函數和 Lambda 表達式都可以作為槽函數
- 信號槽要求信號和槽的參數一致,所謂一致,是參數類型一致。
- 如果信號函數的參數多於槽函數時,多於的參數將被忽略
- 如果信號和槽的參數不一致,允許的情況是,槽函數的參數可以比信號的少,即便如此,槽函數存在的那些參數的順序也必須和信號的前面幾個一致起來。這是因為,你可以在槽函數中選擇忽略信號傳來的數據(也就是槽函數的參數可以比信號的少)。
4.6信號與槽擴展知識
-
一個信號可以和多個槽相連
如果是這種情況,這些槽會一個接一個的被調用,但是它們的調用順序是不確定的。 -
多個信號可以連接到一個槽
只要任意一個信號發出,這個槽就會被調用。 -
一個信號可以連接到另外的一個信號
當第一個信號發出時,第二個信號被發出。除此之外,這種信號-信號的形式和信號-槽的形式沒有什么區別。 -
槽可以被取消連接
這種情況並不經常出現,因為當一個對象delete之后,Qt自動取消所有連接到這個對象上面的槽。 -
信號槽可以斷開
利用disconnect關鍵字是可以斷開信號槽 -
使用Lambda 表達式
在使用 Qt 5 的時候,能夠支持 Qt 5 的編譯器都是支持 Lambda 表達式的。 在連接信號和槽的時候,槽函數可以使用Lambda表達式的方式進行處理。后面我們會詳細介紹什么是Lambda表達式
5.連接信號和槽QObject::connect函數
參考:
Qt 學習之路 2(4):信號槽
上面介紹過最常用的一種connect,下面我們看一下Qt提供的所有的connect。
這5個函數全是[static]
的,返回值這里先不討論。
我們可以直接使用QObject::connect來使用,一般我們直接使用connect多一點.
// 最常用的
QMetaObject::Connection QObject::connect(const QObject *sender,
const char *signal,
const QObject *receiver,
const char *method,
Qt::ConnectionType type = Qt::AutoConnection)
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);
6.信號與槽的連接方式(connect函數的Qt::ConnectionType的參數)
[static] QMetaObject::Connection QObject::connect(const QObject *sender,
const char *signal,
const QObject *receiver,
const char *method,
Qt::ConnectionType type = Qt::AutoConnection)
這個參數 Qt::ConnectionType type = Qt::AutoConnection
6.1 Qt::ConnectionType 介紹
參考:
enum Qt::ConnectionType
This enum describes the types of connection that can be used between signals and slots. In particular, it determines whether a particular signal is delivered to a slot immediately or queued for delivery at a later time.
Constant | Value | Description | 中文解釋 |
---|---|---|---|
Qt::AutoConnection |
0 |
(Default) If the receiver lives in the thread that emits the signal, Qt::DirectConnection is used. Otherwise, Qt::QueuedConnection is used. The connection type is determined when the signal is emitted. | 自動連接:(默認值)如果信號在接收者所依附的線程內發射,則等同於直接連接。如果發射信號的線程和接受者所依附的線程不同,則等同於隊列連接 |
Qt::DirectConnection |
1 |
The slot is invoked immediately when the signal is emitted. The slot is executed in the signalling thread. | 直接連接:當信號發射時,槽函數將直接被調用。無論槽函數所屬對象在哪個線程,槽函數都在發射信號的線程內執行。[這種方式不能跨線程傳遞消息] |
Qt::QueuedConnection |
2 |
The slot is invoked when control returns to the event loop of the receiver's thread. The slot is executed in the receiver's thread. | 隊列連接:當控制權回到接受者所依附線程的事件循環時,槽函數被調用。槽函數在接收者所依附線程執行。[這種方式既可以在線程內傳遞消息,也可以跨線程傳遞消息] |
Qt::BlockingQueuedConnection |
3 |
Same as Qt::QueuedConnection, except that the signalling thread blocks until the slot returns. This connection must not be used if the receiver lives in the signalling thread, or else the application will deadlock. | 與Qt::QueuedConnection 類似,但是發送消息后會阻塞,直到等到關聯的slot 都被執行。[說明它是專門用來多線程間傳遞消息的,而且是阻塞的] |
Qt::UniqueConnection |
0x80 |
This is a flag that can be combined with any one of the above connection types, using a bitwise OR. When Qt::UniqueConnection is set, QObject::connect() will fail if the connection already exists (i.e. if the same signal is already connected to the same slot for the same pair of objects). This flag was introduced in Qt 4.6. | 與默認工作方式相同,只是不能重復連接相同的信號和槽,因為如果重復連接就會導致一個信號發出,對應槽函數就會執行多次。這個flag可以通過按位或運算來和以上結合在一起使用。(意思就是:只有它不是一個重復連接,連接才會成功。如果之前已經有了一個連接(相同的信號連接到同一對象的同一個槽上),那么連接將會失敗並將返回false。) |
With queued connections, the parameters must be of types that are known to Qt's meta-object system, because Qt needs to copy the arguments to store them in an event behind the scenes. If you try to use a queued connection and get the error message:
QObject::connect: Cannot queue arguments of type 'MyType'
Call qRegisterMetaType() to register the data type before you establish the connection.
When using signals and slots with multiple threads, see Signals and Slots Across Threads.
See also Thread Support in Qt, QObject::connect(), qRegisterMetaType(), and Q_DECLARE_METATYPE().
6.2 使用建議
那么如何使用呢?
- 如果是在
同一線程
里面的操作(signal
和slot
都在同一個線程),那么用Qt::DirectConnection
的效率最高(使用默認值Qt::AutoConnection
也OK),主要是Qt::DirectConnection
和Qt::QueuedConnection
都需要儲存到隊列。 - 如果是
多個線程之間
進行消息傳遞(signal
和slot
都在不同線程),那么就要用到Qt::QueuedConnection
或者Qt::BlockingQueuedConnection
,不過一個是無阻塞的(Qt::QueuedConnection
),一個是阻塞的(Qt::BlockingQueuedConnection
,發送消息后會阻塞,直到所有的slot
都被執行)。
7.補充:Qt4版本的信號槽寫法
QObject::connect(scrollBar, SIGNAL(valueChanged(int)),
label, SLOT(setNum(int)));
// SIGNAL(信號函數名(參數類型))
// SLOT(槽的函數名(參數類型))
// Qt4 的寫法是不是更加的簡單和方便
這里使用了SIGNAL和SLOT這兩個宏,將兩個函數名轉換成了字符串。
注意到connect()函數的 signal 和 slot 都是接受字符串,一旦出現連接不成功的情況,Qt4是沒有編譯錯誤的(因為一切都是字符串,編譯期是不檢查字符串是否匹配),而是在運行時給出錯誤。這無疑會增加程序的不穩定性。所以盡量避免這種寫法。不過這種寫法,某些場合也能帶來便利。
注意: Qt5在語法上完全兼容Qt4,而反之是不可以的。
8.Qt5推薦的信號槽寫法
由於函數有重載(注意,信號實際也是一個普通的函數),因此對於有重載的函數不能用一個取址操作符獲取其地址。需要使用函數指針。
QObject::connect(&newspaper,
(void (Newspaper:: *)(const QString &, const QDate &))&Newspaper::newPaper,
&reader,
&Reader::receiveNewspaper);
C++11后最好這樣寫,高端大氣上檔次,就是有點麻煩,更加安全,麻煩點也是值得的。
QObject::connect(&newspaper,
static_cast<void (Newspaper:: *)(const QString &, const QDate &)>(&Newspaper::newPaper),
&reader,
&Reader::receiveNewspaper);
可以看出雖然Qt5 的信號槽的要求更加的多樣化,更加的准確化,同時也加大了寫法的難度。 但是寫的程序更加的健壯。
Qt4的信號槽寫法也可以繼續使用哦!