QT從入門到入土(三)——訊號和槽機制

唯有自己強大發表於2021-07-17

摘要

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


一,訊號和槽機制分析

介於書上的解釋過於繁雜,我選擇用一個阿拉丁神燈的故事來引入這個概念,首先把這個故事抽離出來:

  但是我們可以發現:人摩擦燈神燈出燈神本是不太相關的兩件事情(比如:人摩擦的不一定是神燈,神燈出燈神不一定是因為摩擦),因此我們可以用connect函式把二者關聯起來。

connect(發出訊號的物件,發出的訊號,接收訊號的物件,接收到訊號之後需要呼叫的函式(槽函式))

connect()函式最常用的一般形式:

connect(sender, signal(訊號), receiver, slot(槽));

訊號槽要求訊號和槽的引數一致,所謂一致,是引數型別一致。如果不一致,允許的情況是,槽函式的引數可以比訊號的少,即便如此,槽函式存在的那 些引數的順序也必須和訊號的前面幾個一致起來。(可以忽略部分傳來的訊號引數),但是不能說訊號根本沒有這個資料,你就要在槽函式中使用(就是槽函式的引數比訊號的多,這是不允許的)

??例項演示:(點選按鈕關閉視窗)

 按照上面的步驟,先把這些功能抽離出來:

    //建立第一個按鈕
    QPushButton *btn=new QPushButton;
    //不能用btn->show();//show是以頂層方式彈出控制元件
    //讓btn在widget視窗顯示
    btn->setParent(this);//this指向當前物件的指標(即widget的地址)
    //顯示文字
    btn->setText("關閉視窗");

    //用訊號和槽去實現點選按鈕關閉視窗
    connect(btn,&QPushButton::clicked,this,&QWidget::close);

二,自定義訊號槽

使用 connect()可以讓我們連線系統提供的訊號和槽。但是,Qt 的訊號槽機制 並不僅僅是使用系統提供的那部分,還會允許我們自己設計自己的訊號和槽。

下面我們看看使用 Qt 的訊號槽,實現阿拉丁的故事:

首先需要構建兩個類:阿拉丁類(自定義訊號)和神燈類(槽函式) ,這兩個類應該都是繼承自QObject類的。

然後構建場景:天黑後,阿拉丁會摩擦神燈(自定義訊號觸發訊號),神燈(槽函式響應訊號)出現燈神實現願望。

1️⃣定義自定義訊號

 自定義訊號:只需要宣告在Aladdin.h下的signels裡面,不需要實現。(返回值是void可以有引數,可以過載)

 2️⃣定義槽函式

槽函式:需要先宣告在magiclamp.h(標頭檔案)下的public裡面,再去magiclamp.app(原始檔)下去實現函式。(返回值void,可以有引數,可以過載)

   

 3️⃣用connect連線訊號和槽

 在定義完訊號和槽以後,先在widget.h(視窗類的標頭檔案)中宣告物件,還需要宣告觸發函式(天黑了)。

再在widget.app(原始檔)中建立物件,並實現觸發函式,然後用connect將訊號和槽連線

最後呼叫觸發函式,即可實現。

 

實現結果:

三,自定義訊號和槽發生過載如何解決?

上面我們已經說過了,自定義的訊號和槽可以帶引數,可以過載,但是過載(或者帶引數)後如何去用connect關聯呢?

接著上面的阿拉丁神燈故事:(如果我們給自定義的訊號和槽帶上引數,即摩擦時候許願要一個手機,神燈出現就會給阿拉丁一個手機)

??程式碼實現:

自定義訊號(只需要宣告,不用去實現):

//Aladdin.h

signals:
    void chafe(QString wishes);//宣告自定義訊號(帶引數)
    void chafe();//不帶引數

 槽函式 (即要宣告也要實現):

//magicLamp.h

public:
    explicit magicLamp(QObject *parent = nullptr);
   void Godappears(QString wishes);//建立槽函式(帶引數)
   void Godappears();//建立不帶引數的槽函式
//magicLamp.cpp

