Qt -信号与槽
博客主页:【夜泉_ly】
本文专栏:【暂无】
欢迎点赞👍收藏⭐关注❤️

目录
- 前言
- 引入
- connect调用链
- 模板类型的connect
- QObject::connectImpl
- QObjectPrivate::connectImpl
- qobject_p_p.h
- connect作用总结
- ai对信号与槽的模拟实现
前言
面向对象,
这个词从开始学 C++ 我们就知道了,
但我们或许仍然不能真正理解它。
而本篇的信号与槽,
或许多多少少能加深我们对面向对象的认识。
引入
信号与槽,
本质解决的是对象之间的通信问题。
很简单的一个例子:
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);QPushButton* btn = new QPushButton(this);btn->setText("关闭窗口");connect(btn, &QPushButton::clicked, this, &QWidget::close); // 针对不同对象。点击按钮,关闭窗口
}
在这里,
connect 将一个按钮和一个控件建立了连接。
点击按钮,
按钮会告诉控件:
你该关闭了。
很明显按钮和控件是两个不同的对象,
而它们能够通信,
借助的就是信号与槽。
connect调用链
模板类型的connect
为了加深理解,
下面我们来简单看看Qt中对刚刚的 connect 的处理。
首先,当我们写下:
connect(btn, &QPushButton::clicked, this, &QWidget::close);
会调用 qobject.h 的模板类型的 connect (227行左右)
//connect with context
template <typename Func1, typename Func2>
static inline QMetaObject::Connectionconnect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal,const typename QtPrivate::ContextTypeForFunctor<Func2>::ContextType *context, Func2 &&slot,Qt::ConnectionType type = Qt::AutoConnection)
之后会进行一系列的检查,
用了大量 TMP 的知识,
我暂时看不懂。
总之,大概检查了信号和槽的各种类型后,
调用了 connectImpl ,
impl 是 implementation(实现)的缩写,
所以这里才是连接信号和槽的地方。
(那 connect 的几十行代码全用来检查了?恐怖如斯)
connect 函数末尾:
QObject::connectImpl
这个 connectImpl 也有很多地方实现了,
不过根据参数类型,
我觉得它调的是这个 QObject::connectImpl :
qobject.cpp 5324行左右
QMetaObject::Connection QObject::connectImpl(const QObject *sender, void **signal,const QObject *receiver, void **slot,QtPrivate::QSlotObjectBase *slotObjRaw, Qt::ConnectionType type,const int *types, const QMetaObject *senderMetaObject)
查了查,
Qt 的信号是由 moc 工具生成的元数据,
依赖于 QObject 和 QMetaObject。
而这个 QObject::connectImpl 的作用就是:
使用 QMetaObject 的元信息查找信号的索引 signal_index。
然后在函数末尾调用了 QObjectPrivate::connectImpl:
return QObjectPrivate::connectImpl(sender, signal_index, receiver, slot, slotObj.release(), type, types, senderMetaObject);
QObjectPrivate::connectImpl
qobject.cpp 5370行左右
QMetaObject::Connection QObjectPrivate::connectImpl(const QObject *sender, int signal_index,const QObject *receiver, void **slot,QtPrivate::QSlotObjectBase *slotObjRaw, int type,const int *types, const QMetaObject *senderMetaObject)
这下才是真的来到核心实现了🤣。
我觉得最重要的两句话:
std::unique_ptr<QObjectPrivate::Connection> c{new QObjectPrivate::Connection};
QObjectPrivate::get(s)->addConnection(signal_index, c.get());
这里创建了一个 QObjectPrivate::Connection 对象,
在里面保存信号和槽的连接信息。
又用 QObjectPrivate::get(s)
即指向信号发送者( sender )的 QObjectPrivate 指针,
调用 addConnection()
而 addConnection。。。
qobject_p_p.h
我们还是先看看 QObjectPrivate 中的几个结构体吧:
struct Connection;
struct ConnectionData;
struct ConnectionList;
struct ConnectionOrSignalVector;
struct SignalVector;
struct Sender;
struct TaggedSignalVector;
定义在 qobject_p_p.h 中,
共同构成了 Qt 信号与槽机制的底层实现。
Connection的关键字段(我认为的):
struct QObjectPrivate::Connection : public ConnectionOrSignalVector
{QObject *sender;QAtomicPointer<QObject> receiver;union {StaticMetaCallFunction callFunction;QtPrivate::QSlotObjectBase *slotObj;};signed int signal_index : 27; // In signal range (see QObjectPrivate::signalIndex())ushort connectionType : 2; // 0 == auto, 1 == direct, 2 == queued, 3 == blocking
};
Connection 表示信号与槽之间的一个具体连接,
是个双链表的节点(指针被我省略了,因为我看不懂)。
sender发出信号的对象receiver接收信号的对象union中,提供两种方式去调用函数
且因为是union,所以同时只存在一个方法
Qt会根据你传的槽去选择:callFunction对应 成员函数slotObj对应lambda表达式、仿函数等复杂点的对象
ConnectionData 的关键字段(我认为的):
struct QObjectPrivate::ConnectionData
{QAtomicPointer<SignalVector> signalVector;Connection *senders = nullptr;Sender *currentSender = nullptr;
};
-
signalVector存的与对象相关的所有信号的连接信息。
每个元素对应一个ConnecionList,
存储了与该信号相关的所有槽的连接信息。

-
senders存的连接到当前对象槽的信号的连接信息。
从类型Connection看出,这是个双链表。

-
currentSender指的是当前被激活的信号发送者,
当有信号被激活,会有个Sender对象被创建,并连接到这里。
具体的细节。。嘶,又要跳文件吗 !?
好像激活和qobjectdefs.h的QMetaObject::activate有关,
暂时不看了。
但Sender的构造可以看看,
这里体现了连接到currentSender的过程:Sender(QObject *receiver, QObject *sender, int signal, ConnectionData *receiverConnections): receiver(receiver), sender(sender), signal(signal) {if (receiverConnections) {previous = receiverConnections->currentSender;receiverConnections->currentSender = this;} }
connect作用总结
那么看到这里,
似乎 addConnection 不太需要看了,
Qt的 SignalVector 使用了非常规的方法表示数组,
主要利用的是指针的偏移,
所以相关的代码都涉及大量的指针操作,
提高了性能,
但降低了我这种fw的阅读体验。
总结一下 QObjectPrivate::connectImpl 的主要作用吧:
创建连接信息,用的 Connection 结构体。
将连接信息添加到发送者的 signalVector 中
将连接信息添加到接收者的 senders 中
最后回到开头的例子:
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);QPushButton* btn = new QPushButton(this);btn->setText("关闭窗口");connect(btn, &QPushButton::clicked, this, &QWidget::close); // 针对不同对象。点击按钮,关闭窗口
}
发送者就是 btn ,是个按钮。
接收者就是 this , 是个控件。
通过 connect :
btn 的 singalVector 的 clicked 信号的 ConnecionList 多了一个 Connection,
this 的 senders 双链表中 也多了一个 Connection,
而这两个地方指向的是同一个 Connection 结构体,
而这个 Connection 的内容:
sender 就是 btn,即按钮
receiver 就是 this,即控件
callFunction 就是 QWidget::close。(因为 QWidget::close 是个成员函数,所以union 中是 callFunction,而不是 slotObj)
signal_index 就是 QPushButton::clicked 信号对应的下标。
这就是建立连接的过程了。
至于调用。。力竭了,不想看了。。
ai对信号与槽的模拟实现
最后,看看ai的模拟实现吧:
类型检查虽然非必须,
但 TMP 真的帅啊。。。
#include <iostream>
#include <vector>
#include <functional>
#include <tuple>// 检查每一对参数类型是否兼容的类型特征
template <typename SignalArgsTuple, typename SlotArgsTuple>
struct CheckCompatibleArguments;// 基本情况:参数列表为空
template <>
struct CheckCompatibleArguments<std::tuple<>, std::tuple<>> {static constexpr bool value = true;
};// 递归情况:逐一检查所有参数对
template <typename SignalArg, typename... SignalRest,typename SlotArg, typename... SlotRest>
struct CheckCompatibleArguments<std::tuple<SignalArg, SignalRest...>, std::tuple<SlotArg, SlotRest...>
> {static constexpr bool value =std::is_convertible<SignalArg, SlotArg>::value&&CheckCompatibleArguments<std::tuple<SignalRest...>, std::tuple<SlotRest...>>::value;
};// Signal类定义,用于表示信号,管理连接的槽
template <typename... Args>
class Signal {std::vector<std::function<void(Args...)>> slots;public:// 连接槽函数到当前信号template <typename F>void connect(F&& f) {slots.emplace_back(std::forward<F>(f));}// 触发信号,传播参数至所有已连接的槽void emit(Args... args) {for (auto& slot : slots) {slot(args...);}}
};// 全局connect函数,用于连接信号和成员函数槽
template <typename Receiver, typename... SignalArgs, typename... SlotArgs>
void connect(Signal<SignalArgs...>& signal,Receiver* receiver,void (Receiver::* slot)(SlotArgs...)
) {// 确保信号和槽参数个数匹配static_assert(sizeof...(SignalArgs) == sizeof...(SlotArgs),"Signal and slot have different number of arguments");// 确保参数类型兼容static_assert(CheckCompatibleArguments<std::tuple<SignalArgs...>,std::tuple<SlotArgs...>>::value,"Signal and slot arguments are not compatible");signal.connect([receiver, slot](SignalArgs... args) {(receiver->*slot)(args...);});
}// 示例发送者类:按钮
class Button {
public:Signal<int> clicked; // 带整数参数(点击次数)的信号void press() {static int count = 0;clicked.emit(++count); // 发出信号}
};// 示例接收者类:标签
class Label {
public:void showCount(int num) {std::cout << "点击次数:" << num << std::endl;}
};int main() {// 测试 1: 基本测试:按钮点击信号和标签的显示槽连接Button button;Label label;// 连接按钮的点击信号到标签的显示槽connect(button.clicked, &label, &Label::showCount);// 模拟用户点击按钮button.press(); // 输出:点击次数:1button.press(); // 输出:点击次数:2std::cout << "-------------------" << std::endl;// 测试 2: 多个槽连接到同一个信号Button button2;Label label2;Label label3;// 连接按钮的点击信号到多个槽connect(button2.clicked, &label2, &Label::showCount);connect(button2.clicked, &label3, &Label::showCount);// 模拟用户点击按钮button2.press(); // 输出:点击次数:1button2.press(); // 输出:点击次数:2std::cout << "-------------------" << std::endl;// 测试 3: 使用不同类型的信号和槽class StringLabel {public:void showString(const std::string& str) {std::cout << "显示字符串:" << str << std::endl;}};Signal<std::string> stringSignal;StringLabel stringLabel;// 连接信号和槽connect(stringSignal, &stringLabel, &StringLabel::showString);// 发射一个字符串信号stringSignal.emit("Hello, world!"); // 输出:显示字符串:Hello, world!std::cout << "-------------------" << std::endl;// 测试 4: 测试无参信号Signal<> noArgSignal;class NoArgLabel {public:void notify() {std::cout << "信号发射了!" << std::endl;}};NoArgLabel noArgLabel;// 连接无参信号和槽connect(noArgSignal, &noArgLabel, &NoArgLabel::notify);// 发射无参信号noArgSignal.emit(); // 输出:信号发射了!std::cout << "-------------------" << std::endl;// 测试 5: 参数类型不兼容的错误(编译时错误)// Uncommenting the code below will result in a compilation error.// Signal<double> doubleSignal;// connect(doubleSignal, &label, &Label::showCount); // 编译错误:类型不兼容return 0;
}
运行结果:



希望本篇文章对你有所帮助!并激发你进一步探索编程的兴趣!
本人仅是个C语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!
相关文章:
Qt -信号与槽
博客主页:【夜泉_ly】 本文专栏:【暂无】 欢迎点赞👍收藏⭐关注❤️ 目录 前言引入connect调用链模板类型的connectQObject::connectImplQObjectPrivate::connectImpl qobject_p_p.hconnect作用总结ai对信号与槽的模拟实现 前言 面向对象&am…...
深度解析新能源汽车研发测试中的关键信号采集技术
摘要 随着新能源汽车的快速发展,研发测试环节对信号采集的需求日益复杂。本文结合行业前沿技术方案,系统梳理了新能源汽车测试中需要关注的核心信号类型、采集方法及技术难点,涵盖高压电气、动力电池、热管理、智能驾驶、网络通信等全维度数据…...
Django中使用不同种类缓存的完整案例
Django中使用不同种类缓存的完整案例 推荐超级课程: 本地离线DeepSeek AI方案部署实战教程【完全版】Docker快速入门到精通Kubernetes入门到大师通关课AWS云服务快速入门实战目录 Django中使用不同种类缓存的完整案例步骤1:设置Django项目步骤2:设置URL路由步骤3:视图级别…...
Linux 高级命令与常见操作:文本处理、系统管理与网络调试
下面是一份针对已经熟悉 Linux 基础命令的用户所整理的「高级命令与常见操作」笔记,涵盖文本处理、系统管理、网络调试与其他常用的进阶技巧。请你审核下面笔记,检查是否有过时的内容,如有请进行替换,确保其符合现代化需求&#x…...
解锁健康密码,拥抱品质生活
在生活节奏不断加快的今天,健康养生已成为人们关注的焦点。它不仅关乎当下生活质量,更是对未来幸福的投资。从日常生活的点滴出发,掌握正确养生方法,我们就能轻松收获健康。 饮食是健康的基石。我们应当遵循 “食物多样&#x…...
TLS 1.2 握手过程,每个阶段如何保证通信安全?
TLS 1.2 握手是确保客户端和服务器之间安全通信的关键过程。它涉及多个步骤,包括身份验证、加密算法协商和会话密钥交换。 目录 TLS 1.2 握手是确保客户端和服务器之间安全通信的关键过程。它涉及多个步骤,包括身份验证、加密算法协商和会话密钥交换。…...
ABAP 新语法 - corresponding
在 ABAP 中,CORRESPONDING 操作符用于根据字段名称自动映射结构体(Structure)或内表(Internal Table)的字段值。它比传统的 MOVE-CORRESPONDING 语句更灵活,支持更多控制选项。 基础用法 data: begin of …...
C++ 中为什么构造函数不需要实现虚函数,而析构函数需要?
在C中,构造函数不需要是虚函数,而析构函数往往需要,原因如下: 构造函数 对象创建顺序:构造函数的主要任务是初始化对象的成员变量,创建对象时需要先调用基类的构造函数,再调用派生类的构造函数…...
vscode使用方式
一、常用快捷键与代码操作 注释与代码排版 行注释:Ctrl /;块注释:Shift Alt A。 代码缩进:选中代码段后按 Tab(右移)或 Shift Tab(左移)。 代码导航与编辑 快速跳转文件&…...
存储模块cache
参考:存储模块 --- Cache_cache模块-CSDN博客 一级缓存(L1 Cache) 和 二级缓存(L2 Cache) 都是处理器内的高速缓存,用来减少访问主内存的延迟,提高处理器的性能。它们在计算机体系结构中发挥着…...
HTML零基础入门笔记:狂神版
前言 本笔记是学习狂神的java教程,建议配合视频,学习体验更佳。 【狂神说Java】HTML5完整教学通俗易懂_哔哩哔哩_bilibili 第1-2章:Java零基础入门笔记:(1-2)入门(简介、基础知识)-CSDN博客 第3章&…...
java.util.Collections中常用api
在Java中,java.util.Collections 是一个工具类,提供了大量静态方法用于操作或返回集合(如List、Set、Map等)。以下是常用的API分类整理: 1. 排序与顺序操作 sort(List<T> list) 对List进行自然顺序排序ÿ…...
FreeRTOS移植笔记:让操作系统在你的硬件上跑起来
一、为什么需要移植? FreeRTOS就像一套"操作系统积木",但不同硬件平台(如STM32、ESP32、AVR等)的CPU架构和外设差异大,需要针对目标硬件做适配配置。移植工作就是让FreeRTOS能正确管理你的硬件资源。 二、…...
c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第五式】动态内存管理
c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第五式】动态内存管理 【心法】 【第零章】c语言概述 【第一章】分支与循环语句 【第二章】函数 【第三章】数组 【第四章】操作符 【第五章】指针 【第六章】结构体 【第七章】const与c语言中一些错误代码 【禁忌秘术】 【第一式…...
树莓派超全系列教程文档--(22)使用外部存储设备的相关操作
使用外部存储设备的相关操作 外部存储设备相关操作安装存储设备设置自动挂载卸载存储设备处理 target is busy 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 外部存储设备相关操作 您可以将外部硬盘、SSD或U盘连接到Raspberry Pi上的任何USB端…...
MySQL表的增删改查基础版
这一部分内容比较多,请大家结合目录查看👀 增删改查 这一部分内容比较多,请大家结合目录查看👀 一、新增1.插入2.指定列插入3.一次插入多行记录 二、查询1.全列查询2.指定列查询3.查询字段为表达式4.别名5.去重6.多列去重7.排序8.…...
PDF预览-搜索并高亮文本
在PDF.js中实现搜索高亮功能可以通过自定义一些代码来实现。PDF.js 是一个通用的、基于Web的PDF阅读器,它允许你在网页上嵌入PDF文件,并提供基本的阅读功能。要实现搜索并高亮显示文本,你可以通过以下几个步骤来完成: 1. 引入PDF…...
【备赛】蓝桥杯嵌入式实现led闪烁
原理 由于蓝桥杯的板子带有锁存器,并且与lcd屏幕有冲突,所以这个就成了考点。 主要就是用定时器来实现,同时也要兼顾lcd的冲突。 一、处理LCD函数 首先来解决与lcd屏幕冲突的问题,把我们所有用到的lcd函数改装一下。 以下是基…...
【Python】贝叶斯,条件概率是怎么回事儿
【Python】贝叶斯,条件概率是怎么回事儿 一、原理简介1.1 贝叶斯定理1.2 朴素贝叶斯假设 二、算法实现过程2.1 数据准备与预处理2.2 模型训练与预测2.2.1 高斯朴素贝叶斯 - 对应连续型数据2.2.2 多项式朴素贝叶斯 - 离散型数据 2.3 模型评估 三、算法优缺点分析3.1 …...
Flink介绍——实时计算核心论文之Storm论文详解
引入 我们通过以下两篇文章,深入探索了S4是如何抽象流式计算模型,如何设计架构和系统,存在那些局限: 论文详解论文总结 Yahoo推出的S4 并没有在历史舞台上站稳脚跟,在S4的论文发表的同一年,我们今天的主…...
001 使用单片机实现的逻辑分析仪——吸收篇
本内容记录于韦东山老师的毕设级开源学习项目,含个人观点,请理性阅读。 个人笔记,没有套路,一步到位,欢迎交流! 00单片机的逻辑分析仪与商业版FPGA的逻辑分析仪异同 对比维度自制STM32逻辑分析仪商业版逻…...
es基本概念
Elasticsearch 的架构与基本概念 Elasticsearch(简称 ES)是一个开源的分布式搜索和分析引擎,基于 Apache Lucene 构建。它被广泛用于全文搜索、日志分析、实时数据分析等场景。以下是其架构概述及其基本概念的详细解释。 Elasticsearch 的架…...
可以使用费曼学习法阅读重要的书籍
书本上画了很多线,回头看等于没画出任何重点。 不是所有的触动都是有效的。就像你曾经看过很多好文章,当时被触动得一塌糊涂,还把它们放进了收藏夹,但一段时间之后,你就再也记不起来了。如果让你在一本书上画出令自己…...
11-产品经理-创建产品
在“产品”-“仪表盘”内,可以查看系统中关于产品及相关需求的统计。 在“产品”-“产品列表”页面,可以按项目集、项目查看其关联产品。还可以添加产品、编辑产品线、或者导出产品列表。 产品看板,通过看板方式查看产品、产品计划和产品下的…...
JavaScript学习教程,从入门到精通,JavaScript 基础语法全面指南(5)
JavaScript 基础语法全面指南 一、JavaScript 基本概念 JavaScript 是一种轻量级的解释型或即时编译型的编程语言,主要用于网页开发,为网页添加交互功能。 1.1 JavaScript 的特点 解释型语言:不需要编译,由 JavaScript 引擎直…...
低代码开发平台:飞帆制作网页并集成到自己的网页中
应用场景: 有时,我们的网页使用了某个模版,或者自己写的 html、css、javascript 代码。只是网页中的一部分使用飞帆来制作。这样的混合网页如何实现呢? 其实很容易,来体验一下飞帆提供的功能! 还记得这个…...
语法: result=log (x);
LOG( ) 语法: resultlog (x); 参数: x是一个浮点数; 返回值: result等于返回值,是一个浮点数; 功能: 该函数是用来计算浮点数x的自然对数(即ln x);如果x小于或等于0,或x太大,则行为没有定义; 注意:存在error挂起; 如果在编写程序里包含了errno.h头文件,则范围和等级…...
Hibernate核心方法总结
Session中的核心方法梳理 1、save方法 这个方法表示将一个对象保存到数据库中,可以将一个不含OID的new出来的临时对象转换为一个处于Session缓存中具有OID的持久化对象。 需要注意的是:在save方法前设置OID是无效的但是也不会报错,在save方…...
IntelliJ IDEA Maven 工具栏消失怎么办?
一、问题现象与背景 在使用 IntelliJ IDEA(简称 IDEA)开发 Maven 项目时,偶尔会遇到右侧或侧边栏的 Maven 工具栏(显示依赖、生命周期等信息的窗口)突然消失的情况。这可能影响开发者快速操作 Maven 构建、依赖管理等…...
消息队列(kafka 与 rocketMQ)
为什么要使用消息队列?作用1: 削峰填谷(突发大请求量问题)作用2: 解耦(单一原则)作用3: 异步(减少处理时间) 如何选择消息队列(kafka&RocketMQ)成本功能性能选择 rocketMQ是参考kafka进行实现的为什么rocketMQ与kafka性能差距很大呢?kafka 的底层数据储存实现rocketMQ 的…...
