【Qt】深入Qt信号与槽:事件驱动编程的艺术与实践

前言:

在现代软件开发中,图形用户界面(GUI)的设计和实现是至关重要的一环。Qt作为一个跨平台的C++应用程序框架,为开发者提供了强大的工具集来构建具有丰富交互性的GUI应用程序。Qt的核心机制之一就是信号和槽(Signal and Slot),它允许开发者以一种声明式的方式响应用户操作或其他事件,从而实现组件间的通信和事件驱动的编程范式。
本文旨在深入探讨Qt中的信号和槽机制,从基础概念到高级应用,逐步解析这一机制的工作原理、使用方式以及最佳实践。我们将从Linux信号的基础知识入手,对比Qt信号的实现,进而详细介绍如何使用connect函数建立信号与槽之间的连接,自定义槽函数和信号的方法,以及信号和槽传递参数的技巧。此外,文章还将探讨信号和槽在实际开发中的意义,包括它们如何帮助我们降低代码耦合度,以及如何通过disconnect函数和Lambda表达式进一步优化我们的代码。
无论您是Qt的新手还是有一定经验的开发者,本文都旨在提供一个全面的信号和槽使用指南,帮助您更高效地利用Qt进行GUI编程。通过阅读本文,您将获得必要的知识来构建响应灵敏、交互性强的应用程序,并深入理解Qt框架中的这一强大特性。

1. Linux 信号 与 Qt 信号

1.1. Linux 信号

1.1.1. 概念

信号是系统内部的通知机制,常用于进程间通信。
信号源:发出信号的主体。
信号类型:信号的类别,如SIGINTSIGTERM等。
信号处理方式:为信号注册处理函数,在信号触发时自动调用执行

1.1.2. 信号处理

在处理信号前,先准备好信号的处理方式。
信号处理函数可以通过函数指针、Lambda表达式、或其他方式来定义。

1.2. Qt 信号

1.2.2. 概念

Qt中的信号是事件驱动机制的一部分,通常用于GUI应用中。
信号源:发出信号的控件。
信号类型:不同操作触发不同信号,如点击按钮、移动光标等。

1.2.2. 信号处理方式

槽函数:用于处理信号。
使用connect函数将信号与槽关联起来。
当信号触发时,Qt会自动执行关联的槽函数。

点击按钮,触发点击信号。
在框中移动光标,触发移动光标的信号。
勾选一个复选框 选择一个下拉框,都会触发不同的信号。
(咱们写的GUI程序,就是要让用户进行操作。就是要和用户进行交互。这个过程就要关注,用户当前的操作具体是一个什么样的操作)

槽函数实际上是一种回调函数。

最早学到的C语言回调函数:函数指针
1)实现转移表,降低代码的 “圈复杂度”
2)实现回调函数的效果 => qsort
C++中:
1)STL 中,函数对象仿函数 / 仿函数
2)Lamda 表达式
后来在 Linux 中:
1)信号处理函数
2)线程的入口函数
3)epoll 基于回调函数的机制

1.2.3. 注意事项

在触发信号前,需要先关联信号和槽,否则信号无法正确处理。

2. connect 函数

2.1. connect函数概述

connect函数是QObject类提供的静态成员函数。
与Linux TCP socket中建立连接的函数完全不同,只是名字相同而已。

2.2. Qt类的继承关系

Qt中的类存在一定的继承关系,QObject是其他Qt内置类的“祖先”。在这里插入图片描述

QObject 就是其他 Qt 内置的 “祖宗” Java 中也存在类似的设定,Java 所有的类都是继承自Object类。

2.3. connect具体使用方式:

在这里插入图片描述

  • 使用方式示例:
    界面上包含一个按钮,用户点击按钮,则关闭窗口。

所谓的“信号”也是 Qt 中对象,内部提供一些成员函数

在这里插入图片描述

&QPushButton::clicked

connect要求,这两参数是匹配的。 button 的参数类型如果是 QPushButton*
此时,第二个参数的信号必须是QPushButton 内置的信号(父亲的信号),不能是一个其他的类,比如 QLineEdit 的信号。

&QPushButton::click

closeQWidget 内置的槽函数。Widget继承自QWidget,也就是继承了父亲的槽函数。 像是 close
这种槽函数功能已经是人家内部已经实现好了,不需要咱们去关心,具体的作用就是关闭当前窗口/控件。

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

针对 button 进行点击 Widget 就会关闭。

3. 自定义槽函数

3.1. 代码定义

自定义槽函数:所谓的 slot 就是一个普通的成员函数
所谓自定义一个槽函数,操作步骤和自定义一个普通的成员函数没有区别!

