跳转至

04-信号与槽:Qt 的“事件连线”

目录页:见 Qt 教程目录

目标

  • 用一句话解释“信号与槽”在解决什么问题
  • 会写最常见的 connect:按钮点击 → 执行一段逻辑
  • 知道新手最容易踩的坑:对象生命周期、重复连接、线程误用

先决知识(可选)

  • 会创建并运行一个 Widgets 工程(见 02 章)

1. 事件怎么从 A 传到 B:别把代码写成一团

你很快会遇到这种需求: - 用户点了按钮(A) - 你要更新标签、列表、或者触发网络请求(B)

如果你把所有逻辑都写进按钮类、窗口类里,很快就会变成“到处互相引用”的一团乱。


2. 信号与槽是什么:把“发生了什么”和“怎么处理”拆开

  • 专业名称:Signals & Slots(信号与槽)
  • 类比:插排的插头和插座。信号像“插头抛出一个事件”,槽像“插座接住后做事”
  • 作用:解耦:发消息的人不需要知道谁来处理;处理的人也不需要知道消息从哪来
  • 规则/坑点:连接(connect)依赖对象存在;对象销毁后连接会自动失效,但你要避免把 lambda 里捕获到的东西用成悬空

核心句子: - 信号:某件事发生了 - 槽:发生后要做什么 - connect:把它们连起来


3. 最常见 connect 写法(Qt5/Qt6 都通用)

这是新手最推荐的写法:函数指针 + lambda。

例子:按钮点击让计数 +1,并更新标签。

#include <QApplication>
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QWidget w;
    w.setWindowTitle("Signal/Slot Demo");

    auto *layout = new QVBoxLayout(&w);
    auto *label = new QLabel("count=0");
    auto *btn = new QPushButton("Add");

    layout->addWidget(label);
    layout->addWidget(btn);

    int count = 0;
    QObject::connect(btn, &QPushButton::clicked, &w, [&] {
        count += 1;
        label->setText(QString("count=%1").arg(count));
    });

    w.show();
    return app.exec();
}

你应该把这段理解成: - btn 发出 clicked 信号 - lambda 是“要做的事”(槽) - &w 是连接的“归属对象”(context object):当 w 销毁时,连接自动断开


4. 连接的三种常见形式:你会在项目里都见到

4.1 信号 → 成员函数(槽函数)

当你的逻辑比较多,建议写成成员函数:

  • 作用:代码更清晰、可测试、可复用
  • 坑点:槽函数签名要匹配信号参数

4.2 信号 → lambda(轻量逻辑首选)

适合: - 逻辑很短 - 只影响当前窗口里的几个控件

坑点: - lambda 捕获引用(&)时要确保被引用的变量生命周期足够长

4.3 自动连接(初学者不建议优先用)

Qt Designer 有一套“按命名规则自动连接”的机制,但新手容易被“魔法”搞懵。 结论: - 入门阶段先把 connect 写明白,再考虑自动连接


5. 线程相关的一个关键结论:UI 只能在主线程改

  • 结论:任何 QWidget 的操作都应该发生在主线程
  • 正确姿势:子线程做耗时工作,完成后用信号把结果发回主线程更新 UI(见 09 章)

常见坑

  • 错误:重复 connect 同一个信号到同一个槽(例如初始化函数被调用多次)
    结果:点一次按钮触发多次逻辑
    正确:保证只连接一次;或者在重新连接前先断开(或用更清晰的生命周期管理)

  • 错误:lambda 捕获了已经销毁的对象指针
    结果:随机崩溃
    正确:用 context object(例子里的 &w),并尽量避免捕获裸指针;需要的话用 QPointer 做保护

  • 错误:在子线程里直接改 UI
    结果:不稳定、崩溃
    正确:子线程发信号回主线程,由主线程更新 UI(见 09 章)


小练习

练习 1:做一个“+ / -”计数器

  • 题目:两个按钮 Add/Sub,标签显示当前 count
  • 输入/输出:点击 Add 增加,点击 Sub 减少
  • 约束:必须用 connect + lambda
  • 提示:两次 connect,改同一个 count
  • 目标:把 connect 写熟

练习 2:把逻辑移到成员函数

  • 题目:把计数器逻辑从 lambda 改成窗口类的成员函数
  • 输入/输出:行为不变
  • 约束:槽函数必须是成员函数
  • 提示:窗口类里存 count 和控件指针
  • 目标:练“结构化写法”

小结

  • 信号与槽让“发生了什么”和“怎么处理”解耦
  • 新手最推荐:函数指针 + lambda,并提供 context object
  • UI 只能在主线程改;耗时任务用信号把结果带回来

下一篇:见 05-Widget入门-窗口按钮布局