簡介
這次讨論Qt信号-槽相關的知識點。
信号-槽是Qt架構中最核心的機制,也是每個Qt開發者必須掌握的技能。
網絡上有很多介紹信号-槽的文章,也可以參考。
濤哥的專欄是《Qt進階之路》,如果連信号-槽的文章都沒有,将是沒有靈魂的。
是以這次濤哥就由淺到深地說一說信号-槽。
貓和老鼠的故事
如果一上來就講一大堆概念和定義,讀者很容易讀睡着。是以濤哥從一個故事/場景開始說起。
濤哥小時候喜歡看卡通片《貓和老鼠》, 裡面有湯姆貓(Tom)和傑瑞鼠(Jerry)鬥智鬥勇的故事。。。
現在做個簡單的設定:Tom有個技能叫”喵”,就是發出貓叫,而正在偷吃東西的Jerry,聽見貓叫聲就會逃跑。
我們嘗試用C++面向對象的思想,描述這個設定。
先是定義Tom和Jerry兩種對象
//Tom的定義
class Tom
{
public:
//貓叫
void Miaow()
{
cout << "喵!" << endl;
}
//省略其它
...
};
//Jerry的定義
class Jerry
{
public:
//逃跑
void RunAway()
{
cout << "那隻貓又來了,快溜!" << endl;
}
//省略其它
...
};
接下來模拟場景
int main(int argc, char *argv[])
{
//執行個體化tom
Tom tom;
//執行個體化jerry
Jerry jerry;
//tom發出叫聲
tom.Miaow();
//jerry逃跑
jerry.RunAway();
return 0;
}
這個場景看起來很簡單,tom發出叫聲之後手動調用了jerry的逃跑。
我們再看幾種稍微複雜的場景:
場景一:
假如jerry逃跑後過段時間,又回來偷吃東西。Tom再次發出叫聲,jerry再次逃跑。。。
這個場景要重複幾十次。我們能否實作,隻要tom的Miaow被調用了,jerry的RunAway就自動被調用,而不是每次都手動調用?
場景二:
假如jerry是藏在“廚房的櫃子裡的米袋子後面”,無法直接發現它(不能直接擷取到jerry對象,并調用它的函數)。
這種情況下,該怎麼建立 “貓叫-老鼠逃跑” 的模型?
場景三:
假如有多隻jerry,一隻tom發出叫聲時,所有jerry都逃跑。這種模型該怎麼建立?
假如有多隻tom,任意一隻發出叫聲時,所有jerry都逃跑。這種模型又該怎麼建立?
場景四:
假如不知道貓的确切品種或者名字,也不知道老鼠的品種或者名字,隻要 貓 這種動物發出叫聲,老鼠 這種動物就要逃跑。
這樣的模型又該如何建立?
…
還有很多場景,就不贅述了。
對象之間的通信機制
這裡概括一下要實作的功能:
要提供一種對象之間的通信機制。這種機制,要能夠給兩個不同對象中的函數建立映射關系,前者被調用時後者也能被自動調用。
再深入一些,兩個對象都互相不知道對方的存在,仍然可以建立聯系。甚至一對一的映射可以擴充到多對多,具體對象之間的映射可以擴充到抽象概念之間。
嘗試一:直接調用
應該會有人說, Miaow()的函數中直接調用RunAway()不就行了?
明顯場景二就把這種方案pass掉了。
直接調用的問題是,貓要知道老鼠有個函數/接口叫逃跑,然後主動調用了它。
這就好比Tom叫了一聲,然後Tom主動擰着Jerry的腿讓它跑。這樣是不合理的。(Jerry表示一臉懵逼!)
真實的邏輯是,貓的叫聲在空氣/媒體中傳播,傳到了老鼠的耳朵裡,老鼠就逃跑了。貓和老鼠互相都沒看見呢。
嘗試二:回調函數+映射表
似乎是可行的。
稍微思考一下,我們要做這兩件事情:
1 把RunAway函數取出來存儲在某個地方
2 建立Miaow函數和RunAway的映射關系,能夠在前者被調用時,自動調用後者。
RunAway函數可以用 函數指針|成員函數指針 或者C++11-function 來存儲,都可以稱作 “回調函數”。
(下面的代碼以C++11 function的寫法為主,函數指針的寫法稍微複雜一些,本質一樣)
我們先用一個簡單的Map來存儲映射關系, 就用一個字元串作為映射關系的名字
std::map<std::string, std::function<void()>> callbackMap;
我們還要實作 “建立映射關系” 和 “調用”功能,是以這裡封裝一個Connections類
class Connections
{
public:
//按名稱“建立映射關系”
void connect(const std::string &name, const std::function<void()> &callback)
{
m_callbackMap[name] = callback;
}
//按名稱“調用”
void invok(const std::string &name)
{
auto it = m_callbackMap.find(name);
//疊代器判斷
if (it != m_callbackMap.end()) {
//疊代器有效的情況,直接調用
it->second();
}
}
private:
std::map<std::string, std::function<void()>> m_callbackMap;
};
那麼這個映射關系存儲在哪裡呢? 顯然是一個Tom和Jerry共有的”上下文環境”中。
我們用一個全局變量來表示,這樣就可以簡單地模拟了:
//全局共享的Connections。
static Connections s_connections;
//Tom的定義
class Tom
{
public:
//貓叫
void Miaow()
{
cout << "喵!" << endl;
//調用一下名字為mouse的回調
s_connections.invok("mouse");
}
//省略其它
...
};
//Jerry的定義
class Jerry
{
public:
Jerry()
{
//構造函數中,建立映射關系。std::bind屬于基本用法。
s_connections.connect("mouse", std::bind(&Jerry::RunAway, this));
}
//逃跑
void RunAway()
{
cout << "那隻貓又來了,快溜!" << endl;
}
//省略其它
...
};
int main(int argc, char *argv[])
{
//模拟嵌套層級很深的場景,外部不能直接通路到tom
struct A {
struct B {
struct C {
private:
//Tom在很深的結構中
Tom tom;
public:
void MiaoMiaoMiao()
{
tom.Miaow();
}
}c;
void MiaoMiao()
{
c.MiaoMiaoMiao();
}
}b;
void Miao()
{
b.MiaoMiao();
}
}a;
//模拟嵌套層級很深的場景,外部不能直接通路到jerry
struct D {
struct E {
struct F {
private:
//jerry在很深的結構中
Jerry jerry;
}f;
}e;
}d;
//A間接調用tom的MiaoW,發出貓叫聲
a.Miao();
return 0;
}
RunAway沒有被直接調用,而是被自動觸發。
分析:這裡是以”mouse”這個字元串作為連接配接tom和jerry的關鍵。這隻是一種簡單、粗糙的示例實作。
觀察者模式
在GOF四人幫的書籍《設計模式》中,有一種觀察者模式,可以比較優雅地實作同樣的功能。
(順便說一下,GOF總結的設計模式一共有23種,濤哥曾經用C++11實作了全套的,github位址是:https://github.com/jaredtao/DesignPattern)
初級的觀察者模式,濤哥就不重複了。這裡濤哥用C++11搭配一點模闆技巧,實作一個更加通用的觀察者模式。
也可以叫釋出-訂閱模式。
//Subject.hpp
#pragma once
#include <vector>
#include <algorithm>
//Subject 事件或消息的主體。模闆參數為觀察者類型
template<typename ObserverType>
class Subject {
public:
//訂閱
void subscibe(ObserverType *obs)
{
auto itor = std::find(m_observerList.begin(), m_observerList.end(), obs);
if (m_observerList.end() == itor) {
m_observerList.push_back(obs);
}
}
//取消訂閱
void unSubscibe(ObserverType *obs)
{
m_observerList.erase(std::remove(m_observerList.begin(), m_observerList.end(), obs));
}
//釋出。這裡的模闆參數為函數類型。
template <typename FuncType>
void publish(FuncType func)
{
for (auto obs: m_observerList)
{
//調用回調函數,将obs作為第一個參數傳遞
func(obs);
}
}
private:
std::vector<ObserverType *> m_observerList;
};
//main.cpp
#include "Subject.hpp"
#include <functional>
#include <iostream>
using std::cout;
using std::endl;
//CatObserver 接口 貓的觀察者
class CatObserver {
public:
//貓叫事件
virtual void onMiaow() = 0;
public:
virtual ~CatObserver() {}
};
//Tom 繼承于Subject模闆類,模闆參數為CatObserver。這樣Tom就擁有了訂閱、釋出的功能。
class Tom : public Subject<CatObserver>
{
public:
void miaoW()
{
cout << "喵!" << endl;
//釋出"貓叫"。
//這裡取CatObserver類的成員函數指針onMiaow。而成員函數指針調用時,要傳遞一個對象的this指針才行的。
//是以用std::bind 和 std::placeholders::_1将第一個參數 綁定為 函數被調用時的第一個參數,也就是前面Subject::publish中的obs
publish(std::bind(&CatObserver::onMiaow, std::placeholders::_1));
}
};
//Jerry 繼承于 CatObserver
class Jerry: public CatObserver
{
public:
//重寫“貓叫事件”
void onMiaow() override
{
//發生 “貓叫”時 調用 逃跑
RunAway();
}
void RunAway()
{
cout << "那隻貓又來了,快溜!" << endl;
}
};
int main(int argc, char *argv[])
{
Tom tom;
Jerry jerry;
//拿jerry去訂閱Tom的 貓叫事件
tom.subscibe(&jerry);
tom.miaoW();
return 0;
}
任意類隻要繼承Subject模闆類,提供觀察者參數,就擁有了釋出-訂閱功能。
Qt的信号-槽
信号-槽簡介
信号-槽 是Qt自定義的一種通信機制,它不同于标準C/C++ 語言。
信号-槽的使用方法,是在普通的函數聲明之前,加上signal、slot标記,然後通過connect函數把信号與槽 連接配接起來。
後續隻要調用 信号函數,就可以觸發連接配接好的信号或槽函數。
連接配接的時候,前面的是發送者,後面的是接收者。信号與信号也可以連接配接,這種情況把接收者信号看做槽即可。
信号-槽分兩種
信号-槽要分成兩種來看待,一種是同一個線程内的信号-槽,另一種是跨線程的信号-槽。
同一個線程内的信号-槽,就相當于函數調用,和前面的觀察者模式相似,隻不過信号-槽稍微有些性能損耗(這個後面細說)。
跨線程的信号-槽,在信号觸發時,發送者線程将槽函數的調用轉化成了一次“調用事件”,放入事件循環中。
接收者線程執行到下一次事件處理時,處理“調用事件”,調用相應的函數。
(關于事件循環,可以參考專欄上一篇文章《Qt實用技能3-了解事件循環》)
信号-槽的實作 元對象編譯器moc
信号-槽的實作,借助一個工具:元對象編譯器MOC(Meta Object Compiler)。
這個工具被內建在了Qt的編譯工具鍊qmake中,在開始編譯Qt工程時,會先去執行MOC,從代碼中
解析signals、slot、emit等等這些标準C/C++不存在的關鍵字,以及處理Q_OBJECT、Q_PROPERTY、
Q_INVOKABLE等相關的宏,生成一個moc_xxx.cpp的C++檔案。(使用黑魔法來變現文法糖)
比如信号函數隻要聲明、不需要自己寫實作,就是在這個moc_xxx.cpp檔案中,自動生成的。
MOC之後就是正常的C/C++編譯、連結流程了。
moc的本質-反射
MOC的本質,其實是一個反射器。标準C++沒有反射功能(将來會有),是以Qt用moc實作了反射功能。
什麼叫反射呢? 簡單來說,就是運作過程中,擷取對象的構造函數、成員函數、成員變量。
舉個例子來說明,有下面這樣一個類聲明:
class Tom {
public:
Tom() {}
const std::string & getName() const
{
return m_name;
}
void setName(const std::string &name)
{
m_name = name;
}
private:
std::string m_name;
};
類的使用者,看不到類的聲明,頭檔案都拿不到,不能直接調用類的構造函數、成員函數。
從配置檔案/網絡拿到了一段字元串“Tom”,就要建立一個Tom類的對象執行個體。
然後又拿到一段“setName”的字元串,就要去調用Tom的setName函數。
面對這種需求,就需要把Tom類的構造函數、成員函數等資訊存儲起來,還要能夠被調用到。
這些資訊就是 “元資訊”,使用者通過“元資訊”就可以“使用這個類”。這便是反射了。
設計模式中的“工廠模式”,就是一個典型的反射案例。不過工廠模式隻解決了構造函數的調用,沒有成員函數、成員變量等資訊。
【領QT開發教程學習資料,點選下方連結莬費領取↓↓,先碼住不迷路~】
點選這裡:Qt資料領取(視訊教程+文檔+代碼+項目實戰)