1. 程式人生 > 其它 >Qt - 訊號與槽

Qt - 訊號與槽

1. 訊號和槽概述

訊號槽是 Qt 框架引以為豪的機制之一。所謂訊號槽,實際就是觀察者模式(釋出-訂閱模式)。當某個事件發生之後,比如,按鈕檢測到自己被點選了一下,它就會發出一個訊號(signal)。這種發出是沒有目的的,類似廣播。如果有物件對這個訊號感興趣,它就會使用連線(connect)函式,意思是,將想要處理的訊號和自己的一個函式(稱為槽(slot))繫結來處理這個訊號。也就是說,當訊號發出時,被連線的槽函式會自動被回撥。這就類似觀察者模式:當發生了感興趣的事件,某一個操作就會被自動觸發。

1.1 訊號的本質

訊號是由於使用者對視窗或控制元件進行了某些操作,導致視窗或控制元件產生了某個特定事件,這時候Qt對應的視窗類會發出某個訊號,以此對使用者的挑選做出反應。

因此根據上述的描述我們得到一個結論:訊號的本質就是事件,比如:

  • 按鈕單擊、雙擊

  • 視窗重新整理

  • 滑鼠移動、滑鼠按下、滑鼠釋放

  • 鍵盤輸入

那麼在Qt中訊號是通過什麼形式呈現給使用者的呢?

  • 我們對哪個視窗進行操作, 哪個視窗就可以捕捉到這些被觸發的事件。

  • 對於使用者來說觸發了一個事件我們就可以得到Qt框架給我們發出的某個特定訊號。

  • 訊號的呈現形式就是函式, 也就是說某個事件產生了, Qt框架就會呼叫某個對應的訊號函式, 通知使用者。

在QT中訊號的發出者是某個例項化的類物件,物件內部可以進行相關事件的檢測。

1.2 槽的本質

槽(Slot)就是對訊號響應的函式。槽就是一個函式,與一般的C++函式是一樣的,可以定義在類的任何部分(public、private或 protected),可以具有任何引數,可以被過載,也可以被直接呼叫(但是不能有預設引數)。槽函式與一般的函式不同的是:槽函式可以與一個訊號關聯,當訊號被髮射時,關聯的槽函式被自動執行。

舉個簡單的例子:

女朋友說:“我肚子餓了!”,於是我帶她去吃飯。

上邊例子中相當於女朋友發出了一個訊號, 我收到了訊號並其將其處理掉了。

  • 女朋友 -> 傳送訊號的物件, 訊號內容: 我餓了

  • 我 -> 接收訊號的物件並且處理掉了這個訊號, 處理動作: 帶她去吃飯

在Qt中槽函式的所有者也是某個類的例項物件。

寫信:發件人 信的內容 收件人 收到信做事情

 

1.3 訊號和槽的關係

在Qt中訊號和槽函式都是獨立的個體,本身沒有任何聯絡,但是由於某種特性需求我們可以將二者連線到一起,好比牛郎和織女想要相會必須要有喜鵲為他們搭橋一樣。

訊號與槽關聯是用 QObject::connect() 函式實現的,其基本格式是:

[static] QMetaObject::Connection  QObject::connect(
    const QObject *sender,                              
    const char *signal,                                 
    const QObject *receiver,                            
    const char *method,                                 
    Qt::ConnectionType type = Qt::AutoConnection);  
  • 引數:

    • sender: 發出訊號的物件

    • signal: sender物件的訊號,訊號是一個函式

    • receiver: 訊號接收者

    • method: receiver物件的槽函式, 當檢測到sender發出了signal訊號, receiver物件呼叫method方法

connect函式相對於做了訊號處理動作的註冊,呼叫conenct連線訊號與槽時,sender物件的訊號並沒有產生, 因此receiver物件的method也不會被呼叫,method槽函式本質是一個回撥函式, 呼叫的時機是訊號產生之後。 呼叫槽函式是Qt框架來執行的,connect中的sender和recever兩個指標必須被例項化了, 否則conenct不會成功

2. 標準訊號槽使用

2.1 標準訊號/槽

在Qt提供的很多類中都可以對使用者觸發的某些特定事件進行檢測, 當事件被觸發後就會產生對應的訊號, 這些訊號都是Qt類內部自帶的, 因此稱之為標準訊號。

同樣的,在Qt的很多類內部為我了提供了很多功能函式,並且這些函式也可以作為觸發的訊號的處理動作,有這類特性的函式在Qt中稱之為標準槽函式。

