前言
Qt 引入信号与槽(Signal & Slot)机制的主要原因是为了提供一种灵活、松耦合的方式,使对象之间能够进行安全、方便的通信,尤其在事件驱动的编程环境中(如 GUI 应用)特别有效。
松耦合:
- 在传统的C++回调函数模式中,回调函数需要直接引用目标对象或函数指针,这导致了对象之间的强耦合,增加了代码的复杂性和维护难度。
- 信号与槽机制通过将信号发射者和槽接收者分离,允许对象之间进行松耦合的通信。这种松耦合性使得代码更易维护和扩展。
机制允许对象独立地演化,可以在不破坏现有代码的情况下添加新功能。
正文
信号与槽的本质
信号与槽的本质是一种发布-订阅(Publish-Subscribe)模式,它基于以下几个关键点:
-
信号(Signal):
- 信号本质上是一个没有具体实现的函数声明。它代表了某个事件的发生,如按钮点击、数据变化等。
- 当事件发生时,信号被发射(emit),通知所有连接到这个信号的槽。
-
槽(Slot):
- 槽是一个可以与信号连接的普通函数。当信号被发射时,所有连接的槽函数都会被依次调用。
- 槽可以是任意的成员函数、静态函数、或 Lambda 表达式,只要它与信号的参数匹配即可。
-
信号与槽的连接(Connect):
- Qt 中,信号与槽的连接由
connect()
函数实现。它将信号与槽绑定在一起,使得信号发射时可以自动调用相应的槽函数。 - 连接的过程是动态的,信号与槽可以在运行时自由连接和断开,不需要修改源码。
- Qt 中,信号与槽的连接由
-
消息传递系统:
- Qt 的信号与槽系统背后是一个消息传递系统。信号的发射会生成一个消息,该消息被发送到消息队列中,然后由 Qt 的事件循环(Event Loop)调度执行,调用相应的槽函数。
- 在跨线程通信中,这种消息传递系统还负责将信号与槽的调用安全地在适当的线程上下文中执行。
信号与槽的使用
前提:需要在私有区域定义Q_OBJECT
这个宏
Qt 的 connect()
函数有多个重载版本,目的是为了提供灵活性,适应不同的编程需求和使用场景。具体来说,这些重载函数主要为以下几种情况设计:
1. 不同类型的连接
- 静态连接:这是 Qt 早期(Qt4)的信号与槽连接方式,使用 SIGNAL() 和 SLOT() 宏。
- 带参数的连接:允许信号和槽带参数,并且信号的参数可以传递给槽函数。
- 带返回值的槽:在 Qt 5.0 之后的版本中,槽函数可以有返回值。
- Lambda 表达式:允许将 Lambda 表达式作为槽函数进行连接,从而简化代码结构和提高代码灵活性。
2. 静态连接(Qt 4 及以前的方式)
QObject::connect(sender, SIGNAL(signalName()), receiver, SLOT(slotName()));
- 这种方式在编译期进行检查,但在运行时解析信号与槽的连接(这样即使函数名或者参数写错了,也可以编译通过,这样把问题留在了运行阶段)。它使用了 Qt 的元对象系统,通过字符串匹配信号与槽的名称。
3. 类型安全的连接(Qt 5 及以后)
QObject::connect(sender, &SenderClass::signalName, receiver, &ReceiverClass::slotName);
- 这种方式是类型安全的,编译器会在编译期进行检查,因此可以捕捉到更多的编译期错误。
- 连接的函数指针明确指定了信号和槽的类型,使代码更易读、可维护性更高。
4. Lambda 表达式作为槽
QObject::connect(sender, &SenderClass::signalName, [=](){
// Lambda 表达式代码
});
- 这种方式允许在连接信号时使用 Lambda 表达式,从而减少槽函数的数量,简化代码逻辑。特别适合简单的回调或处理逻辑。但是使用这种方法要注意三点:
-
捕获列表(Capture List):
- 捕获列表是 Lambda 表达式的一部分,用于指定 Lambda 表达式可以访问哪些变量。在 Qt 中,常见的捕获方式包括:
[ ]
:不捕获变量,也就是说槽函数不需要使用外部变量,也就没必要捕获[=]
:按值捕获所有外部变量。[&]
:按引用捕获所有外部变量。[this]
:按引用捕获当前对象的this
指针,允许在 Lambda 中访问当前对象的成员函数和变量。
捕获变量时要注意如果 Lambda 需要访问某个变量,但该变量在槽函数触发时可能已经销毁,捕获该变量可能会导致未定义行为。因此,捕获列表应根据需要小心设置。
- 捕获列表是 Lambda 表达式的一部分,用于指定 Lambda 表达式可以访问哪些变量。在 Qt 中,常见的捕获方式包括:
-
参数列表(Parameter List):
- 这部分是 Lambda 表达式的参数,通常对应信号发出的参数。例如,如果信号发出一个
int
参数,Lambda 表达式也应该接受一个int
参数。 - 在某些情况下,参数列表可以为空,这意味着 Lambda 不需要直接使用信号传递的任何参数。
- 这部分是 Lambda 表达式的参数,通常对应信号发出的参数。例如,如果信号发出一个
-
函数体(Function Body):
- Lambda 表达式的主体部分,它包含了执行的代码。这个部分定义了槽函数应该在信号发出时执行什么操作。
- 例如,Lambda 可以更新 UI 元素、处理数据,或者触发其他信号。
5. 跨线程连接
QObject::connect(sender, &SenderClass::signalName, receiver, &ReceiverClass::slotName, Qt::QueuedConnection);
- 这个重载版本支持跨线程的信号槽连接,通过指定连接类型(如
Qt::QueuedConnection
),确保信号和槽在不同的线程中安全调用。
6. 连接方式的控制
Qt::DirectConnection
:信号发射时立即调用槽函数,信号与槽在同一线程中运行。Qt::QueuedConnection
:信号发射时,将槽函数调用放入接收者线程的事件队列,异步执行。Qt::BlockingQueuedConnection
:类似于QueuedConnection
,但会阻塞发送信号的线程,直到槽函数执行完毕。Qt::AutoConnection
:这是默认值,Qt 自动选择适当的连接方式(在同一线程时使用DirectConnection
,跨线程时使用QueuedConnection
)。
大多数情况连接方式都不需要更改,使用默认的方式即可
7. 使用 QMetaObject::Connection 进行管理
- 在 Qt 5 及以后版本中,
connect()
函数返回一个QMetaObject::Connection
对象,可以用来管理和断开连接。这对动态调整连接的场景特别有用,它的使用如下(注意只有Qt5及以后的版本才能这样使用):
7-1. 创建连接并保存句柄
当使用 QObject::connect()
建立连接时,可以得到一个 QMetaObject::Connection
对象,表示该连接的句柄。这个句柄可以用来在需要时断开连接。
QMetaObject::Connection connection;
connection = QObject::connect(sender, SIGNAL(signalName()), receiver, SLOT(slotName()));
7-2. 断开连接
使用 QObject::disconnect()
函数可以断开通过 QMetaObject::Connection
管理的连接。
QObject::disconnect(connection);
这种方法可以确保只断开特定的连接,而不影响其他可能存在的连接。
7-3. 检查连接是否有效
在某些情况下,可能希望检查连接句柄是否有效。可以通过将 QMetaObject::Connection
转换为 bool
来检查:
if (connection) {
// 连接有效
} else {
// 连接无效
}
7-4. 使用多个连接管理复杂的连接关系
有时需要管理多个连接时,可以保存多个 QMetaObject::Connection
对象:
QMetaObject::Connection conn1 = QObject::connect(sender1, SIGNAL(signal1()), receiver1, SLOT(slot1()));
QMetaObject::Connection conn2 = QObject::connect(sender2, SIGNAL(signal2()), receiver2, SLOT(slot2()));
// Later, disconnect specific connections
QObject::disconnect(conn1);
QObject::disconnect(conn2);
7-5. 自动断开连接
QMetaObject::Connection
可以在连接的发送者或接收者被销毁时自动失效。因此,在某些情况下,不需要手动断开连接。当涉及到使用 lambda 表达式或函数对象时,这种特性尤为重要。
8.例子
下面是一个简单的信号与槽使用的例子
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "commander.h"
#include "soldier.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
//基础信号与槽的使用
// 1.最大化显示(SIANAL/SLOT方式)
//原理通过SIGNAL/SLOT这两个宏,将函数名以及对应的参数转换成字符串,这是Qt4中使用的方式,Qt5也支持,有个很严重的缺点是编译器不会对这种方式进行错误检查
//即使函数名或者参数写错了,也可以编译通过,这样把问题留在了运行阶段
connect(ui->btnMax,SIGNAL(clicked()),this,SLOT(showMaximized()));
//2.最小化显示(函数地址)
//即通过传递函数地址的方式来进行,这种方式在编译时就会对函数类型,参数个数进行检查
connect(ui->btnMin,&QPushButton::clicked, this, &QMainWindow::showMinimized);
//3.修改窗口标题(lambda表达式)
connect(ui->btnSetWindowTitle,&QPushButton::clicked,this,[this](){
this->setWindowTitle("lambda表达式中修改标题");
});
//使用自定义信号与槽
#if 0
//1初始化对象
Commander commander;
Soldier soldier;
//2连接信号与槽
//对函数重载连接信号与槽时有两种方法
//1.在Qt4中
//connect(&commander,SIGNAL(go()),&soldier,SLOT(fight()));
//connect(&commander,SIGNAL(go(QString)),&soldier,SLOT(fight(QString)));
//2.在Qt5中,需要定以两个单独的函数指针
void (Commander::*pGo)() = &Commander::go;
void (Soldier::*pFight)() = &Soldier::fight;
connect(&commander,pGo,&soldier,pFight);
void (Commander::*mGo)(QString) = &Commander::go;
void (Soldier::*mFight)(QString) = &Soldier::fight;
connect(&commander,mGo,&soldier,mFight);
//3发送信号 ,emit可以省略
emit commander.go();
emit commander.go("哥布林");
#endif
//信号与槽扩展
//1一个信号连接多个槽函数
#if 0
Commander commander1;
Soldier soldier1,soldier2;
connect(&commander1,SIGNAL(go(QString)),&soldier1,SLOT(fight(QString)));
connect(&commander1,SIGNAL(go(QString)),&soldier2,SLOT(escape(QString)));
commander1.go("赛亚人");
#endif
//2多个信号连接一个槽
#if 0
Commander commanders;
Soldier soldier3;
connect(&commander1,SIGNAL(go(QString)),&soldier3,SLOT(fight(QString)));
connect(&commander1,SIGNAL(move()),&soldier3,SLOT(escape(QString)));
commander1.go("赛亚人");
#endif
//3信号连接信号
#if 1
commanders = new Commander();
soldiers = new Soldier();
connect(ui->btnAction,SIGNAL(clicked()),commanders,SIGNAL(move()));
connect(commanders,SIGNAL(move()),soldiers,SLOT(escape()));
#endif
//4断开连接
#if 1
//1初始化对象
Commander commander;
Soldier soldier;
//2连接信号与曹
//对函数重载连接信号与槽时有两种方法
//1在Qt4中
//connect(&commander,SIGNAL(go()),&soldier,SLOT(fight()));
//connect(&commander,SIGNAL(go(QString)),&soldier,SLOT(fight(QString)));
//2在Qt5中,需要定以两个单独的函数指针
void (Commander::*pGo)() = &Commander::go;
void (Soldier::*pFight)() = &Soldier::fight;
connect(&commander,pGo,&soldier,pFight);
void (Commander::*mGo)(QString) = &Commander::go;
void (Soldier::*mFight)(QString) = &Soldier::fight;
connect(&commander,mGo,&soldier,mFight);
//3发送信号 ,emit可以省略
emit commander.go();
commander.disconnect();
emit commander.go("哥布林");
#endif
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_btnNormal_clicked()
{
showNormal();
}