第3章:信号与槽机制

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

我刚开始学Qt时,觉得这东西有点绕。后来做项目多了,才明白它的设计有多巧妙。说白了,它就是一种对象间的通信方式。一个对象发出信号,另一个对象的槽函数响应。松耦合,高内聚,这就是Qt的设计哲学。

3.1 信号与槽的概念

想象一下这个场景:你点击了一个按钮,窗口关闭了。按钮不知道谁会处理这个点击事件,窗口也不知道谁会通知它关闭。它们之间通过信号与槽连接起来。

信号(Signal)就是事件的通知。比如按钮被点击了,它就发出一个clicked()信号。槽(Slot)就是处理事件的函数。比如窗口的close()槽函数,负责关闭窗口。

我遇到过不少新手问:「这不就是回调函数吗?」嗯,有点像,但更灵活。回调函数是函数指针,类型不安全。信号与槽是类型安全的,编译器会帮你检查参数类型是否匹配。

核心特点:

  • 一个信号可以连接多个槽
  • 多个信号可以连接同一个槽
  • 信号可以连接信号(形成信号链)
  • 连接可以是同步的,也可以是异步的(跨线程时)

3.2 connect函数详解

connect函数,是信号与槽的桥梁。它的原型是这样的:

QMetaObject::Connection connect(
    const QObject *sender,      // 发送者
    const char *signal,         // 信号
    const QObject *receiver,    // 接收者
    const char *method,         // 槽
    Qt::ConnectionType type = Qt::AutoConnection  // 连接类型
);

我习惯用新式语法,更安全:

connect(sender, &QPushButton::clicked,
        receiver, &QWidget::close);

为什么推荐新式语法?因为旧式语法用字符串,写错了编译器不报错,运行时才崩溃。我曾经被这个坑过,调试了一下午才发现是信号名拼错了。

连接类型 说明
AutoConnection 默认值。同线程用直接连接,跨线程用队列连接
DirectConnection 立即调用槽函数,像普通函数调用
QueuedConnection 通过事件循环异步调用,跨线程必须用这个
BlockingQueuedConnection 阻塞等待槽函数执行完毕,小心死锁
UniqueConnection 防止重复连接,可以和前面类型组合使用

注意:跨线程连接时,一定要确保接收者对象的线程有事件循环在运行。否则槽函数永远不会被调用。我曾经在子线程里new了一个QObject,忘了调用exec(),结果信号发出去石沉大海。

3.3 自定义信号与槽

Qt自带的信号不够用?那就自己写。

自定义信号很简单,在类里声明一下就行:

class MyClass : public QObject
{
    Q_OBJECT  // 这个宏必须有

public:
    MyClass(QObject *parent = nullptr);

signals:
    void mySignal(int value);  // 信号只需要声明,不需要实现
    void anotherSignal(const QString &text);

public slots:
    void mySlot(int value);    // 槽函数需要实现
};

你想想看,信号为什么不需要实现?因为Qt的moc(元对象编译器)会自动生成实现代码。你只需要在合适的地方发射信号:

void MyClass::doSomething()
{
    // 做一些操作...
    emit mySignal(42);  // 发射信号
}

槽函数就是普通的成员函数,可以加virtual,可以被重载。唯一的区别是它可以被信号触发。

个人经验:我建议信号和槽的参数类型尽量用Qt的基本类型(int、QString、QColor等),这样跨线程传递时能自动注册。自定义类型需要调用qRegisterMetaType()注册,否则队列连接会失败。

3.4 Lambda表达式

C++11引入了Lambda,Qt5开始全面支持。Lambda让信号连接变得极其简洁:

connect(button, &QPushButton::clicked, [=]() {
    qDebug() << "按钮被点击了";
    // 这里可以访问外部变量
});

Lambda的捕获方式要注意:

  • [=]:值捕获所有外部变量
  • [&]:引用捕获所有外部变量
  • [this]:捕获当前对象的指针
  • [=, &x]:混合捕获,x用引用,其他用值

避坑指南:我曾经在Lambda里用了[&]捕获局部变量,结果那个变量在Lambda执行前就被销毁了。程序直接崩溃。记住:异步回调里,尽量用值捕获或者确保变量的生命周期足够长。

Lambda配合信号,可以写出非常优雅的代码。比如:

// 传统写法:需要定义一个槽函数
connect(timer, &QTimer::timeout, this, &MyClass::onTimeout);

// Lambda写法:一行搞定
connect(timer, &QTimer::timeout, [this]() {
    static int count = 0;
    count++;
    ui->label->setText(QString("计数: %1").arg(count));
});

我个人更喜欢Lambda写法,尤其是处理简单的UI更新逻辑。不用在头文件里声明槽函数,代码更集中,阅读起来更流畅。

知识体系总览

下面这张图,是我梳理的信号与槽核心知识结构:

信号与槽机制知识体系 信号与槽 核心概念 connect函数 自定义信号槽 Lambda表达式 松耦合 类型安全 新式语法 连接类型 Q_OBJECT宏 emit发射 捕获方式 简洁回调 核心思想:发送者不需要知道接收者是谁 接收者也不需要知道信号从哪里来

嗯,信号与槽的内容就这些。记住一点:多用,多练,自然就熟练了。我刚开始也觉得抽象,但做了几个项目后,发现它真的是Qt最优雅的设计之一。

可可精品资料库