系統自帶的訊號和槽通常如何查詢呢,這個就需要利用幫助文件了,在幫助文件中比如我們上面的按鈕的點選訊號,在幫助文件中輸入QPushButton,首先我們可以在Contents中尋找關鍵字 signals,訊號的意思,但是我們發現並沒有找到,這時候我們應該看當前類從父類繼承下來了哪些訊號,因此我們去他的父類QAbstractButton中就可以找到該關鍵字,點選signals索引到系統自帶的訊號有如下幾個

 

2.2 使用

功能實現: 點選視窗上的按鈕, 關閉視窗

  • 按鈕: 訊號發出者 -> QPushButton

  • 視窗: 訊號的接收者和處理者 -> QWidget

// 單擊按鈕發出的訊號
[signal] void QAbstractButton::clicked(bool checked = false)
// 關閉視窗的槽函式
[slot] bool QWidget::close();
​
// 單擊按鈕關閉視窗
connect(ui->closewindow, &QPushButton::clicked, this, &MainWindow::close);

 

3. 自定義訊號槽使用

Qt框架提供的訊號槽在某些特定場景下是無法滿足我們的專案需求的,因此我們還設計自己需要的的訊號和槽,同樣還是使用connect()對自定義的訊號槽進行連線。

 

如果想要使用自定義的訊號和槽, 首先要編寫新的類並且讓其繼承Qt的某些標準類,我們自己編寫的類想要在Qt中使用使用訊號槽機制, 那麼必須要滿足的如下條件:

  • 這個類必須從QObject類或者是其子類進行派生

  • 在定義類的第一行標頭檔案中加入 Q_OBJECT 巨集

// 在標頭檔案派生類的時候,首先像下面那樣引入Q_OBJECT巨集:
class MyMainWindow : public QWidget
{
    Q_OBJECT
public:
    ......
}

 

3.1 自定義訊號

  • 訊號是類的成員函式

  • 返回值是 void 型別

  • 引數可以隨意指定, 訊號也支援過載

  • 訊號需要使用 signals 關鍵字進行宣告, 使用方法類似於public等關鍵字

  • 訊號函式只需要宣告, 不需要定義(沒有函式體實現)

  • 在程式中傳送自定義訊號: 傳送訊號的本質就是呼叫訊號函式

    emit mysignals();   //傳送訊號

    emit是一個空巨集,沒有特殊含義,僅用來表示這個語句是發射一個訊號,不寫當然可以,但是不推薦。

// 舉例: 訊號過載
// Qt中的類想要使用訊號槽機制必須要從QObject類派生(直接或間接派生都可以)
class MyButton : public QPushButton
{
    Q_OBJECT
signals:
    void testsignal();
    void testsignal(int a);
};
//qRegisterMetaType

訊號引數的作用是資料傳遞, 誰呼叫訊號函式誰就指定實參,實參最終會被傳遞給槽函式

3.2 自定義槽

槽函式就是訊號的處理動作,自定義槽函式和自定義的普通函式寫法是一樣的。

特點:

  • 返回值是 void 型別

  • 槽函式也支援過載

    • 槽函式引數個數, 需要看連線的訊號的引數個數

    • 槽函式的引數是用來接收訊號傳送的資料的, 訊號的引數就是需要傳送的資料

    • 舉例:

      • 訊號函式: void testsig(int a, double b);

      • 槽函式: void testslot(int a, double b);

    • 總結:

      • 槽函式的引數應該和對應的訊號的引數個數, 型別一一對應

      • 訊號的引數可以大於等於槽函式的引數個數,未被槽函式接受的資料會被忽略

        • 訊號函式: void testsig(int a, double b);

        • 槽函式: void testslot(int a);

槽函式的型別:

  • 成員函式

    • 普通成員函式

    • 靜態成員函式

  • 全域性函式

  • lambda表示式(匿名函式)

  1. 槽函式可以使用關鍵字進行宣告: slots (Qt5中slots可以省略不寫)

    • public slots:

    • private slots:

    • protected slots:

場景舉例

// 女朋友餓了, 我請她吃飯
// class GirlFriend
// class OneSelf
class GirlFriend:public QObject
{
    Q_OBJECT
public:
    GirlFriend(QObject*parent = nullptr):QObject(parent)
    {}
signals:
    void hungry();
public slots:
};
​
class OneSelf:public QObject
{
    Q_OBJECT
public:
    OneSelf(QObject*parent = nullptr):QObject(parent)
    {}
    void goEat()
    {
        qDebug()<<"goEat";
    }
    static void goEatFood()
    {
        qDebug()<<"goEatFood";
    }
signals:
public slots:
    void onHungry()
    {
        qDebug()<<"寶貝餓了呀,多喝熱水喲~";
    }
};
​
class Widget : public QWidget
{
    Q_OBJECT
​
public:
    Widget(QWidget *parent = nullptr);
    ~Widget();
public slots:
    void onBtnClicked();
private:
    GirlFriend *girl;
    OneSelf* self;
};
  • widget.cpp

