当前位置: 首页 > article >正文

如何使用C++ 实现类似 Qt 的信号与槽机制

信号与槽机制是 Qt 框架中的核心设计,用于实现对象之间的解耦通信。在纯 C++ 中,我们也可以设计出类似的机制,利用模板、函数指针和哈希表,实现高效且灵活的信号与槽功能。


1. 什么是信号与槽?

信号与槽是一个发布-订阅模式的变种。我们可以将它理解为:

  • 信号: 一个事件源(Publisher),当某个事件发生时,它会触发(emit)信号。
  • 槽: 一个事件处理器(Subscriber),当信号触发时,它会被调用,完成具体的响应任务。

例如:

  • 一个按钮点击时发出信号,槽函数负责处理点击事件。
  • 一个定时器触发信号,槽函数完成定时任务。

在 C++ 中,我们可以用模板和函数对象来模拟这种机制。


2. 设计目标

实现的功能

  1. 允许多个槽连接到同一个信号。
  2. 支持动态添加和移除槽。
  3. 触发信号时,自动调用所有已连接的槽。
  4. 使用模板支持不同的信号参数类型。
  5. 灵活注册普通函数、类成员函数和 Lambda 表达式作为槽。

3. 模块设计

(1)Signal 模板类

Signal 是我们设计的核心类,用于管理信号与槽的连接和触发。它需要实现以下功能:

  • connect 注册一个槽函数到信号。
  • disconnect 通过唯一 ID 动态移除槽函数。
  • emit 触发信号,调用所有已注册的槽。

下面是 Signal 类的完整实现:

#ifndef SIGNAL_H
#define SIGNAL_H

#include <unordered_map>   // 用于存储槽的哈希表
#include <functional>      // 用于存储任意形式的槽函数
#include <iostream>        // 用于输出调试信息

// 信号类
template <typename... Args>
class Signal {
public:
    using SlotType = std::function<void(Args...)>; // 定义槽的类型
    using SlotID = int;                           // 槽的唯一标识符

    // 连接一个槽,返回槽的唯一 ID
    SlotID connect(SlotType slot) {
        SlotID id = nextID++;
        slots[id] = slot; // 将槽存入哈希表
        return id;
    }

    // 断开一个槽,通过其唯一 ID
    void disconnect(SlotID id) {
        auto it = slots.find(id);
        if (it != slots.end()) {
            slots.erase(it); // 从哈希表中移除槽
        }
    }

    // 触发信号,调用所有已连接的槽
    void emit(Args... args) const {
        for (const auto &pair : slots) {
            pair.second(args...); // 调用槽函数
        }
    }

private:
    std::unordered_map<SlotID, SlotType> slots; // 存储槽的哈希表
    SlotID nextID = 0;                          // 用于生成唯一 ID 的计数器
};

#endif // SIGNAL_H

(2)连接槽的示例

我们使用 Signal 模板类连接多个槽,包括普通函数、Lambda 表达式和类成员函数。

#include "Signal.h"
#include <iostream>
#include <string>

// 普通函数作为槽
void slot1(const std::string &message) {
    std::cout << "槽1 收到消息: " << message << std::endl;
}

// 普通函数作为槽
void slot2(const std::string &message) {
    std::cout << "槽2 收到消息: " << message << std::endl;
}

// 测试类,拥有自己的槽
class TestClass {
public:
    // 成员函数作为槽
    void classSlot(const std::string &message) {
        std::cout << "TestClass::classSlot 收到消息: " << message << std::endl;
    }
};

(3)主程序示例

通过主程序,我们测试以下功能:

  1. 注册普通函数、Lambda 表达式和成员函数到信号。
  2. 触发信号,调用所有槽。
  3. 动态断开某个槽,验证槽移除功能。
#include "Signal.h"
#include <iostream>
#include <string>

