用 VS Code 搞 Qt6:信號、槽,以及QObject

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 都調用了。如下圖:

 

 

好了,今天的故事就講到這兒了,欲知後事如何,且待下回分解。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章