humid1ch blogs

本篇文章

手机用户建议
PC模式 或 横屏
阅读


QT 2024 年 12 月 13 日

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

QT中, 什么信号和槽? connect()有什么作用? 如何自定义槽? 如何自定义信号?
之前的文章中, 通过PushButton这个控件简单见了一下信号
实现了通过点击PushButton改变按钮上显示的文本
那么QT中, 究竟什么是信号, 什么是槽?

信号和槽

Linux系统中, 系统可以向进程发送信号, 进程收到信号之后, 进程默认以不同的形式终止
QT中的信号与Linux中的信号没有任何关系, 但是使用非常相似

什么是信号、槽

用比较简单的话来介绍:
QT的信号(signal), 是由QObject的各种派生类对象产生的, 一般表示对象发生了某种事情, 以类的成员函数的形式存在, 但一般不实现函数体
比如, 在QPushButton对象被点击时, 会产生一个clicked信号, 表示对象被点击了
QT的槽(slot), 是在特定信号发生后, 对此信号要执行的处理函数, 同样以类的成员函数的形式存在
如果使用connect()将对象的特定信号与槽函数连接起来, 当特定对象产生特定信号时, 与此信号连接的槽函数就会被调用执行
所以槽函数其实是一种回调函数, 信号与槽机制实际是QT中的一种对象间通信的机制

connect() **

connect()可以将对象的信号与信号处理槽函数连接起来, 接口具体长这样:
static QMetaObject::Connection connect(
    const QObject *sender, const char *signal,
    const QObject *receiver, const char *member, 
    Qt::ConnectionType = Qt::AutoConnection);
但是下面这样使用connect()也能将信号与槽连接起来:
创建一个最基本的QWidget项目, 修改widget.cppWidget构造函数的内容:
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槽函数连接起来, 可以实现以下效果:
connect()这个函数来自于QObject类, 是QObject类的成员函数
QObject类是非常多QT类的祖先基类, 包括QWidget:
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实际是什么类型呢?
QPushButton::clicked()Widget::close()实际就是函数
函数指针和char*类型是禁止隐式转换的, 除了都是指针, 是两个完全不相干的东西
但为什么connect()对应的的参数却不是函数指针而是char*类型呢?
实际上, 信号和槽参数类型是const char*connect()QT4及以前版本的接口
QT4中的connect(), 在使用时需要这样使用:
connect(btn, SIGNAL(clicked()), this, SLOT(close()));
第二个参数, 需要借助SIGNAL宏来传参, SIGNAL()的参数不需要指明类, 但要加上()
第四个参数, 需要借助SLOT宏来传参, SLOT的用法与SIGNAL相同
SIGNAL()SLOT()宏会根据传入的参数生成字符串, 进而传入connect()

QT5中, 重载了一个泛型的connect()
QT5中的connect(), 函数原型实际是这样的:
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拥有的槽

槽和信号的自定义

直接或间接继承自QObject的类, 都默认拥有一些信号和槽
除此之外, 信号和槽也可以自定义实现

自定义槽

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

途径1: 完全通过代码

QT4之前, 自定义槽需要将槽函数声明在slots关键词下:
class Widget : public QWidget {
    Q_OBJECT

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

private slots:
    void aSlotFunc();

private:
    Ui::Widget* ui;
};
只有声明在slots下的函数, 才是槽

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

而在QT5之后, 自定义槽函数就不需要在slots关键字下声明了:
完成函数的定义之后, 就可以当作一个正常的槽使用了:
widget.cc:
#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:
#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
运行此程序:

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

QT不仅能通过代码创建控件, 还能直接通过图形化的方法添加控件
自然能够通过图形化的方法, 添加自定义槽
先通过Designer添加PushButton控件
右键PushButton控件, 选择转到槽
然后可以选择控件继承树中的所有信号:
选择clicked()信号之后, QT Creator就会自动跳转到widget.cc中并创建槽函数定义, 同时Widget类中也会声明好相同的槽函数:
对此槽函数实现与上面相同的功能, 并运行:
void Widget::on_pushButton_clicked() {
    ui->pushButton->setText("按钮已经被点击");
    this->setWindowTitle("图形化自定义槽函数");
}
从执行结果来看, 可以正常运行
但是, 代码中并没有调用connect()
明明没有通过connect()连接信号和槽, 为什么点击按钮 还是能够正确的执行槽函数呢?
答案就在QT Creator自动创建的槽函数上
通过图形化的方法自动创建的槽函数, 函数名默认有一定的规则: on_pushButton_clicked()
观察这个槽函数名可以发现它其实是由3部分组成的: on_<objectName>_<signal>()
即, 当槽函数名以上面这样的规则定义时, 可以不通过connect()实现信号与槽的连接, 而是自动的通过草函数名与特定的信号建立连接
当然, 并不是只定义好槽函数就能实现了, 还需要调用另外一个函数:
根据.ui文件自动生成的UI_Widget类中, 调用了**QMetaObject::connectSlotsByName(Widget);**
这个调用, 可以让整个Widget对象树上的所有控件通过特定规则的槽函数名和特定信号自动连接, 而不用手动调用connect()
只有槽函数名遵循on_<objectName>_<signal>()这个规则时, 才能实现通过槽函数名与信号自动连接

自定义信号

自定义槽函数时, QT4及以前必须将槽定义在slots关键字下, QT5之后才不需要
但是自定义信号, 就必须使用一个关键词signals
即, 自定义信号必须要声明在signals关键字下
widget.h:
#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:
#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被接收到, 并执行槽函数");
}
这段代码的执行结果为:
signals:关键字下定义了diySignal信号, 并定义了此信号的槽, 并建立连接
通过emit关键字发送了diySignal信号, 能够实现对信号的处理

emit是发送信号的关键字

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

带参的信号和槽

信号和槽是可以携带参数的
当信号携带参数被发出时, 与其链接的槽函数是可以捕捉信号携带的参数的
举个例子:
widget.h
#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
#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&的信号和槽
并在发出信号时传参:
这段代码的执行结果为:
槽函数能够捕捉信号携带的参数

信号与槽的带参规则

信号与槽的定义都可以带参, 并且 槽能够捕捉信号的参数
不过, 带参的规则是 信号与槽的对应顺序参数类型必须一致, 且信号参数的个数需小于等于槽参数个数
信号与槽的对应参数类型必须一致, 即 如果信号的参数的类型是const QString&, int, 那么 对应槽的参数类型也应该是const QString&, int, 而不能是int, const QString&, 否则编译不通过
将上述例子中, 槽的参数 交换顺序
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:
#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:
#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()

已经了解了connect()的用法, 从disconnect()函数名上来看, 就已经可以知道它的用途
disconnect()可以让信号与槽之间建立的连接断开, 比较常用的disconnect()的函数原型为:
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()的原型是这样的:
static QMetaObject::Connection connect(****);
它的返回值, 就是建立的信号与槽的句柄
将句柄传入disconnect(), 当然就能实现信号与槽连接的断开
另外一个, 就与connect()用法相似了, 需要传入信号发送者、信号、信号接收者、接收者拥有的槽
能够完成信号与槽之间连接的断开
举个简单的例子就能理解:
先通过图形化方式, 创建两个按钮, 并图形化创建 两个按钮clicked信号的槽
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 aSlot();

    void on_pushButton_clicked();

    void on_pushButton_2_clicked();

private:
    Ui::Widget* ui;
};
#endif // WIDGET_H
widget.cc:
#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);
}
这段代码的执行结果为:

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

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)

作者: 哈米d1ch 发表日期:2024 年 12 月 13 日