Qt - 訊號與槽
訊號槽是 Qt 框架引以為豪的機制之一。所謂訊號槽,實際就是觀察者模式(釋出-訂閱模式)。當某個
事件
發生之後,比如,按鈕檢測到自己被點選了一下,它就會發出一個訊號(signal)。這種發出是沒有目的的,類似廣播。如果有物件對這個訊號感興趣,它就會使用連線(connect)函式,意思是,將想要處理的訊號和自己的一個函式(稱為槽(slot))繫結來處理這個訊號。也就是說,當訊號發出時,被連線的槽函式會自動被回撥。這就類似觀察者模式:當發生了感興趣的事件,某一個操作就會被自動觸發。
1.1 訊號的本質
訊號是由於使用者對視窗或控制元件進行了某些操作,導致視窗或控制元件產生了某個特定事件,這時候Qt對應的視窗類會發出某個訊號,以此對使用者的挑選做出反應。
因此根據上述的描述我們得到一個結論:訊號的本質就是事件,比如:
-
按鈕單擊、雙擊
-
視窗重新整理
-
滑鼠移動、滑鼠按下、滑鼠釋放
-
鍵盤輸入
那麼在Qt中訊號是通過什麼形式呈現給使用者的呢?
-
我們對哪個視窗進行操作, 哪個視窗就可以捕捉到這些被觸發的事件。
-
對於使用者來說觸發了一個事件我們就可以得到Qt框架給我們發出的某個特定訊號。
-
訊號的呈現形式就是函式, 也就是說某個事件產生了, Qt框架就會呼叫某個對應的訊號函式, 通知使用者。
在QT中訊號的發出者是某個例項化的類物件,物件內部可以進行相關事件的檢測。
1.2 槽的本質
槽(Slot)就是對訊號響應的函式。槽就是一個函式,與一般的C++函式是一樣的,可以定義在類的任何部分(public、private或 protected),可以具有任何引數,可以被過載,也可以被直接呼叫(但是不能有預設引數)。槽函式與一般的函式不同的是:槽函式可以與一個訊號關聯,當訊號被髮射時,關聯的槽函式被自動執行。
舉個簡單的例子:
女朋友說:“我肚子餓了!”,於是我帶她去吃飯。
上邊例子中相當於女朋友發出了一個訊號, 我收到了訊號並其將其處理掉了。
-
女朋友 -> 傳送訊號的物件, 訊號內容: 我餓了
-
我 -> 接收訊號的物件並且處理掉了這個訊號, 處理動作: 帶她去吃飯
在Qt中槽函式的所有者也是某個類的例項物件。
寫信:發件人 信的內容 收件人 收到信做事情
1.3 訊號和槽的關係
在Qt中訊號和槽函式都是獨立的個體,本身沒有任何聯絡,但是由於某種特性需求我們可以將二者連線到一起,好比牛郎和織女想要相會必須要有喜鵲為他們搭橋一樣。
訊號與槽關聯是用 QObject::connect() 函式實現的,其基本格式是:
[static] QMetaObject::Connection QObject::connect(
const QObject *sender,
const char *signal,
const QObject *receiver,
const char *method,
Qt::ConnectionType type = Qt::AutoConnection);
-
引數:
-
sender: 發出訊號的物件
-
signal: sender物件的訊號,訊號是一個函式
-
receiver: 訊號接收者
-
method: receiver物件的槽函式, 當檢測到sender發出了signal訊號, receiver物件呼叫method方法
-
connect函式相對於做了訊號處理動作的註冊,呼叫conenct連線訊號與槽時,sender物件的訊號並沒有產生, 因此receiver物件的method也不會被呼叫,method槽函式本質是一個回撥函式, 呼叫的時機是訊號產生之後。 呼叫槽函式是Qt框架來執行的,connect中的sender和recever兩個指標必須被例項化了, 否則conenct不會成功
。
2. 標準訊號槽使用
2.1 標準訊號/槽
在Qt提供的很多類中都可以對使用者觸發的某些特定事件進行檢測, 當事件被觸發後就會產生對應的訊號, 這些訊號都是Qt類內部自帶的, 因此稱之為標準訊號。
同樣的,在Qt的很多類內部為我了提供了很多功能函式,並且這些函式也可以作為觸發的訊號的處理動作,有這類特性的函式在Qt中稱之為標準槽函式。
系統自帶的訊號和槽通常如何查詢呢,這個就需要利用幫助文件了,在幫助文件中比如我們上面的按鈕的點選訊號,在幫助文件中輸入QPushButton,首先我們可以在Contents
中尋找關鍵字 signals
,訊號的意思,但是我們發現並沒有找到,這時候我們應該看當前類從父類繼承下來了哪些訊號,因此我們去他的父類QAbstractButton中就可以找到該關鍵字,點選signals索引到系統自帶的訊號有如下幾個
2.2 使用
功能實現: 點選視窗上的按鈕, 關閉視窗
按鈕: 訊號發出者 ->
QPushButton
視窗: 訊號的接收者和處理者 ->
QWidget
// 單擊按鈕發出的訊號
[signal] void QAbstractButton::clicked(bool checked = false)
// 關閉視窗的槽函式
[slot] bool QWidget::close();
// 單擊按鈕關閉視窗
connect(ui->closewindow, &QPushButton::clicked, this, &MainWindow::close);
3. 自定義訊號槽使用
Qt框架提供的訊號槽在某些特定場景下是無法滿足我們的專案需求的,因此我們還設計自己需要的的訊號和槽,同樣還是使用connect()對自定義的訊號槽進行連線。
如果想要使用自定義的訊號和槽, 首先要編寫新的類並且讓其繼承Qt的某些標準類,我們自己編寫的類想要在Qt中使用使用訊號槽機制, 那麼必須要滿足的如下條件:
-
這個類必須從QObject類或者是其子類進行派生
-
在定義類的第一行標頭檔案中加入 Q_OBJECT 巨集
// 在標頭檔案派生類的時候,首先像下面那樣引入Q_OBJECT巨集:
class MyMainWindow : public QWidget
{
Q_OBJECT
public:
......
}
3.1 自定義訊號
-
訊號是類的成員函式
-
返回值是 void 型別
-
引數可以隨意指定, 訊號也支援過載
-
訊號需要使用 signals 關鍵字進行宣告, 使用方法類似於public等關鍵字
-
訊號函式只需要宣告, 不需要定義(沒有函式體實現)
-
在程式中傳送自定義訊號: 傳送訊號的本質就是呼叫訊號函式
emit mysignals(); //傳送訊號
emit是一個空巨集,沒有特殊含義,僅用來表示這個語句是發射一個訊號,不寫當然可以,但是不推薦。
// 舉例: 訊號過載
// Qt中的類想要使用訊號槽機制必須要從QObject類派生(直接或間接派生都可以)
class MyButton : public QPushButton
{
Q_OBJECT
signals:
void testsignal();
void testsignal(int a);
};
//qRegisterMetaType
訊號引數的作用是資料傳遞, 誰呼叫訊號函式誰就指定實參,實參最終會被傳遞給槽函式
3.2 自定義槽
槽函式就是訊號的處理動作,自定義槽函式和自定義的普通函式寫法是一樣的。
特點:
-
返回值是 void 型別
-
槽函式也支援過載
-
槽函式引數個數, 需要看連線的訊號的引數個數
-
槽函式的引數是用來接收訊號傳送的資料的, 訊號的引數就是需要傳送的資料
-
舉例:
-
訊號函式: void testsig(int a, double b);
-
槽函式: void testslot(int a, double b);
-
-
總結:
-
槽函式的引數應該和對應的訊號的引數個數, 型別一一對應
-
訊號的引數可以大於等於槽函式的引數個數,未被槽函式接受的資料會被忽略
-
訊號函式: void testsig(int a, double b);
-
槽函式: void testslot(int a);
-
-
-
槽函式的型別:
-
成員函式
-
普通成員函式
-
靜態成員函式
-
-
全域性函式
-
lambda表示式(匿名函式)
-
槽函式可以使用關鍵字進行宣告: slots (Qt5中slots可以省略不寫)
-
public slots:
-
private slots:
-
protected slots:
-
場景舉例
// 女朋友餓了, 我請她吃飯
// class GirlFriend
// class OneSelf
class GirlFriend:public QObject
{
Q_OBJECT
public:
GirlFriend(QObject*parent = nullptr):QObject(parent)
{}
signals:
void hungry();
public slots:
};
class OneSelf:public QObject
{
Q_OBJECT
public:
OneSelf(QObject*parent = nullptr):QObject(parent)
{}
void goEat()
{
qDebug()<<"goEat";
}
static void goEatFood()
{
qDebug()<<"goEatFood";
}
signals:
public slots:
void onHungry()
{
qDebug()<<"寶貝餓了呀,多喝熱水喲~";
}
};
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
public slots:
void onBtnClicked();
private:
GirlFriend *girl;
OneSelf* self;
};
-
widget.cpp
#include "widget.h"
#include<QPushButton>
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
girl = new GirlFriend(this);
self = new OneSelf(this);
//連線槽函式
connect(girl,&GirlFriend::hungry,self,&OneSelf::onHungry);
//連線普通成員函式
connect(girl,&GirlFriend::hungry,self,&OneSelf::goEat);
//連線靜態成員函式
connect(girl,&GirlFriend::hungry,self,&OneSelf::goEatFood);
QPushButton*btn = new QPushButton("按下就餓了",this);
//通過widget間接傳送girl的hungry訊號
connect(btn,&QPushButton::clicked,this,&Widget::onBtnClicked);
//連線訊號,直接傳送girl的hungry訊號
//connect(btn,&QPushButton::clicked,girl,&GirlFriend::hungry);
}
Widget::~Widget()
{
}
void Widget::onBtnClicked()
{
emit girl->hungry();
}
4. 訊號槽拓展
4.1 訊號槽使用拓展
-
一個訊號可以連線多個槽函式, 傳送一個訊號有多個處理動作
-
需要寫多個
connect
連線 -
訊號的接收者可以是一個物件, 也可以是多個物件
-
-
一個槽函式可以連線多個訊號, 多個不同的訊號, 處理動作是相同的
-
寫多個
connect
就可以
-
-
訊號可以連線訊號
-
訊號接收者可以不出來接收的訊號, 繼續發出新的訊號 -> 傳遞了資料, 並沒有進行處理
QPushButton*btn = new QPushButton("one",this); QPushButton*btn2 = new QPushButton("two",this); btn2->move(100,0); //點選btn按鈕,會讓btn2按鈕發出clicked訊號 connect(btn,&QPushButton::clicked,btn2,&QPushButton::clicked); connect(btn2,&QPushButton::clicked,this,&Widget::onClicked); void Widget::onClicked() { qDebug()<<"okok"; }
-
-
訊號槽是可以斷開的
disconnect(const QObject *sender, &QObject::signal, const QObject *receiver, &QObject::method);
4.2 訊號槽的連線方式
-
Qt5的連線方式
// 語法: QMetaObject::Connection QObject::connect( const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method, Qt::ConnectionType type = Qt::AutoConnection); // 訊號和槽函式也就是第2,4個引數傳遞的是地址, 編譯器在編譯過程中會對資料的正確性進行檢測 connect(const QObject *sender, &QObject::signal, const QObject *receiver, &QObject::method);
-
Qt4的連線方式
這種舊的訊號槽連線方式在Qt5中是支援的, 但是不推薦使用, 因為這種方式在進行訊號槽連線的時候, 訊號槽函式通過巨集
SIGNAL
和SLOT
轉換為字串型別。因為訊號槽函式的轉換是通過巨集來進行轉換的,因此傳遞到巨集函式內部的資料不會被進行檢測, 如果使用者傳錯了資料,編譯器也不會報錯,但實際上訊號槽的連線已經不對了,只有在程式執行起來之後才能發現問題,而且問題不容易被定位。
// Qt4的訊號槽連線方式 [static] QMetaObject::Connection QObject::connect( const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection); connect(const QObject *sender,SIGNAL(訊號函式名(引數1, 引數2, ...)), const QObject *receiver,SLOT(槽函式名(引數1, 引數2, ...)));
-
應用舉例
class Me : public QObject { Q_OBJECT // Qt4中的槽函式必須這樣宣告, qt5中的關鍵字 slots 可以被省略 public slots: void eat(); void eat(QString somthing); signals: void hungury(); void hungury(QString somthing); };
基於上面寫的訊號與槽,我們來處理如下邏輯: 我餓了, 我要吃東西
-
分析: 訊號的發出者是我自己, 訊號的接收者也是我自己
Me m; // Qt4處理方式 注意不要把訊號與槽的名字寫錯了,因為是轉為字串寫錯了不會報錯,但是連線會失敗 connect(&m, SIGNAL(eat()), &m, SLOT(hungury())); connect(&m, SIGNAL(eat(QString)), &m, SLOT(hungury(QString))); // Qt5處理方式 connect(&m, &Me::eat, &m, &Me::hungury); // error:no matching member function for call to 'connect'
-
為什麼Qt4的方式沒有錯誤,Qt5的方式卻有問題了呢?
-
Qt4的方式在傳訊號和槽的時候用了巨集進行強轉,而且都帶了引數,不會有二義性問題產生
-
Qt5中,訊號和槽都有過載,此事connect函式根本就不知道你要使用的是過載中的哪一個,所以只能報錯咯!
-
-
如何解決Qt5中的訊號和槽過載中的二義性問題呢?
-
一,通過函式指標解決
//訊號 void (Me::*funchungury)() = &Me::hungury; void (Me::*funchungury_QString)(QString) = &Me::hungury; //槽 void (Me::*funceat)() = &Me::eat; void (Me::*funceat_QString)(QString) = &Me::eat; //有參連線 connect(me,funchungury_QString,me,funceat_QString); //無參連線 connect(me,funchungury,me,funceat);
-
二,通過Qt提供的過載類(QOverload)解決
//有參連線 connect(this,QOverload<QString>::of(&MyButton::hungury),this,QOverload<QString>::of(&MyButton::eat)); //無參連線 connect(this,QOverload<>::of(&MyButton::hungury),this,QOverload<>::of(&MyButton::eat));
-
-
總結
-
Qt4的訊號槽連線方式因為使用了巨集函式, 巨集函式對使用者傳遞的訊號槽不會做錯誤檢測, 容易出bug
-
Qt5的訊號槽連線方式, 傳遞的是訊號槽函式的地址, 編譯器會做錯誤檢測, 減少了bug的產生
-
當訊號槽函式被過載之後, Qt4的訊號槽連線方式不受影響
-
當訊號槽函式被過載之後, Qt5中需要給被過載的訊號或者槽定義函式指標
-
4.3 Lambda表示式
QPushButton*btn = new QPushButton("touch me",this);
QPushButton*btn2 = new QPushButton("天王蓋地虎",this);
btn2->move(100,0);
//禁止用&引用捕獲臨時變數,因為函式結束變數會銷燬,在lambda中使用會產生錯誤
//應該使用按值捕獲 =
connect(btn,&QPushButton::clicked,this,[=]()
{
static int flag = false; //可以這樣用
if(!flag)
{
btn2->setText("小雞頓蘑菇");
}else
{
btn2->setText("天王蓋地虎");
}
flag = !flag;
});
Lambda表示式是C++11最重要也是最常用的特性之一,是現代程式語言的一個特點,簡潔,提高了程式碼的效率並且可以使程式更加靈活,Qt是完全支援c++語法的, 因此在Qt中也可以使用Lambda表示式。
Lambda表示式就是一個匿名函式, 語法格式如下:
[capture](params) opt -> ret {body;};
- capture: 捕獲列表
- params: 引數列表
- opt: 函式選項
- ret: 返回值型別
- body: 函式體
// 示例程式碼->匿名函式的呼叫:
int ret = [](int a) -> int
{
return a+1;
}(100);
關於Lambda表示式的細節介紹:
-
捕獲列表: 捕獲一定範圍內的變數
-
[]
- 不捕捉任何變數 -
[&]
- 捕獲外部作用域中所有變數, 並作為引用在函式體內使用 (按引用捕獲
) -
[=]
- 捕獲外部作用域中所有變數, 並作為副本在函式體內使用 (按值捕獲
)-
拷貝的副本在匿名函式體內部是隻讀的
-
-
[=, &foo]
- 按值捕獲外部作用域中所有變數, 並按照引用捕獲外部變數 foo -
[bar]
- 按值捕獲 bar 變數, 同時不捕獲其他變數 -
[&bar]
- 按值捕獲 bar 變數, 同時不捕獲其他變數 -
[this]
- 捕獲當前類中的this指標-
-
如果已經使用了 & 或者 =, 預設新增此選項
-
-
-
引數列表: 和普通函式的引數列表一樣
-
opt 選項 -->
可以省略
-
mutable: 可以修改按值傳遞進來的拷貝(注意是能修改拷貝,而不是值本身)
-
exception: 指定函式丟擲的異常,如丟擲整數型別的異常,可以使用throw();
-
-
返回值型別:
-
標識函式返回值的型別,當返回值為void,或者函式體中只有一處return的地方(此時編譯器可以自動推斷出返回值型別)時,這部分可以省略
-
-
函式體:
-
函式的實現,這部分不能省略,但函式體可以為空。
-
搜尋
複製