Qt 信号与槽机制 ------ 从入门到精通的完整指南
1. 信号和槽概述
1.1 什么是信号?什么是槽?
在 Qt 编程中,用户和控件的每一次交互过程称为一个事件。例如:
- "用户点击按钮" 是一个事件
- "用户关闭窗口" 是一个事件
- "鼠标移动" 是一个事件
- "键盘输入" 是一个事件
每一个事件发生时,Qt 框架都会发出一个信号(Signal)。例如:
- 用户点击按钮 → 发出
clicked()信号 - 用户关闭窗口 → 发出
close()信号 - 鼠标按下 → 发出
mousePressEvent()信号
Qt 中所有的控件都具有接收信号的能力 ,一个控件还可以接收多个不同的信号。对于接收到的每个信号,控件都会做出相应的响应动作------这个响应动作就叫做槽(Slot)。
举例理解:
- 按钮所在的窗口接收到"按钮被点击"的信号后,做出"关闭自己"的响应动作
- 输入框接收到"自己被点击"的信号后,做出"显示闪烁的光标,等待用户输入数据"的响应动作
信号和槽是 Qt 特有的消息传输机制,它能将相互独立的控件关联起来。比如,"按钮"和"窗口"本身是两个独立的控件,点击按钮并不会对窗口造成任何直接影响。通过信号和槽机制,我们可以将两者关联起来,实现"点击按钮会使窗口关闭"的效果。
1.2 信号的本质
信号本质上是事件,产生的条件是用户对窗口或控件进行了某些操作。常见的信号来源包括:
- 按钮单击、双击
- 窗口刷新
- 鼠标移动、鼠标按下、鼠标释放
- 键盘输入
在 Qt 中信号通过什么形式呈现给使用者呢?这里有三个核心要点:
- 我们对哪个窗口进行操作,哪个窗口就可以捕捉到这些被触发的事件。
- 对于使用者来说,触发了一个事件,我们就可以得到 Qt 框架给我们发出的某个特定信号。
- 信号的呈现形式就是函数。 也就是说某个事件产生了,Qt 框架就会调用某个对应的信号函数,通知使用者。
关键理解 :在 Qt 中,信号的发出者是某个实例化的类对象。信号函数用
signals关键字修饰,只需要声明,不需要实现(定义由 Qt 的元对象编译器 MOC 自动生成)。
1.3 槽的本质
槽(Slot)就是对信号响应的函数。 槽就是一个普通的 C++ 函数,与一般的 C++ 函数几乎一样:
- 可以定义在类的任何位置(
public、protected或private) - 可以具有任何参数
- 可以被重载
- 也可以被直接调用(但是不能有默认参数)
槽函数与普通函数唯一的不同是:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行。
1.4 信号和槽的底层原理
信号和槽机制底层是通过函数间的相互调用实现的:
- 每个信号都可以用函数来表示,称为信号函数
- 每个槽也可以用函数表示,称为槽函数
例子 :
"按钮被按下"这个信号可以用clicked()函数表示,"窗口关闭"这个槽可以用close()函数表示。使用信号和槽机制实现"点击按钮会关闭窗口",本质上就是clicked()函数调用close()函数的效果。
信号函数和槽函数通常位于某个类中,和普通成员函数相比,它们有两个特别之处:
| 特征 | 信号函数 | 槽函数 |
|---|---|---|
| 关键字修饰 | signals |
public slots / protected slots / private slots |
| 定义要求 | 只需声明,无需实现(MOC 自动生成) | 需要声明,也需要实现(自己写函数体) |
特别说明 :
signals和slots是 Qt 在 C++ 的基础上扩展的关键字,专门用来指明信号函数和槽函数。信号函数的定义是 Qt 在编译程序之前自动生成的,编写 Qt 应用程序的程序员无需关注。这种自动生成代码的机制称为元编程(Meta Programming)。
2. 信号和槽的使用
2.1 connect() 函数详解
在 Qt 中,QObject 类提供了一个静态成员函数 connect(),该函数专门用来关联指定的信号函数和槽函数。
关于 QObject :
QObject是 Qt 内置的父类,Qt 中提供的很多类都是直接或者间接继承自QObject。这类似于 Java 中Object是所有类的父类的设定。
connect() 函数原型:
cpp
QMetaObject::Connection connect(
const QObject *sender, // 信号的发送者
const char *signal, // 发送的信号(信号函数)
const QObject *receiver, // 信号的接收者
const char *method, // 接收信号的槽函数
Qt::ConnectionType type = Qt::AutoConnection // 关联方式
);
参数详解:
| 参数 | 说明 |
|---|---|
sender |
信号的发送者,是指向某个 QObject 子类对象的指针 |
signal |
发送的信号,使用 &ClassName::signalName 形式指定 |
receiver |
信号的接收者,是指向某个 QObject 子类对象的指针 |
method |
接收信号的槽函数,使用 &ClassName::slotName 形式指定 |
type |
关联方式,默认 Qt::AutoConnection,通常不需要手动设定 |
最简单的代码示例 ------ 点击按钮关闭窗口:
cpp
// widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QPushButton *btn = new QPushButton("关闭窗口", this);
connect(btn, &QPushButton::clicked, this, &QWidget::close);
}
2.2 如何查看内置信号和槽
系统自带的信号和槽通常通过 Qt 帮助文档来查询,这是开发中最常用的查阅方式。
查找步骤以 QPushButton 的 clicked 信号为例:
- 打开 Qt Creator,在帮助文档搜索框中输入:
QPushButton - 首先在当前类的文档中寻找关键字
signals - 如果当前类没有找到,继续去父类 中查找------去
QAbstractButton中找signals - 在
QAbstractButton的文档中就可以找到clicked()信号
查找原则 :信号和槽都具有继承性,如果子类中没有找到,就要沿着继承链向上查找父类。槽函数的查找方式和信号一样,只不过关键字是
slot或Public Slots。
2.3 通过 Qt Creator 可视化生成信号槽代码
Qt Creator 可以快速帮助我们生成信号槽相关的代码,这是日常开发中非常高效的方式。
完整步骤:
第 1 步:新建项目
创建项目时要勾选生成 UI 设计文件(.ui 文件)。新建完成后项目包含的文件结构如下:
项目名/
├── 项目名.pro // 项目配置文件
├── main.cpp // 主函数
├── widget.h // 窗口类头文件
├── widget.cpp // 窗口类实现文件
└── widget.ui // UI 设计文件
第 2 步:进入 UI 设计界面
双击 widget.ui 文件,进入 UI 设计界面。
第 3 步:拖入控件并设置属性
在 UI 设计窗口中拖入一个 Push Button,修改按钮的显示文字(text 属性)以及字体大小(font 属性)等。最关键的是设置控件的 objectName 属性,这个属性是代码中引用该控件的标识。
第 4 步:可视化生成槽函数
在按钮上点击鼠标右键 → 选择 "转到槽..."(Go to slot...)。
此时弹出信号选择对话框,对于普通按钮来说,最常用的是 clicked() 信号:
clicked()------ 普通按钮最常用的信号clicked(bool)------ 具有特殊状态的按钮(比如复选框、单选按钮)才会用到,普通按钮使用它没有意义
选择 clicked() 后,Qt Creator 会自动完成以下操作:
第 5 步:自动生成槽函数框架
Qt Creator 会在两个文件中自动添加代码:
(1) 在 widget.h 头文件中自动添加槽函数的声明:
cpp
// widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots: // ← 自动生成的槽函数声明
void on_pushButton_clicked(); // ← 自动生成
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
(2) 在 widget.cpp 中自动生成槽函数的空定义:
cpp
// widget.cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
// ← 自动生成的槽函数定义框架
void Widget::on_pushButton_clicked()
{
// 在这里添加功能代码
}
第 6 步:在槽函数中添加要实现的功能
例如,实现点击按钮关闭窗口:
cpp
void Widget::on_pushButton_clicked()
{
this->close(); // 关闭当前窗口
}
💡 槽函数的命名规则
自动生成的槽函数遵循固定的命名规范:
on_XXX_SSS
| 组成部分 | 说明 |
|---|---|
on |
固定的前缀 |
XXX |
控件的 objectName 属性值 |
SSS |
对应的信号名称 |
例子 :
on_pushButton_clicked()
on→ 固定前缀pushButton→ 控件的对象名(objectName属性)clicked→ 对应的信号名
重要说明 :按照这种命名风格定义的槽函数,Qt 会自动 将其与对应的信号进行连接(通过 connectSlotsByName 机制),无需手动调用 connect()。
但是,在日常写代码时,除非是 IDE 自动生成的代码,否则最好还是不要依赖这种命名约定,而是显式使用 connect() 函数。原因有两点:
- 显式
connect()可以更清晰直观地描述信号和槽的连接关系,便于代码阅读和维护 - 防止信号或者槽的名字拼写错误导致连接失效(编译期无法检查命名规则连接的正确性)
关于"配置大于约定"还是"约定大于配置"的争论在业界持续存在。在这里更建议优先考虑显式 connect()。
3. 自定义信号和槽
3.1 基本语法与规范
在 Qt 中,除了使用系统内置的信号和槽,还允许我们自定义信号的发送方以及接收方。但是对于自定义的信号函数和槽函数,有严格的书写规范。
自定义信号函数的书写规范
| 规范项 | 要求 |
|---|---|
| 声明位置 | 必须写在 signals: 关键字下 |
| 返回值 | 必须为 void |
| 定义 | 只需要声明,不需要实现(MOC 自动生成) |
| 参数 | 可以有参数,也可以发生重载 |
cpp
// 示例:信号函数声明
signals:
void mySignal(); // 无参信号
void mySignal(int value); // 带参信号(重载)
void mySignal(QString msg); // 带参信号(重载)
自定义槽函数的书写规范
| 规范项 | 要求 |
|---|---|
| 声明位置 | 早期 Qt 要求写在 public slots: 下,现在高版本 Qt 允许写在类的 public 作用域中或全局下 |
| 返回值 | 必须为 void |
| 定义 | 需要声明,也需要实现 |
| 参数 | 可以有参数,可以发生重载 |
cpp
// 示例:槽函数声明(现代写法)
public:
void mySlot(); // 无参槽
void mySlot(int value); // 带参槽(重载)
void mySlot(QString msg); // 带参槽(重载)
// 或者传统写法
public slots:
void mySlot();
void mySlot(int value);
发送信号 ------ emit 关键字
使用 emit 关键字来发送信号:
cpp
emit mySignal(); // 发送无参信号
emit mySignal(42); // 发送带参信号
说明 :
emit是一个空的宏,本身没有任何实际含义,只是用于提醒开发人员"这里在发射信号"。emit其实是可选的,但保留它能让代码意图更加清晰。
示例 1:窗口内自定义信号和槽
设计要求:在窗口类中自定义一个信号和一个槽,将它们关联起来。
(1)在 widget.h 中声明自定义的信号和槽:
cpp
// widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
// 自定义槽函数(现代写法可以放在 public 中)
void handleMySignal();
signals:
// 自定义信号函数
void mySignal();
private:
void emitMySignal(); // 用于触发信号的辅助函数
};
#endif // WIDGET_H
(2)在 widget.cpp 中实现槽函数并关联信号和槽:
cpp
// widget.cpp
#include "widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
// ① 第一步:关联信号和槽(必须先于信号发射)
connect(this, &Widget::mySignal, this, &Widget::handleMySignal);
// ② 第二步:发射信号
emitMySignal();
}
Widget::~Widget()
{
}
void Widget::handleMySignal()
{
qDebug() << "槽函数被调用了!";
}
void Widget::emitMySignal()
{
emit mySignal(); // 发射信号
}
⚠️ 关键注意 :
connect()必须在emit之前调用!先关联信号和槽,一旦检测到信号发射之后就会立即执行关联的槽函数。反之,若先发射信号,此时还没有关联槽函数,当信号发射之后槽函数不会响应。
示例 2:跨类信号和槽 ------ "老师说上课,学生回座位"
这是一个经典的跨类通信示例,展示了如何在不同类的对象之间通过信号槽进行通信。
设计要求:
- 老师类(Teacher):当老师发出"上课了"的信号
- 学生类(Student):学生收到信号后执行"回到座位,开始学习"的动作
第 1 步:创建 Teacher 类和 Student 类
在项目中新建两个类。选中项目名称,鼠标右键 → "Add New..." → 选择 C++ Class。
创建时需要注意选择正确的基类:
| 类名 | 基类 | 原因 |
|---|---|---|
| Teacher | QObject |
不是窗口/控件类,只需继承 QObject 来获得信号槽能力 |
| Student | QObject |
同上 |
为什么选择 QObject 作为基类?
- 当前项目中没有适合做新类父类的已有类
- 新类也不是一个"窗口"或"控件"
- 继承
QObject可以让新类的对象搭配 Qt 的对象树机制,便于对象的正确(自动)释放
第 2 步:在 teacher.h 中声明信号函数:
cpp
// teacher.h
#ifndef TEACHER_H
#define TEACHER_H
#include <QObject>
class Teacher : public QObject
{
Q_OBJECT
public:
explicit Teacher(QObject *parent = nullptr);
signals:
// 老师发出"上课了"的信号
void classBegin();
};
#endif // TEACHER_H
第 3 步:在 student.h 中声明槽函数:
cpp
// student.h
#ifndef STUDENT_H
#define STUDENT_H
#include <QObject>
class Student : public QObject
{
Q_OBJECT
public:
explicit Student(QObject *parent = nullptr);
public slots:
// 学生响应"上课"的槽函数
void goToSeat();
};
#endif // STUDENT_H
第 4 步:在 student.cpp 中实现槽函数:
cpp
// student.cpp
#include "student.h"
#include <QDebug>
Student::Student(QObject *parent) : QObject(parent)
{
}
void Student::goToSeat()
{
qDebug() << "学生收到信号:回到座位,开始学习!";
}
第 5 步:在 widget.h 中实例化 Teacher 和 Student 对象:
cpp
// widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include "teacher.h"
#include "student.h"
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Teacher *teacher; // 老师对象
Student *student; // 学生对象
};
#endif // WIDGET_H
第 6 步:在 widget.cpp 中连接自定义信号和槽:
cpp
// widget.cpp
#include "widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
teacher = new Teacher(this);
student = new Student(this);
// 连接:老师发出"上课"信号 → 学生执行"回座位"槽函数
connect(teacher, &Teacher::classBegin, student, &Student::goToSeat);
// 发射信号(模拟老师说"上课了")
emit teacher->classBegin();
}
Widget::~Widget()
{
}
运行结果 :控制台输出 "学生收到信号:回到座位,开始学习!"
示例 3:通过按钮触发信号
在示例 2 的基础上,增加一个按钮,点击按钮 → 老师发出"上课了"信号 → 学生响应。
cpp
// widget.cpp
#include "widget.h"
#include <QPushButton>
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
teacher = new Teacher(this);
student = new Student(this);
QPushButton *btn = new QPushButton("上课", this);
// 连接1:按钮点击 → 老师发信号
connect(btn, &QPushButton::clicked, teacher, &Teacher::classBegin);
// 连接2:老师信号 → 学生响应
connect(teacher, &Teacher::classBegin, student, &Student::goToSeat);
}
当点击"上课"按钮时,信号传递的链路是:
按钮 clicked() → 老师 classBegin() → 学生 goToSeat()
3.2 带参数的信号和槽
Qt 的信号和槽支持带有参数,同时也可以支持重载。
核心要求:信号函数的参数列表要和对应连接的槽函数参数列表一致。这样当信号触发调用槽函数时,信号函数中的实参就能被传递到槽函数的形参当中。
这就是信号给槽传递数据的机制。
示例 1:重载信号和槽
(1)在 widget.h 中声明重载的信号函数及重载的槽函数:
cpp
// widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
public slots:
void handleSignal(); // 无参槽
void handleSignal(QString msg); // 带参槽(重载)
signals:
void mySignal(); // 无参信号
void mySignal(QString msg); // 带参信号(重载)
private:
void testOverload();
};
#endif // WIDGET_H
(2)在 widget.cpp 中实现重载槽函数以及连接信号和槽:
cpp
// widget.cpp
#include "widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent) : QWidget(parent)
{
testOverload();
}
Widget::~Widget() {}
void Widget::testOverload()
{
// 当信号和槽存在重载时,需要使用函数指针明确指定版本
// 否则编译器无法判断使用的是哪个重载版本
// 无参版本
void (Widget::*signal1)() = &Widget::mySignal;
void (Widget::*slot1)() = &Widget::handleSignal;
connect(this, signal1, this, slot1);
// 带参版本
void (Widget::*signal2)(QString) = &Widget::mySignal;
void (Widget::*slot2)(QString) = &Widget::handleSignal;
connect(this, signal2, this, slot2);
// 发射信号
emit mySignal(); // 触发无参版本
emit mySignal("Hello Qt"); // 触发带参版本
}
void Widget::handleSignal()
{
qDebug() << "无参槽函数被调用";
}
void Widget::handleSignal(QString msg)
{
qDebug() << "带参槽函数被调用,参数为:" << msg;
}
⚠️ 重要 :当信号和槽存在重载时,必须使用函数指针 来明确指定要使用的重载版本。务必指明函数指针的作用域(即类名限定)。
示例 2:信号和槽参数个数的匹配规则
规则:信号的参数个数可以多于槽函数的参数个数,但槽的参数个数不能多于信号参数个数。
cpp
// 这是合法的:信号有2个参数,槽有1个参数
// 信号: signal(int, QString)
// 槽 : slot(int)
// 连接时,槽函数接收信号的第1个参数,忽略额外的参数
// 这是非法的:信号有1个参数,槽有2个参数 ❌
// 信号: signal(int)
// 槽 : slot(int, QString)
// 槽函数需要的参数无法从信号获取
代码示例:
cpp
// widget.h
signals:
void mySignal(int value, QString msg); // 信号:2个参数
public slots:
void mySlot(int value); // 槽:1个参数 ✓ 合法
// void mySlot(int value, QString msg, double d); // 槽:3个参数 ✗ 非法
cpp
// widget.cpp 中的连接
connect(this, &Widget::mySignal, this, &Widget::mySlot);
// 发射信号
emit mySignal(42, "Hello");
// 槽函数 mySlot(42) 被调用,QString 参数被忽略
实际开发建议 :虽然信号参数个数可以多于槽参数个数,但最好还是保持参数个数和类型完全匹配,这样代码最清晰,也最容易维护。
示例 3:完整的参数传递示例
cpp
// widget.h
signals:
void scoreChanged(int newScore); // 分数变化信号
public slots:
void onScoreChanged(int score); // 处理分数变化的槽
cpp
// widget.cpp
connect(this, &Widget::scoreChanged, this, &Widget::onScoreChanged);
void Widget::onScoreChanged(int score)
{
qDebug() << "分数更新为:" << score;
}
// 某处发射信号
emit scoreChanged(95); // 输出:分数更新为:95
4. 信号与槽的连接方式
4.1 一对一连接
一对一连接有两种形式:
- 一个信号连接一个槽
- 一个信号连接另一个信号
(1)一个信号连接一个槽
这是最基础、最常见的连接方式。
cpp
// 连接格式
connect(发送者, 信号, 接收者, 槽函数);
// 示例
connect(btn, &QPushButton::clicked, this, &QWidget::close);
完整示例:
cpp
// widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
signals:
void mySignal(); // 自定义信号
public slots:
void mySlot(); // 自定义槽
private:
void triggerSignal(); // 发射信号的辅助函数
};
#endif // WIDGET_H
cpp
// widget.cpp
#include "widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent) : QWidget(parent)
{
// 一个信号连接一个槽
connect(this, &Widget::mySignal, this, &Widget::mySlot);
}
void Widget::triggerSignal()
{
emit mySignal();
}
void Widget::mySlot()
{
qDebug() << "槽函数被调用 ------ 一对一连接";
}
(2)一个信号连接另一个信号
信号不仅可以连接槽,还可以连接另一个信号。当第一个信号被发射时,第二个信号也会被自动发射。
cpp
// 连接格式
connect(发送者, 信号1, 接收者, 信号2);
// 效果:发射信号1时,信号2也会被自动发射
示例(在上述一对一示例基础上添加):
cpp
// widget.h 中添加
signals:
void signalA();
void signalB(); // 新增信号B
public slots:
void slotA();
cpp
// widget.cpp 中添加
// 信号A 连接 信号B(信号连信号)
connect(this, &Widget::signalA, this, &Widget::signalB);
// 信号B 连接 槽A(信号连槽)
connect(this, &Widget::signalB, this, &Widget::slotA);
// 发射 signalA → 自动发射 signalB → 触发 slotA
emit signalA();
// 输出结果:slotA 被调用
信号连信号的适用场景:当某个信号发出的时机恰好也是另一个信号的触发条件时,可以用这种方式简化代码,避免写额外的槽函数来转发信号。
4.2 一对多连接
一个信号可以连接多个槽函数。 当该信号被发射时,所有连接的槽函数都会被依次调用。
┌→ 槽函数1()
一个信号 signal() ──┼→ 槽函数2()
└→ 槽函数3()
完整示例:
cpp
// widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
signals:
void dataReady(); // 一个信号
public slots:
void handleData_1(); // 槽1
void handleData_2(); // 槽2
void handleData_3(); // 槽3
};
#endif // WIDGET_H
cpp
// widget.cpp
#include "widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent) : QWidget(parent)
{
// 一个信号连接三个槽
connect(this, &Widget::dataReady, this, &Widget::handleData_1);
connect(this, &Widget::dataReady, this, &Widget::handleData_2);
connect(this, &Widget::dataReady, this, &Widget::handleData_3);
// 发射信号,三个槽函数都会被调用
emit dataReady();
}
void Widget::handleData_1()
{
qDebug() << "槽函数1执行";
}
void Widget::handleData_2()
{
qDebug() << "槽函数2执行";
}
void Widget::handleData_3()
{
qDebug() << "槽函数3执行";
}
运行结果:
槽函数1执行
槽函数2执行
槽函数3执行
说明 :多个槽函数的执行顺序与
connect()的调用顺序一致。
4.3 多对一连接
多个信号可以连接到同一个槽函数。 任何一个信号被发射,都会触发该槽函数执行。
信号1() ─┐
信号2() ─┼→ 槽函数()
信号3() ─┘
完整示例:
cpp
// widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
signals:
void signal_1(); // 信号1
void signal_2(); // 信号2
public slots:
void commonSlot(); // 公共槽函数
};
#endif // WIDGET_H
cpp
// widget.cpp
#include "widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent) : QWidget(parent)
{
// 两个信号连接到同一个槽
connect(this, &Widget::signal_1, this, &Widget::commonSlot);
connect(this, &Widget::signal_2, this, &Widget::commonSlot);
// 分别发射两个信号,都会触发同一个槽
emit signal_1();
emit signal_2();
}
void Widget::commonSlot()
{
qDebug() << "公共槽函数被执行";
}
运行结果:
公共槽函数被执行
公共槽函数被执行
📊 三种连接方式总结:
| 连接方式 | 描述 | 典型场景 |
|---|---|---|
| 一对一 | 一个信号 ↔ 一个槽 / 一个信号 ↔ 一个信号 | 按钮点击关闭窗口 |
| 一对多 | 一个信号 ↔ 多个槽 | 数据更新通知多个 UI 组件 |
| 多对一 | 多个信号 ↔ 一个槽 | 多个操作触发统一的保存逻辑 |
5. 信号和槽的其他说明
5.1 信号与槽的断开 disconnect
使用 disconnect() 函数可以断开已经建立的信号和槽之间的连接。disconnect() 的用法和 connect() 基本一致。
函数原型:
cpp
bool disconnect(
const QObject *sender,
const char *signal,
const QObject *receiver,
const char *method
);
示例:
cpp
// 先建立连接
connect(btn, &QPushButton::clicked, this, &QWidget::close);
// ... 某些操作 ...
// 断开连接:按钮点击不再关闭窗口
disconnect(btn, &QPushButton::clicked, this, &QWidget::close);
断开后,点击按钮就不会再触发关闭窗口的动作了。
5.2 Qt4 版本的信号与槽连接
在 Qt4 中,connect() 的写法与 Qt5 相比要更加复杂,需要搭配 SIGNAL() 和 SLOT() 宏来完成。
Qt4 写法:
cpp
connect(sender, SIGNAL(clicked()), receiver, SLOT(close()));
// ↑ 宏包裹信号 ↑ 宏包裹槽
Qt5 写法(推荐):
cpp
connect(sender, &QPushButton::clicked, receiver, &QWidget::close);
// ↑ 函数指针 ↑ 函数指针
完整示例对比:
cpp
// ==== Qt4 写法 ====
// widget.h
class Widget : public QWidget
{
Q_OBJECT
signals:
void mySignal();
public slots:
void mySlot();
};
// widget.cpp
connect(this, SIGNAL(mySignal()), this, SLOT(mySlot()));
// ↑ 信号名用字符串 ↑ 槽名用字符串
// ==== Qt5 写法 ====
// widget.h(相同)
class Widget : public QWidget
{
Q_OBJECT
signals:
void mySignal();
public slots:
void mySlot();
};
// widget.cpp
connect(this, &Widget::mySignal, this, &Widget::mySlot);
// ↑ 函数指针(编译期检查)↑ 函数指针(编译期检查)
Qt4 方式的优缺点分析:
| Qt4 方式 (SIGNAL/SLOT 宏) | Qt5 方式 (函数指针) | |
|---|---|---|
| 优点 | 参数看起来直观(直接写参数类型) | 编译期类型检查,参数类型不匹配会报编译错误 |
| 缺点 | 参数类型不做检测,运行时才发现问题 | 函数重载时需要函数指针指定版本 |
Qt4 方式最大的问题 ------ 缺少类型检查:
cpp
// Qt4 写法,以下错误的代码在编译期不会报错!
// 信号 click() 没有参数,但槽函数接收了一个 int 参数
connect(btn, SIGNAL(clicked()), this, SLOT(onClicked(int)));
// 这种错误要到运行时才能发现,非常危险!
// Qt5 写法,同样的错误在编译期就会报错 ✓
connect(btn, &QPushButton::clicked, this, &Widget::onClicked);
// 编译错误:槽函数参数不匹配!
建议:始终使用 Qt5 的函数指针写法,利用编译器的类型检查能力,尽早发现错误。
5.3 使用 Lambda 表达式定义槽函数
Qt5 在 Qt4 的基础上大大提高了信号与槽的灵活性,允许使用任意函数 作为槽函数。如果想更方便地编写槽函数(比如连函数名都不想定义),可以通过 Lambda 表达式来达到这个目的。
Lambda 表达式是 C++11 增加的特性,用于定义并创建匿名的函数对象,以简化编程工作。
Lambda 表达式的完整语法
cpp
[capture](params) opt -> ret {
// 函数体
Function body;
};
| 组成部分 | 名称 | 说明 |
|---|---|---|
[capture] |
捕获列表 | 标识 Lambda 的开始,不可省略 |
(params) |
参数表 | 类似于普通函数的参数列表,可以省略(相当于无参函数) |
opt |
函数选项 | 可选项,最常用的是 mutable 声明,可以省略 |
-> ret |
返回值类型 | 指定返回值类型,可以省略(编译器自动推导) |
{ body } |
函数体 | 函数的具体实现,不可省略(但可以为空) |
5.3.1 捕获列表 [capture] ------ 详解
捕获列表是 Lambda 最核心的概念,它决定了 Lambda 表达式内部如何访问外部的局部变量。
| 捕获方式 | 说明 |
|---|---|
[] |
空捕获列表。Lambda 表达式不能访问外部函数体的任何局部变量 |
[a] |
值传递 方式访问变量 a(函数体内使用的是 a 的副本) |
[&b] |
引用传递 方式访问变量 b(函数体内使用的是 b 本身) |
[=] |
外部所有 局部变量都通过值传递方式使用(函数体内使用副本) |
[&] |
外部所有 局部变量都通过引用传递方式使用 |
[=, &foo] |
foo 使用引用 方式,其余所有变量使用值传递方式 |
[&, foo] |
foo 使用值传递 方式,其余所有变量使用引用传递方式 |
[this] |
在函数体内可以使用类的成员函数和成员变量(= 和 & 形式也会默认引入 this) |
⚠️ 重要警示 :
使用引用方式 捕获对象时,可能出现局部变量已经释放了而 Lambda 函数还没有被调用 的情况。此时执行 Lambda 函数,引用传递方式捕获进来的局部变量的值将不可预知(悬挂引用/野指针问题)。
因此,绝大多数场合推荐使用
[=]() {}形式,即值传递方式,安全且足够满足大部分需求。
5.3.2 参数列表 (params) ------ 详解
(params) 表示 Lambda 函数对象接收的参数,类似于普通函数的小括号中表示的函数参数类型和个数。
- 参数可以通过按值 (如
(int a, int b))方式传递 - 也可以通过按引用 (如
(int &a, int &b))方式传递 - 函数参数部分可以省略,省略后相当于无参的函数
示例:
cpp
// 带参数的 Lambda
auto add = [](int a, int b) -> int {
return a + b;
};
int result = add(3, 5); // result = 8
// 无参数的 Lambda(省略括号也是可以的)
auto sayHello = [] {
qDebug() << "Hello Qt!";
};
sayHello();
5.3.3 函数选项 opt ------ 详解
opt 部分是可选项,最常用的是 mutable 声明。
Lambda 表达式外部局部变量通过值传递 进来时,其默认是 const 的,所以不能修改这个局部变量的副本 。加上 mutable 关键字后就可以修改了。
示例:
cpp
int count = 10;
// ❌ 错误:值传递进来的变量默认是 const,不能修改
auto lambda1 = [count]() {
// count++; // 编译错误!count 是只读的
};
// ✅ 正确:加上 mutable 后可以修改副本
auto lambda2 = [count]() mutable {
count++; // 合法,但修改的是副本,不影响外部的 count
qDebug() << count; // 输出 11
};
lambda2();
qDebug() << count; // 输出 10(外部变量未被修改)
5.3.4 返回值类型 -> ret ------ 详解
可以指定 Lambda 表达式的返回值类型:
- 如果不指定返回值类型,编译器会根据代码实现为函数自动推导一个返回类型
- 如果没有返回值,则可忽略此部分
示例 1:指定返回值类型
cpp
auto func1 = [](int a, int b) -> int {
return a + b;
};
示例 2:自动推导返回值类型
cpp
// 编译器自动推导返回类型为 int
auto func2 = [](int a, int b) {
return a + b;
};
// 无返回值,自动推导为 void
auto func3 = []() {
qDebug() << "无返回值";
};
5.3.5 函数体 { } ------ 详解
Lambda 表达式的函数体部分与普通函数体一致。用 {} 标识函数的实现:
- 不能省略 (即使为空也要写
{}) - 函数体可以为空
{}
示例:
cpp
auto emptyFunc = [] {}; // 合法的空 Lambda
auto greet = [] {
qDebug() << "Hello, World!";
};
5.3.6 Lambda 表达式作为槽函数
这是 Lambda 在 Qt 中最实用的场景------直接在 connect() 中编写槽函数逻辑。
示例 1:点击按钮,用 Lambda 关闭窗口
cpp
// 不需要单独定义槽函数,直接在 connect 中写逻辑
connect(btn, &QPushButton::clicked, this, [=]() {
this->close(); // 关闭窗口
});
示例 2:当 connect 的第三个参数是 this 时,可以省略 Lambda 中的 this
cpp
// 完整写法
connect(btn, &QPushButton::clicked, this, [this]() {
this->close();
});
// 简化写法(推荐):context 为 this 时,Lambda 中可以直接用 this
connect(btn, &QPushButton::clicked, this, [=]() {
close(); // 等价于 this->close()
});
示例 3:Lambda 中使用外部变量
cpp
int threshold = 100;
connect(btn, &QPushButton::clicked, this, [=]() {
// 值捕获的 threshold 可以在 Lambda 内部使用
qDebug() << "阈值为:" << threshold;
if (someValue > threshold) {
// 做一些处理
}
});
示例 4:Qt4 风格与 Qt5+Lambda 风格对比
cpp
// ===== Qt4 传统方式 =====
// 需要单独声明和定义槽函数
// widget.h
class Widget : public QWidget {
Q_OBJECT
private slots:
void onButtonClicked();
};
// widget.cpp
void Widget::onButtonClicked() {
// 处理逻辑
}
connect(btn, SIGNAL(clicked()), this, SLOT(onButtonClicked()));
// ===== Qt5 + Lambda 方式(推荐)=====
// 无需在头文件中声明槽函数,代码更紧凑
connect(btn, &QPushButton::clicked, this, [=]() {
// 直接在原地写处理逻辑
});
📝 早期版本注意 :若使用早期版本的 Qt,要在
.pro文件中添加CONFIG += C++11,因为 Lambda 表达式是 C++11 标准提出的。Qt5 以上的版本无需手动添加,在新建项目时会自动配置。
5.4 信号与槽的优缺点
✅ 优点:松散耦合
信号和槽机制最大的优势在于松散耦合:
- 信号发送者不需要知道发出的信号被哪个对象的哪个槽函数接收
- 槽函数也不需要知道哪些信号关联了自己
- Qt 的信号槽机制自动保证了信号与槽函数的正确调用
这种设计让组件之间的依赖降到最低,极大提高了代码的可维护性和可扩展性。
前提条件 :支持信号槽机制的类(或其父类)必须继承于
QObject类。
❌ 缺点:效率较低
与直接的回调函数相比,信号和槽稍微慢一些 ,这是因为它们提供了更高的灵活性。具体来说,通过信号调用的槽函数比直接函数调用慢约 10 倍。
速度慢的原因主要在于信号槽机制需要额外的开销:
- 定位信号的接收对象 ------ 需要遍历所有关联的 connection
- 遍历所有关联 ------ 查找被连接的槽函数
- 编组/解组传递的参数 ------ 参数的封装和传递需要额外开销
- 多线程时信号可能需要排队 ------ 跨线程的信号槽调用涉及事件队列
💡 实际影响分析:
假设基于回调的方式调用耗时是 10μs ,使用信号槽的方式是 100μs。
对于一个客户端程序来说,最慢的环节往往是"人"------用户根本无法感知 100μs 和 10μs 之间的差异。
结论 :这种调用速度上的差异对性能要求不是非常高的场景是完全可以忽略的,能够满足绝大部分应用场景。只有在极端的高频调用场景(如实时音视频渲染、高频传感器数据采集等)才需要考虑使用回调来替代。
总结
| 维度 | 要点 |
|---|---|
| 核心概念 | 信号 = 事件通知,槽 = 响应函数 |
| 连接函数 | connect(发送者, 信号, 接收者, 槽) |
| 自定义信号 | signals: 下声明,只需声明无需实现,MOC 自动生成 |
| 自定义槽 | 普通成员函数,需要声明也需要实现 |
| 发射信号 | 使用 emit 关键字(可选的宏,增强可读性) |
| 参数规则 | 信号参数 ≥ 槽参数,类型需兼容 |
| 重载处理 | 使用函数指针明确指定版本 |
| 连接方式 | 一对一、一对多、多对一,灵活组合 |
| 断开连接 | disconnect() 用法与 connect() 基本一致 |
| Qt5 vs Qt4 | Qt5 使用函数指针(编译期检查),Qt4 使用 SIGNAL/SLOT 宏(运行时检查) |
| Lambda 槽 | connect(sender, &Signal, context, [=]() { ... }) |
| 优点 | 松散耦合,代码清晰,易于维护 |
| 缺点 | 比直接调用慢约 10 倍(绝大多数场景不影响用户体验) |
信号和槽是 Qt 框架的灵魂机制。掌握它,就掌握了 Qt 编程的核心。通过灵活运用信号和槽的各种连接方式、Lambda 表达式简化代码、合理设计自定义信号和槽,可以构建出结构清晰、易于扩展的 Qt 应用程序。