Qt开发-认清信号槽的本质
ztj100 2025-01-03 20:47 11 浏览 0 评论
简介
这次讨论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开发教程学习资料,点击下方链接莬费领取↓↓,先码住不迷路~】
相关推荐
- 从IDEA开始,迈进GO语言之门(idea got)
-
前言笔者在学习GO语言编程的时候,GO语言在国内还没有像JAVA/Php/Python那样普及,绕了不少的弯路,要开始入门学习一门编程语言,最好就先从选择一个好的编程语言的开发环境开始,有了这个开发环...
- 基于SpringBoot+MyBatis的私人影院java网上购票jsp源代码Mysql
-
本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目介绍基于SpringBoot...
- 基于springboot的个人服装管理系统java网上商城jsp源代码mysql
-
本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目介绍基于springboot...
- 基于springboot的美食网站Java食品销售jsp源代码Mysql
-
本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目介绍基于springboot...
- 贸易管理进销存springboot云管货管账分析java jsp源代码mysql
-
本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目描述贸易管理进销存spring...
- SpringBoot+VUE员工信息管理系统Java人员管理jsp源代码Mysql
-
本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目介绍SpringBoot+V...
- 目前见过最牛的一个SpringBoot商城项目(附源码)还有人没用过吗
-
帮粉丝找了一个基于SpringBoot的天猫商城项目,快速部署运行,所用技术:MySQL,Druid,Log4j2,Maven,Echarts,Bootstrap...免费给大家分享出来前台演示...
- SpringBoot+Mysql实现的手机商城附带源码演示导入视频
-
今天为大家带来的是基于SpringBoot+JPA+Thymeleaf框架的手机商城管理系统,商城系统分为前台和后台、前台用的是Bootstrap框架后台用的是SpringBoot+JPA都是现在主...
- 全网首发!马士兵内部共享—1658页《Java面试突击核心讲》
-
又是一年一度的“金九银十”秋招大热门,为助力广大程序员朋友“面试造火箭”,小编今天给大家分享的便是这份马士兵内部的面试神技——1658页《Java面试突击核心讲》!...
- SpringBoot数据库操作的应用(springboot与数据库交互)
-
1.JDBC+HikariDataSource...
- SpringBoot 整合 Flink 实时同步 MySQL
-
1、需求在Flink发布SpringBoot打包的jar包能够实时同步MySQL表,做到原表进行新增、修改、删除的时候目标表都能对应同步。...
- SpringBoot + Mybatis + Shiro + mysql + redis智能平台源码分享
-
后端技术栈基于SpringBoot+Mybatis+Shiro+mysql+redis构建的智慧云智能教育平台基于数据驱动视图的理念封装element-ui,即使没有vue的使...
- Springboot+Mysql舞蹈课程在线预约系统源码附带视频运行教程
-
今天发布的是由【猿来入此】的优秀学员独立做的一个基于springboot脚手架的Springboot+Mysql舞蹈课程在线预约系统,系统项目源代码在【猿来入此】获取!https://www.yuan...
- SpringBoot+Mysql在线众筹系统源码+讲解视频+开发文档(参考论文
-
今天发布的是由【猿来入此】的优秀学员独立做的一个基于springboot脚手架的在线众筹管理系统,主要实现了普通用户在线参与众筹基本操作流程的全部功能,系统分普通用户、超级管理员等角色,除基础脚手架外...
- Docker一键部署 SpringBoot 应用的方法,贼快贼好用
-
这两天发现个Gradle插件,支持一键打包、推送Docker镜像。今天我们来讲讲这个插件,希望对大家有所帮助!GradleDockerPlugin简介...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- 从IDEA开始,迈进GO语言之门(idea got)
- 基于SpringBoot+MyBatis的私人影院java网上购票jsp源代码Mysql
- 基于springboot的个人服装管理系统java网上商城jsp源代码mysql
- 基于springboot的美食网站Java食品销售jsp源代码Mysql
- 贸易管理进销存springboot云管货管账分析java jsp源代码mysql
- SpringBoot+VUE员工信息管理系统Java人员管理jsp源代码Mysql
- 目前见过最牛的一个SpringBoot商城项目(附源码)还有人没用过吗
- SpringBoot+Mysql实现的手机商城附带源码演示导入视频
- 全网首发!马士兵内部共享—1658页《Java面试突击核心讲》
- SpringBoot数据库操作的应用(springboot与数据库交互)
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- node卸载 (33)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- exceptionininitializererror (33)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)