// 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();

    void handleClicked();

private:
    Ui::Widget *ui;
};
#endif // WIDGET_H
// widget.cpp
#include "widget.h"
#include "ui_widget.h"

#include <QPushButton>

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

    QPushButton* button = new QPushButton(this);
    button->setText("按钮");
    button->move(100, 100);

    connect(button, &QPushButton::clicked, this, &Widget::handleClicked);
}

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

void Widget::handleClicked()
{
    // 按下按钮,就修改一下窗口标题
    this->setWindowTitle("按钮已经按下!");
}

以前版本的 Qt 中,槽函数必须放到

public/private/protected slots;

此处的slots是Qt自己扩展的关键字.(不是C++标准中的语法)
Qt里广泛使用了元编程 技术。(基于代码,生成代码)
qmake 构建Qt项目的时候,就会调用专门的扫描器,扫描代码中特定的关键字。(slots这种)基于关键字自动生成一大堆相关的代码。

3.2. 通过图形化界面自定义槽的方式:

在这里插入图片描述

在这里插入图片描述
这个串口就列出了 QPushButton 给我们提供的所有信号(还包含了 QPushButton 父类的信号)
点击后声明、定义,Qt Creator 就直接给我们生成好了:

private slots:
    void on_pushButton_clicked();
void Widget::on_pushButton_clicked()
{
	//直接写需要的代码就好
}

在Qt中,除了通过 connect 来连接信号槽之外,还可以通过函数名的方式来自动连接!

void Widget::on_pushButton_clicked()

pushButton: 按钮的objectName。
clicked:信号的名字。
当函数名符合上述规则之后,Qt就能自动的把信号和槽给建立上联系。
在这里插入图片描述

如果我们通过图形化界面创建控件,还是推荐使用这种快速的方式来连接信号槽.
如果我们是通过代码的方式来创建控件,还是得手动connect.(你的代码中没有调用connectSlotsByName)

4. Qt 中自定义信号

自定义槽函数,非常关键,在大部分开发中我们都需要自定义槽函数的。
槽函数,就是用户触发的某给操作
自定义信号比较少见,实际开发中很少会需要自定义信号。
信号就对应到用户的操作。
在 GUI,用户能够进行哪些操作,是可以穷举的,Qt内置的信号,基本上已经覆盖了上述所有可能的用户操作。因此,使用Qt内置的信号,就足以应付大部分的开发场景了。

QWidget:
咱们的Widget 虽然还没有定义任何信号,由于继承自QWidget,和QObject,这俩类里面已经提供了一些信号了,可以直接使用.

所谓的Qt的信号,本质上也就是一个“函数”
Qt 5 以及更高版本中,槽函数和普通的成员函数之间,没啥差别了.

但是,信号,则是一类非常特殊的函数.

  1. 程序员只要写出函数声明,并且告诉Qt,这是一个“信号”即可.
    这个函数的定义,是Qt在编译过程中,自动生成的.(自动生成的过程,程序员无法干预)
    信号在Qt中是特殊的机制.Qt生成的信号函数的实现,要配合Qt框架做很多既定的操作 ~~

  2. 作为信号函数,这个函数的返回值,必须是void.
    有没有参数都可以.甚至也可以支持重载.

signals: 这个也是Qt 自己扩展出来的关键字
qmake 的时候,调用一些代码的的分析/生成工具,扫描到类中包含的 signal 这个关键字的时候,此时,就会自动的把下面的函数声明认定为是信号,并且给这些信号函数自动生成函数定义。

connect(this, &Widget::mySignal, this, &Widget::handleMySignal);

建立连接,不代表信号发出来了!!!
如何才能触发自定义信号呢?
Qt 内部的信号,都不需要咱们手动通过代码来触发,用户在GUI,进行某些操作,就会自动触发对应信号。(发射信号的代码已经内置到Qt框架中了)

// 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();


signals:
    void mySignal();

public:
    void handleMySignal();


private slots:
    void on_pushButton_clicked();

private:
    Ui::Widget *ui;
};
#endif // WIDGET_H
// widget.cpp
#include "widget.h"
#include "ui_widget.h"

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

    connect(this, &Widget::mySignal, this, &Widget::handleMySignal);

}

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

void Widget::handleMySignal()
{
    this->setWindowTitle("处理自定义信号");
}


void Widget::on_pushButton_clicked()
{
    // 发送自定义的信号
    // 发送信号的操作,也可以在任意合适的代码中,不一定非得在构造函数里

    // 此时就是点击按钮的时候,发送自定义信号了!
    emit mySignal();
}