#include "widget.h"
#include<QPushButton>
​
Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    girl = new GirlFriend(this);
    self = new OneSelf(this);
    //連線槽函式
    connect(girl,&GirlFriend::hungry,self,&OneSelf::onHungry);
    //連線普通成員函式
    connect(girl,&GirlFriend::hungry,self,&OneSelf::goEat);
    //連線靜態成員函式
    connect(girl,&GirlFriend::hungry,self,&OneSelf::goEatFood);
        
​
    QPushButton*btn = new QPushButton("按下就餓了",this);
    //通過widget間接傳送girl的hungry訊號    
    connect(btn,&QPushButton::clicked,this,&Widget::onBtnClicked);
     //連線訊號,直接傳送girl的hungry訊號   
    //connect(btn,&QPushButton::clicked,girl,&GirlFriend::hungry);    
​
}
​
Widget::~Widget()
{
}
​
void Widget::onBtnClicked()
{
    emit girl->hungry();
}

 

4. 訊號槽拓展

4.1 訊號槽使用拓展

  • 一個訊號可以連線多個槽函式, 傳送一個訊號有多個處理動作

    • 需要寫多個connect連線

    • 訊號的接收者可以是一個物件, 也可以是多個物件

  • 一個槽函式可以連線多個訊號, 多個不同的訊號, 處理動作是相同的

    • 寫多個connect就可以

  • 訊號可以連線訊號

    • 訊號接收者可以不出來接收的訊號, 繼續發出新的訊號 -> 傳遞了資料, 並沒有進行處理

      QPushButton*btn = new QPushButton("one",this);
      QPushButton*btn2 = new QPushButton("two",this);
      btn2->move(100,0);
      //點選btn按鈕,會讓btn2按鈕發出clicked訊號
      connect(btn,&QPushButton::clicked,btn2,&QPushButton::clicked);
      ​
      connect(btn2,&QPushButton::clicked,this,&Widget::onClicked);
      void Widget::onClicked()
      {
          qDebug()<<"okok";
      }
  • 訊號槽是可以斷開的

    disconnect(const QObject *sender, &QObject::signal, 
            const QObject *receiver, &QObject::method);

 

4.2 訊號槽的連線方式

  • Qt5的連線方式

    // 語法:
    QMetaObject::Connection QObject::connect(
            const QObject *sender, PointerToMemberFunction signal, 
            const QObject *receiver, PointerToMemberFunction method, 
            Qt::ConnectionType type = Qt::AutoConnection);
    ​
    // 訊號和槽函式也就是第2,4個引數傳遞的是地址, 編譯器在編譯過程中會對資料的正確性進行檢測
    connect(const QObject *sender, &QObject::signal, 
            const QObject *receiver, &QObject::method);

     

  • Qt4的連線方式

    這種舊的訊號槽連線方式在Qt5中是支援的, 但是不推薦使用, 因為這種方式在進行訊號槽連線的時候, 訊號槽函式通過巨集SIGNALSLOT轉換為字串型別。

    因為訊號槽函式的轉換是通過巨集來進行轉換的,因此傳遞到巨集函式內部的資料不會被進行檢測, 如果使用者傳錯了資料,編譯器也不會報錯,但實際上訊號槽的連線已經不對了,只有在程式執行起來之後才能發現問題,而且問題不容易被定位。

    // Qt4的訊號槽連線方式
    [static] QMetaObject::Connection QObject::connect(
        const QObject *sender, const char *signal, 
        const QObject *receiver, const char *method, 
        Qt::ConnectionType type = Qt::AutoConnection);
    ​
    connect(const QObject *sender,SIGNAL(訊號函式名(引數1, 引數2, ...)),
            const QObject *receiver,SLOT(槽函式名(引數1, 引數2, ...)));
  • 應用舉例

    class Me : public QObject
    {
        Q_OBJECT
    // Qt4中的槽函式必須這樣宣告, qt5中的關鍵字 slots 可以被省略
    public slots:
        void eat();
        void eat(QString somthing);
    signals:
        void hungury();
        void hungury(QString somthing);
    };

基於上面寫的訊號與槽,我們來處理如下邏輯: 我餓了, 我要吃東西

  • 分析: 訊號的發出者是我自己, 訊號的接收者也是我自己

    Me m;
    // Qt4處理方式  注意不要把訊號與槽的名字寫錯了,因為是轉為字串寫錯了不會報錯,但是連線會失敗
    connect(&m, SIGNAL(eat()), &m, SLOT(hungury()));
    connect(&m, SIGNAL(eat(QString)), &m, SLOT(hungury(QString)));
    ​
    // Qt5處理方式
    connect(&m, &Me::eat, &m, &Me::hungury);    // error:no matching member function for call to 'connect'
  • 為什麼Qt4的方式沒有錯誤,Qt5的方式卻有問題了呢?

    • Qt4的方式在傳訊號和槽的時候用了巨集進行強轉,而且都帶了引數,不會有二義性問題產生

    • Qt5中,訊號和槽都有過載,此事connect函式根本就不知道你要使用的是過載中的哪一個,所以只能報錯咯!

  • 如何解決Qt5中的訊號和槽過載中的二義性問題呢?

    • 一,通過函式指標解決

      //訊號
      void (Me::*funchungury)() = &Me::hungury;
      void (Me::*funchungury_QString)(QString) = &Me::hungury;
      //槽
      void (Me::*funceat)() = &Me::eat;
      void (Me::*funceat_QString)(QString) = &Me::eat;
      //有參連線
      connect(me,funchungury_QString,me,funceat_QString);
      //無參連線
      connect(me,funchungury,me,funceat);
    • 二,通過Qt提供的過載類(QOverload)解決

      //有參連線
      connect(this,QOverload<QString>::of(&MyButton::hungury),this,QOverload<QString>::of(&MyButton::eat));
      //無參連線
      connect(this,QOverload<>::of(&MyButton::hungury),this,QOverload<>::of(&MyButton::eat));
  • 總結

    • Qt4的訊號槽連線方式因為使用了巨集函式, 巨集函式對使用者傳遞的訊號槽不會做錯誤檢測, 容易出bug

    • Qt5的訊號槽連線方式, 傳遞的是訊號槽函式的地址, 編譯器會做錯誤檢測, 減少了bug的產生

    • 當訊號槽函式被過載之後, Qt4的訊號槽連線方式不受影響

    • 當訊號槽函式被過載之後, Qt5中需要給被過載的訊號或者槽定義函式指標

4.3 Lambda表示式

QPushButton*btn = new QPushButton("touch me",this);
QPushButton*btn2 = new QPushButton("天王蓋地虎",this);
btn2->move(100,0);
​
//禁止用&引用捕獲臨時變數,因為函式結束變數會銷燬,在lambda中使用會產生錯誤
//應該使用按值捕獲 = 
connect(btn,&QPushButton::clicked,this,[=]()
{
    static  int flag = false;   //可以這樣用
    if(!flag)
    {
        btn2->setText("小雞頓蘑菇");
    }else
    {
        btn2->setText("天王蓋地虎");
    }
    flag = !flag;
});

 

Lambda表示式是C++11最重要也是最常用的特性之一,是現代程式語言的一個特點,簡潔,提高了程式碼的效率並且可以使程式更加靈活,Qt是完全支援c++語法的, 因此在Qt中也可以使用Lambda表示式。

Lambda表示式就是一個匿名函式, 語法格式如下:

[capture](params) opt -> ret {body;};
    - capture: 捕獲列表
    - params: 引數列表
    - opt: 函式選項
    - ret: 返回值型別
    - body: 函式體
        
// 示例程式碼->匿名函式的呼叫:
int ret = [](int a) -> int
{
    return a+1;
}(100);

關於Lambda表示式的細節介紹:

  1. 捕獲列表: 捕獲一定範圍內的變數

    • []- 不捕捉任何變數

    • [&]- 捕獲外部作用域中所有變數, 並作為引用在函式體內使用 (按引用捕獲)

    • [=]- 捕獲外部作用域中所有變數, 並作為副本在函式體內使用 (按值捕獲)

      • 拷貝的副本在匿名函式體內部是隻讀的

    • [=, &foo] - 按值捕獲外部作用域中所有變數, 並按照引用捕獲外部變數 foo

    • [bar] - 按值捕獲 bar 變數, 同時不捕獲其他變數

    • [&bar] - 按值捕獲 bar 變數, 同時不捕獲其他變數

    • [this] - 捕獲當前類中的this指標

      • 讓lambda表示式擁有和當前類成員函式同樣的訪問許可權

      • 如果已經使用了 & 或者 =, 預設新增此選項

  2. 引數列表: 和普通函式的引數列表一樣

  3. opt 選項 --> 可以省略

    • mutable: 可以修改按值傳遞進來的拷貝(注意是能修改拷貝,而不是值本身)

    • exception: 指定函式丟擲的異常,如丟擲整數型別的異常,可以使用throw();

  4. 返回值型別:

    • 標識函式返回值的型別,當返回值為void,或者函式體中只有一處return的地方(此時編譯器可以自動推斷出返回值型別)時,這部分可以省略

  5. 函式體:

    • 函式的實現,這部分不能省略,但函式體可以為空。

搜尋

複製