摘要
訊號槽是 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);
總結:
- 訊號可以連線訊號
- 一個訊號可以連線多個槽(點選按鈕,觸發訊號並關閉視窗)
- 多個訊號可以連線同一個槽(比如多個按鈕都可以關閉視窗)
- 自定義槽函式可以寫成:
- 類的任意成員函式
- 靜態函式
- 全域性函式
- 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常用表示式:
[=](){}