//實現槽函式(無參)
void magicLamp::Godappears()
{
  qDebug() <<"Djinn appears, realize the wish! !";
}
//實現槽函式(有參)
void magicLamp::Godappears(QString wishes)
{
    qDebug()<<"Djinn appears,here you are:"<<wishes;
}

 由於槽函式進行了函式過載,因此在用connect進行關聯的時候需要先用指標函式獲取帶參的函式地址。

//Widget.cpp (部分)

//用函式指標獲取帶參函式地址 void (Aladdin::*AladdinSign)(QString)=&Aladdin::chafe; void (magicLamp::*magicLampSign)(QString)=&magicLamp::Godappears;

注意:在宣告一個成員函式的函式地址的時候,需要把成員的函式的作用域放在指標的前面。

Widget.cpp的完整程式碼:

#include "widget.h"
#include "ui_widget.h"
#include<QPushButton>//按鈕控制元件的標頭檔案

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
   //建立阿拉丁類的物件(直接指定父類為widget)
    this->ald=new Aladdin(this);
   //建立神燈類的物件
    this->mlp=new magicLamp(this);
    //用函式指標獲取帶參函式地址
    void (Aladdin::*AladdinSign)(QString)=&Aladdin::chafe;
    void (magicLamp::*magicLampSign)(QString)=&magicLamp::Godappears;
    //連線訊號和槽magicLampSign
    connect(ald,AladdinSign,mlp,magicLampSign);
    //呼叫觸發函式
    dark();
}

Widget::~Widget()
{
    delete ui;
}

void Widget::dark()
{
    //觸發摩擦函式
    emit ald->chafe("iphone 12");
}

 實現效果:

 如果要把QString轉為char*(即消除"  ") :先轉成QByteArray(.toUtf8())再轉char*(.data())。

即修改槽函式:

void magicLamp::Godappears(QString wishes)
{
    qDebug()<<"Djinn appears,here you are:"<<wishes.toUtf8().data();
}

四,訊號連線訊號

 上面的程式碼都是自動觸發,即執行程式就自動許願。那我可不可以再用按鈕去控制觸發訊號(以訊號連線訊號)。

前面一篇已經說明了如何建立按鈕,這裡不過多解釋。QT從入門到入土(二)——物件模型(物件樹)和視窗座標體系 - 唯有自己強大 - 部落格園 (cnblogs.com)

程式碼實現:

#include "widget.h"
#include "ui_widget.h"
#include<QPushButton>//按鈕控制元件的標頭檔案

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
   //建立阿拉丁類的物件(直接指定父類為widget)
    this->ald=new Aladdin(this);
   //建立神燈類的物件
    this->mlp=new magicLamp(this);
    //用函式指標獲取無參函式地址
    void (Aladdin::*AladdinSign)(void)=&Aladdin::chafe;
    void (magicLamp::*magicLampSign)(void)=&magicLamp::Godappears;

    //建立觸發訊號的按鈕
     QPushButton *btn=new QPushButton("許願",this);
     //重置視窗大小(resize是widget下的方法)
     this->resize(400,400);
     //按鈕訊號連線無參訊號
     connect(btn,&QPushButton::clicked,ald,AladdinSign);
     //連線訊號和槽magicLampSign
     connect(ald,AladdinSign,mlp,magicLampSign);


}

 

 注:如果需要斷開訊號呼叫disconnect即可。

disconnect(ald,AladdinSign,mlp,magicLampSign);

總結:

  1. 訊號可以連線訊號
  2. 一個訊號可以連線多個槽(點選按鈕,觸發訊號並關閉視窗)
  3. 多個訊號可以連線同一個槽(比如多個按鈕都可以關閉視窗)
  4. 自定義槽函式可以寫成:
    1. 類的任意成員函式
    2. 靜態函式
    3. 全域性函式
    4. lambda表示式

歸根究底:連線的原則就是訊號和槽的引數必須一一對應!!

五,lambad表示式

C++11 中的 Lambda 表示式用於定義並建立匿名的函式物件,以簡化程式設計工作。 首先看一下 Lambda表示式的基本構成:

[函式物件引數](操作符過載函式引數)mutable或exception->返回值
{
函式體
}

1️⃣函式物件引數

