Qt 裡面的訊號(Signal)和槽(Slot)雖然看著像事件,但它實際上是用來在兩個物件之間進行通訊的。既然是通訊,就會有傳送者和接收者。
1、訊號是傳送者,觸發時通過特有的關鍵字「emit」來發出訊號。
2、槽是訊號的接收者,它實則是一個方法(函數 )成員,當收到訊號後會被呼叫。
為了讓C++類別能夠使用訊號和槽機制,必須從 QObject 類派生。QObject 類是 Qt 物件的公共基礎類別。它的第一個作用是讓 Qt 物件之形成一株「物件樹」。當某個 Qt 物件發生解構時,它的子級物件都會發生解構。比如,視窗中包含兩個按鈕,當視窗類解構時,裡面的兩個按鈕也會跟著發生解構。所以,在 Qt 的視窗應用程式裡面,一般不用手動去 delete 指標型別的物件。位於物件樹上的各個物件會自動清理。
QObject 類的另一個關鍵作用是實現訊號和槽的功能。
1、從 QObject 類派生的類,在類內部要使用 Q_OBJECT 宏。
2、跟在 signals 關鍵字後面的函數被視為訊號。這個關鍵字實際上是 Q_SIGNALS 宏,是 Qt 專案專用的,並不是 C++ 的標準關鍵字。
3、跟在 slots 或 public slots 後面的成員函數(方法)被認為是槽,當接收到訊號時會自動呼叫。
訊號和槽之間相互不認識,需要找個「媒婆」讓它們走到一起。因此,在發出訊號前要呼叫 QObject :: connect 方法在訊號與槽之間建立連線。
老周不喜歡說得太複雜,上面的介紹應該算比較簡潔了,接下來咱們來個範例,就好理解了。
這裡老周定義了兩個類:DemoObject 類裡面包含了一個 QStack<int> 物件,是個棧集合,這個應該都懂,後進先出。兩個公共方法,AddOne 用來向 Stack 物件壓入元素,TakeOne 方法從 Stack 物件中彈出一個元素。不過,彈出的元素不是經 TakeOne 方法返回,而是發出 GetItem 訊號,用這個訊號將彈出的元素傳送給接收者(槽在 TestRecver 類中)。第二個類是 TestRecver,對,上面 DemoObject 類發出的 GetItem 訊號可以在 TestRecver 類中接收,槽函數是 setItem。
#include <iostream> #include <qobject.h> #include <qstack.h> class DemoObject : public QObject { // 這個是宏 Q_OBJECT private: QStack<int> _inner; public: void AddOne(int val) { _inner.push(val); } void TakeOne() { if(_inner.empty()){ return; } int x = _inner.pop(); // 發出訊號 emit GetItem(x); } // 訊號 signals: void GetItem(int n); }; class TestRecver : public QObject { // 記得用這個宏 Q_OBJECT // 槽 public slots: void setItem(int n) { std::cout << "取出項:" << n << std::endl; } };
在 main 函數中,先建立 DemoObject 範例,用 AddOne 方法壓入三個元素。然後建立 TestRecver 範例,用 connect 方法建立訊號和槽的連線。
int main(int argc, char **argv) { DemoObject a; a.AddOne(50); a.AddOne(74); a.AddOne(80); TestRecver r; // 訊號與槽連線 QObject::connect(&a, &DemoObject::GetItem, &r, &TestRecver::setItem); // 下面這三行會傳送GetItem訊號 a.TakeOne(); a.TakeOne(); a.TakeOne(); return 0; }
下面是 CMakeLists.txt 檔案:
cmake_minimum_required(VERSION 3.0.0) project(myapp LANGUAGES CXX) find_package(Qt6 REQUIRED COMPONENTS Core) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) add_executable(myapp main.cpp) target_link_libraries(myapp PRIVATE Qt6::Core)
注意,這裡一定要把 CMAKE_AUTOMOC 選項設定為 ON,1,或者 YES。因為我們用到了 Q_OBJECT 宏,它需要 MOC 生成一些特定C++程式碼和後設資料。這個範例只用到 QtCore 模組的類,所以 find_package 和 target_link_libraries 中只要引入這個就行。
當你興奮異常地編譯和執行本程式時,會發生錯誤:
這個錯誤是因為 MOC 生成的程式碼最終要用回到我們的程式中的,但程式碼檔案沒有包含這些程式碼。所以你看上面已經提示你了,解決方法是包含 main.moc。這個檔名和你定義 DemoObject 類的程式碼檔案名相同。我剛剛的程式碼檔案是 main.cpp,所以它生成的程式碼檔案就是 main.moc。
不過,#include 指令一定要寫在 DemoObject 和 TestRecver 類的定義之後,這樣才能正確放入生成的程式碼。# include 放在檔案頭部仍然會報錯的,此時,DemoObject 和 TestRecver 類還沒有定義,無法將 main.moc 中的原始碼插入到 main.cpp 中(會找不到類)。
#include <iostream> #include <qobject.h> #include <qstack.h> class DemoObject : public QObject { // 這個是宏 Q_OBJECT …… }; class TestRecver : public QObject { // 記得用這個宏 Q_OBJECT …… }; #include "main.moc" int main(int argc, char **argv) { …… return 0; }
要是你覺得這樣麻煩,最省事的做法是把類的定義寫在標頭檔案中,實現程式碼寫在cpp檔案中。MOC 預設會處理標頭檔案,所以不會報錯。
之後再編譯執行,就不會報錯了。
如果用的是 Windows 系統,cmd 預設編碼是 GBK,不是 UTF-8,VS Code 的程式碼預設是 UTF8 的,控制檯可能會列印出來亂碼。這裡老周不建議改程式碼檔案的編碼,因為說不定你還要把這程式碼放到 Linux 系統中編譯的。在 cmd 中用 CHCP 命令改一下控制檯的編碼,再執行程式就行了。
chcp 65001
其實,訊號和槽的函數簽名可以不一致。下面我們再來做一例。這個例子咱們用到 QWidget 類的 windowTitleChanged 訊號。當視窗標題列中的文字發生改變時會發出這個訊號。它的簽名如下:
void windowTitleChanged(const QString &title);
這個訊號有一個 title 引數,表示修改的視窗標題文字(指新的標題)。而咱們這個例子中用於和它連線的槽函數是無引數的。
private slots: // 這個是槽 void onTitleChanged();
儘管簽名不一致,但可以用。
在這個例子中,只要滑鼠點一下視窗區域,就會修改視窗標題——顯示滑鼠指標在視窗中的座標。視窗標題被修改,就會發出 windowTitleChanged 訊號,然後,onTitleChanged 也會被呼叫。
接下來是實現步驟:
1、準備 CMakeLists.txt 檔案。
cmake_minimum_required(VERSION 3.0.0) project(demo VERSION 0.1.0) find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) file(GLOB SRC_LIST ./*.h ./*.cpp) add_executable(demo WIN32 ${SRC_LIST}) target_link_libraries(demo PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets)
這裡老周就偷懶一下。add_executable(demo ....) 是新增標頭檔案和原始碼檔案的。老周嫌麻煩,加一個檔案又要改一次,於是就用 file 命令搜尋專案根目錄下的所有標頭檔案和 C++ 程式碼檔案。然後把這些搜到的檔案新增到變數 SRC_LIST 中。在 add_executable 命令中參照 SRC_LIST 變數,就可以自動新增檔案了。
2、定義一個自定義視窗類,從 QWidget 類派生。
/* 標頭檔案 */ #include <QWidget> #include <QMessageBox> #include <QMouseEvent> #include <QString> #include <QApplication> class MyWindow : public QWidget { Q_OBJECT public: MyWindow(QWidget* parent = nullptr); private slots: // 這個是槽 void onTitleChanged(); protected: void mousePressEvent(QMouseEvent *event) override; };
/* 實現程式碼 */ #include "MyWindow.h" /****************************************************************/ MyWindow::MyWindow(QWidget *parent) : QWidget::QWidget(parent) { // 視窗大小 resize(300, 275); connect(this, &MyWindow::windowTitleChanged, this, &MyWindow::onTitleChanged); } void MyWindow::onTitleChanged() { QMessageBox::information(this, "Test", "看,視窗標題變了。", QMessageBox::Ok); } void MyWindow::mousePressEvent(QMouseEvent *event) { auto pt = event->pos(); QString s = QString("滑鼠指標位置:%1, %2") .arg(pt.x()) .arg(pt.y()); setWindowTitle(s); QWidget::mousePressEvent(event); } /*****************************************************************/
重寫了 mousePressEvent 方法,當滑鼠按鈕按下時觸發,先通過事件引數的 pos 函數得到滑鼠座標,再用 setWindowTitle 方法修改視窗標題。隨即 windowTitleChanged 訊號發出,在槽函數 onTitleChanged 中只是用 QMessgeBox 類彈出了一個提示框。執行結果如下圖所示。
一個訊號可以連線多個槽,一個槽可以與多個訊號建立連線。這外交能力是真的強,來者不拒。下面咱們做一個 SaySomething 訊號連線三個槽的實驗。
#include <QObject> class SomeObj : public QObject { Q_OBJECT public: SomeObj(QObject *parent = nullptr); void SpeakOut(); // 用這個方法發訊號 signals: void SaySomething(); }; class SlotsObj : public QObject { Q_OBJECT public slots: // 來幾個cao void slot1(); void slot2(); void slot3(); };
以上是標頭檔案。SomeObj 類負責發出訊號,SlotsObj 類負責接收訊號,它有三個 cao:slot1、slot2、slot3。
下面是 SomObj 類的實現程式碼。
SomeObj::SomeObj(QObject *parent) : QObject::QObject(parent) { // 無事幹 } void SomeObj::SpeakOut() { emit SaySomething(); }
emit 關鍵字(Qt 特有)發出 SaySomething 訊號。
下面是 SlotsObj 類的實現程式碼。
#include "app.h" #include <iostream> using namespace std; void SlotsObj::slot1() { cout << "第一個cao觸發了" << endl; } void SlotsObj::slot2() { cout << "第二個cao觸發了" << endl; } void SlotsObj::slot3() { cout << "第三個cao觸發了" << endl; }
來,咱們試一試,分別範例化 SomeObj 和 SlotsObj 類,然後讓 SaySomething 訊號依次與 slot1、slot2、slot3 建立連線。這是典型的「一號戰三槽」。
int main(int argc, char** argv) { // 分別範例化 SomeObj sender; SlotsObj recver; // 建立連線 QObject::connect(&sender, &SomeObj::SaySomething, &recver, &SlotsObj::slot1); QObject::connect(&sender, &SomeObj::SaySomething, &recver, &SlotsObj::slot2); QObject::connect(&sender, &SomeObj::SaySomething, &recver, &SlotsObj::slot3); // 發訊號 sender.SpeakOut(); return 0; }
結果表明:訊號一旦發出,三個 slot 都呼叫了。如下圖:
好了,今天的故事就講到這兒了,欲知後事如何,且待下回分解。