文章目录
前言:
在现代软件开发中,图形用户界面(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. 概念
信号是系统内部的通知机制,常用于进程间通信。
信号源:发出信号的主体。
信号类型:信号的类别,如SIGINT
、SIGTERM
等。
信号处理方式:为信号注册处理函数,在信号触发时自动调用执行
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
close
是QWidget
内置的槽函数。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 以及更高版本中,槽函数和普通的成员函数之间,没啥差别了.
但是,信号,则是一类非常特殊的函数.
-
程序员只要写出函数声明,并且告诉Qt,这是一个“信号”即可.
这个函数的定义,是Qt在编译过程中,自动生成的.(自动生成的过程,程序员无法干预)
信号在Qt中是特殊的机制.Qt生成的信号函数的实现,要配合Qt框架做很多既定的操作 ~~ -
作为信号函数,这个函数的返回值,必须是
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 信号槽总结
- 信号槽是什么?
信号槽是 Qt 中用于处理事件和通信的机制,类似于 Linux 中的信号。
信号槽包括信号源、信号类型以及信号的处理方式。 - 如何使用信号槽?
使用connect
函数建立信号与槽之间的连接。 - 如何查阅文档?
查阅文档可以了解每个控件内置了哪些信号和槽,以及它们的作用和触发时机。 - 自定义槽函数:
自定义槽函数本质上是普通的成员函数,可以手动编写或由 Qt Creator 自动生成。 - 自定义信号:
自定义信号是成员函数的声明,由signals:
关键字标识。 - 信号和槽可以带有参数:
发射信号时可以传递参数给槽函数,但参数类型和数量需匹配。 - 信号槽存在的意义:
解耦合,降低模块之间的耦合度。
实现多对多的通信效果,类似于数据库中的多对多关系。 disconnect
的使用方式:
用于断开信号和槽之间的连接。- Lambda 表达式:
可以使用 Lambda 表达式简化槽函数的定义,使代码更加简洁明了。