上节课,我们讲解信号和槽,都是Qt框架定义好的。Qt还允许我们自定义信号和槽。
自定义信号和槽的条件:

  • 自定义的类,要继承自QObject
  • 自定义的类,其中要声明一个宏Q_OBJECT

只有满足了这两个条件才可以正常使用信号槽机制

1. 案例演示

接下来,我们通过一个案例,演示自定义信号槽的使用。
案例:长官(Commander)发送一个冲锋(go)的信号,然后士兵(Soldier)执行战斗(fight)的槽函数

1.1 新建项目

新建一个名为CustomSignalAndSlot的项目
qt-base

1.2 Commander类

1.2.1 创建 Commander 类

首先,在左侧项目文件名上右键 -> Add New…
qt-base

然后,指定类名和父类
qt-base

最后,点击完成,即可添加commander.cppcommander.h文件到项目中,并在CMakeLists.txt中引用它们:

  • commander.h
#ifndef COMMANDER_H
#define COMMANDER_H

#include <QObject>

// 1.自定义的类,需要继承自 QObject 类
class Commander : public QObject
{
    // 2.并且添加 Q_OBJECT 宏,才能正常使用 Qt 的信号和槽机制
    Q_OBJECT
public:
    explicit Commander(QObject *parent = nullptr);

// 3.在 signals 后面添加自定义的信号即可
signals:
};

#endif  // COMMANDER_H
  • commander.cpp
#include "commander.h"

Commander::Commander(QObject *parent) : QObject{parent} {
}

1.2.2 添加自定义信号

来到commander.h中,在signals下面添加自定义的信号,如下:

class Commander : public QObject
{
    Q_OBJECT
public:
    explicit Commander(QObject *parent = nullptr);

signals:
    // 1.信号只需声明,无需实现
    // 2.信号返回值为 void
    void go();
};

1.3 Soldier类

1.3.1 创建 Soldier 类

按照同样的方法,添加Soldier类,创建完成之后的soldier.hsoldier.cpp文件,内容如下:

  • soldier.h
#ifndef SOLDIER_H
#define SOLDIER_H

#include <QObject>

class Soldier : public QObject
{
    Q_OBJECT
public:
    explicit Soldier(QObject *parent = nullptr);

signals:
};

#endif  // SOLDIER_H
  • soldier.cpp
#include "soldier.h"

Soldier::Soldier(QObject *parent) : QObject{parent} {
}

1.3.2 添加自定义槽

首先,来到soldier.h中,声明一个fight()的槽函数

class Soldier : public QObject
{
    Q_OBJECT
public:
    explicit Soldier(QObject *parent = nullptr);

signals:

public slots:
    // 通常将槽函数添加到slots后面
    // 这个slots也可以不写。不过建议写上,以指明这是一个槽函数
    // pulic表示槽函数既可以在当前类及其子类的成员函数中调用,也可以在类外部的其它函数(比如`main()`函数)中调用
    void fight();
};

然后,来到soldier.cpp中实现fight槽函数:
可以在槽函数声明处,直接按Alt+Enter快捷键,快速生成函数定义

#include <QDebug>

void Soldier::fight() {
    qDebug() << "fight";
}

1.4 连接信号和槽

信号和槽都已经定义完毕, 接下来就可以进行连接了

首先,来到mywidget.h中,声明两个成员变量:

#include "commander.h"
#include "soldier.h"

class MyWidget : public QWidget
{
private:
    Commander *commander;
    Soldier   *soldier;
};

然后,来到mywidget.cpp中,创建CommanderSoldier类的实例,并建立信号和槽的连接,如下:

MyWidget::MyWidget(QWidget *parent) : QWidget(parent), ui(new Ui::MyWidget) {
    ui->setupUi(this);

    // 1. 创建两个类的实例
    commander = new Commander(this);
    soldier   = new Soldier(this);

    // 2. 建立信号和槽的连接
    connect(commander, &Commander::go, soldier, &Soldier::fight);

    // 3. 发送信号
    // emit 可省略
    /*emit*/ commander->go();
}

此时,在程序执行后,就可以在【Application Output】窗口看到槽函数执行的结果了:
qt-base

1.5. 信号和槽的重载

我们知道,信号和槽的本质就是函数,是函数就可以重载,因此,我们可以重载同名的信号,和重载同名的槽函数。
仍然以CommanderSoldier为例:

