第三章 信号与槽机制

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

我刚开始学Qt的时候,觉得这东西挺玄乎的。后来做项目多了,才真正体会到它的设计有多巧妙。说白了,信号与槽就是Qt用来处理对象间通信的一套方案。你想想看,传统的回调函数方式,类型不安全,耦合度高,用起来束手束脚的。Qt这套机制,从根本上解决了这些问题。

3.1 信号与槽的原理

信号与槽的本质,是一种观察者模式的实现。当一个对象的状态发生变化时,它会发射一个信号。其他对象可以连接这个信号,当信号被发射时,连接的槽函数就会被自动调用。

我打个比方你就明白了:

  • 信号就像广播电台发射的电波
  • 就像收音机,调对频率就能收到节目
  • connect函数就是调频旋钮,把信号和槽配对

这里有个关键点:信号和槽都是普通的C++函数,只是被Qt的元对象系统(MOC)做了特殊处理。信号只需要声明,不需要实现。槽就是普通的成员函数,可以像普通函数一样调用。

核心特性:

  • 一个信号可以连接多个槽
  • 多个信号可以连接同一个槽
  • 信号可以连接信号
  • 连接可以是同步的,也可以是异步的
  • 参数类型和数量必须匹配(允许参数类型隐式转换)

我曾经在一个项目中,因为没搞清楚信号和槽的参数匹配规则,调试了整整一个下午。嗯,这里要提醒你:信号和槽的参数个数可以不同,但信号的参数个数必须大于等于槽的参数个数,而且对应位置的参数类型要兼容。

3.2 connect函数详解

connect函数是信号与槽机制的桥梁。它的标准形式是这样的:

QObject::connect(sender, SIGNAL(signalName(args)),
                 receiver, SLOT(slotName(args)));

不过,我建议你使用Qt5引入的新语法,类型安全,编译期就能检查出错误:

QObject::connect(sender, &SenderClass::signalName,
                 receiver, &ReceiverClass::slotName);

新语法好在哪?我举个例子:

// 旧语法 - 运行时才能发现错误
connect(button, SIGNAL(clicked()), this, SLOT(onButtonClicked()));

// 新语法 - 编译期检查
connect(button, &QPushButton::clicked, this, &MyClass::onButtonClicked);

新语法还有一个好处:支持重载函数的连接。比如QComboBox有两个currentIndexChanged信号,一个带int参数,一个带QString参数。用旧语法你得用SIGNAL()宏包裹,用新语法可以这样:

connect(combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
        this, &MyClass::onIndexChanged);

我的习惯:新项目一律用新语法。旧语法只用来维护老代码。新语法更安全,IDE的自动补全也支持得更好。

3.3 自定义信号与槽

实际项目中,你肯定需要定义自己的信号和槽。自定义信号很简单,只需要在类声明中添加signals关键字:

class MyWidget : public QWidget
{
    Q_OBJECT

public:
    MyWidget(QWidget *parent = nullptr);

signals:
    void valueChanged(int newValue);
    void errorOccurred(const QString &message);

public slots:
    void onButtonClicked();
    void updateValue(int value);
};

注意几个要点:

  • 类必须继承自QObject或其子类
  • 类声明中必须有Q_OBJECT宏
  • 信号只需要声明,不需要实现
  • 信号返回类型必须是void
  • 槽可以是public、protected或private

发射信号用emit关键字:

void MyWidget::updateValue(int value)
{
    if (value < 0) {
        emit errorOccurred("值不能为负数");
        return;
    }
    m_value = value;
    emit valueChanged(m_value);
}

我记得有一次,我忘了在类里加Q_OBJECT宏,结果信号死活连不上。编译没问题,运行就是没反应。查了半天才发现是这个问题。嗯,Q_OBJECT宏是必须的,它让MOC能处理你的信号和槽。

3.4 Lambda表达式

Qt5开始,connect函数支持Lambda表达式。这让代码变得特别简洁:

connect(button, &QPushButton::clicked, [this]() {
    qDebug() << "按钮被点击了";
    this->doSomething();
});

Lambda表达式特别适合处理简单的逻辑,不用专门写一个槽函数。我常用的几种写法:

