1. 程式人生 > 實用技巧 >[訊號槽]-Qt訊號和槽機制

[訊號槽]-Qt訊號和槽機制

目錄

推薦參考
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.概述

基礎介紹,很推薦 IBM的老文章
訊號和槽機制是 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);

4.3自定義一個槽

public slots:
    void setValue(int value);
// 自定義槽函式 實現
void Student::treat()
{
 qDebug() << "您好 我是槽函式的實現!";
}

4.4 同名訊號和同名槽函式(允許有過載的訊號和槽函式哦)

過載的訊號和槽函式 ,需要利用函式指標來指向函式地址來確定究竟是需要哪一個訊號和槽函式。(TODO)

4.5自定義訊號槽需要注意的事項

  • 傳送者和接收者都需要是QObject的子類(當然,槽函式是全域性函式、Lambda 表示式等無需接收者的時候除外);
  • 訊號和槽函式返回值是 void --TODO:訊號是必須為void,槽是否必須為void有待考察
  • 訊號只需要宣告,不需要實現,只有Qt類才能定義訊號,且必須在標頭檔案中宣告。返回值必須為void型別
  • 傳送訊號時,只需要通過emit關鍵字呼叫訊號函式即可
  • 訊號函式的屬性會被自動設定為protected型別
  • 槽函式需要宣告也需要實現
  • 槽函式是普通的成員函式,作為成員函式,會受到 public、private、protected 的影響;
  • 使用connect()函式連線訊號和槽。
  • 任何成員函式、static 函式、全域性函式和 Lambda 表示式都可以作為槽函式
  • 訊號槽要求訊號和槽的引數一致,所謂一致,是引數型別一致。
  • 如果訊號函式的引數多於槽函式時,多於的引數將被忽略
  • 如果訊號和槽的引數不一致,允許的情況是,槽函式的引數可以比訊號的少,即便如此,槽函式存在的那些引數的順序也必須和訊號的前面幾個一致起來。這是因為,你可以在槽函式中選擇忽略訊號傳來的資料(也就是槽函式的引數可以比訊號的少)。

4.7訊號與槽擴充套件

  • 一個訊號可以和多個槽相連
    如果是這種情況,這些槽會一個接一個的被呼叫,但是它們的呼叫順序是不確定的。

  • 多個訊號可以連線到一個槽
    只要任意一個訊號發出,這個槽就會被呼叫。

  • 一個訊號可以連線到另外的一個訊號
    當第一個訊號發出時,第二個訊號被髮出。除此之外,這種訊號-訊號的形式和訊號-槽的形式沒有什麼區別。

  • 槽可以被取消連線
    這種情況並不經常出現,因為當一個物件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.QObject::connect函式的第5個引數(訊號與槽的連線方式)

[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. 這個標誌可以和上述標誌通過或OR來結合使用。用於失能已經存在的connection

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 使用建議

那麼如何使用呢?

  • 如果是在同一執行緒裡面的操作(signalslot都在同一個執行緒),那麼用Qt::DirectConnection的效率最高(使用預設值Qt::AutoConnection也OK),主要是Qt::DirectConnectionQt::QueuedConnection都需要儲存到佇列。
  • 如果是多個執行緒之間進行訊息傳遞(signalslot都在不同執行緒),那麼就要用到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,而反之是不可以的。