[QT5] 信号与槽: 认识信号与槽, 认识connect, 自定义信号和槽...
Table of Contents

之前的文章中, 通过PushButton这个控件简单见了一下信号

实现了通过点击PushButton改变按钮上显示的文本

那么QT中, 究竟什么是信号, 什么是槽?

信号和槽 Link to 信号和槽

Linux系统中, 系统可以向进程发送信号, 进程收到信号之后, 进程默认以不同的形式终止

QT中的信号与Linux中的信号没有任何关系, 但是使用非常相似

什么是信号、槽 Link to 什么是信号、槽

用比较简单的话来介绍:

QT的信号(signal), 是由QObject的各种派生类对象产生的, 一般表示对象发生了某种事情, 以类的成员函数的形式存在, 但一般不实现函数体

比如, 在QPushButton对象被点击时, 会产生一个clicked信号, 表示对象被点击了

QT的槽(slot), 是在特定信号发生后, 对此信号要执行的处理函数, 同样以类的成员函数的形式存在

如果使用connect()将对象的特定信号与槽函数连接起来, 当特定对象产生特定信号时, 与此信号连接的槽函数就会被调用执行

所以槽函数其实是一种回调函数, 信号与槽机制实际是QT中的一种对象间通信的机制


connect() ** Link to connect() **

connect()可以将对象的信号与信号处理槽函数连接起来, 接口具体长这样:

CPP
1
2
3
4
static QMetaObject::Connection connect(
    const QObject *sender, const char *signal,
    const QObject *receiver, const char *member, 
    Qt::ConnectionType = Qt::AutoConnection);

但是下面这样使用connect()也能将信号与槽连接起来:

创建一个最基本的QWidget项目, 修改widget.cppWidget构造函数的内容:

CPP
1
2
3
4
5
6
7
8
9
10
Widget::Widget(QWidget* parent)
    : QWidget(parent)
    , ui(new Ui::Widget) {
    ui->setupUi(this);

    btn = new QPushButton(this);
    btn->setText("hello close");

    connect(btn, &QPushButton::clicked, this, &Widget::close);
}

在界面中添加一个QPushButton控件, 将此按钮的clicked信号 与 Widegtclose槽函数连接起来, 可以实现以下效果:

|huger

connect()这个函数来自于QObject类, 是QObject类的成员函数

QObject类是非常多QT类的祖先基类, 包括QWidget:

|lwide

connect()是它的成员函数, 所以可以在Widget构造函数中直接调用

connect()有四个参数非缺省参数:

  1. const QObject *sender

    第一个参数, 需要传入一个QObject对象指针

    表示可以发出特定信号的对象, 即 信号的发送者

  2. const char *signal

    第二个参数, 需要传入一个char*对象

    表示信号发送者 发送的信号

  3. const QObject *receiver

    第三个参数, 需要传入一个QObject对象指针

    表示发送者 发送的特定信号 的接收者, 即 信号的接收者

  4. const char *member

    第四个参数, 需要传入一个char*对象

    表示信号接收者拥有的槽函数, 即 信号处理函数

第一和第三个参数, 不用过多说明 传入的是 发送信号的控件对象和接收信号的控件对象

而第二和第四个参数, 需要传入信号槽函数, 但是类型并不是函数指针, 而是char*

QPushButton::clickedWidget::close实际是什么类型呢?

|lwide

QPushButton::clicked()Widget::close()实际就是函数

函数指针和char*类型是禁止隐式转换的, 除了都是指针, 是两个完全不相干的东西

但为什么connect()对应的的参数却不是函数指针而是char*类型呢?

实际上, 信号和槽参数类型是const char*connect()QT4及以前版本的接口

QT4中的connect(), 在使用时需要这样使用:

CPP
1
connect(btn, SIGNAL(clicked()), this, SLOT(close()));

第二个参数, 需要借助SIGNAL宏来传参, SIGNAL()的参数不需要指明类, 但要加上()

第四个参数, 需要借助SLOT宏来传参, SLOT的用法与SIGNAL相同

SIGNAL()SLOT()宏会根据传入的参数生成字符串, 进而传入connect()


QT5中, 重载了一个泛型的connect()

QT5中的connect(), 函数原型实际是这样的:

CPP
1
2
3
4
5
6
7
template <typename Func1, typename Func2>
static inline QMetaObject::Connection connect(
    const typename QtPrivate::FunctionPointer<Func1>::Object *sender,
    Func1 signal,
    const typename QtPrivate::FunctionPointer<Func2>::Object *receiver,
    Func2 slot,
    Qt::ConnectionType type = Qt::AutoConnection);

