第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更新逻辑。不用在头文件里声明槽函数,代码更集中,阅读起来更流畅。
知识体系总览
下面这张图,是我梳理的信号与槽核心知识结构:
嗯,信号与槽的内容就这些。记住一点:多用,多练,自然就熟练了。我刚开始也觉得抽象,但做了几个项目后,发现它真的是Qt最优雅的设计之一。
可可精品资料库