int main() {
    // 创建一个信号
    Signal<std::string> signal;

    // 连接普通函数到信号
    auto id1 = signal.connect(slot1);
    auto id2 = signal.connect(slot2);

    // 创建一个类实例,并连接成员函数到信号
    TestClass obj;
    auto id3 = signal.connect([&obj](const std::string &message) {
        obj.classSlot(message);
    });

    // 第一次触发信号,所有槽都会被调用
    std::cout << "第一次触发信号:" << std::endl;
    signal.emit("你好,信号与槽!");

    // 从信号中断开槽1
    std::cout << "\n断开槽1后,第二次触发信号:" << std::endl;
    signal.disconnect(id1);

    // 第二次触发信号,仅槽2和成员函数槽会被调用
    signal.emit("这是第二条消息!");

    return 0;
}

4. 运行结果

运行程序后,输出如下:

第一次触发信号:
槽1 收到消息: 你好,信号与槽!
槽2 收到消息: 你好,信号与槽!
TestClass::classSlot 收到消息: 你好,信号与槽!

断开槽1后,第二次触发信号:
槽2 收到消息: 这是第二条消息!
TestClass::classSlot 收到消息: 这是第二条消息!

5. 代码解析

  1. 槽的管理

    • 每个槽函数通过 connect 方法注册到信号,信号会为每个槽分配一个唯一标识符(SlotID)。
    • 槽函数存储在 std::unordered_map 中,键为 SlotID,值为槽函数。
  2. 信号的触发

    • 调用 emit 方法时,会遍历所有注册的槽,并依次调用它们。
  3. 槽的动态移除

    • 通过槽的唯一标识符(SlotID),调用 disconnect 方法,可以从信号中移除指定的槽。
  4. 支持多种类型的槽

    • 使用 std::function 存储槽,可以轻松支持普通函数、Lambda 表达式和类成员函数。

6. 特点与优点

优点
  1. 模块化设计:
    • Signal 类实现信号的管理与触发,独立、易用。
  2. 支持多样化槽:
    • 既支持普通函数,又支持成员函数和 Lambda 表达式。
  3. 高性能:
    • 使用 std::unordered_map 存储槽,添加、移除和触发的时间复杂度为 O(1)。

特点

  • 轻量级实现: 仅依赖 C++ 标准库,无需额外框架。
  • 模板化设计: 可以适配任意参数类型的信号与槽。

7. 应用场景

  1. 事件驱动开发:
    • 如 GUI 按钮点击、窗口关闭事件等场景。
  2. 解耦模块:
    • 观察者模式中,用信号与槽代替观察者通知机制。
  3. 回调机制:
    • 替代传统的回调函数方式,提供更灵活的信号与槽功能。

8. 总结

通过本文,我们实现了一个轻量级、功能完善的信号与槽系统。它借鉴了 Qt 的设计思想,但更加轻量化和灵活。这个设计可以轻松应用于任意纯 C++ 项目,特别适合事件驱动和解耦通信的场景。如果需要扩展到多线程环境,可以在此基础上加入线程安全机制,如 std::mutex

你可以将此代码作为基础,进一步改造和优化,打造符合你需求的高效信号与槽系统!


http://www.kler.cn/a/461624.html

相关文章:

  • 碰一碰矩阵发视频的技术开发,支持OEM
  • I.MX6ULL-GPT实现延时
  • 亚矩阵云手机技术形态与应用方向
  • STM32闭环控制直流电机和LCD界面方案
  • 路由器和交换机之作用、区别(The Role and Difference between Routers and Switches)
  • sqoop将MySQL数据导入hive
  • Python的秘密基地--[章节8] Python 数据科学与机器学习
  • 【GeekBand】C++设计模式笔记17_Mediator_中介者模式
  • Elasticsearch:normalizer
  • 快速将一个项目的 `package.json` 中的所有模块更新到最新版本
  • SQL进阶技巧:如何计算相互连接的计算机组成的集合?
  • CSS2笔记
  • 【LeetCode: 100. 相同的树 + 二叉树 + 递归】
  • 112、Qt MSVC编译Qtxlsx
  • 【中间件】docker+kafka单节点部署---zookeeper模式
  • Java安全—SpringBootJWT身份权鉴打包部署
  • 地理数据库Telepg面试内容整理-如何在高并发情况下保证GIS服务的高可用性?
  • VMware安装Windows11虚拟机,图文详细,附免费资源,持续更新
  • 常用的linux命令介绍
  • 【视觉SLAM:六、视觉里程计Ⅰ:特征点法】