3、信号与槽机制(上):信号与槽概念、connect函数详解、自定义信号与槽、Lambda表达式
信号与槽,是Qt最核心的机制,没有之一。
我刚开始学Qt时,觉得这东西挺玄乎的。后来做项目多了才发现,说白了它就是一套对象间通信的解决方案。你想想看,一个按钮被点击了,怎么告诉窗口“我被人按了”?传统做法是回调函数,但回调有个问题——类型不安全,而且耦合太紧。Qt的信号与槽,完美解决了这个问题。
3.1 信号与槽的基本概念
信号(Signal)和槽(Slot),是Qt对观察者模式的一种实现。简单来说:
- 信号:某个事件发生时,对象发射的信号。比如按钮被点击,就发射clicked()信号。
- 槽:响应信号的函数。比如点击按钮后,你想关闭窗口,就把close()函数作为槽。
它们之间通过connect()函数连接起来。一旦信号发射,与之连接的槽函数就会自动执行。
核心特点:信号与槽是松耦合的。发射信号的对象不需要知道谁在接收,接收信号的对象也不需要知道信号从哪里来。这就是Qt设计的精妙之处。
我在项目中遇到过这样一个场景:一个网络数据接收模块,需要把数据分发给多个界面模块更新显示。如果用传统回调,我得维护一个函数指针列表。用信号与槽,直接一个信号连接多个槽,代码干净多了。
3.2 connect函数详解
connect()是信号与槽的“接线员”。它的完整形式是这样的:
QObject::connect(sender, signal, receiver, slot, connectionType);
参数说明:
| 参数 | 说明 |
|---|---|
| sender | 发射信号的对象指针 |
| signal | 信号函数,用SIGNAL()宏包裹 |
| receiver | 接收信号的对象指针 |
| slot | 槽函数,用SLOT()宏包裹 |
| connectionType | 连接类型,默认Qt::AutoConnection |
举个最简单的例子:
QPushButton *btn = new QPushButton("关闭");
QWidget *w = new QWidget;
connect(btn, SIGNAL(clicked()), w, SLOT(close()));
嗯,这里要注意。老版本的Qt用SIGNAL()和SLOT()宏,但这种方式有个问题——编译期不检查参数类型。如果你写错了函数名,编译能通过,运行时却连不上。
我的建议:从Qt5开始,推荐使用新式语法。类型安全,编译期就能发现错误。
connect(btn, &QPushButton::clicked, w, &QWidget::close);
新式语法还能处理重载信号。比如QComboBox的currentIndexChanged有重载版本:
// 老式语法
connect(combo, SIGNAL(currentIndexChanged(int)), this, SLOT(onIndexChanged(int)));
// 新式语法需要指定函数指针
connect(combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &MyClass::onIndexChanged);
我曾经因为没注意重载问题,调试了半天才发现信号没连上。从那以后,我统一用新式语法,省心不少。
3.3 自定义信号与槽
Qt内置的信号和槽够用吗?说实话,大部分场景够用。但当你需要定义自己的通信协议时,就得自己动手了。
自定义信号和槽的规则:
- 类必须继承自
QObject - 类声明中必须包含
Q_OBJECT宏 - 信号用
signals:关键字声明,不需要实现 - 槽用
public slots:、protected slots:或private slots:声明
来看一个完整的例子:
#include <QObject>
class Sensor : public QObject
{
Q_OBJECT
public:
explicit Sensor(QObject *parent = nullptr) : QObject(parent) {}
void startMeasuring() {
// 模拟测量
int value = 42;
emit dataReady(value);
}
signals:
void dataReady(int value); // 信号,只声明不实现
public slots:
void reset() {
// 重置传感器
emit dataReady(0);
}
};
class Display : public QObject
{
Q_OBJECT
public:
explicit Display(QObject *parent = nullptr) : QObject(parent) {}
public slots:
void showData(int value) {
qDebug() << "当前数据:" << value;
}
};
// 使用
Sensor *sensor = new Sensor;
Display *display = new Display;
connect(sensor, &Sensor::dataReady, display, &Display::showData);
sensor->startMeasuring(); // 输出: 当前数据: 42
注意看,emit关键字用来发射信号。它其实是个宏,展开后就是调用信号函数。信号函数不需要你写实现,Qt的元对象编译器(MOC)会自动生成。
避坑指南:我曾经在自定义类里忘了加Q_OBJECT宏,结果信号怎么都连不上。编译没问题,运行时就是没反应。折腾了半天才发现是这个问题。记住:凡是需要信号槽的类,必须加Q_OBJECT。
3.4 Lambda表达式作为槽
Qt5引入了一个很实用的特性——Lambda表达式可以直接作为槽函数。这意味着你不需要专门写一个槽函数,代码更紧凑。
基本用法:
QPushButton *btn = new QPushButton("点击我");
connect(btn, &QPushButton::clicked, [=]() {
qDebug() << "按钮被点击了";
});
Lambda表达式可以捕获外部变量:
int counter = 0;
QPushButton *btn = new QPushButton("计数");
connect(btn, &QPushButton::clicked, [&counter]() {
counter++;
qDebug() << "点击次数:" << counter;
});
这里[&counter]表示按引用捕获counter变量。如果你用[=],就是按值捕获所有外部变量。我个人习惯用[=],省事,但要注意性能——按值捕获会复制对象。
Lambda表达式还能接收信号参数:
QSlider *slider = new QSlider(Qt::Horizontal);
connect(slider, &QSlider::valueChanged, [=](int value) {
qDebug() << "滑块值:" << value;
});
你看,信号valueChanged(int)的参数int,直接传给了Lambda的参数。这比写一个专门的槽函数方便多了。
我的经验:Lambda表达式适合简单的槽逻辑。如果槽函数逻辑复杂,或者需要复用,还是建议写成独立的槽函数。代码可读性更重要。
3.5 知识体系总览
为了让你更直观地理解信号与槽的整体结构,我画了一张图:
这张图展示了信号与槽的核心流程:发送者发射信号,通过connect()连接到接收者的槽函数。连接类型决定了信号是直接调用还是排队调用。
好了,信号与槽的上半部分就讲到这里。记住三个要点:信号只声明不实现,槽就是普通函数,connect是它们的桥梁。下节课我们继续深入,聊聊信号与槽的高级用法和性能优化。
本章小结:
- 信号与槽是Qt对象间通信的核心机制
- connect函数有新旧两种语法,推荐新式语法
- 自定义信号槽需要Q_OBJECT宏支持
- Lambda表达式让槽函数更简洁灵活
可可精品资料库