在这里插入图片描述

  • emit:
    其实在 Qt 5 中 emit 现在啥都没做
    真正的操作都包含在 mySignal 内部生成的函数定义了
    即使不写 emit,信号也能发出去!
    即使如此,实际开发中,带上emit,可以提高代码可读性,更明显的的标识出,这里是发射自定义的信号了!

5. 信号和槽 带参数

当信号带有参数的时候,槽的参数必须和信号的参数一致
此时发射信号的时候,就可以给信号传递实参,与之对应的这个参数就会传递到对应的槽函数中,此时就可以起到让信号给槽传参效果了。

signals:
    void mySignal(const QString& text);

public:
    void handleMySignal(const QString& text);
void Widget::handleMySignal(const QString& text)
{
    this->setWindowTitle(text);
}


void Widget::on_pushButton_clicked()
{
    // 发送自定义的信号
    // 发送信号的操作,也可以在任意合适的代码中,不一定非得在构造函数里

    // 此时就是点击按钮的时候,发送自定义信号了!
    emit mySignal("带参数的信号");
}

传参可以起到复用代码的效果,有多个逻辑,逻辑上整体一致,但是涉及到的数据不同。
就可以通过函数参数来复用代码,并且不同的场景中传入不同的参数即可。

connect(this, &Widget::mySignal, this, &Widget::handleMySignal);

通过这一套信号槽,搭配不同的参数,就可以起到设置不同标题的效果

void Widget::on_pushButton_clicked()
{
    // 发送自定义的信号
    // 发送信号的操作,也可以在任意合适的代码中,不一定非得在构造函数里

    // 此时就是点击按钮的时候,发送自定义信号了!
    emit mySignal("把标题设置为标题1");
}

void Widget::on_pushButton_2_clicked()
{
    emit mySignal("把标题设置为标题2");
}

Qt 中很多内置的信号,也是带有参数的。(这些参数不是咱们自己传递的)
clicked 信号就带有一个参数

clicked(bool):

这个bool 参数标识当前按钮是否处于“选中”状态,这个选中状态对于 QPushButton 没有啥意义。
对于 QCheckBox 复选框,就很有用了。

信号函数的参数个数,超过了槽函数的参数个数,此时,都是可以正常使用的。
信号函数的参数个数,少于槽函数的参数个数,此时代码无法编译通过。

直观思考,因该是要求信号的参数和槽函数的参数个数,严格一致
此处为啥允许信号的参数比槽的参数多呢?
一个槽函数,有可能会绑定多个信号。
如果我们严格的要求参数个数一致,就意味着信号绑定到槽函数的要求变高了,换言之,当下这样的规则,就允许信号和槽之间的绑定灵活了。
更多的信号可以绑定到这个槽函数上了。

个数不一致,槽函数就会按照参数顺序,拿到信号的前 N 个参数,至少需要确保,槽函数的每个参数都是有值的

要求信号给的参数,可以有富裕,但是不能少

Q_OBJECT :
在这里插入图片描述

所谓的信号槽,终究要解决的问题,就是响应用户的操作。
信号槽,是实现在GUI 开发的各种框架中,是一个比较有特色的存在

其他的GUI 开发框架搞的方式都要简洁一些 网页开发(js + dom api) 网页开发中响应用户操作,主要就是挂回调函数

js button.onclick = handle; function handle() { ... }
不需要搞一个单独的connect 完成上述的信号槽连接,处理函数就像控件的一个属性/成员一样,大部分的GUI 开发框架都是这么搞的。

Qt 信号槽,connect 这个机制,设想是美好的
1)解耦合.把触发 用户操作的控件和 处理对应用户的操作逻辑 解耦合.
2)“多对多”效果 ~~
一个信号,可以connect到多个槽函数上.
一个槽函数,也可以被多个信号 connect.

数据库(MySQL) 设计数据库表的表结构,就需要理清楚和实体之间的关系 一对一、一对多、多对多
三种不同的关系,设计表的时候,就有不同的写法(定式)

Qt 中谈到信号 和 槽 “多对多” 就和数据库中 的 多对多 非常类似

在这里插入图片描述
一个学生,可以选择多门课来学习。 张三这个同学,可以选择 语文和数学。 李四这个同学可以选择 语文 和 英语。
一门课程,也可以被多个同学来选择。语文这门课程,既可以被张三选择,也可以被李四选择。

实际上,随着程序开发这个事情,大家经验越来越多,其实在 GUI 开发的过程中,“多对多” 这件事情,其实是一个“伪需求”,实际开发很少会用到,绝大部分情况,一对一就够用了。