更乱了, 但还是可以理解一下:

  1. 模板声明: template <typename Func1, typename Func2>

    即, 此函数两个模板参数Func1Func2

  2. const typename QtPrivate::FunctionPointer<Func1>::Object *sender

    第一个参数, 需要传入信号发送者对象

    参数类型很长

  3. Func1 signal

    第二个参数, 需要传入信号

    信号类型, 即为 模板参数Func1的类型

  4. const typename QtPrivate::FunctionPointer<Func2>::Object *receiver

    第三个参数, 需要传入信号接收者对象

    参数类型很长

  5. Func2 slot

    第四个参数, 需要传入槽函数

    槽函数类型, 即为 模板参数Func2的类型

冷静的分析一下参数类型, 可以发现在正常使用connect()时, Func1Func2这两个类型是确定的

就是传入的信号和槽的类型

QtPrivate::FunctionPointer< Func1/2 >::Object就能根据Func1Func2, 萃取出 传入的信号发送者和信号接收者的原类型(目前不需要太过关注这个过程)

这样, 就能实现信号和槽的连接

connect()的返回值是QMetaObject::Connection类型, 可以表示连接的句柄, 可以通过这个句柄断开连接

connect()的使用需要注意的一个点是: 调用时传参, 需要保证参数2确实是参数1的信号, 参数4确实是参数3拥有的槽

槽和信号的自定义 Link to 槽和信号的自定义

直接或间接继承自QObject的类, 都默认拥有一些信号和槽

除此之外, 信号和槽也可以自定义实现

自定义槽 Link to 自定义槽

槽函数的自定义途径, 不仅仅只有一种

途径1: 完全通过代码 Link to 途径1: 完全通过代码

QT4之前, 自定义槽需要将槽函数声明在slots关键词下:

CPP
1
2
3
4
5
6
7
8
9
10
11
12
13
class Widget : public QWidget {
    Q_OBJECT

public:
    Widget(QWidget* parent = nullptr);
    ~Widget();

private slots:
    void aSlotFunc();

private:
    Ui::Widget* ui;
};

|huge

只有声明在slots下的函数, 才是槽

slotsQT自己扩展的关键词, 与C++标准库无关

而在QT5之后, 自定义槽函数就不需要在slots关键字下声明了:

|huge

完成函数的定义之后, 就可以当作一个正常的槽使用了:

widget.cc:

CPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "widget.h"
#include "ui_widget.h"

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

    btn = new QPushButton(this);
    btn->setText("按钮");
    btn->move(300, 270);
    btn->setFixedSize(200, 30);

    connect(btn, &QPushButton::clicked, this, &Widget::aSlotFunc);
}

Widget::~Widget() {
    delete ui;
}

void Widget::aSlotFunc() {
    btn->setText("按钮已经被点击");
    this->setWindowTitle("自定义槽");
}

widget.h:

CPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QPushButton>

QT_BEGIN_NAMESPACE
namespace Ui {
    class Widget;
}
QT_END_NAMESPACE

class Widget : public QWidget {
    Q_OBJECT

public:
    Widget(QWidget* parent = nullptr);
    ~Widget();

    void aSlotFunc();

private:
    Ui::Widget* ui;
    QPushButton* btn;
};

#endif // WIDGET_H

运行此程序:

|huger

途径2: 图形化创建槽函数 Link to 途径2: 图形化创建槽函数

QT不仅能通过代码创建控件, 还能直接通过图形化的方法添加控件

自然能够通过图形化的方法, 添加自定义槽

先通过Designer添加PushButton控件

右键PushButton控件, 选择转到槽

然后可以选择控件继承树中的所有信号:

选择clicked()信号之后, QT Creator就会自动跳转到widget.cc中并创建槽函数定义, 同时Widget类中也会声明好相同的槽函数:

对此槽函数实现与上面相同的功能, 并运行:

CPP
1
2
3
4
void Widget::on_pushButton_clicked() {
    ui->pushButton->setText("按钮已经被点击");
    this->setWindowTitle("图形化自定义槽函数");
}

|huger

从执行结果来看, 可以正常运行

但是, 代码中并没有调用connect()

明明没有通过connect()连接信号和槽, 为什么点击按钮 还是能够正确的执行槽函数呢?

答案就在QT Creator自动创建的槽函数上

通过图形化的方法自动创建的槽函数, 函数名默认有一定的规则: on_pushButton_clicked()

观察这个槽函数名可以发现它其实是由3部分组成的: on_<objectName>_<signal>()

即, 当槽函数名以上面这样的规则定义时, 可以不通过connect()实现信号与槽的连接, 而是自动的通过草函数名与特定的信号建立连接

