奇牛Qt02-03 信号槽 —— 奇牛+gemini
奇牛Qt02-03 信号槽 —— 奇牛+gemini
1. 信号和槽机制概述
1.1 什么是信号和槽?
信号和槽是 Qt 的核心机制,用于实现对象之间的通信。当某个对象的状态发生改变、想通知另一个对象做某件事时,该对象就会发送一个信号(Signal),另一个对象收到信号后,就会执行之前约定好的特定函数,即槽(Slot)。
信号和槽机制的核心要素包括:
- 信号发送者(Sender)
- 信号接收者(Receiver)
- 信号:由发送者触发的特殊函数,不需要程序员实现函数体。
- 槽:由接收者执行的响应函数。
在使用信号槽之前,必须先做好它们之间的关联,并在需要的时候由发送者通过代码或用户操作触发信号。
2. 信号槽 vs C/C++ 回调函数
- 松耦合:信号发送者不需要了解接收者的具体接口信息;而回调函数属于紧密耦合,必须直接调用目标对象的特定函数。
- 灵活性:信号槽相比回调函数使用起来更灵活,支持一对多、多对一的连接关系。
- 缺点(性能损耗):信号槽的执行速度比回调函数慢。因为回调函数全是同步执行的,而信号槽在跨线程等特殊连接方式下可能是异步的(仅在直连方式下才是同步执行)。
2. 信号和槽的关联方式
2.1 使用 connect 函数手动
2.1.1 写法一:基于 SIGNAL 和 SLOT 宏(旧版本/传统写法)
该方式使用宏将信号和槽函数转换成 const char* 字符串。
connect(ui->scrollBar, SIGNAL(valueChanged(int)), ui->label, SLOT(setNum(int)));
- 注意点:
- 信号和槽有参数时,必须带上参数类型,但不能带变量名(如写
int,不能写int value)。 - 函数不需要、也不能使用类名限定符(直接写函数名即可)。
- 信号和槽有参数时,必须带上参数类型,但不能带变量名(如写
2.1.2 写法二:基于函数指针(Qt 5 推荐写法)
直接传递信号和槽函数的地址。
connect(ui->lineEdit, &QLineEdit::textChanged, ui->label, &QLabel::setText);
- 注意点:
- 信号和槽必须使用类名进行作用域限定(如
&QLineEdit::textChanged)。 - 要求信号和槽的参数个数相同(或信号的更多),且能进行合适的隐式类型转换,否则编译失败。
- 二义性陷阱:如果槽函数存在重载(例如
QLabel::setNum(int)和QLabel::setNum(double)并存),使用函数指针编译器会因为不知道关联哪个而报错。
- 信号和槽必须使用类名进行作用域限定(如
2.2 UI 设计器自动连接(转到槽)
在 Qt Creator 的 UI 设计器中,可以通过可视化的方式快速生成槽函数:
- 在控件上右键单击,选择 “转到槽...” (Go to slot...)。
- 在弹出的窗口中选择对应的信号(如
textChanged(QString)),点击 OK。
此时 Qt Creator 会在类中自动生成符合特定命名规则的空槽函数:
- 命名规则:
void on_接收者对象名_信号名(参数列表); - 内部原理:Qt 框架会在运行时通过内省机制,根据槽函数的命名自动识别发送者和信号并完成关联。
C++
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots: // 自动连接或手写规则槽函数时,slots 宏必不可少!
void on_lineEdit_textChanged(const QString &arg1); // 设计器自动生成
void on_lineEdit2_textChanged(const QString &arg); // 按规则手动添加
};
- 提示:通过
connect手动关联的槽函数,在现代 Qt 5 中不需要放在private slots:下,放在普通的public或private下作为普通函数即可。
3. 自定义信号与槽
3.1 自定义信号
自定义信号必须声明在 signals: 访问权限下。
- 信号只是一个空壳函数,不需要、也不能有对应的函数体实现。
- 信号的参数个数可以大于槽函数的参数个数,但绝对不能少于槽函数的参数个数。
- 构建项目时,元对象编译器(moc)扫描到
signals会在moc_*.cpp中自动生成其底层实现代码。
class ReceivePackThread : public QThread {
Q_OBJECT
public:
ReceivePackThread(QSerialPort *port);
signals:
// 自定义信号,不需要在.cpp中写实现
void receiveResponsePack(char cmd, char param, int data);
};
3.2 发送信号
- 自动发送:Qt 标准组件在用户交互时会自动触发对应信号(如点击按钮触发
clicked())。 - 代码手动发送:在需要触发信号的地方,使用
emit关键字激发信号(非必须,但写上方便阅读)。
void MainWindow::on_pushButton_clicked() {
Student s;
s.age = 18;
s.score = 100;
// 手动触发自定义信号,并将参数传递出去
emit mySignal(s);
}
4. 信号和槽的连接方式(第 5 个参数)
在使用 connect 关联信号槽时,可以指定第 5 个参数 Qt::ConnectionType,它决定了信号和槽的同步/异步行为(默认值为 Qt::AutoConnection):
| 连接类型 | 通俗解释 | 核心特点与适用场景 |
|---|---|---|
Qt::DirectConnection (直接连接) |
面谈 / 打电话 | 同步执行。信号一发出,槽函数立刻在发送者所在的线程执行。执行完了,后面的代码才会继续走。 |
Qt::QueuedConnection (队列连接) |
发微信留言 | 异步执行。信号发出后变成一个任务,丢进接收者线程的任务队列排队。发完信号代码直接往下走,不管槽函数执行完没有。多线程通信最常用。 |
Qt::BlockingQueuedConnection (阻塞队列连接) |
急件派送,原地等待 | 强行阻塞。信号丢给接收者线程去排队执行,但发送者线程会原地死等,直到接收者把槽函数执行完了,发送者才继续动。绝对不能在同一个线程用,否则自己等自己,直接卡死(死锁)。 |
Qt::AutoConnection (自动连接) |
智能判断 (默认值) | Qt 自动选择:如果发送者和接收者在同一个线程,自动变更为直接连接;如果在不同线程,自动变更为队列连接。 |
Qt::UniqueConnection (唯一连接) |
防重加锁 | 不能单独用,需要用 ` |
UniqueConnection示例:
// 第一次调用,连接成功
auto ret = connect(ui->lineEdit, &QLineEdit::textChanged, ui->label, &QLabel::setText,
Qt::ConnectionType(Qt::AutoConnection | Qt::UniqueConnection));
qDebug() << ret; // 输出 true
// 第二次调用,由于 UniqueConnection 限制,连接会失败,避免了重复触发
ret = connect(ui->lineEdit, &QLineEdit::textChanged, ui->label, &QLabel::setText,
Qt::ConnectionType(Qt::QueuedConnection | Qt::UniqueConnection));
qDebug() << ret; // 输出 false
5. 信号和槽的对应与断开
5.1 对应关系
信号与槽支持灵活的多对多映射关系:
- 一个信号关联多个槽:当信号发出时,槽函数按照连接建立的先后顺序依次被调用。
- 多个信号关联同一个槽。
5.2 信号槽的断开 (disconnect)
可以使用 QObject::disconnect 显式断开某个信号槽的连接。
// 动态断开指定的信号槽连接
disconnect(btn, &QPushButton::clicked, this, &MainWindow::myTest);
补充:disconnect 的通配符(使用 0 代替)高级用法:
-
断开该对象的所有信号连接:
disconnect(myObject, 0, 0, 0); // 等效于非静态成员函数:myObject->disconnect(); -
断开与特定信号的所有连接:
disconnect(myObject, SIGNAL(mySignal()), 0, 0); // 等效于:myObject->disconnect(SIGNAL(mySignal())); -
断开与特定接收者的所有连接:
disconnect(myObject, 0, myReceiver, 0); // 等效于:myObject->disconnect(myReceiver);
6. 信号槽传递自定义类型的参数
6.1 核心问题
在自定义信号中传递自定义数据类型(如 class 或 struct)时,如果在同一个线程内(同步直连),可以直接安全地使用。但如果是跨线程使用信号槽(队列连接),由于涉及到参数生命周期的拷贝与队列排队,直接使用自定义类型可能会导致编译运行错误。
6.2 解决方案:qRegisterMetaType
必须通过 qRegisterMetaType 将自定义数据类型注册到 Qt 的元对象系统中。
- 注册时机:在使用该信号槽之前(通常在构造函数中)进行注册。
- 基本语法:
qRegisterMetaType<MyClass>("MyClass"); - 注意:如果信号槽参数里包含引用类型,引用类型需要单独注册,如
qRegisterMetaType<MyClass>("MyClass&");。

浙公网安备 33010602011771号