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() 传入的 senderreceiver 两个指针必须已经完成实例化,否则连接将失败。

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 方法。

补充注意事项

  1. 对象访问:必须通过 ui 指针(如 ui->closeBtn)才能访问在 UI 文件中定义并实例化的控件对象。
  2. 函数地址语法要求:在 C++ 中获取函数地址有严格规范。对于非成员函数,func&func 均可正确获取地址且等价;但对于类成员函数,必须显式使用作用域和取地址符(如 &QPushButton::clicked&QWidget::close),直接使用函数名无法获取有效地址。

3. 自定义信号槽的使用

Qt 框架提供的标准信号槽在某些特定场景下无法满足项目需求,因此我们需要设计自定义的信号和槽。无论是标准还是自定义,底层依然是通过 connect() 函数进行连接。

3.0 自定义信号/槽的前提条件

想要在 Qt 中自定义信号与槽,自定义的类必须满足以下条件:

  1. 继承标准类:新类必须从 QObject 或者是 QObject 的子类(如 QWidget、QMainWindow 等)进行派生。
  2. 引入宏定义:在定义类的头文件中,必须加入 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::不能在类外部当作普通函数调用,但依然可以被信号成功触发
  • ⚠️ 核心规则:参数匹配原则

    槽函数的参数个数和类型,严格受制于连接它的信号:

    1. 参数个数:信号函数参数个数 $\ge$ 槽函数参数个数
      • 信号可以传递多余的数据,槽可以选择不接收,但槽不能向信号索要它没有的数据。
    2. 参数类型:槽函数的参数类型必须和信号函数完全相同(指槽需要接收的那部分)

    信号/槽参数匹配示例:

    • 参数个数一样:
      • 信号: 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);(只接收第一个参数)

示例代码:

// ================= 全局函数作为槽 =================
// 写在任何类外部的普通全局函数,只要参数和返回值匹配即可
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),执行流程如下:

  1. 界面操作:用户按下主窗口上的 hungryBtn 按钮。
  2. 第一层传递:按钮触发 clicked 信号,主窗口的槽函数 girlSayHungry() 接收该信号。
  3. 第二层传递:在 girlSayHungry() 中,调用GirlFriend::hungry()函数让 GirlFriend 对象发出 hungry 信号。
  4. 最终响应: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:在主窗口中实例化并注册信号与槽

要点:主窗口作为中介,管理 GirlFriendMe 的生命周期,并使用 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.2MainWindow 的中转槽函数删去,点击按钮后,直接将 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, ...)));

底层机制SIGNALSLOT 宏的作用,是在预编译阶段将括号内的函数签名转换为纯字符串(例如转换为 "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,分别命名为:hungryBtnhungryRamenBtn

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 连接方式的局限性与传统解决方案

在使用函数地址进行连接时,会遇到两个主要的使用限制:

  1. 无法处理重载:如果目标函数存在重载,直接传入函数地址会让编译器产生歧义,直接报错。
  2. 无法自定义传参:标准的函数地址绑定无法在连接时硬编码传入特定的实参。

传统解决方案:

  1. 使用函数指针:通过明确指出带有特定参数类型的函数指针,来消除重载带来的歧义。
  2. 使用中转槽函数:新建一个无参的槽函数进行连接,并在该中转槽函数内部手动调用目标函数以实现传参。

示例代码: (只需修改 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 (函数选项):可选修饰符(如 mutablenoexcept)。

  • 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");
}
posted @ 2026-05-22 21:55  学习笔记草稿存放账号  阅读(14)  评论(0)    收藏  举报