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

摘要

     信號槽是 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常用表達式:
[=](){}

 

Tags: