Qt -信号与槽

news2025/4/7 16:37:00

博客主页:【夜泉_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::Connection
    connect(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
implimplementation(实现)的缩写,
所以这里才是连接信号和槽的地方。
(那 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 工具生成的元数据,
依赖于 QObjectQMetaObject
而这个 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.hQMetaObject::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
btnsingalVectorclicked 信号的 ConnecionList 多了一个 Connection
thissenders 双链表中 也多了一个 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(); // 输出:点击次数:1
    button.press(); // 输出:点击次数:2

    std::cout << "-------------------" << std::endl;

    // 测试 2: 多个槽连接到同一个信号
    Button button2;
    Label label2;
    Label label3;

    // 连接按钮的点击信号到多个槽
    connect(button2.clicked, &label2, &Label::showCount);
    connect(button2.clicked, &label3, &Label::showCount);

    // 模拟用户点击按钮
    button2.press(); // 输出:点击次数:1
    button2.press(); // 输出:点击次数:2

    std::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语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2329958.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Django中使用不同种类缓存的完整案例

Django中使用不同种类缓存的完整案例 推荐超级课程: 本地离线DeepSeek AI方案部署实战教程【完全版】Docker快速入门到精通Kubernetes入门到大师通关课AWS云服务快速入门实战目录 Django中使用不同种类缓存的完整案例步骤1:设置Django项目步骤2:设置URL路由步骤3:视图级别…

解锁健康密码,拥抱品质生活

在生活节奏不断加快的今天&#xff0c;健康养生已成为人们关注的焦点。它不仅关乎当下生活质量&#xff0c;更是对未来幸福的投资。从日常生活的点滴出发&#xff0c;掌握正确养生方法&#xff0c;我们就能轻松收获健康。​ 饮食是健康的基石。我们应当遵循 “食物多样&#x…

ABAP 新语法 - corresponding

在 ABAP 中&#xff0c;CORRESPONDING 操作符用于根据字段名称自动映射结构体&#xff08;Structure&#xff09;或内表&#xff08;Internal Table&#xff09;的字段值。它比传统的 MOVE-CORRESPONDING 语句更灵活&#xff0c;支持更多控制选项。 基础用法 data: begin of …

HTML零基础入门笔记:狂神版

前言 本笔记是学习狂神的java教程&#xff0c;建议配合视频&#xff0c;学习体验更佳。 【狂神说Java】HTML5完整教学通俗易懂_哔哩哔哩_bilibili 第1-2章&#xff1a;Java零基础入门笔记&#xff1a;(1-2)入门&#xff08;简介、基础知识&#xff09;-CSDN博客 第3章&…

FreeRTOS移植笔记:让操作系统在你的硬件上跑起来

一、为什么需要移植&#xff1f; FreeRTOS就像一套"操作系统积木"&#xff0c;但不同硬件平台&#xff08;如STM32、ESP32、AVR等&#xff09;的CPU架构和外设差异大&#xff0c;需要针对目标硬件做适配配置。移植工作就是让FreeRTOS能正确管理你的硬件资源。 二、…

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第五式】动态内存管理

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第五式】动态内存管理 【心法】 【第零章】c语言概述 【第一章】分支与循环语句 【第二章】函数 【第三章】数组 【第四章】操作符 【第五章】指针 【第六章】结构体 【第七章】const与c语言中一些错误代码 【禁忌秘术】 【第一式…

MySQL表的增删改查基础版

这一部分内容比较多&#xff0c;请大家结合目录查看&#x1f440; 增删改查 这一部分内容比较多&#xff0c;请大家结合目录查看&#x1f440; 一、新增1.插入2.指定列插入3.一次插入多行记录 二、查询1.全列查询2.指定列查询3.查询字段为表达式4.别名5.去重6.多列去重7.排序8.…

【备赛】蓝桥杯嵌入式实现led闪烁

原理 由于蓝桥杯的板子带有锁存器&#xff0c;并且与lcd屏幕有冲突&#xff0c;所以这个就成了考点。 主要就是用定时器来实现&#xff0c;同时也要兼顾lcd的冲突。 一、处理LCD函数 首先来解决与lcd屏幕冲突的问题&#xff0c;把我们所有用到的lcd函数改装一下。 以下是基…

【Python】贝叶斯,条件概率是怎么回事儿

【Python】贝叶斯&#xff0c;条件概率是怎么回事儿 一、原理简介1.1 贝叶斯定理1.2 朴素贝叶斯假设 二、算法实现过程2.1 数据准备与预处理2.2 模型训练与预测2.2.1 高斯朴素贝叶斯 - 对应连续型数据2.2.2 多项式朴素贝叶斯 - 离散型数据 2.3 模型评估 三、算法优缺点分析3.1 …

Flink介绍——实时计算核心论文之Storm论文详解

引入 我们通过以下两篇文章&#xff0c;深入探索了S4是如何抽象流式计算模型&#xff0c;如何设计架构和系统&#xff0c;存在那些局限&#xff1a; 论文详解论文总结 Yahoo推出的S4 并没有在历史舞台上站稳脚跟&#xff0c;在S4的论文发表的同一年&#xff0c;我们今天的主…

001 使用单片机实现的逻辑分析仪——吸收篇

本内容记录于韦东山老师的毕设级开源学习项目&#xff0c;含个人观点&#xff0c;请理性阅读。 个人笔记&#xff0c;没有套路&#xff0c;一步到位&#xff0c;欢迎交流&#xff01; 00单片机的逻辑分析仪与商业版FPGA的逻辑分析仪异同 对比维度自制STM32逻辑分析仪商业版逻…

11-产品经理-创建产品

在“产品”-“仪表盘”内&#xff0c;可以查看系统中关于产品及相关需求的统计。 在“产品”-“产品列表”页面&#xff0c;可以按项目集、项目查看其关联产品。还可以添加产品、编辑产品线、或者导出产品列表。 产品看板&#xff0c;通过看板方式查看产品、产品计划和产品下的…

低代码开发平台:飞帆制作网页并集成到自己的网页中

应用场景&#xff1a; 有时&#xff0c;我们的网页使用了某个模版&#xff0c;或者自己写的 html、css、javascript 代码。只是网页中的一部分使用飞帆来制作。这样的混合网页如何实现呢&#xff1f; 其实很容易&#xff0c;来体验一下飞帆提供的功能&#xff01; 还记得这个…

语法: result=log (x);

LOG( ) 语法: resultlog (x); 参数: x是一个浮点数; 返回值: result等于返回值,是一个浮点数; 功能: 该函数是用来计算浮点数x的自然对数(即ln x);如果x小于或等于0,或x太大,则行为没有定义; 注意:存在error挂起; 如果在编写程序里包含了errno.h头文件,则范围和等级…

Hibernate核心方法总结

Session中的核心方法梳理 1、save方法 这个方法表示将一个对象保存到数据库中&#xff0c;可以将一个不含OID的new出来的临时对象转换为一个处于Session缓存中具有OID的持久化对象。 需要注意的是&#xff1a;在save方法前设置OID是无效的但是也不会报错&#xff0c;在save方…

IntelliJ IDEA Maven 工具栏消失怎么办?

一、问题现象与背景 在使用 IntelliJ IDEA&#xff08;简称 IDEA&#xff09;开发 Maven 项目时&#xff0c;偶尔会遇到右侧或侧边栏的 Maven 工具栏&#xff08;显示依赖、生命周期等信息的窗口&#xff09;突然消失的情况。这可能影响开发者快速操作 Maven 构建、依赖管理等…

消息队列(kafka 与 rocketMQ)

为什么要使用消息队列?作用1: 削峰填谷(突发大请求量问题)作用2: 解耦(单一原则)作用3: 异步(减少处理时间) 如何选择消息队列(kafka&RocketMQ)成本功能性能选择 rocketMQ是参考kafka进行实现的为什么rocketMQ与kafka性能差距很大呢?kafka 的底层数据储存实现rocketMQ 的…

【STM32】Flash详解

【STM32】Flash详解 文章目录 【STM32】Flash详解1.Flash闪存概念1. 1核心区别&#xff1a;NOR Flash vs. NAND Flash1.2 为什么常说的“Flash”多指 NAND Flash&#xff1f;1.3技术细节对比(1) 存储单元结构(2) 应用场景(3) 可靠性要求 1.4总结 2.STM32内部的Flash2.1为什么是…

CV - 目标检测

物体检测 目标检测和图片分类的区别&#xff1a; 图像分类&#xff08;Image Classification&#xff09; 目的&#xff1a;图像分类的目的是识别出图像中主要物体的类别。它试图回答“图像是什么&#xff1f;”的问题。 输出&#xff1a;通常输出是一个标签或一组概率值&am…

node-modules-inspector 可视化node_modules

1、node_modules 每个vue的项目都有很多的依赖&#xff0c;有的是dev的&#xff0c;有的是生产的。 2、使用命令pnpx node-modules-inspector pnpx node-modules-inspector 3、node_modules可视化 4、在线体验 Node Modules Inspector 5、github地址 https://github.com/a…