6. 关于型号槽两个补充知识

6.1. 使用disconnect来断开信号槽的连接.

disconnect 使用的方式和connect 是非常类似的.
disconnect用的比较少的.
大部分的情况下,把信号和槽连上了之后,就不必管了.
主动断开往往是把信号重新绑定到另一个槽函数上~

// 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();

    void handleClick();
    void handleClick2();
private slots:
    void on_pushButton_2_clicked();

private:
    Ui::Widget *ui;
};
#endif // WIDGET_H
// widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>

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

    connect(ui->pushButton, &QPushButton::clicked, this, &Widget::handleClick);
}

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

void Widget::handleClick()
{
    this->setWindowTitle("修改窗口的标题");
}

void Widget::handleClick2()
{
    this->setWindowTitle("修改窗口的标题2");
    qDebug() << "handleClick2";
}


void Widget::on_pushButton_2_clicked()
{
    //1.先断开 pushButton 原来的信号槽
    disconnect(ui->pushButton, &QPushButton::clicked, this, &Widget::handleClick);
    //2.重新绑定信号槽
    connect(ui->pushButton, &QPushButton::clicked, this, &Widget::handleClick2);
}

6.2. 定义槽函数的时候,也可以使用lambda 表达式!

很多编程语言都支持(语法糖);
本质上就是一个“匿名函数”, 主要应用在“回调函数”场景中。

QPushButton* button = new QPushButton(this);
button->setText("按钮");
button->move(200, 200);

connect(button, &QPushButton::clicked, this, [](){
    qDebug() << "lambda 被执行了!";
});

在这里插入图片描述
lambda 为了解决上述问题,引入了“变量获取” 语法。
通过变量捕获,获取到外层作用域的变 量。

    connect(button, &QPushButton::clicked, this, [button, this](){
        qDebug() << "lambda 被执行了!";
        button->move(300,300);
        this->move(200,200);
    });

如果当前 lambda 里面想使用更多的外层变量咋办?
写作 [=]
这个写法就是把上层作用域中所有的变量名都给捕获进来。

后续如果我们对应的槽函数比较简单,而且是一次性使用的,就经常会写作这种lambda的形式。
另外也要确认捕获到lambda 内部的变量是有意义的
回调函数执行时机是不确定的 (用户啥时候点击按钮不知道的)
无论何时用户点击了按钮,捕获到的变量都能正确使用

QPushButton* button = new QPushButton(this);

由于此处 button是new 出来的变量,生命周期跟随整个窗口(挂到对象树上,窗口关闭才会释放)
这个东西就可以后面随时使用了。
类似的,this指向对象 widget
在这里插入图片描述
如果你在这里自已创建一些对象,lambda 捕获
关于对象生命周期管理,C++ 程序员的必修课!

lambda 除了可以按照 值的方式来捕获变量[=]还可以按照引用的方式来捕获[&](Qt中很少这么写)捕获到的变量一般就是各种控件的指针.
指针变量按照值传递或者引用来传递,都无所谓
如果按引用,还得更关注这个引用的变量本身的生命周期

lambda 语法是C++11中引入的.
对于Qt 5及其更高版本,默认就是按照C++11来编译的.
如果使用Qt 4或者更老的版本,就需要手动在.pro文件中加上C++11的编译选项.

CONFIG += c++11

Qt 信号槽总结

  1. 信号槽是什么?
    信号槽是 Qt 中用于处理事件和通信的机制,类似于 Linux 中的信号。
    信号槽包括信号源、信号类型以及信号的处理方式。
  2. 如何使用信号槽?
    使用 connect 函数建立信号与槽之间的连接。
  3. 如何查阅文档?
    查阅文档可以了解每个控件内置了哪些信号和槽,以及它们的作用和触发时机。
  4. 自定义槽函数:
    自定义槽函数本质上是普通的成员函数,可以手动编写或由 Qt Creator 自动生成。
  5. 自定义信号:
    自定义信号是成员函数的声明,由 signals: 关键字标识。
  6. 信号和槽可以带有参数:
    发射信号时可以传递参数给槽函数,但参数类型和数量需匹配。
  7. 信号槽存在的意义:
    解耦合,降低模块之间的耦合度。
    实现多对多的通信效果,类似于数据库中的多对多关系。
  8. disconnect 的使用方式:
    用于断开信号和槽之间的连接。
  9. Lambda 表达式:
    可以使用 Lambda 表达式简化槽函数的定义,使代码更加简洁明了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Q_hd

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值