Qt入门03 Qt信号槽——大丙+Gemini
Qt入门03 Qt信号槽——大丙+Gemini
https://www.bilibili.com/video/BV1Jp4y167R9
1. 信号和槽概述
信号槽(Signals and Slots)是 Qt 框架的核心机制,其实质是观察者模式(或称为发布-订阅模式)的具体实现。当特定事件发生时(例如按钮被点击),发生事件的对象会发出一个信号(Signal)。这种发出的动作类似于广播,没有特定的目的接收方。若其他对象对该信号感兴趣,可通过 connect() 函数将需要处理的信号与自身的一个函数(即槽函数,Slot)进行绑定。绑定完成后,一旦信号发出,对应的槽函数就会自动被回调触发。
1.1 信号的本质
信号的本质就是事件。当用户对窗口或控件进行特定操作(如单击、双击按钮,移动、按下、释放鼠标,或者键盘输入等),会导致相应控件产生特定事件,此时 Qt 对应的窗口类会发出某个对应的信号,以此对用户的操作做出反应。
在 Qt 框架中,信号的表现形式是函数。某个事件产生后,Qt 框架会调用对应的信号函数来通知使用者。信号的发出者必定是某个实例化的类对象,该对象内部封装了相关事件的检测逻辑。
1.2 槽的本质
槽函数是 Qt 中一类特殊的功能函数,在编码过程中完全可以作为类的普通成员函数来使用。其核心特殊职责是对 Qt 框架中产生的信号进行捕获和处理。
在信号槽交互中存在两个核心角色:
- 信号发出者(Sender):检测事件并发射携带事件信息的信号。
- 信号接收者(Receiver):接收发出的信号,并通过执行槽函数来处理该信号。
与信号类似,槽函数的所有者也必须是某个类的实例化对象。
1.3 信号和槽的关系
信号和槽本身是独立的个体,互不干扰。在开发中,我们需要使用 QObject 类提供的 connect() 函数将二者强制关联。
以下是 connect() 函数的完整原型(其中 PointerToMemberFunction 表示指向函数地址的指针):
QMetaObject::Connection QObject::connect(
const QObject *sender, PointerToMemberFunction signal,
const QObject *receiver, PointerToMemberFunction method,
Qt::ConnectionType type = Qt::AutoConnection);
参数解析:
- sender:发出信号的对象指针。
- signal:属于
sender对象的信号函数的地址指针。 - receiver:信号接收者对象指针。
- method:属于
receiver对象的槽函数地址。- 当检测到
sender发出了signal信号时,receiver对象将调用该method方法进行处理。
- 当检测到
简化的 connect() 调用格式如下:
connect(const QObject *sender, &QObject::signal,
const QObject *receiver, &QObject::method);
连接与调用注意事项:
connect()函数的操作本质是对信号处理动作的“提前注册”。通常写在窗口的构造函数中。- 调用
connect()时,sender对象的信号并未真正产生,因此receiver对象的method此时绝对不会被执行。 method槽函数本质是一个回调函数。其真正的调用时机是在信号产生之后,由 Qt 框架在后台调度执行。如果信号始终未产生,槽函数就永远不会被调用。connect()传入的sender和receiver两个指针必须已经完成实例化,否则连接将失败。
2. 标准信号槽使用
2.1 标准信号槽及查找
Qt 提供的许多标准类内部已经封装了对特定事件的检测。用户进行操作后,这些类内部会自动产生自带的信号,称为标准信号。
同时,Qt 许多类自带的功能函数也可以直接作为触发后的处理动作,称为标准槽函数。
查找系统自带的信号和槽需依赖 Qt 帮助文档。以查找按钮的点击信号为例:
首先在文档中搜索 QPushButton
若在当前类的 Contents 目录中未发现 signals 关键字,应追溯其父类。跳转至父类 QAbstractButton 的文档页即可查找到系统自带的信号(例如 clicked)
最后在父类中发现标准信号
2.2 标准信号槽的使用细节
以“点击窗口上的按钮,关闭窗口”这一功能为例进行逻辑分析:
- 信号发出者:按钮对象(属于
QPushButton类型)。 - 信号接收处理者:当前窗口对象(属于
QMainWindow类型,QWidget的子类)。
查阅文档后提取所需使用的标准函数如下:
void QAbstractButton::clicked(bool checked = false);
bool QWidget::close();
具体实现步骤:
步骤 1:在 UI 设计界面(.ui 文件)的 MainWindow 中放置一个 Push Button 控件,并将其 objectName 命名为 closeBtn。
步骤 2:在 MainWindow.cpp 的构造函数中使用 connect() 注册信号槽。
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(ui->closeBtn, &QPushButton::clicked, this, &QWidget::close);
}
执行逻辑:connect 函数执行完毕后仅代表注册成功。程序运行中,只有当用户点击了按钮发出 clicked 信号,Qt 框架才会触发 this(当前窗口)的 close 方法。
补充注意事项:
- 对象访问:必须通过
ui指针(如ui->closeBtn)才能访问在 UI 文件中定义并实例化的控件对象。 - 函数地址语法要求:在 C++ 中获取函数地址有严格规范。对于非成员函数,
func和&func均可正确获取地址且等价;但对于类成员函数,必须显式使用作用域和取地址符(如&QPushButton::clicked或&QWidget::close),直接使用函数名无法获取有效地址。
3. 自定义信号槽的使用
Qt 框架提供的标准信号槽在某些特定场景下无法满足项目需求,因此我们需要设计自定义的信号和槽。无论是标准还是自定义,底层依然是通过 connect() 函数进行连接。
3.0 自定义信号/槽的前提条件
想要在 Qt 中自定义信号与槽,自定义的类必须满足以下条件:
- 继承标准类:新类必须从 QObject 或者是 QObject 的子类(如 QWidget、QMainWindow 等)进行派生。
- 引入宏定义:在定义类的头文件中,必须加入 Q_OBJECT 宏。(注意:无论是只包含信号的类,还是只包含槽的类,只要用到这套机制都必须加)。
示例代码:基础类结构
// 必须继承于 QObject 或其子类
class SignalOrSlotClass : public QWidget
{
// 在该派生类的头文件中,必须首先像下面那样引入 Q_OBJECT 宏
Q_OBJECT
public:
// 补充:通常会带上 parent 参数以加入 Qt 的对象树,方便内存自动管理
explicit SignalOrSlotClass(QWidget *parent = nullptr);
......
};
3.1 自定义信号 (Signals)
在 Qt 中,信号的本质是事件。在代码层面,它以函数的形式存在,但只有声明,没有定义(实现)。当特定的事件产生后,我们人为地将这个信号“发射”出去即可。
要求与注意事项:
- 声明位置:必须使用
signals关键字声明。 - 函数形态:信号必须是类的成员函数;只有声明,没有定义;返回值必须是
void。 - 命名与参数:名字可以任意;参数可以任意指定,且支持重载。
- 发射机制:在程序中发射信号本质就是「调用信号函数」。
- 调用时通常在前面加上
emit关键字(可省略)。 emit仅用于提示开发者这是一个信号函数,方便代码阅读。在底层逻辑中它是一个空宏:#define emit,无实际意义。
- 调用时通常在前面加上
示例代码:
// 举例: 信号重载
// Qt中的类想要使用信号槽机制必须要从QObject类派生(直接或间接派生都可以)
class Test : public QObject
{
Q_OBJECT
signals:
void testsignal();
// 参数的作用是数据传递, 谁调用信号函数谁就指定实参
// 实参最终会被传递给槽函数
void testsignal(int a);
};
3.2 自定义槽 (Slots)
槽函数就是信号触发后的处理动作。在 Qt 中,槽函数其实就可以作为普通的 C++ 成员函数来使用(可以包含业务逻辑、可以被直接调用)。
要求与注意事项:
-
函数形态:返回值必须是 void;包含具体的函数实现;同样支持重载。
-
作用:接收信号传递的数据(即信号函数的参数)。
-
形态多样性:在 Qt5 中,槽函数的类型非常灵活,可以是:类成员函数、全局函数、静态函数,甚至是 Lambda 表达式。
-
访问控制符:
- public slots::公有槽,可在类外部调用。
- private / protected slots::不能在类外部当作普通函数调用,但依然可以被信号成功触发。
-
⚠️ 核心规则:参数匹配原则
槽函数的参数个数和类型,严格受制于连接它的信号:
- 参数个数:信号函数参数个数 $\ge$ 槽函数参数个数
- 信号可以传递多余的数据,槽可以选择不接收,但槽不能向信号索要它没有的数据。
- 参数类型:槽函数的参数类型必须和信号函数完全相同(指槽需要接收的那部分)
信号/槽参数匹配示例:
- 参数个数一样:
- 信号:
void testsig1(int a, double b); - 槽函数:
void testslot1(int a, double b);
- 信号:
- 参数个数不一样,信号 > 槽:
- 信号:
void testsig2(int a, double b, char c, QString& d); - 槽函数:
void testslot2(int a);(只接收第一个参数)
- 信号:
- 参数个数:信号函数参数个数 $\ge$ 槽函数参数个数
示例代码:
// ================= 全局函数作为槽 =================
// 写在任何类外部的普通全局函数,只要参数和返回值匹配即可
void globalTestFunc() { /* 全局函数的处理逻辑... */ }
// ================= 类成员函数作为槽 =================
class Test : public QObject
{
Q_OBJECT // 包含槽的类同样需要 Q_OBJECT 宏
public:
// 普通的成员函数作为槽函数
void testSlot();
// 静态成员函数作为槽
static void staticTestFunc();
public slots:
// 传统的槽函数声明方式,支持重载
void testSlot(int id);
private slots:
// 私有槽函数:不能在类外部被直接调用(如 obj->testPrivateSlot() 会报错)
// 但通过 connect() 绑定后依然能正常触发
void testPrivateSlot();
};
// ================= Lambda表达式作为槽 =================
// 补充:Lambda 表达式通常不需要提前声明,而是直接写在 connect() 函数的参数中
/*
Test* obj = new Test();
connect(obj, &Test::testsignal, [=](int a) {
// 这里就是 Lambda 表达式作为槽函数的处理逻辑
// 接收到的参数 a 可以直接在这里使用
});
*/
3.3 示例代码:自定义信号与槽的完整实现
1. 场景说明
- 触发场景:女朋友说:“我肚子饿了!”,于是我带她去吃饭。
- 角色映射:
GirlFriend类:发送者(Sender)。发出hungry信号,表示肚子饿。Me类:接收者(Receiver)。提供eat槽函数,接收信号并输出“带女朋友吃饭”。
2. 设计思路与执行流程
在主窗口(MainWindow)中放置一个按钮(QPushButton),执行流程如下:
- 界面操作:用户按下主窗口上的
hungryBtn按钮。 - 第一层传递:按钮触发
clicked信号,主窗口的槽函数girlSayHungry()接收该信号。 - 第二层传递:在
girlSayHungry()中,调用GirlFriend::hungry()函数让GirlFriend对象发出hungry信号。 - 最终响应:
Me对象的eat()槽函数被触发,执行打印操作。
3. 逐步实现步骤
Step1:创建 GirlFriend 类(定义信号)
要点:信号函数只需声明,绝对不要去实现它(其底层代码由 Qt 的 MOC 元对象编译器自动生成)。
GirlFriend.h
#ifndef GIRLFRIEND_H
#define GIRLFRIEND_H
#include <QObject>
class GirlFriend : public QObject // 必须继承 QObject 或其子类
{
Q_OBJECT // 必须加入 Q_OBJECT 宏,以启用元对象系统
public:
// 规范写法:增加 parent 参数,以便挂载到 Qt 对象树
explicit GirlFriend(QObject *parent = nullptr);
signals:
void hungry(); // 自定义信号:仅声明,不定义。返回类型必须为 void
};
#endif // GIRLFRIEND_H
GirlFriend.cpp
#include "girlfriend.h"
GirlFriend::GirlFriend(QObject *parent) : QObject(parent) {}
Step2:创建 Me 类(定义槽函数)
要点:槽函数本质上就是普通的 C++ 成员函数,必须有声明和完整的实现。
Me.h
#ifndef ME_H
#define ME_H
#include <QObject>
class Me : public QObject // 必须继承 QObject 或其子类
{
Q_OBJECT // 必须加入宏
public:
explicit Me(QObject *parent = nullptr);
public slots:
void eat(); // 槽函数:需要声明 + 定义
};
#endif // ME_H
Me.cpp
#include "me.h"
#include <QDebug>
Me::Me(QObject *parent) : QObject(parent) {}
// 槽函数的具体实现
void Me::eat() { qDebug() << "带女朋友吃饭"; }
Step3:配置 UI 界面
在 mainwindow.ui 中拖入一个 QPushButton 控件,并将其 objectName 命名为 hungryBtn。
Step4:在主窗口中实例化并注册信号与槽
要点:主窗口作为中介,管理 GirlFriend 和 Me 的生命周期,并使用 Qt5 的函数指针语法连接它们。
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "girlfriend.h"
#include "me.h"
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
public slots:
// 自定义槽函数:用于触发女朋友的信号
void girlSayHungry();
private:
Ui::MainWindow *ui;
// 在主窗口中声明指向外部类的指针
GirlFriend* _girl;
Me* _me;
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
// 实例化对象,并传入 this 指针挂载到 MainWindow 的对象树上,防止内存泄漏
_girl = new GirlFriend(this);
_me = new Me(this);
// 1. 注册按钮与主窗口的信号-槽(UI 触发中介)
connect(ui->hungryBtn, &QPushButton::clicked, this, &MainWindow::girlSayHungry);
// 2. 注册业务逻辑的信号-槽(中介触发实际动作)
connect(_girl, &GirlFriend::hungry, _me, &Me::eat);
}
MainWindow::~MainWindow()
{
delete ui;
// 注意:_girl 和 _me 因为传入了 this 作为 parent,会由 Qt 对象树自动回收,此处无需手动 delete
}
// 槽函数实现:主动发射自定义信号
void MainWindow::girlSayHungry()
{
// emit 是一个空宏,主要作用是提高代码可读性,告知开发者这里正在发射信号
emit _girl->hungry();
}
4. 信号槽的使用与连接机制进阶
4.1 信号槽使用的补充
4.1.1 一个信号可以连接多个槽函数
- 一个信号可以有多个不同的处理动作,此时需要编写多个
connect()进行连接。 - 信号的接收者可以是同一个对象,也可以是多个不同的对象。
- 执行顺序问题: 早期 Qt 版本中槽函数的执行顺序是随机的。但从 Qt 4.6 版本开始,如果一个信号连接了多个槽,槽函数的执行顺序与
connect的调用顺序严格一致(由底层的连接链表维护机制决定)。。
示例代码:
场景:女朋友发出“肚子饿”信号,我不仅要“带她吃饭”,还要“让女朋友多喝热水”
// 1. 接收者:Me 类中增加了 drink() 槽函数
// 1.1 Me.h
void Me::drink();
// 1.2 Me.cpp
void Me::drink() { qDebug() << "让女朋友多喝热水"; }
// 2. 在 MainWindow 构造函数中进行连接
// 执行顺序逻辑:
// 若 Qt版本 > 4.6:先执行 eat(),再执行 drink(),与 connect 声明顺序一致。
// 若 Qt版本 <= 4.6:执行顺序随机。
connect(_girl, &GirlFriend::hungry, _me, &Me::eat);
connect(_girl, &GirlFriend::hungry, _me, &Me::drink);
4.1.2 一个槽函数可以连接多个信号
-
多个不同的信号可以触发同一个处理动作,即复用同一个槽函数逻辑。
-
同样需要编写多个
connect()函数将不同的信号指向同一个槽。
示例代码:
场景:界面上有“肚子痛”和“发烧”,点击任何一个,我都执行“让女朋友多喝热水”的动作
// 0. 前置准备:在 UI 界面中增加两个 QPushButton,命名为 stomachacheBtn 和 feverBtn
// 1. 在GirlFriend.h中声明“肚子疼”和“发烧”两个信号
signals:
void stomachache();
void fever();
// 2. 在 MainWindow 中声明+定义相对应的两个中转槽函数,点击按钮时调用“肚子疼”和“发烧”两个信号函数
// 2.1 mainwindow.h:
public slots:
void girlStomachache();
void girlFever();
// 2.2 mainwindow.cpp:
void MainWindow::girlStomachache() { emit _girl->stomachache(); }
void MainWindow::girlFever() { emit _girl->fever(); }
// 3. 在mainwindow.cpp的构造函数中注册4对信号槽
// 3.1 肚子痛的2对信号槽
connect(ui->stomachacheBtn, &QPushButton::clicked, this, &MainWindow::girlStomachache);
connect(_girl, &GirlFriend::stomachache, _me, &Me::drink);
// 3.2 发烧的2对信号槽
connect(ui->feverBtn, &QPushButton::clicked, this, &MainWindow::girlFever);
connect(_girl, &GirlFriend::fever, _me, &Me::drink);
4.1.3 信号可以连接信号
信号的接收者可以不执行具体的业务逻辑处理(不调用槽函数),而是直接将信号“转发”出去。
这种机制本质上是事件的透传,非常适合在多层嵌套的 UI 组件或复杂的对象树中传递事件,降低模块之间的耦合度。
语法规范:
connect(const QObject *sender, &Sender::signal1,
const QObject *receiver, &Receiver::signal2);
示例代码:
将上述 4.1.2 中 MainWindow 的中转槽函数删去,点击按钮后,直接将 UI 控件的点击信号转发给 _girl 对象对应的信号。
mainwindow.cpp中的构造函数截选:
// 肚子饿的相关信号槽(UI 信号直连 对象信号)
connect(ui->hungryBtn, &QPushButton::clicked, _girl, &GirlFriend::hungry);
connect(_girl, &GirlFriend::hungry, _me, &Me::eat);
connect(_girl, &GirlFriend::hungry, _me, &Me::drink);
// 肚子疼的相关信号槽(UI 信号直连 对象信号)
connect(ui->stomachacheBtn, &QPushButton::clicked, _girl, &GirlFriend::stomachache);
connect(_girl, &GirlFriend::stomachache, _me, &Me::drink);
// 发烧的相关信号槽(UI 信号直连 对象信号)
connect(ui->feverBtn, &QPushButton::clicked, _girl, &GirlFriend::fever);
connect(_girl, &GirlFriend::fever, _me, &Me::drink);
4.1.4 信号槽是可以断开的
使用 disconnect() 函数可以随时解除信号与槽的绑定关系。
应用场景: 当对象在生命周期内频繁重建,或者同一个槽函数由于界面刷新被多次重复 connect 时(会导致触发一次信号,槽函数执行多次的 Bug),需要在重新连接前或满足特定业务条件时调用 disconnect()。
语法规范: 参数列表与 connect() 完全一致。
disconnect(const QObject *sender, &Sender::signal,
const QObject *receiver, &Receiver::method);
示例代码:
场景:增加“女朋友生气”按钮,点击该按钮后,即便再次点击“肚子饿”按钮,也不再触发女朋友发出“肚子饿”的信号。
// 0. 前置准备:在 UI 中添加名为 girlAngryBtn 的 QPushButton
// 1. 在MainWindow中添加girlAngry()的槽函数,在这个槽函数中使用disconnect()
// 1.1 mainwindow.h
public slots:
void girlAngry();
// 1.2 mainwindow.cpp
void MainWindow::girlAngry()
{
// 精准断开指定的信号槽连接
disconnect(ui->hungryBtn, &QPushButton::clicked, _girl, &GirlFriend::hungry);
}
// 2. 在 MainWindow 的构造函数中注册“生气”的控制槽
// 逻辑:点击生气按钮 -> 执行 girlAngry 槽函数 -> 断开“按钮点击发出肚子饿信号”的链路
connect(ui->girlAngryBtn, &QPushButton::clicked, this, &MainWindow::girlAngry);
4.2 信号槽的连接方式
4.2.1 Qt4 中信号槽的连接方式(宏连接)
1. 语法规范与底层机制
在 Qt4 中,信号与槽的连接主要依赖 SIGNAL() 和 SLOT() 这两个宏。
其标准语法如下:
// Qt4 信号槽连接的标准原型
[static] QMetaObject::Connection QObject::connect(
const QObject *sender, const char *signal,
const QObject *receiver, const char *method,
Qt::ConnectionType type = Qt::AutoConnection);
// 实际使用时的写法
connect(const QObject *sender, SIGNAL(信号函数名(参数类型1, 参数类型2, ...)),
const QObject *receiver, SLOT(槽函数名(参数类型1, 参数类型2, ...)));
底层机制:SIGNAL 和 SLOT 宏的作用,是在预编译阶段将括号内的函数签名转换为纯字符串(例如转换为 "clicked(bool)")。
2. Qt4 连接方式的三大缺陷(⚠️ 面试重点,强烈建议 Qt5 以后弃用此方式)
-
无编译期类型检查(最致命缺点):
因为宏只是机械地将其转换成字符串,编译器不会检查函数名拼写是否正确,也不会检查参数类型是否匹配。如果写错了,编译完全通过不报错,只有在程序运行起来后,才会在控制台输出警告,导致 Bug 极难被发现和定位。
-
强制关键字要求:
在 Qt4 时代,声明槽函数必须写在
public slots:/private slots:等关键字下方,不能省略。而在 Qt5 以后,只要符合签名的普通成员函数即可作为槽。 -
无法在连接时绑定实参:
使用宏的方式只能写类型签名,不能硬编码传值(即绝不能写成
SLOT(eat("Ramen")))。如果信号本身没有参数,而你希望槽函数执行时带上特定的参数,只能通过在当前类中新建一个“无参中转槽函数”来手动调用带参函数。
3. 示例代码:Qt4 方式的实际应用与传参困境
场景说明:
- 触发场景 1:点击
hungryButton,输出 "I have to eat something."。 - 触发场景 2:点击
hungryRamenButton,希望输出 "I want to eat Ramen!"。 - 角色说明:
Me类作为接收者,提供了两个重载的槽函数:eat()和eat(const QString&)。
Step 0:准备 UI 界面
在 mainwindow.ui 中创建两个 QPushButton,分别命名为:hungryBtn 和 hungryRamenBtn。
Step 1:声明定义接收者 Me 类
Me.h:
#ifndef ME_H
#define ME_H
#include <QObject>
class Me : public QObject
{
Q_OBJECT
public:
// 规范:继承 QObject 的类,传入 QObject* parent
explicit Me(QObject* parent = nullptr);
public slots: // Qt4 必须保留 slots 关键字
void eat();
void eat(const QString& food);
};
#endif // ME_H
Me.cpp:
// Me.cpp
#include "me.h"
#include <QDebug>
Me::Me(QObject* parent) : QObject(parent) {}
void Me::eat() { qDebug() << "I have to eat something."; }
void Me::eat(const QString& food)
{
qDebug() << "I want to eat " << food << "!";
}
Step 2:主窗口中的连接与中转方案
由于 Qt4 宏连接无法直接传递 "Ramen" 这种字面量实参,必须通过主窗口充当“中介”:在主窗口中声明定义meEatRamen()作为中转槽,向eat(QString)传递参数。
mainwindow.h:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "me.h"
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
public slots:
// Qt4 宏无法直接绑定实参,被迫新建一个中介槽函数
void meEatRamen();
private:
Ui::MainWindow *ui;
Me* _me;
};
#endif // MAINWINDOW_H
mainwindow.cpp:
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
_me = new Me(this); // 挂载到主窗口对象树上
// 【场景 1】不涉及硬编码传参的 Qt4 宏连接
// 注意:信号带有 bool 参数,槽没有参数是合法的(Qt 允许槽函数参数少于信号)
connect(ui->hungryBtn, SIGNAL(clicked(bool)), _me, SLOT(eat()));
// 【场景 2】涉及自定义传参的 Qt4 宏连接困境
// 错误写法:connect(..., SLOT(eat("Ramen"))); -> 宏不支持字面量,必定失败
// 正确方案:连接到一个无参的中转槽函数
connect(ui->hungryRamenBtn, SIGNAL(clicked(bool)), this, SLOT(meEatRamen()));
}
MainWindow::~MainWindow()
{
delete ui;
}
// 在中转槽函数内部,显式调用业务方法并传递特定参数
void MainWindow::meEatRamen()
{
_me->eat("Ramen");
}
4.2.2 Qt5 中信号槽的连接方式与重载处理
1. Qt5 的基本连接方式(函数地址)
Qt5 摒弃了宏,推荐直接传入函数的内存地址。
- 优势:编译器在编译阶段就会对信号和槽的参数类型、个数以及函数名拼写进行严格的静态检测,极大提高了代码的安全性。
标准语法:
// 语法原型:
QMetaObject::Connection QObject::connect(
const QObject *sender, PointerToMemberFunction signal,
const QObject *receiver, PointerToMemberFunction method,
Qt::ConnectionType type = Qt::AutoConnection);
// 实际使用:第 2、4 个参数传递的是成员函数地址
connect(const QObject *sender, &QObject::signal,
const QObject *receiver, &QObject::method);
2. Qt5 连接方式的局限性与传统解决方案
在使用函数地址进行连接时,会遇到两个主要的使用限制:
- 无法处理重载:如果目标函数存在重载,直接传入函数地址会让编译器产生歧义,直接报错。
- 无法自定义传参:标准的函数地址绑定无法在连接时硬编码传入特定的实参。
传统解决方案:
- 使用函数指针:通过明确指出带有特定参数类型的函数指针,来消除重载带来的歧义。
- 使用中转槽函数:新建一个无参的槽函数进行连接,并在该中转槽函数内部手动调用目标函数以实现传参。
示例代码: (只需修改 4.1 中的 mainwindow.cpp)
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
_me = new Me(this);
// ❌ 报错:有重载函数时,直接使用 Qt5 函数地址的方式会导致编译器歧义!
// connect(ui->hungryBtn, &QPushButton::clicked, _me, &Me::eat);
// ✅ 解决重载:使用明确类型的函数指针
void (Me::*pMemFunc1)() = &Me::eat;
connect(ui->hungryBtn, &QPushButton::clicked, _me, pMemFunc1);
// ✅ 解决重载 + 传参:使用中转槽 + 函数指针(或直接取中转槽地址)
// 说明:Qt5 单靠函数地址或函数指针均无法传递额外参数,只能连接到另一个无参槽函数
void (MainWindow::*pMemFunc2)() = &MainWindow::meEatRamen;
connect(ui->hungryRamenBtn, &QPushButton::clicked, this, pMemFunc2);
}
MainWindow::~MainWindow()
{
delete ui;
}
// 必须新建的中转槽函数:在这个槽函数中显式调用目标函数并传入具体参数
void MainWindow::meEatRamen()
{
_me->eat("Ramen");
}
3. 补充:处理重载的现代解决方案
除了传统地手写函数指针外,Qt 提供了更为优雅的模板辅助工具,但注意:这些工具仅能解决“重载歧义”问题,依然无法解决“传参”问题。
① 使用 QOverload<参数类型>::of(函数地址) (要求 Qt 5.7 及以上版本)
当类中声明了同名的重载槽函数时(例如 eat() 和 eat(QString)),QOverload 的作用就是让你在尖括号 < > 中显式填入目标参数的类型,从而帮编译器完成精准的重载决议。若目标函数无参,< > 中留空即可(即 <>)。
标准语法:
// 语法原型 (底层基于 C++ 模板):
template <typename... Args>
static constexpr auto QOverload<Args...>::of(PointerToMemberFunction functionPointer);
// 实际使用:第 2 或 第 4 个参数传递被 QOverload 包裹的函数地址
// 示例:匹配带 int 和 QString 参数的信号与槽
connect(const QObject *sender, QOverload<int, QString>::of(&QObject::signal),
const QObject *receiver, QOverload<int, QString>::of(&QObject::method));
② 使用 qOverload<参数类型>(函数地址) (要求支持 C++14 编译器)
在 VS2019 等默认支持 C++14 标准的编译环境下,Qt 利用 C++14 的新特性提供了一个更简洁的全局辅助函数 qOverload(注意首字母是小写的 q)。
它省去了调用静态方法 ::of 的繁琐,写起来更像一个原生的类型转换函数,同样地,如果没有参数,< > 中留空即可。这是目前官方最推荐的处理重载的写法。
标准语法:
// 语法原型 (底层基于 C++14 的 constexpr 特性):
template <typename... Args>
constexpr auto qOverload(PointerToMemberFunction functionPointer);
// 实际使用:第 2 或 第 4 个参数传递被 qOverload 包裹的函数地址
// 示例:匹配带一个 QString 参数的信号与槽
connect(const QObject *sender, qOverload<QString>(&QObject::signal),
const QObject *receiver, qOverload<QString>(&QObject::method));
补充:能同时完美解决 “重载解析 + 传参” 问题:Lambda表达式
5. Lambda 表达式(C++11 核心特性)
Lambda 表达式是 C++11 最重要也是最常用的特性之一。它本质上是一个匿名函数,能够让代码更加简洁、内聚,并且使程序结构更加灵活。
Qt 框架完全支持现代 C++ 语法,因此在 Qt 中使用 Lambda 表达式,是目前解决信号槽重载与传参问题的最佳解决方案。
5.1 Lambda表达式的语法格式
语法模板如下:
[capture](params) opt -> ret
{
body;
};
-
capture (捕获列表):捕获外部变量供函数体使用。
-
params (参数列表):与普通函数一致,没有参数时可省略
()。 -
opt (函数选项):可选修饰符(如
mutable、noexcept)。 -
ret (返回值类型):
-> 类型。当返回值为void或能被编译器自动推导时,可省略。 -
body (函数体):核心逻辑实现,不能为空,但内部可以是空语句。
【核心考点】捕获列表速查表:
| 捕获符号 | 机制说明 |
|---|---|
[] |
不捕获任何外部变量。 |
[=] |
按值捕获外部作用域中的所有变量(作为副本在函数体内使用)。 注意:默认情况下,拷贝进来的副本在函数体内是只读 (const) 的。 |
[&] |
按引用捕获外部作用域中的所有变量(直接操作原变量)。 |
[=, &var1] |
按值捕获所有变量,但 var1 按引用捕获。 |
[var2] |
仅按值捕获 var2,不捕获其他变量。 |
[&var3] |
仅按引用捕获 var3,不捕获其他变量。 |
[this] |
捕获当前类的 this 指针。让 Lambda 拥有和当前类成员函数完全相同的访问权限。 注:如果已经使用了 & 或 =,编译器会默认隐式捕获 this。 |
关于 opt (函数选项) 的补充:
mutable:取消按值捕获(=)副本的只读属性,允许在 Lambda 内部修改这些副本。(重点:修改的仅仅是拷贝到内部的副本,对外部的原变量毫无影响)。- 异常说明:在现代 C++ 中,通常使用
noexcept替代throw()来声明该 Lambda 不会抛出任何异常。
5.2 Lambda 表达式的定义与调用
因为 Lambda 表达式是匿名的,如果仅仅定义它,程序执行时是不会调用它的。要想执行,必须在定义后加上 () 立即调用,或者将其赋值给一个变量(如 auto 接收)后续调用。
基础定义与立即调用:
// 1. 仅仅是定义,不会被执行
[]()
{
qDebug() << "hello, 我是一个未被调用的 lambda 表达式...";
};
// 2. 定义 + 立即调用 (注意末尾的 `(100)`)
int ret = [](int a) -> int
{
return a + 1;
}(100); // 100 是传递给形参 a 的数据
捕获列表示例代码:
int a = 100, b = 200, c = 300;
// 1. 错误示范:空捕获
/*
[]()
{
// error: 'a' is not captured. 空捕获不能使用任何外部变量
qDebug() << "a:" << a;
}();
*/
// 2. 按引用捕获
[&]()
{
qDebug() << "使用引用的方式传递数据: ";
// 直接修改外部原变量
qDebug() << "a+1:" << a++ << ", b+c= " << b + c;
}();
// 3. 按值捕获 + mutable
[=](int m, int n) mutable
{
qDebug() << "使用拷贝的方式传递数据: ";
// 如果没有 mutable,下面修改 a 的语句会直接编译报错
// 这里的 a 只是副本,a++ 不会影响外部真正的 a
qDebug() << "a+1:" << a++ << ", b+c= " << b + c;
qDebug() << "m+1: " << ++m << ", n: " << n;
}(1, 2);
5.3 终极杀器:在 Qt5 中通过 Lambda 表达式传参
结合上述知识,Qt5 中信号槽最优雅的写法诞生了。Lambda 表达式不仅完美绕过了重载函数的指针识别问题,还彻底消灭了毫无意义的“中转槽函数”。
示例代码(仅修改 mainwindow.cpp):
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
_me = new Me(this);
// 有重载函数直接使用Qt5函数地址的方式会报错!
// connect(ui->hungryBtn, &QPushButton::clicked, _me, &Me::eat);
// 涉及重载的Qt5方式,使用Lambda表达式,捕获 this 指针
connect(ui->hungryBtn, &QPushButton::clicked, this, [this](){_me->eat();});
// 涉及重载和传参的Qt5方式,使用Lambda表达式,捕获this指针
connect(ui->hungryRamenBtn, &QPushButton::clicked, this, [this](){_me->eat("Ramen");});
}
MainWindow::~MainWindow()
{
delete ui;
}
//说明:用不上了,但是懒得删才留在这里
void MainWindow::meEatRamen()
{
_me->eat("Ramen");
}

浙公网安备 33010602011771号