当然, 并不是只定义好槽函数就能实现了, 还需要调用另外一个函数:

根据.ui文件自动生成的UI_Widget类中, 调用了**QMetaObject::connectSlotsByName(Widget);**

这个调用, 可以让整个Widget对象树上的所有控件通过特定规则的槽函数名和特定信号自动连接, 而不用手动调用connect()

只有槽函数名遵循on_<objectName>_<signal>()这个规则时, 才能实现通过槽函数名与信号自动连接

自定义信号 Link to 自定义信号

自定义槽函数时, QT4及以前必须将槽定义在slots关键字下, QT5之后才不需要

但是自定义信号, 就必须使用一个关键词signals

即, 自定义信号必须要声明在signals关键字下

widget.h:

CPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QPushButton>

QT_BEGIN_NAMESPACE
namespace Ui {
    class Widget;
}
QT_END_NAMESPACE

class Widget : public QWidget {
    Q_OBJECT

public:
    Widget(QWidget* parent = nullptr);
    ~Widget();

signals:
    void diySignal();	// 自定义信号

private slots:
    void on_pushButton_clicked();
    void diySignalHandler();

private:
    Ui::Widget* ui;
};

#endif // WIDGET_H

widget.cc:

CPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "widget.h"
#include "ui_widget.h"

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

    connect(this, &Widget::diySignal, this, &Widget::diySignalHandler);
}

Widget::~Widget() {
    delete ui;
}

// 按钮被点击槽, 发送自定义信号
void Widget::on_pushButton_clicked() {
    ui->pushButton->setText("按钮已被点击, 槽发送diySignal");
    emit diySignal();
}
// 自定义信号的槽
void Widget::diySignalHandler() {
    this->setWindowTitle("diySignal被接收到, 并执行槽函数");
}

这段代码的执行结果为:

|huger

signals:关键字下定义了diySignal信号, 并定义了此信号的槽, 并建立连接

通过emit关键字发送了diySignal信号, 能够实现对信号的处理

emit是发送信号的关键字

不过, 在QT5之后 不使用emit直接调用信号, 也能实现信号的发送

带参的信号和槽 Link to 带参的信号和槽

信号和槽是可以携带参数的

当信号携带参数被发出时, 与其链接的槽函数是可以捕捉信号携带的参数的

举个例子:

widget.h

CPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QPushButton>

QT_BEGIN_NAMESPACE
namespace Ui {
    class Widget;
}
QT_END_NAMESPACE

class Widget : public QWidget {
    Q_OBJECT

public:
    Widget(QWidget* parent = nullptr);
    ~Widget();

signals:
    void diySignal(int, const QString&); 	// 带参数的信号

private slots:
    void on_pushButton_clicked();
    void diySignalHandler(int, const QString&);	// 带参数的槽

private:
    Ui::Widget* ui;
};

#endif // WIDGET_H

widget.cc

CPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "widget.h"
#include "ui_widget.h"

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

    connect(this, &Widget::diySignal, this, &Widget::diySignalHandler);
}

Widget::~Widget() {
    delete ui;
}

void Widget::on_pushButton_clicked() {
    ui->pushButton->setText("按钮已被点击, 槽发送diySignal");
    // 按钮点击信号的槽函数, 发送自定义信号
    emit diySignal(20, "diySignal携带一个标题参数");
}

void Widget::diySignalHandler(int num, const QString& title) {
    this->setWindowTitle(title);
    qDebug("Receive type(int) data: %d\n", num);
}

定义了参数为const QString&的信号和槽

|huger

并在发出信号时传参:

|huger

这段代码的执行结果为:

槽函数能够捕捉信号携带的参数

信号与槽的带参规则 Link to 信号与槽的带参规则

信号与槽的定义都可以带参, 并且 槽能够捕捉信号的参数

不过, 带参的规则是 信号与槽的对应顺序参数类型必须一致, 且信号参数的个数需小于等于槽参数个数

信号与槽的对应参数类型必须一致, 即 如果信号的参数的类型是const QString&, int, 那么 对应槽的参数类型也应该是const QString&, int, 而不能是int, const QString&, 否则编译不通过

将上述例子中, 槽的参数 交换顺序

CPP
1
2
3
4
5
6
void diySignalHandler(const QString&, int);

void Widget::diySignalHandler(const QString& title, int num) {
    this->setWindowTitle(title);
    qDebug("Receive type(int) data: %d\n", num);
}

再试图编译运行:

编译不通过

所以, 信号与槽的对应顺序参数类型必须一致

除此之外, 信号与槽的参数个数是不需要完全一致的, 槽的参数个数不能大于信号的参数个数