[ ],標識一個 Lambda 的開始,這部分必須存在,不能省略。函式物件引數 是傳遞給編譯器自動生成的函式物件類的建構函式的。函式物件引數只能使 用那些到定義 Lambda 為止時 Lambda 所在作用範圍內可見的區域性變數(包括 Lambda 所在類的 this)。函式物件引數有以下形式:(常用的就是= & this a)

  • 。沒有使用任何函式物件引數。
  • =。函式體內可以使用 Lambda 所在作用範圍內所有可見的區域性變數(包 括 Lambda 所在類的 this),並且是值傳遞方式(相當於編譯器自動為我 們按值傳遞了所有區域性變數)。
  • &。函式體內可以使用 Lambda 所在作用範圍內所有可見的區域性變數(包 括 Lambda 所在類的 this),並且是引用傳遞方式(相當於編譯器自動為 我們按引用傳遞了所有區域性變數)。
  • this。函式體內可以使用 Lambda 所在類中的成員變數。
  • a。將 a 按值進行傳遞。按值進行傳遞時,函式體內不能修改傳遞進來的 a 的拷貝,因為預設情況下函式是 const 的。要修改傳遞進來的 a 的拷貝,可以新增 mutable 修飾符。
  • &a。將 a 按引用進行傳遞。
  • a, &b。將 a 按值進行傳遞,b 按引用進行傳遞。
  • =,&a, &b。除 a 和 b 按引用進行傳遞外,其他引數都按值進行傳遞。
  • &, a, b。除 a 和 b 按值進行傳遞外,其他引數都按引用進行傳遞。

如何用lambda表示式去修改按鈕的名稱:

//函式物件引數: =
[=](){
         btn->setText("aaaa");
     }();

//函式物件引數:a
     [btn](){
          btn->setText("aaaa");
   //由於函式物件引數為btn,因此只能對btn操作,引入btn1會報錯
          //btn1->setText("bbbb");
      }();

注意:不加( )只是對lambad表示式的宣告,加上( )才是對它的呼叫。(由於btn在建立的時候lambad作用範圍內是不可見的,因此需要用=讓lambad表示式認識btn這個區域性變數)

2️⃣操作符過載函式引數

標識過載的()操作符的引數,沒有引數時,這部分可以省略。引數可以通過 按值(如:(a,b))按引用(如:(&a,&b))兩種方式進行傳遞

3️⃣可修改標示符

mutable 宣告,這部分可以省略。按值傳遞函式物件引數時,加上 mutable 修飾符後,可以修改按值傳遞進來的拷貝(注意是能修改拷貝,而不是值本身)

4️⃣錯誤丟擲標示符

exception 宣告,這部分也可以省略。exception 宣告用於指定函式丟擲的異常,如丟擲整數型別的異常,可以使用 throw(int)

5️⃣函式返回值

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

:int一個ret去接收lanbda表示式返回的結果(注意:要用->標識返回值的型別

    int ret=[]()->int{return 1000;}();
    qDebug()<<"ret=:"<<ret;  

6️⃣函式體

{ },標識函式的實現,這部分不能省略,但函式體可以為空

???槽函式也可以使用 Lambda 表示式的方式進行處理:

   //建立兩個按鈕
 QPushButton *myBtn=new QPushButton(this);
 QPushButton *myBtn1=new QPushButton(this);
 //移動第二個按鈕
 myBtn1->move(100,100);
 int m =10;
 //用槽函式(lambda表示式)改變m的copy值
 connect(myBtn,&QPushButton::clicked,this,[m]()mutable{m=100+10;qDebug()<<m;});
 connect(myBtn1,&QPushButton::clicked,this,[=]() {qDebug()<<m;});
 qDebug()<<m;

}

對於第一個connect函式來說:

connect(myBtn,&QPushButton::clicked,this,[m]()mutable{m=100+10;qDebug()<<m;});

當函式物件引數為m時候,若要修改該值傳遞進來的拷貝,需要加上mutable 關鍵字。(注意只能修改拷貝,而不是值本身)

一般來說lambda表示式中很少去加關鍵字的,除非你有什麼特殊需求。

總的來說:

  • 用lambda寫槽函式可以在lambda表示式的函式體內寫多個函式。(如上面m=100+10;和qDebug()<<m;)
  • lambda常用表示式:
[=](){}

 

相關文章