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);
總結:
- 信號可以連接信號
- 一個信號可以連接多個槽(點擊按鈕,觸發信號並關閉窗口)
- 多個信號可以連接同一個槽(比如多個按鈕都可以關閉窗口)
- 自定義槽函數可以寫成:
- 類的任意成員函數
- 靜態函數
- 全局函數
- 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常用表達式:
[=](){}