即, 如果信号存在两个参数, 那么槽最多能够拥有两个参数, 一个参数也可以:

widget.h:

CPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QPushButton>

QT_BEGIN_NAMESPACE
namespace Ui {
    class Widget;
}
QT_END_NAMESPACE

class Widget : public QWidget {
    Q_OBJECT

public:
    Widget(QWidget* parent = nullptr);
    ~Widget();

signals:
    void diySignal(int, const QString&); 	// 一个自定义带参信号

private slots:
    void on_pushButton_clicked();
    void diySignalHandler1(int num, const QString&);	// 两个参数的槽
    void diySignalHandler2(int num);	// 一个参数的槽

private:
    Ui::Widget* ui;
};

#endif // WIDGET_H

widget.cc:

CPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "widget.h"
#include "ui_widget.h"

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

    // 一个信号连接两个槽 
    connect(this, &Widget::diySignal, this, &Widget::diySignalHandler1);
    connect(this, &Widget::diySignal, this, &Widget::diySignalHandler2);
}

Widget::~Widget() {
    delete ui;
}

void Widget::on_pushButton_clicked() {
    ui->pushButton->setText("按钮已被点击, 槽发送diySignal");
    // 按钮点击信号的槽函数, 发送自定义信号
    emit diySignal(20, "diySignal携带一个标题参数"); // 发出两个参数的信号
}

void Widget::diySignalHandler1(int num, const QString& title) {
    this->setWindowTitle(title);
    qDebug("Handler1 receive type(int) data: %d\n", num);
}

void Widget::diySignalHandler2(int num) {
    qDebug("Handler2 receive type(int) data: %d\n", num);
}

这段代码的执行结果是:

可以看到, 两个槽都能够成功接收并处理信号

diySignal的参数有两个int, const QString&, 但是有一个槽的参数为int

这个槽也能够与diySignal连接, 接收并处理发出的diySignal信号


上面的结果显示:

信号与槽的对应顺序参数类型必须一致, 且信号参数的个数需小于等于槽参数个数

disconnect() Link to disconnect()

已经了解了connect()的用法, 从disconnect()函数名上来看, 就已经可以知道它的用途

disconnect()可以让信号与槽之间建立的连接断开, 比较常用的disconnect()的函数原型为:

CPP
1
2
3
4
5
6
7
bool disconnect(const QMetaObject::Connection &);

template <typename Func1, typename Func2>
bool disconnect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender,
                Func1 signal,
                const typename QtPrivate::FunctionPointer<Func2>::Object *receiver,
                Func2 slot);

只有一个参数的disconnect(const QMetaObject::Connection &)

它的参数应该传入的, 其实是connect的返回值

connect()的原型是这样的:

CPP
1
static QMetaObject::Connection connect(****);

它的返回值, 就是建立的信号与槽的句柄

将句柄传入disconnect(), 当然就能实现信号与槽连接的断开

另外一个, 就与connect()用法相似了, 需要传入信号发送者、信号、信号接收者、接收者拥有的槽

能够完成信号与槽之间连接的断开

举个简单的例子就能理解:

先通过图形化方式, 创建两个按钮, 并图形化创建 两个按钮clicked信号的槽

widget.h:

CPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#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 aSlot();

    void on_pushButton_clicked();

    void on_pushButton_2_clicked();

private:
    Ui::Widget* ui;
};
#endif // WIDGET_H

widget.cc:

CPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#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::aSlot() {
    ui->pushButton->setText("在按钮2被按下之后, 按钮1再被按下");
    this->setWindowTitle("按钮1的clicked信号连接的槽, 已成功被修改为aSlot()");
}

void Widget::on_pushButton_clicked() {
    // 按钮1的clicked信号的槽
    ui->pushButton->setText("按钮1已被按下");
    this->setWindowTitle("按钮1已被按下, 标题被修改");
}

void Widget::on_pushButton_2_clicked() {
    // 按钮2的clicked信号的槽
    ui->pushButton_2->setText("按钮2已被按下, 修改按钮1 clicked信号连接的槽");
    // 断开按钮1与原槽的连接
    disconnect(ui->pushButton, &QPushButton::clicked, this, &Widget::on_pushButton_clicked);
    // 按钮1与另一个槽建立连接
    connect(ui->pushButton, &QPushButton::clicked, this, &Widget::aSlot);
}

这段代码的执行结果为:

|huger

这个例子, 是通过disconnect()断开连接, 然后再通过connect()建立新的连接

Thanks for reading!

[QT5] 信号与槽: 认识信号与槽, 认识connect, 自定义信号和槽...

Fri Dec 13 2024
3983 字 · 22 分钟