首先,在commander.h中添加重载的go信号:

class Commander : public QObject
{
    Q_OBJECT
public:
    explicit Commander(QObject *parent = nullptr);

signals:
    void go();
    void go(QString);
};

然后,在soldier.h中添加重载的fight槽函数:

class Soldier : public QObject
{
    Q_OBJECT
public:
    explicit Soldier(QObject *parent = nullptr);

public slots:
    void fight();
    void fight(QString);
};

然后,在soldier.cpp中实现重载的fight槽函数:

void Soldier::fight(QString s) {
    // qDebug() << "fight for" << s;  // fight for "freedom"

    // qDebug().noquote() 用于禁用字符串输出的自动引号包裹,让输出更干净、可读。
    qDebug().noquote() << "fight for" << s;  // fight for freedom
}

最后,在mywidget.cpp中同时发送重载的两个go信号:

MyWidget::MyWidget(QWidget *parent) : QWidget(parent), ui(new Ui::MyWidget) {
    ui->setupUi(this);

    // 1. 创建两个类的实例
    commander = new Commander(this);
    soldier   = new Soldier(this);

    // 2. 建立信号和槽的连接

    // 会报错:会产生二义性,编译器不知道哪个go? 哪个fight?
    // connect(commander, &Commander::go, soldier, &Soldier::fight);

    // 解决方法:QOverload
    // 它是 Qt5.7 引入的一个模板类,用于解决信号和槽重载时的连接歧义问题,让代码更简洁、可读
    // 基本形式:QOverload<参数类型列表>::of(&类名::函数名)
    connect(commander, QOverload<>::of(&Commander::go), soldier, QOverload<>::of(&Soldier::fight));
    connect(commander, QOverload<QString>::of(&Commander::go), soldier, QOverload<QString>::of(&Soldier::fight));

    // 3. 发送信号
    // emit 可省略
    /*emit*/ commander->go();
    commander->go("freedom");
}

此时执行程序,就可以在【Application Output】窗口看到两个重载的槽函数执行的结果了:
qt-base

3. 信号槽总结

3.1 使用信号和槽的条件

如果要使用信号和槽,需要满足如下两个条件

  • 自定义的类,要继承自QObject
  • 自定义的类,其中要声明一个宏Q_OBJECT

只有满足了这两个条件才可以正常使用信号槽机制。

3.2 信号

  • 无需实现
    信号的本质是函数,并且只需要声明,不需要实现
  • 可以重载
    信号本质是函数,因此可以重载
  • 声明在类头文件的signals域下
  • 返回值类型为void,参数的类型和个数不限
  • 可以使用emit关键字发射,emit也可以省略

3.3 槽

  • 需要实现
    槽的本质是函数,需要实现

  • 可以重载
    槽本质是函数,因此可以重载

  • 槽函数用slots关键字修饰(其实在Qt5及其以后可以省略slots关键字)

  • 返回值
    通常会被忽略,因为,当槽函数是通过信号触发而被调用时,它的返回值是无法被信号发送者接收到的。
    因此,尽管语法上允许槽有返回值,但在信号槽连接的上下文中,这个返回值是没有意义的。
    只有当你像调用普通函数一样直接调用槽函数时,才能获取其返回值。

  • 参数
    槽函数中对应位置的参数类型,必须能与信号传递过来的参数类型相匹配。
    例如,一个发送int类型的信号,不能连接到一个期望接收QString类型参数的槽上。

    信号参数数量可以多于槽:这是信号槽机制灵活性的一个体现。
    如果信号的参数多于槽的参数,那么多余的参数会被槽函数自动忽略。
    例如,一个信号valueChanged(int, QString)可以安全地连接到一个只需要int类型参数的槽onValueChanged(int)上。

    相反,槽参数数量不能多于信号,
    如果槽函数定义的参数比信号传递过来的多,
    那么多出来的参数将无法获得有效值,这会导致未定义行为或编译/运行时错误。因此,这条规则是强制性的。

4. 点赞、获取源码

看到这里的小伙伴,去B站给明王一个【免费的点赞】吧,你的支持,是我持续更新优质内容的动力,感谢~

源码下载地址
链接:https://pan.baidu.com/s/1eBVAuoPWwZ7k7UzbYNNAoQ
提取码: ming