17、Qt信号与槽深入:自定义信号与槽、Lambda表达式作为槽、信号与槽连接方式、断开连接

信号与槽,是Qt最核心的机制,没有之一。

我刚开始学Qt那会儿,觉得这东西不就是个回调函数嘛。后来踩了几个坑才明白——它比回调函数强太多了。类型安全、松耦合、支持多对多连接,这些特性让Qt在GUI框架里独树一帜。

今天咱们就把信号与槽彻底聊透。从自定义信号开始,到Lambda表达式,再到连接方式和断开连接,一步到位。

17.1 自定义信号与槽

Qt内置的信号槽够用吗?说实话,大部分场景够。但一旦你开始封装自己的类,就绕不开自定义信号了。

先看一个最简单的例子。假设我们做一个温度传感器类,温度超过阈值时要通知界面:

// sensor.h
#ifndef SENSOR_H
#define SENSOR_H

#include <QObject>

class TemperatureSensor : public QObject
{
    Q_OBJECT

public:
    explicit TemperatureSensor(QObject *parent = nullptr);
    
    void setTemperature(double value);

signals:
    void temperatureChanged(double newTemp);
    void overHeat(double currentTemp, double threshold);

public slots:
    void resetAlarm();

private:
    double m_temperature = 25.0;
    double m_threshold = 80.0;
    bool m_alarmActive = false;
};

#endif // SENSOR_H

注意几个关键点:

  • 类必须继承自 QObject,并且开头要有 Q_OBJECT
  • 信号用 signals: 关键字声明,不需要实现
  • 信号返回类型必须是 void
  • 槽函数用 public slots: 声明,需要写实现

实现文件长这样:

// sensor.cpp
#include "sensor.h"
#include <QDebug>

TemperatureSensor::TemperatureSensor(QObject *parent)
    : QObject(parent)
{
}

void TemperatureSensor::setTemperature(double value)
{
    if (qFuzzyCompare(m_temperature, value))
        return;  // 没变化就不发信号
    
    m_temperature = value;
    emit temperatureChanged(m_temperature);
    
    if (m_temperature > m_threshold && !m_alarmActive) {
        m_alarmActive = true;
        emit overHeat(m_temperature, m_threshold);
    }
}

void TemperatureSensor::resetAlarm()
{
    m_alarmActive = false;
    qDebug() << "Alarm reset";
}

核心要点:用 emit 关键字发射信号。发射信号时,所有连接到这个信号的槽函数会被立即调用——除非你用了队列连接。

我在项目中遇到过一个问题:信号发射了,但槽没反应。查了半天,发现是忘了加 Q_OBJECT 宏。嗯,这个坑我帮你们踩过了。

17.2 Lambda表达式作为槽

C++11之后,Lambda表达式成了Qt开发者的好帮手。以前写槽函数,要么声明一个成员函数,要么用 SLOT() 宏。现在?直接写Lambda,干净利落。

看个例子:

TemperatureSensor *sensor = new TemperatureSensor(this);
QLabel *tempLabel = new QLabel("-- °C", this);

// 用Lambda连接信号
connect(sensor, &TemperatureSensor::temperatureChanged,
        [tempLabel](double newTemp) {
    tempLabel->setText(QString("%1 °C").arg(newTemp, 0, 'f', 1));
});

// 也可以捕获多个变量
connect(sensor, &TemperatureSensor::overHeat,
        [this, tempLabel](double current, double threshold) {
    tempLabel->setStyleSheet("color: red; font-weight: bold;");
    qDebug() << "Overheat!" << current << "exceeds" << threshold;
});

Lambda作为槽有几个好处:

  • 代码紧凑,逻辑集中
  • 不需要额外声明槽函数
  • 可以捕获上下文变量

注意:Lambda里捕获指针要小心。如果对象被销毁了,Lambda还在执行,那就野指针了。我建议用 QPointer 或者 QWeakPointer 来保护。

举个例子,安全捕获:

QPointer<QLabel> safeLabel(tempLabel);
connect(sensor, &TemperatureSensor::temperatureChanged,
        [safeLabel](double newTemp) {
    if (safeLabel) {
        safeLabel->setText(QString("%1 °C").arg(newTemp, 0, 'f', 1));
    }
});

说白了,Lambda就是匿名槽函数。你想想看,以前写个简单的更新UI操作,还得去头文件里加个槽声明,多麻烦。现在一行搞定。

17.3 信号与槽的连接方式

