• 简介
  • 猫和老鼠的故事
  • 对象之间的通信机制
    • 尝试一:直接调用
    • 尝试二:回调函数+映射表
  • 观察者模式
  • Qt的信号-槽
    • 信号-槽简介
    • 信号-槽分两种
    • 信号-槽的实现 元对象编译器moc
    • moc的本质-反射
  • 参考文献

简介

这次讨论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[])
{//实例化tomTom tom;//实例化jerryJerry 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[])
{//模拟嵌套层级很深的场景,外部不能直接访问到tomstruct 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;//模拟嵌套层级很深的场景,外部不能直接访问到jerrystruct 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中的obspublish(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类的构造函数、成员函数等信息存储起来,还要能够被调用到。

这些信息就是 “元信息”,使用者通过“元信息”就可以“使用这个类”。这便是反射了。

设计模式中的“工厂模式”,就是一个典型的反射案例。不过工厂模式只解决了构造函数的调用,没有成员函数、成员变量等信息。

反射包括 编译期静态反射 和 运行期动态反射。。。

文章有点长了,这次先到这里,剩下的下次再讨论。

参考文献

[1] Qt帮助文档, 搜索关键词 Signals & Slots
[2] IBM文档库 https://www.ibm.com/developerworks/cn/linux/guitoolkit/qt/signal-slot/index.html

玩转Qt(6)-认清信号槽的本质相关推荐

  1. Qt线程间信号槽传递自定义数据类型(qRegisterMetaType的使用)

    Qt线程间信号槽传递自定义数据类型(qRegisterMetaType的使用) #include <QMetaType> CFileDataModel::CFileDataModel(QO ...

  2. Qt / Moc 和信号 - 槽解析

    目录 一. MOC 二. moc_test.cpp 分析 三. connect 四. activate 五. 总结 版本 Qt5.12.3 moc_test.cpp 位于可执行文件目录下,其余源代码都 ...

  3. Qt学习笔记-----信号槽

    Qt提供signals and slots mechanism(信号槽机制)来保证两个对象之前的关联(connection). 所谓信号槽,简单理解就是两部分,一个是某对象发出的信号,一个是某对象接收 ...

  4. Qt中绑定信号槽之后,信号槽无效

    下面程序编译没有错误,运行却未达到想要的效果,最后调试发现,是信号槽绑定后无效,即槽函数没有受到信号的触发.具体代码如下: main.cpp #include <QtCore> #incl ...

  5. Qt多线程间信号槽传递非QObject类型对象的参数

    一.以前就发现过这个问题: 在Qt项目中,有时候为了让自己的类,可以重载操作符 '=','<<','>>'. 也有时候需要用一个类进行文件的读写,所以很多C++类还是要简单化的 ...

  6. QT C++ 《信号-槽 基本操作》

    目录 <信号-槽 基本操作> 信号-槽连接 信号-槽断开连接 信号-槽连接 connect(ui.pushButton_SIGNAL, SIGNAL(clicked()), Client_ ...

  7. QT:PushButton+信号槽+Label简单使用

    实现内容 创建一个简单的QT GUI项目,实现点击按钮修改Label的内容 创建QT GUI 项目:ButtonTest 创建后的目录为: 添加界面布局 在界面添加两个PushButton和一个Lab ...

  8. Qt-认清信号槽的本质

    目录 (放个目录方便预览.这个目录是从博客复制过来的,点击会跳转到博客) 简介 猫和老鼠的故事 对象之间的通信机制 尝试一:直接调用 尝试二:回调函数+映射表 观察者模式 Qt的信号-槽 信号-槽简介 ...

  9. qt 多线程、信号槽、moveToThread等机制之拨乱反正

    之所以要"拨乱反正",是因为很多教科书上的说法,还有网页上的说法,都是错误的. 我没有看过qt源码,看过一些书籍,做过一些实验,说下我的理解.如有谬误,还请讨论. 首先来看看教科书 ...

最新文章

  1. O(N)的时间复杂度找出a[N]中那个重复的数字
  2. Nginx服务器的安装配置
  3. java中session对象登录_JavaWeb中Session对象的学习笔记
  4. 前端学习(2634):修改webstrom颜色
  5. win10看计算机属性,win10系统查看windows7版本号要比计算机属性具体的图文步骤
  6. iphone分辨率_AppStore今日推荐 iphone放大分辨率减少白噪点的照片处理工具
  7. powerbuilder提示不是下拉窗口_制作简易工资查询模板,下拉菜单选择姓名即可显示...
  8. js关于字面量与构造函数创建对象的几点理解
  9. [转]中国IT富翁们的第一份工作(组图)。
  10. ArcGIS 设置暂时固定存储地址
  11. Atitit 数据与模板绑定法 目录 1.1. templet - 自定义列模板 1 1.2. 方式三:直接赋值模版字符。事实上,templet 也可以直接是一段 html 内容,如: 1 1.2.
  12. 【小米商城首页简单设计】
  13. ubuntu系统给u盘拷贝文件
  14. okhttp请求使用cookie
  15. 按年、月、日统计数据sql写法
  16. bat实现ftp上传文件
  17. 阿里云mysql怎么查看数据库_阿里云服务器如何查看数据库
  18. 高质量图片无损压缩算法
  19. mahout LDA
  20. 【编译原理】— 求最小化DFA

热门文章

  1. 浙江机器人黑匣子代理_机器人“道德黑匣子”:追踪决策记录 为机器人辩解...
  2. C语言if条件语句(单分支结构)
  3. 机场调度管理系统(客户端+服务器端+Java+MySQL)
  4. Tensorflow2.0 复现 NNLM
  5. 使用ffmpeg给视频加滤镜技巧
  6. 07-Spring面试题总结
  7. C51和4G模块使用
  8. excel统计某个字符出现的次数,判断某单元格的数据是否在另外一列
  9. 防抖动(debounce )和 节流阀(throttle )
  10. “节流阀“ 在轮播图的使用