// 捕获this指针
connect(button, &QPushButton::clicked, [this]() {
    m_counter++;
    label->setText(QString::number(m_counter));
});

// 带参数的Lambda
connect(spinBox, QOverload<int>::of(&QSpinBox::valueChanged),
        [this](int value) {
    progressBar->setValue(value);
});

// 捕获外部变量
int threshold = 100;
connect(slider, &QSlider::valueChanged,
        [threshold](int value) {
    if (value > threshold) {
        qDebug() << "超过阈值了";
    }
});

注意:Lambda表达式中捕获的指针一定要确保有效。我曾经在Lambda里捕获了一个局部对象的指针,对象销毁后Lambda被调用,程序直接崩溃。建议用QPointer或者weak_ptr来管理。

3.5 信号与槽高级用法

掌握了基础用法,咱们来看看一些高级技巧。这些在实际项目中非常实用。

3.5.1 信号连接信号

信号可以直接连接信号,实现链式触发:

connect(slider, &QSlider::valueChanged,
        spinBox, &QSpinBox::setValue);
connect(spinBox, &QSpinBox::valueChanged,
        this, &MyClass::onValueChanged);

3.5.2 断开连接

用disconnect可以断开信号与槽的连接:

// 断开所有连接到该信号的槽
disconnect(sender, &SenderClass::signalName, nullptr, nullptr);

// 断开特定的接收者
disconnect(sender, &SenderClass::signalName, receiver, nullptr);

// 断开特定的槽
disconnect(sender, &SenderClass::signalName, receiver, &ReceiverClass::slotName);

3.5.3 连接类型

connect函数的第五个参数可以指定连接类型:

连接类型 说明 使用场景
AutoConnection 默认值,自动选择 大多数情况
DirectConnection 立即调用槽函数 同一线程
QueuedConnection 通过事件队列调用 跨线程通信
BlockingQueuedConnection 阻塞等待槽执行完毕 跨线程同步调用
UniqueConnection 防止重复连接 避免多次连接

跨线程的信号与槽连接,我建议用QueuedConnection。DirectConnection在跨线程时会有线程安全问题。我曾经在一个多线程项目里用了DirectConnection,结果数据竞争导致程序时不时崩溃,排查了好久才找到原因。

3.5.4 使用QMetaObject::invokeMethod

有时候你需要在特定线程中调用一个对象的槽函数,可以用invokeMethod:

// 在主线程中执行
QMetaObject::invokeMethod(this, "updateUI", Qt::QueuedConnection);

// 带参数
QMetaObject::invokeMethod(this, "setProgress", Qt::QueuedConnection,
                          Q_ARG(int, 50));

实用技巧:在子线程中更新UI时,用invokeMethod比直接发射信号更灵活。你可以精确控制调用时机和参数。

3.6 知识体系总览

下面这张图帮你梳理信号与槽的核心知识结构:

信号与槽机制知识体系 信号与槽 原理与机制 观察者模式实现 MOC元对象系统 类型安全通信 connect函数 旧语法 SIGNAL/SLOT 新语法 类型安全 重载函数连接 自定义信号与槽 Q_OBJECT宏必须 信号只声明不实现 emit发射信号 Lambda表达式 捕获this指针 带参数Lambda 注意指针生命周期 高级用法 信号连接信号 disconnect断开 连接类型选择 invokeMethod调用 跨线程通信 UniqueConnection QueuedConnection BlockingQueued

信号与槽机制,说白了就是Qt的灵魂。你用得越熟练,写出来的代码就越优雅、越健壮。我做了这么多年Qt开发,可以说90%的交互逻辑都是用信号与槽实现的。掌握了它,你就掌握了Qt的精髓。

记住一点:信号与槽不是为了炫技,而是为了让你的代码更清晰、更解耦。每个信号只负责通知"发生了什么",每个槽只关心"要做什么"。这样设计出来的系统,维护起来特别舒服。

本章要点回顾:

  • 信号与槽是观察者模式的实现,用于对象间通信
  • connect函数有新旧两种语法,推荐使用新语法
  • 自定义信号需要Q_OBJECT宏,信号只声明不实现
  • Lambda表达式让连接更简洁,注意指针生命周期
  • 高级用法包括信号连信号、连接类型选择、invokeMethod等

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