Qt5提供了五种连接方式,定义在 Qt::ConnectionType 枚举里。我整理了一张表:

连接类型 枚举值 行为说明
直接连接 Qt::DirectConnection 信号发射时立即调用槽,在发射线程执行
队列连接 Qt::QueuedConnection 槽在接收对象的事件循环中执行,异步
阻塞队列连接 Qt::BlockingQueuedConnection 类似队列连接,但发射线程会阻塞直到槽执行完毕
自动连接 Qt::AutoConnection 默认方式。同线程用直接连接,跨线程用队列连接
唯一连接 Qt::UniqueConnection 与自动连接组合使用,防止重复连接

默认情况下,你写 connect(sender, signal, receiver, slot) 就是自动连接。大部分场景够用了。

但跨线程时要注意。我记得有一次写多线程程序,子线程发信号,主线程更新UI。我忘了用队列连接,结果界面直接崩了。为什么?因为直接连接会在子线程里调用UI更新函数,而Qt的UI操作必须在主线程。

正确的写法:

connect(workerThread, &Worker::progressUpdated,
        this, &MainWindow::updateProgressBar,
        Qt::QueuedConnection);

或者直接用自动连接,只要两个对象在不同线程,Qt会自动切换成队列连接。

17.4 断开连接

有连接就有断开。Qt提供了 disconnect 函数,用法和 connect 对称。

几种断开方式:

// 断开特定信号和特定槽
disconnect(sensor, &TemperatureSensor::temperatureChanged,
           this, &MainWindow::onTemperatureChanged);

// 断开某个对象的所有连接
disconnect(sensor, nullptr, nullptr, nullptr);

// 断开某个信号的所有连接
disconnect(sensor, &TemperatureSensor::overHeat, nullptr, nullptr);

// 断开某个接收对象的所有连接
disconnect(sensor, nullptr, this, nullptr);

什么时候需要手动断开?我总结了几种场景:

  • 对象生命周期管理:临时对象用完要断开,防止悬空连接
  • 动态UI:比如Tab页关闭时,断开所有相关连接
  • 性能优化:大量信号发射时,临时断开不需要的槽

小技巧:Qt5之后,如果连接用了Lambda,断开时比较麻烦。因为Lambda没有地址。我一般用 QMetaObject::Connection 保存连接句柄,需要时断开。

QMetaObject::Connection conn = connect(sensor, &TemperatureSensor::temperatureChanged,
    [this](double val) {
    // ...
});

// 需要断开时
disconnect(conn);

还有一种情况:对象销毁时自动断开。Qt的元对象系统会帮你处理——只要发送者或接收者被销毁,连接会自动移除。但如果你用了裸指针的Lambda捕获,那就自求多福了。

17.5 知识体系总览

下面这张图把本章的核心逻辑串起来了:

信号与槽深入:知识体系 信号与槽机制 自定义信号与槽 Lambda表达式作为槽 五种连接方式 断开连接 signals: 关键字声明 emit 发射信号 Q_OBJECT 宏必须 匿名槽函数 捕获上下文变量 注意野指针问题 Direct / Queued BlockingQueued / Auto UniqueConnection disconnect() 函数 QMetaObject::Connection 对象销毁自动断开 核心:类型安全 · 松耦合 · 多对多 · 线程安全

这张图把四个核心知识点串在了一起。你想想看,自定义信号是基础,Lambda是便捷写法,连接方式决定行为,断开连接是资源管理。四者缺一不可。

17.6 避坑指南

最后分享几个我踩过的坑:

  • 信号重载问题:如果信号有重载,用 &Class::signal 取地址会编译报错。需要用 static_cast 或者 QOverload 来指定具体版本。
  • Lambda生命周期:Lambda里捕获了 this,但 this 指向的对象可能先被销毁。用 QPointer 或者 QWeakPointer 做保护。
  • 信号发射顺序:同一信号的多个槽,执行顺序是未定义的。别依赖顺序。
  • 循环连接:A的信号连到B的槽,B的信号又连回A的槽,容易死循环。设计时注意。

我曾经在一个项目里,因为Lambda捕获了已经销毁的 QWidget,导致程序随机崩溃。查了两天才定位到问题。从那以后,我所有Lambda里涉及UI指针的,一律用 QPointer 包裹。

好了,信号与槽的深入内容就聊到这儿。这些东西看着简单,但用好了能写出非常优雅的Qt代码。多练练,自然就熟了。


公众号:蓝海资料掘金营,微信deep3321