摘要
訊號槽是 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常用表示式:
[=](){}