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是便捷写法,连接方式决定行为,断开连接是资源管理。四者缺一不可。
17.6 避坑指南
最后分享几个我踩过的坑:
- 信号重载问题:如果信号有重载,用
&Class::signal取地址会编译报错。需要用static_cast或者QOverload来指定具体版本。 - Lambda生命周期:Lambda里捕获了
this,但this指向的对象可能先被销毁。用QPointer或者QWeakPointer做保护。 - 信号发射顺序:同一信号的多个槽,执行顺序是未定义的。别依赖顺序。
- 循环连接:A的信号连到B的槽,B的信号又连回A的槽,容易死循环。设计时注意。
我曾经在一个项目里,因为Lambda捕获了已经销毁的 QWidget,导致程序随机崩溃。查了两天才定位到问题。从那以后,我所有Lambda里涉及UI指针的,一律用 QPointer 包裹。
好了,信号与槽的深入内容就聊到这儿。这些东西看着简单,但用好了能写出非常优雅的Qt代码。多练练,自然就熟了。
公众号:蓝海资料掘金营,微信deep3321