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 知识体系总览

为了让你更直观地理解信号与槽的整体结构,我画了一张图:

信号与槽机制知识体系 信号发送者 QPushButton QSlider 信号 clicked() valueChanged(int) connect() 槽接收者 QWidget 自定义类 槽函数 close() Lambda表达式 连接类型 AutoConnection QueuedConnection

这张图展示了信号与槽的核心流程:发送者发射信号,通过connect()连接到接收者的槽函数。连接类型决定了信号是直接调用还是排队调用。

好了,信号与槽的上半部分就讲到这里。记住三个要点:信号只声明不实现,槽就是普通函数,connect是它们的桥梁。下节课我们继续深入,聊聊信号与槽的高级用法和性能优化。

本章小结:

  • 信号与槽是Qt对象间通信的核心机制
  • connect函数有新旧两种语法,推荐新式语法
  • 自定义信号槽需要Q_OBJECT宏支持
  • Lambda表达式让槽函数更简洁灵活

可可精品资料库