1. Qt 事件
(1)事件介绍:
- 事件是应用程序内部或者外部产生的事情或者动作的统称。在 Qt 中使用一个对象来表示一个事件。所有的 Qt 事件均继承于抽象类 QEvent。
- 事件是由系统或者 Qt 平台本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出⼀个相应的事件。⼀些事件是在用户操作时发出,如键盘事件、鼠标事件等,另⼀些事件则是由系统本身自动发出,如定时器事件。常见的 Qt 事件如下:
(2)常见事件描述:
事件名称 | 描述 |
---|---|
鼠标事件 | 鼠标左键、鼠标右键、鼠标滚轮,鼠标的移动,鼠标按键的按下和松开 |
键盘事件 | 按键类型、按键按下、按键松开 |
定时器事件 | 定时时间到达 |
进入离开事件 | 鼠标的进入和离开 |
滚轮事件 | 鼠标滚轮滚动 |
绘屏事件 | 重绘屏幕的某些部分 |
显示隐藏事件 | 窗口的显示和隐藏 |
移动事件 | 窗口位置的变化 |
窗口事件 | 是否为当前窗口 |
大小改变事件 | 窗口大小改变 |
焦点事件 | 键盘焦点移动 |
拖拽事件 | 用鼠标进行拖拽 |
1.1 事件的处理
(1)事件处理一般常用的方法为:重写相关的 Event 函数。
- 在 Qt 中几乎所有的 Event 函数都是虚函数,所以可以重新实现。如:在实现鼠标的进入和离开事件时,直接重新实现 enterEvent() 和 leaveEvent() 即可。enterEvent() 和 leaveEvent() 函数原型如下:
(2)示例1:
- 新建 Qt 项目,基类选择 QWidget,同时勾选 UI 界面文件,如下图示;
- 设计 UI 文件,如下图示;
- 在项目中新添加一个类:MyLabel;
- 先选中项目名称 QEvent,点击鼠标右键,选择 add new … ,弹出如下对话框:
- 选择:Choose … ,弹出如下界面:
- 此时项目中会新添加以下两个文件:
- 在帮助文档中查找对应的内容;
- 点击 “显示” 之后,出现如下内容:
- 复制 enterEvent() ,粘贴在项目文件 “mylabel.h” 中;
- 重写 enterEvent() 方法;
- 在 UI 文件中选中 Label,右键 ------> 提升为… :
- 当点击 "提升为… " 之后,弹出如下对话框:
- 修改基类:
- 执行效果如下:当鼠标进入设计好的标签之后,就会在应用程序输出栏中打印:鼠标进入。
(3)示例2:当鼠标点击时,获取对应的坐标值;
- 在上述示例的基础上,在 mylabel.h 中声明 mousePressEvent() 方法;
- 在 mylabel.cpp 中重写 mousePressEvent() 方法;
- 实现效果如下:
(4)示例:鼠标左键点击时,打印对应的坐标值,鼠标右键点击时,打印基于屏幕的坐标。
1.2 按键事件
(1)Qt 中的按键事件是通过 QKeyEvent 类来实现的。当键盘上的按键被按下或者被释放时,键盘事件便会触发。在帮助文档中查找 QKeyEvent 类如下:
(2)查找按键事件中所有的按键类型:在帮助文档中输入:Qt::Key,如下图:
1.2.1 单个按键
(1)示例:当某个按键被按下时,输出:某个按键被按下了;
- 新建项目,在头文件 “widget.h” 中声明虚函数 keyPressEvent();如下图:
- 在 “widget.cpp” 文件中重写 keyPressEvent() 虚函数;
1.2.2 组合按键
(1)在 Qt 助手中搜索:Qt::KeyboardModifier,如下图示:
(2)Qt::KeyboardModifier 中定义了在处理键盘事件时对应的修改键。在 Qt 中,键盘事件可以与修改键一起使用,以实现⼀些复杂的交互操作。KeyboardModifier 中修改键的具体描述如下:
修改键 | 说明 |
---|---|
Qt::NoModifier | 无修改键 |
Qt::ShiftModifier | Shift 键 |
Qt::ControlModifier | Ctrl 键 |
Qt::AltModifier | Alt 键 |
Qt::MetaModifier | Meta键(在Windows上指Windows键,在macOS上指Command键) |
Qt::KeypadModifier | 使用键盘上的数字键盘进行输入时,Num Lock键处于打开状态 |
Qt::GroupSwitchModifier | 用于在输入法 组之间 切换 |
(3)示例:
1.3 鼠标事件
(1)在 Qt 中,鼠标事件是用 QMouseEvent 类来实现的。当在窗口中按下鼠标或者移动鼠标时,都会产生鼠标事件。
- 利用 QMouseEvent 类可以获取鼠标的哪个键被按下了以及鼠标的当前位置等信息。在 Qt 帮助文档中查找QMouseEvent类。如下图示:
1.3.1 鼠标单击事件
(1)在 Qt 中,鼠标按下是通过虚函数 mousePressEvent() 来捕获的。
- mousePressEvent() 函数原型如下:
[virtual protected] void QWidget::mousePressEvent(QMouseEvent *event);
(2)鼠标左右键及滚的表示如下:
- Qt::LeftButton:鼠标左键
- Qt::RightButton:鼠标右键
- Qt::MidButton:鼠标滚轮
(3)示例1:鼠标左键。
- 在 “widget.h” 头文件中声明鼠标按下事件;
- 在 “widget.cpp” 文件中重新实现 mousePressEvent() 函数;
- 实现效果如下:
(4)示例2:鼠标右键。
- 实现效果如下:
(5)示例3:鼠标滚轮。
- 实现效果如下:
1.3.2 鼠标释放事件
(1)鼠标释放事件是通过虚函数 mouseReleaseEvent() 来捕获的。
- mouseReleaseEvent() 函数原型如下:
[virtual protected] void QWidget::mouseReleaseEvent(QMouseEvent *event);
(2)示例:
- 执行效果如下:
1.3.3 鼠标双击事件
(1)鼠标双击事件是通过虚函数:mouseDoubleClickEvent() 来实现的。
- mouseDoubleClickEvent() 函数原型如下:
[virtual protected] void QWidget::mouseDoubleClickEvent(QMouseEvent *event);
(2)示例:鼠标左键双击。
- 执行效果如下:
1.3.4 鼠标移动事件
(1)鼠标移动事件是通过虚函数:mouseMoveEvent() 来实现的。
- 同时为了实时捕获鼠标位置信息,需要通过函数 setMouseTracking() 来追踪鼠标的位置。mouseMoveEvent()函数原型如下:
[virtual protected] void QWidget::mouseMoveEvent(QMouseEvent *event) ;
- setMouseTracking()函数原型如下:
void setMouseTracking(bool enable);
(2)说明:
- setMouseTracking() 函数默认是 false,需要设置为 true,才能实时捕获鼠标位置信息。否则只有当鼠标按下时才能捕获其位置信息。
(3)示例:
- 执行效果:
1.3.5 滚轮事件
(1)在 Qt 中,鼠标滚轮事件是通过 QWheelEvent 类来实现的。滚轮滑动的距离可以通过 delta() 函数获取。delta() 函数原型如下:
int QGraphicsSceneWheelEvent::delta() const
- 其中返回值代表滚轮滑动的距离。正数表示滚轮相对于用户向前滑动,负数表示滚轮相对于用户向后滑动。
(2)示例:
- 执行效果如下:
1.4 定时器
(1)Qt 中在进行窗口程序的处理过程中,经常要周期性的执行某些操作,或者制作⼀些动画效果,使用定时器就可以实现。所谓定时器就是在间隔⼀定时间后,去执行某⼀个任务。定时器在很多场景下都会使用到,如弹窗自动关闭之类的功能等。Qt中的定时器分为 QTimerEvent 和 QTimer 这2个类。
- QTimerEvent类 用来描述⼀个定时器事件。在使用时需要通过 startTimer() 函数来开启⼀个定时器,这个函数需要输入一个以毫秒为单位的整数作为参数来表明设定的时间,它返回的整型值代表这个定时器。当定时器溢出时(即定时时间到达)就可以在 timerEvent() 函数中获取该定时器的编号来进行相关操作。
- QTimer类 来实现一个定时器,它提供了更高层次的编程接口,如:可以使用信号和槽,还可以设置只运行一次的定时器。
1.4.1 QTimerEvent 类
(1)示例1:在UI界面上放置两个 Label 控件,一个让其1秒数字累加一次,一个让其2秒数字累加一次。
- 新建项目,在UI界面文件放置两个 Label 控件;
- 在 “widget.h” 头文件中声明 timerEvent() 函数,并定义两个整型变量;
- 在 “widget.cpp” 文件中重写 timerEvent() 函数;
- 实现效果如下:
1.4.2 QTimer 类
(1)示例:在UI界面放置⼀个 Label 标签,两个按钮,分别是 “开始” 和 “停止” ,当点击 “开始” 按钮时,开始每隔1秒计数⼀次,点击 “停止” 按钮时,暂停计数。
- 设计 UI 界面如下:
- 在 “widget.cpp” 文件中实现对应功能:
- 实现效果如下:
1.4.3 获取系统日期及时间
(1)在 Qt 中,获取系统的日期及实时时间可以通过 QTimer 类 和 QDateTime类。
- QDateTime类 提供了字符串格式的时间。字符串形式的时间输出格式由 toString() 方法中的 format 参数列表决定,可用的参数列表如下:
(2)示例:获取系统日期及实时时间;
- 设计UI界面文件;放置⼀个 Label控件,用来显示日期及时间,放置两个按钮:“开始” 和 “停止” ;
- 在 “widget.h” 头文件中声明更新时间的槽函数;
- 在 “widget.cpp” 文件中实现对应功能;
- 实现效果如下:
1.5 事件分发器
(1)事件分发器概述:
- 在 Qt 中,事件分发器(Event Dispatcher) 是⼀个核心概念,用于处理 GUI 应用程序中的事件。
- 事件分发器负责将事件从⼀个对象传递到另⼀个对象,直到事件被处理或被取消。每个继承自 QObject类 或 QObject类 本身都可以在本类中重写 bool event(QEvent *e) 函数,来实现相关事件的捕获和拦截。
(2)事件分发器工作原理:
- 在 Qt 中,我们发送的事件都是传给了 QObject 对象,更具体点是传给了 QObject 对象的 event() 函数。所有的事件都会进入到这个函数里面,那么我们处理事件就要重写这个 event() 函数。event() 函数本身不会去处理事件,而是根据 事件类型(type值)调用不同的事件处理函数。事件分发器就是工作在应用程序向下分发事件的过程中,如下图:
- 如上图,事件分发器用于分发事件。在此过程中,事件分发器也可以做拦截操作。事件分发器主要是通过 bool event(QEvent *e) 函数来实现。其返回值为布尔类型,若为 ture,代表拦截,不向下分发。
- Qt 中的事件是封装在 QEvent类 中,在 Qt 助手中输入 QEvent 可以查看其所包括的事件类型,如下图示:
(3)示例:
- 在 “widget.h” 头文件中声明 鼠标点击事件 和 事件分发器;如下图示:
- 在 “widget.cpp” 文件中实现 鼠标点击事件 和 拦截事件;
- 执行结果如下:
1.6 事件过滤器
(1)事件过滤器概述:
- 在 Qt 中,⼀个对象可能经常要查看或拦截另外⼀个对象的事件,如对话框想要拦截按键事件,不让别的组件接收到,或者修改按键的默认值等。通过上面的学习,我们已经知道,Qt 创建了 QEvent事件 对象之后,会调用QObject 的 event()函数 处理事件的分发。
- 显然,我们可以在 event()函数 中实现拦截的操作。由于 event()函数是 protected 的,因此,需要继承已有类。如果组件很多,就需要重写很多个event()函数。这当然相当麻烦,更不用说重写 event()函数还得小心一堆问题。好在 Qt 提供了另外⼀种机制来达到这⼀目的:事件过滤器。
(2)事件过滤器是在应用程序分发到 event事件分发器 之前,再做一次更高级的拦截。如下图示:
(3)事件过滤器的⼀般使用步骤:
- 安装事件过滤器;
- 重写事件过滤器函数:eventfilter() 。
(4)示例:
- 新建 Qt 项目,基类选择 QWidget,同时勾选 UI 界面文件,如下图示;
- 设计 UI 文件,如下图示;
- 在项目新添加⼀个类:MyLabel;
- 先选中项目名称 QEvent,点击鼠标右键,选择 add new … ,弹出如下对话框:
- 选择:Choose … ,弹出如下界面:
- 此时项目中会新添加以下两个文件:
- 在 UI 文件中选中 Label,右键 ------> 提升为…:
- 当点击 "提升为… " 之后,弹出如下对话框:
- 在 “mylabel.h” 中声明 鼠标点击事件 和 事件分发器;
- 在 “mylabel.cpp” 文件中实现鼠标点击事件和事件分发器;
- 在 “widget.h” 头文件中声明事件过滤器函数;
- 在 “widget.cpp” 文件中实现事件过滤器的两个步骤;
- 执行结果如下所示:
2. Qt 文件
(1)Qt 文件概述:
- 文件操作是应用程序必不可少的部分。Qt 作为⼀个通用开发库,提供了跨平台的文件操作能力。 Qt 提供了很多关于文件的类,通过这些类能够对文件系统进行操作,如文件读写、文件信息获取、文件复制或重命名等。
2.1 输入输出设备类
(1)在 Qt 中,文件读写的类为 QFile 。QFile 的父类为 QFileDevice ,QFileDevice 提供了文件交互操作的底层功能。 QFileDevice 的父类是 QIODevice,QIODevice 的父类为 QObject 。
- QIODevice 是 Qt 中所有输入输出设备(input/output device,简称 I/O 设备)的基础类,I/O 设备就是能进行数据输入和输出的设备,例如文件是⼀种 I/O 设备,王络通信中的 socket 是 I/O 设备, 串口、蓝牙等通信接口也是 I/O 设备,所以它们也是从 QIODevice 继承来的。Qt 中主要的⼀些 I/O 设备类的继承关系如下图所示:
(2)上图中各类的说明如下:
- QFile 是用于文件操作和文件数据读写的类,使用 QFile 可以读写任意格式的文件。
- QSaveFile 是用于安全保存文件的类。使用 QSaveFile 保存文件时,它会先把数据写入一个临时文件,成功提交后才将数据写入最终的文件。如果保存过程中出现错误,临时文件里的数据不会被写入最终文件,这样就能确保最终文件中不会丢失数据或被写入部分数据。 在保存比较大的文件或复杂格式的文件时可以使用这个类,例如从网络上下载文件等。
- QTemporaryFile 是用于创建临时文件的类。使用函数 QTemporaryFile::open() 就能创建一个文件名唯一的临时文件,在 QTemporaryFile 对象被删除时,临时文件被自动删除。
- QTcpSocket 和 QUdpSocket 是分别实现了 TCP 和 UDP 的类。
- QSerialPort 是实现了串口通信的类,通过这个类可以实现计算机与串口设备的通信。
- QBluetoothSocket 是用于蓝牙通信的类。手机和平板计算机等移动设备有蓝牙通信模块,笔记本电脑⼀般也有蓝牙通信模块。通过QBluetoothSocket类,就可以编写蓝牙通信程。如编程实现笔记本电脑与手机的蓝牙通信。
- QProcess 类用于启动外部程序,并且可以给程序传递参数。
- QBuffer 以⼀个 QByteArray 对象作为数据缓冲区,将 QByteArray 对象当作⼀个 I/O 设备来读写。
2.2 文件读写类
(1)在 Qt 中,文件的读写主要是通过 QFile 类来实现。在 QFile 类中提供了一些用来读写文件的方法。对于文件的操作主要有:
- 读数据:QFile 类中提供了多个方法用于读取文件内容;如 read()、readAll()、readLine()等。
- 写数据:QFile 类中提供了多个方法用于往文件中写内容;如 write()、writeData()等。
- 关闭文件:文件使用结束后必须用函数 close() 关闭文件。
(2)访问一个设备之前,需要使用 open()函数 打开该设备,而且必须指定正确的打开模式,QIODevice 中所有的打开模式由 QIODevice::OpenMode 枚举变量定义,其取值如下:
打开方式 | 说明 |
---|---|
QIODevice::NotOpen | 没有打开设备 |
QIODevice::ReadOnly | 以只读方式打开设备 |
QIODevice::WriteOnly | 以只写方式打开设备 |
QIODevice::ReadWrite | 以读写方式打开设备 |
QIODevice::Append | 以追加方式打开设备,数据将写到文件末尾 |
QIODevice::Truncate | 每次打开文件后重写文件内容,原内容将被删除 |
QIODevice::Text | 在读文件时,行尾终止符会被转换为 ‘\n’;当写⼊文件时,行尾终止符会被转换为本地编码。如 Win32上为’\r\n’; |
QIODevice::Unbuffered | 无缓冲形式打开文件,绕过设备中的任何缓冲区 |
QIODevice::NewOnly | 文件存在则打开失败,不存在则创建文件 |
(3)示例1:读取文件内容。
- 新建 Qt 项目,在 UI 文件中拖人⼀个 LineEdit,⼀个pushButton,⼀个 TextEdit。当点击按钮时,弹出窗口选择要读取的文件,并将读取到的内容在 TextEdit 中显示;
- 在 “widget.cpp” 文件中实现对应功能;
#include "widget.h"
#include "ui_widget.h"
#include <QFileDialog>
Widget::Widget(QWidget *parent)
:QWidget(parent)
,ui(new Ui::Widget)
{
ui->setupUi(this);
connect(ui->btn,&QPushButton::clicked,[=]()
{
QString path = QFileDialog::getOpenFileName(this,"打开⽂件","C:\\Users\\Lenovo\\Desktop");
ui->lineEdit->setText(path);
QFile file(path); //path:代表⽂件路径
//打开⽂件
file.open(QIODevice::ReadOnly); //以只读的⽅式打开⽂件
QString str = file.readAll();
ui->textEdit->setText(str);
//关闭⽂件
file.close();
});
}
- 实现效果如下:
(4)示例2:写文件。
- 在上述示例的基础上修改 “widget.cpp” 文件;
Widget::Widget(QWidget *parent)
:QWidget(parent)
,ui(new Ui::Widget)
{
ui->setupUi(this);
connect(ui->btn,&QPushButton::clicked,[=]()
{
QString path = QFileDialog::getOpenFileName(this,"打开⽂件","C:\\Users\\Lenovo\\Desktop");
ui->lineEdit->setText(path);
QFile file(path); //path:代表⽂件路径
//打开⽂件 进⾏写操作
file.open(QIODevice::Append); //以追加的⽅式进⾏写
file.write("【这是⽰例!!!】");
//关闭⽂件
file.close();
});
}
- 实现效果如下:
2.3 文件和目录信息类
(1)QFileInfo 是 Qt 提供的一个用于获取文件和目录信息的类,如获取文件名、文件大小、文件修改日期等。QFileInfo类中提供了很多的方法,常用的有:
- isDir() 检查该文件是否是目录;
- isExecutable() 检查该文件是否是可执行文件;
- fileName() 获得文件名;
- completeBaseName() 获取完整的文件名;
- suffix() 获取文件后缀名;
- completeSuffix() 获取完整的文件后缀;
- size() 获取文件大小;
- isFile() 判断是否为文件;
- fileTime() 获取文件创建时间、修改时间、最近访问时间等;
(2)示例:
- 在 “widget.cpp” 文件中添加如下代码:
#include <QFileInfo>
#include <QFileDialog>
#include <QDebug>
#include <QDateTime>
Widget::Widget(QWidget *parent)
:QWidget(parent)
,ui(new Ui::Widget)
{
ui->setupUi(this);
connect(ui->btn,&QPushButton::clicked,[=]()
{
QString path = QFileDialog::getOpenFileName(this,"打开⽂件","C:\\Users\\Lenovo\\Desktop");
//QFileInfo ⽂件信息类
QFileInfo fileinfo(path);
//⽂件名
qDebug() << "⽂件名为:" << fileinfo.fileName().toUtf8().data();
//⽂件后缀名
qDebug() << "后缀名为:" << fileinfo.suffix().toUtf8().data();
//⽂件⼤⼩
qDebug() << "⽂件⼤⼩为:" << fileinfo.size();
//⽂件路径
qDebug() << "⽂件路径为:" << fileinfo.path().toUtf8().data();
//判断是否为⽂件
qDebug() << "是否为⽂件:"<< fileinfo.isFile();
//⽂件创建时间
QDateTime time1 = fileinfo.fileTime(QFileDevice::FileBirthTime);
qDebug() << "创建时间为:" << time1.toString("yyyy-MM-dd
hh:mm:ss").toUtf8().data();
//⽂件的最后修改⽇期
QDateTime time2 = fileinfo.lastModified();
qDebug() << "最后修改时间为:"<< time2.toString("yyyy-MM-dd
hh:mm:ss").toUtf8().data();
//判断是否为⽂件夹
qDebug() << "是否为⽬录:" << fileinfo.isDir();
});
}
- 实现效果如下:
3. Qt 多线程
(1)Qt 多线程概述:
- 在 Qt 中,多线程的处理⼀般是通过 QThread类 来实现。
- QThread 代表一个在应用程序中可以独立控制的线程,也可以和进程中的其他线程共享数据。
- QThread 对象管理程序中的一个控制线程。
3.1 QThread 常用 API
(1)QThread常用 API如下所示:
API | 说明 |
---|---|
run() | 线程的入口函数… |
start() | 通过调用 run() 开始执行线程。操作系统将根据优先级参数调度线程。如果线程已经在运行,这个函数什么也不做。 |
currentThread() | 返回⼀个指向管理当前执行线程的 QThread的指针。 |
isRunning() | 如果线程正在运行则返回true;否则返回false。 |
sleep() / msleep() / usleep() | 使线程休眠,单位为秒 / 毫秒 / 微秒 |
wait() | 阻塞线程,直到满足以下任何⼀个条件: 1. 与此 QThread 对象关联的线程已经完成执行(即当它从run()返回时)。如果线程已经完成,这个函数将返回 true。如果线程尚未启动,它也返回 true。 2. 已经过了几毫秒。如果时间是 ULONG_MAX(默认值),那么等待永远不会超时(线程必须从run()返回)。如果等待超时,此函数将返回 false。 3. 这提供了与 POSIX pthread_join() 函数类似的功能。 |
terminate() | 终止线程的执行。线程可以立即终止,也可以不立即终止,这取决于操作系统的调度策略。在terminate() 之后使用 QThread::wait() 来确保。 |
finished() | 当线程结束时会发出该信号,可以通过该信号来实现线程的清理工作。 |
3.2 使用线程
(1)创建线程的步骤:
- 自定义⼀个类,继承于 QThread,并且只有⼀个线程处理函数(和主线程不是同⼀个线程),这个线程处理函数主要就是重写父类中的 run() 函数。
- 线程处理函数里面写入需要执行的复杂数据处理;
- 启动线程不能直接调⽤ run() 函数,需要使用对象来调用 start() 函数实现线程启动;
- 线程处理函数执行结束后可以定义⼀个信号来告诉主线程;
- 最后关闭线程。
(2)示例:
- 首先新建 Qt 项目,设计 UI界面如下:
- 新建⼀个类,继承于 QThread类;
- 程序如下:
/******************************** timethread.h********************************/
#ifndef TIMETHREAD_H
#define TIMETHREAD_H
#include <QThread> //添加头⽂件
class TimeThread : public QThread
{
Q_OBJECT
public:
TimeThread();
void run(); //线程任务函数
signals:
void sendTime(QString Time); //声明信号函数
};
#endif // TIMETHREAD_H
/******************************** widget.h ********************************/
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <timethread.h> //添加头⽂件
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 on_btn_clicked();
void showTime(QString Time);
private:
Ui::Widget *ui;
TimeThread t; //定义线程对象
};
#endif // WIDGET_H
/******************************** timethread.cpp********************************/
#include "timethread.h"
#include <QTime>
#include <QDebug>
TimeThread::TimeThread()
{}
void TimeThread::run()
{
while(1)
{
QString time = QTime::currentTime().toString("hh:mm:ss");
qDebug() << time;
emit sendTime(time); //发送信号
sleep(1);
}
}
/******************************** widget.cpp ********************************/
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
:QWidget(parent)
,ui(new Ui::Widget)
{
ui->setupUi(this);
connect(&t,&TimeThread::sendTime,this,&Widget::showTime);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_btn_clicked()
{
t.start(); //开启线程
}
void Widget::showTime(QString Time)
{
ui->label->setText(Time);
}
- 执行效果:
(3)说明:
- 线程函数内部不允许操作 UI 图形界面,一般用数据处理;
- connect() 函数第五个参数表示的为连接的方式,且只有在多线程的时候才意义。
(4)connect() 函数第五个参数为 Qt::ConnectionType,用于指定信号和槽的连接类型。同时影响信号的传递方式和槽函数的执行顺序。Qt::ConnectionType 提供了以下五种方式:
连接类型 | 说明 |
---|---|
Qt::AutoConnection | 在 Qt 中,会根据信号和槽函数所在的线程自动选择连接类型。如果信号和槽函数在同⼀线程中,那么使用 Qt:DirectConnection 类型;如果它们位于不同的线程中,那么使用Qt::QueuedConnection 类型。 |
Qt::DirectConnection | 当信号发出时,槽函数会立即在同一线程中执行。这种连接类型适用于信号和槽函数在同一线程中的情况,可以实现直接的函数调用,但需要注意线程安全性。 |
Qt::QueuedConnection | 当信号发出时,槽函数会被插入到接收对象所属的线程的事件队列中,等待下一次事件循环时执行。这种连接类型适用于信号和槽函数在不同线程中的情况,可以确保线程安全。 |
Qt::BlockingQueuedConnection | 与 Qt:QueuedConnection 类似,但是发送信号的线程会被阻塞,直到槽函数执行完毕,这种连接类型适用于需要等待槽函数执行完毕再继续的场景,但需要注意可能引起线程死锁的风险。 |
Qt::UniqueConnection | 这是⼀个标志,可以使用位或与上述任何⼀种连接类型组合使用。 |
3.3 线程安全
(1)实现线程互斥和同步常用的类有:
- 互斥锁:QMutex、QMutexLocker
- 条件变量:QWaitCondition
- 信号量:QSemaphore
- 读写锁:QReadLocker、QWriteLocker、QReadWriteLock
3.3.1 互斥锁
(1)互斥锁是一种保护和防止多个线程同时访问同一对象实例的方法,在 Qt 中,互斥锁主要是通过QMutex类来处理。QMutex介绍如下:
- 特点:QMutex 是 Qt 框架提供的互斥锁类,用于保护共享资源的访问,实现线程间的互斥操作。
- 用途:在多线程环境下,通过互斥锁来控制对共享数据的访问,确保线程安全。
QMutex mutex;
mutex.lock(); //上锁
//访问共享资源
//...
mutex.unlock(); //解锁
(2)QMutexLocker 介绍:
- 特点:QMutexLocker 是 QMutex 的辅助类,使用 RAII(Resource Acquisition Is Initialization)方式对互斥锁进行上锁和解锁操作。
- 用途:简化对互斥锁的上锁和解锁操作,避免忘记解锁导致的死锁等问题。
QMutex mutex;
{
QMutexLocker locker(&mutex); //在作⽤域内⾃动上锁
//访问共享资源
//...
} //在作⽤域结束时⾃动解锁
(3)QReadWriteLocker、QReadLocker、QWriteLocker 介绍:
- 特点:
- QReadWriteLock 是读写锁类,用于控制读和写的并发访问。
- QReadLocker 用于读操作上锁,允许多个线程同时读取共享资源。
- QWriteLocker 用于写操作上锁,只允许一个线程写入共享资源。
- 用途:在某些情况下,多个线程可以同时读取共享数据,但只有⼀个线程能够进行写操作。读写锁提供了更高效的并发访问方式。
QReadWriteLock rwLock;
//在读操作中使⽤读锁
{
QReadLocker locker(&rwLock); //在作⽤域内⾃动上读锁
//读取共享资源
//...
} //在作⽤域结束时⾃动解读锁
//在写操作中使⽤写锁
{
QWriteLocker locker(&rwLock); //在作⽤域内⾃动上写锁
//修改共享资源
//...
} //在作⽤域结束时⾃动解写锁
(4)示例1:
/******************************* myThread.h **********************************/
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QThread>
#include <QMutex>
class myThread : public QThread
{
Q_OBJECT
public:
explicit myThread(QObject *parent = nullptr);
void run();
private:
static QMutex mutex; //多个线程使⽤⼀把锁
static int num; //多个线程访问⼀个数据
};
#endif // MYTHREAD_H
/******************************* myThread.cpp**********************************/
#include "mythread.h"
#include <QDebug>
QMutex myThread::mutex;
int myThread::num = 0;
myThread::myThread(QObject *parent) : QThread(parent)
{}
void myThread::run()
{
while(1)
{
this->mutex.lock(); //加锁
qDebug() << "Current Thread: " << this << ", Value: " << this->num++;
this->mutex.unlock(); //解锁
QThread::sleep(1); //线程睡眠两秒
}
}
/******************************* mainwindow.h**********************************/
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
/******************************* mainwindow.cpp**********************************/
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "mythread.h"
MainWindow::MainWindow(QWidget *parent)
:QMainWindow(parent)
,ui(new Ui::MainWindow)
{
ui->setupUi(this);
myThread *t1 = new myThread(this);
myThread *t2 = new myThread(this);
t1->start();
t2->start();
}
MainWindow::~MainWindow()
{
delete ui;
}
- 执行效果:两个线程使用一把锁,操作一个数据,数据会被两个线程依次打印:0、1、2、3、4 …。
(5)示例2:在上述示例的基础上使用 QMutexLocker 锁。
/******************************* myThread.cpp**********************************/
#include "mythread.h"
#include <QDebug>
QMutex myThread::mutex;
int myThread::num = 0;
myThread::myThread(QObject *parent) : QThread(parent)
{}
void myThread::run()
{
while (1)
{
//QMutexLocker:创建的时候加锁,当QMutexLocker局部销毁的时候解锁
{
QMutexLocker lock(&this->mutex);
qDebug() << "Current Thread: " << this << ", Value: " << this->num++;
}
QThread::sleep(1);// 线程睡眠两秒
}
}
3.3.2 条件变量
(1)在多线程编程中,假设除了等待操作系统正在执行的线程之外,某个线程还必须等待某些条件满足才能执行,这时就会出现问题。
- 这种情况下,线程会很自然地使用锁的机制来阻塞其他线程,因为这只是线程的轮流使用,并且该线程等待某些特定条件,人们会认为需要等待条件的线程,在释放互斥锁或读写锁之后进入了睡眠状态,这样其他线程就可以继续运行。当条件满足时,等待条件的线程将被另⼀个线程唤醒。
(2)在 Qt 中,专门提供了 QWaitCondition类 来解决像上述这样的问题。
- 特点:QWaitCondition 是 Qt 框架提供的条件变量类,用于线程之间的消息通信和同步。
- 用途:在某个条件满足时等待或唤醒线程,用于线程的同步和协调。
QMutex mutex;
QWaitCondition condition;
//在等待线程中
mutex.lock();
//检查条件是否满⾜,若不满⾜则等待
while (!conditionFullfilled())
{
condition.wait(&mutex); //等待条件满⾜并释放锁
}
//条件满⾜后继续执⾏
//...
mutex.unlock();
//在改变条件的线程中
mutex.lock();
//改变条件
changeCondition();
condition.wakeAll(); //唤醒等待的线程
mutex.unlock();
3.3.3 信号量
(1)有时在多线程编程中,需要确保多个线程可以相应的访问一个数量有限的相同资源。
- 例如,运行程序的设备可能是非常有限的内存,因此我们更希望需要大量内存的线程将这⼀事实考虑在内,并根据可用的内存数量进行相关操作,多线程编程中类似问题通常用信号量来处理。信号量类似于增强的互斥锁,不仅能完成上锁和解锁操作,而且可以跟踪可用资源的数量。
- 特点:QSemaphore 是 Qt 框架提供的计数信号量类,用于控制同时访问共享资源的线程数量。
- 用途:限制并发线程数量,用于解决⼀些资源有限的问题。
QSemaphore semaphore(2); //同时允许两个线程访问共享资源
//在需要访问共享资源的线程中
semaphore.acquire(); //尝试获取信号量,若已满则阻塞
//访问共享资源
//...
semaphore.release(); //释放信号量
//在另⼀个线程中进⾏类似操作
4. Qt 网络
(1)和多线程类似,Qt 为了支持跨平台,对网络编程的 API 也进行了重新封装。
- 咱们接下来重点介绍 Qt 的网络相关的 API 的使用。对于之前 Linux 上介绍过的网络相关概念和原理,均不解释。
(2)注意:
- 网络部分的原理非常复杂,需要对 Linux 中的 “网络编程” 和 “网络原理” 章节熟悉之后再来学习本章内容。
- 实际 Qt 开发中进行网络编程, 也不⼀定使用 Qt 封装的网络 API,也有⼀定可能使用的是系统原生 API 或者其他第三方框架的 API。
(3)在进行网络编程之前,需要在项目中的 .pro 文件中添加 network 模块。
- 添加之后要首动编译⼀下项目,使 Qt Creator 能够加载对应模块的头文件。
4.1 UDP Socket
4.1.1 核心 API 概览
(1)主要的类有两个:QUdpSocket 和 QNetworkDatagram。
- QUdpSocket 表示⼀个 UDP 的 socket 文件。
名称 | 类型 | 说明 | 对标原生 API |
---|---|---|---|
bind(const QHostAddress&, quint16) | 方法 | 绑定指定的端口号。 | bind |
receiveDatagram() | 方法 | 返回 QNetworkDatagram。读取⼀个 UDP 数据报。 | recvfrom |
writeDatagram(const QNetworkDatagram&) | 方法 | 发送⼀个 UDP 数据报。 | sendto |
readyRead | 信号 | 在收到数据并准备就绪后触发。 | 无 (类似于 IO 多路复用的通知机制) |
(2)QNetworkDatagram 表示一个 UDP 数据报。
名称 | 类型 | 说明 | 对标原生 API |
---|---|---|---|
QNetworkDatagram(const QByteArray&, const QHostAddress& , quint16) | 构造函数 | 通过 QByteArray,目标 IP 地址,目标端口号 构造⼀个 UDP 数据报。通常用于发送数据时。 | 无 |
data() | 方法 | 获取数据报内部持有的数据。返回 QByteArray | 无 |
senderAddress() | 方法 | 获取数据报中包含的对端的 IP 地址。 | 无,recvfrom 包含了该功能。 |
senderPort() | 方法 | 获取数据报中包含的对端的端口号。 | 无,recvfrom 包含了该功能。 |
4.1.2 回显服务器
(1)创建界面,包含一个 QListWidget 用来显示消息。
(2)创建 QUdpSocket 成员:
- 修改 widget.h
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
QUdpSocket* socket;
}
- 修改 widget.cpp,完成 socket 后续的初始化。
- ⼀般来说,要先连接信号槽,再绑定端口。
- 如果顺序反过来,可能会出现端口绑定好了之后,请求就过来了。此时还没来得及连接信号槽。那么这个请求就有可能错过了。
Widget::Widget(QWidget *parent)
:QWidget(parent)
,ui(new Ui::Widget)
{
ui->setupUi(this);
// 1. 设置窗⼝标题
this->setWindowTitle("服务器");
// 2. 实例化 socket
socket = new QUdpSocket(this);
// 3. 连接信号槽, 处理收到的请求
connect(socket, &QUdpSocket::readyRead, this, &Widget::processRequest);
// 4. 绑定端⼝
bool ret = socket->bind(QHostAddress::Any, 9090);
if (!ret)
{
QMessageBox::critical(nullptr, "服务器启动出错", socket->errorString());
return;
}
}
(3)实现 processRequest,完成处理请求的过程。
- 读取请求并解析。
- 根据请求计算响应。
- 把响应写回到客户端。
void Widget::processRequest()
{
// 1. 读取请求
const QNetworkDatagram& requestDatagram = socket->receiveDatagram();
QString request = requestDatagram.data();
// 2. 根据请求计算响应
const QString& response = process(request);
// 3. 把响应写回到客⼾端
QNetworkDatagram responseDatagram(response.toUtf8(),
requestDatagram.senderAddress(), requestDatagram.senderPort());
socket->writeDatagram(responseDatagram);
// 显⽰打印⽇志
QString log = "[" + requestDatagram.senderAddress().toString() + ":" + QString::number(requestDatagram.senderPort()) + "] req: " + request + ", resp: " + response;
ui->listWidget->addItem(log);
}
(4)实现 process 函数:
- 由于我们此处是实现回显服务器。所以 process 方法中并没有包含实质性的内容。
QString Widget::process(const QString& request)
{
return request;
}
(5)根据请求处理响应" 是服务器开发中的最核心的步骤。一个商业服务器程序,这里的逻辑可能是几万行几十万行代码量级的。
- 此时服务器程序编写完毕。
- 但是直接运行还看不出效果。还需要搭配客户端来使用。
4.1.3 回显客户端
(1)创建界面。包含一个 QLineEdit , QPushButton , QListWidget。
- 先使用水平布局把 QLineEdit 和 QPushButton 放好,并设置这两个控件的垂直方向的 sizePolicy 为 Expanding 。
- 再使用垂直布局把 QListWidget 和上面的水平布局放好。
- 设置垂直布局的 layoutStretch 为 5, 1 (当然这个尺寸比例根据个人喜好微调)。
(2)在 widget.cpp 中,先创建两个全局常量,表示服务器的 IP 和 端口。
// 提前定义好服务器的 IP 和 端⼝
const QString& SERVER_IP = "127.0.0.1";
const quint16 SERVER_PORT = 9090;
(3)创建 QUdpSocket 成员。
- 修改 widget.h,定义成员:
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
// 创建 socket 成员
QUdpSocket* socket;
}
- 修改 widget.cpp,初始化 socket:
Widget::Widget(QWidget *parent)
:QWidget(parent)
,ui(new Ui::Widget)
{
ui->setupUi(this);
// 1. 设置窗⼝名字
this->setWindowTitle("客⼾端");
// 2. 实例化 socket
socket = new QUdpSocket(this);
}
(4)给发送按钮 slot 函数,实现发送请求。
void Widget::on_pushButton_clicked()
{
// 1. 获取到输⼊框的内容
const QString& text = ui->lineEdit->text();
// 2. 构造请求数据
QNetworkDatagram requestDatagram(text.toUtf8(), QHostAddress(SERVER_IP), SERVER_PORT);
// 3. 发送请求
socket->writeDatagram(requestDatagram);
// 4. 消息添加到列表框中
ui->listWidget->addItem("客⼾端说: " + text);
// 5. 清空输⼊框
ui->lineEdit->setText("");
}
(5)再次修改 Widget 的构造函数,通过信号槽,来处理服务器的响应。
connect(socket, &QUdpSocket::readyRead, this, [=]()
{
const QNetworkDatagram responseDatagram = socket->receiveDatagram();
QString response = responseDatagram.data();
ui->listWidget->addItem(QString("服务器说: ") + response);
});
(6)最终执行效果:启动多个客户端都可以正常工作。
4.2 TCP Socket
4.2.1 核心 API 概览
(1)核心类是两个: QTcpServer 和 QTcpSocket 。
- QTcpServer 用于监听端口和获取客户端连接。
名称 | 类型 | 说明 | 对标原生 API |
---|---|---|---|
listen(const QHostAddress&, quint16 port) | 方法 | 绑定指定的地址和端口号,并开始监听。 | bind 和 listen |
nextPendingConnection() | 方法 | 从系统中获取到⼀个已经建立好的 tcp 连接。 返回⼀个 QTcpSocket,表示这个客户端的连接。通过这个 socket 对象完成和客户端之间的通信。 | accept |
newConnection | 信号 | 有新的客户端建立连接好之后触发。 | 无 (但是类似于 IO 多路复用中的通知机制) |
(2)QTcpSocket 用户客户端和服务器之间的数据交互。
名称 | 类型 | 说明 | 对标原生 API |
---|---|---|---|
readAll() | 方法 | 读取当前接收缓冲区中的所有数据。返回 QByteArray 对象。 | read |
write(const QByteArray& ) | 方法 | 把数据写入 socket 中。 | write |
deleteLater | 方法 | 暂时把 socket 对象标记为无效。 Qt 会在下个事件循环中析构释放该对象。 | 无 (但是类似于 “半自动化的垃圾回收”) |
readyRead | 信号 | 有数据到达并准备就绪时触发。 | 无 (但是类似于 IO 多路复用中的通知机制) |
disconnected | 信号 | 连接断开时触发。 | 无 (但是类似于 IO 多路复用中的通知机制) |
(3)QByteArray 用于表示一个字节数组。可以很方便的和 QString 进行相互转换。例如:
- 使用 QString 的构造函数即可把 QByteArray 转成 QString。
- 使用 QString 的 toUtf8 函数即可把 QString 转成 QByteArray。
4.2.2 回显服务器
(1)创建界面。包含一个 QListWidget,用于显示收到的数据。
(2)创建 QTcpServer 并初始化。
- 修改 widget.h,添加 QTcpServer 指针成员。
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
// 创建 QTcpServer
QTcpServer* tcpServer;
};
- 修改 widget.cpp,实例化 QTcpServer 并进行后续初始化操作。
- 设置窗口标题。
- 实例化 TCP server。(父元素设为当前控件,会在父元素销毁时被⼀起销毁)。
- 通过信号槽,处理客户端建立的新连接。
- 监听端口。
Widget::Widget(QWidget *parent)
:QWidget(parent)
,ui(new Ui::Widget)
{
ui->setupUi(this);
// 1. 设置窗⼝标题
this->setWindowTitle("服务器");
// 2. 实例化 TCP server
tcpServer = new QTcpServer(this);
// 3. 通过信号槽, 处理客⼾端建⽴的新连接.
connect(tcpServer, &QTcpServer::newConnection, this, &Widget::processConnection);
// 4. 监听端⼝
bool ret = tcpServer->listen(QHostAddress::Any, 9090);
if (!ret)
{
QMessageBox::critical(nullptr, "服务器启动失败!", tcpServer->errorString());
exit(1);
}
}
(3)继续修改 widget.cpp,实现处理连接的具体方法 processConnection。
- 获取到新的连接对应的 socket。
- 通过信号槽, 处理收到请求的情况。
- 通过信号槽, 处理断开连接的情况。
void Widget::processConnection()
{
// 1. 获取到新的连接对应的 socket.
QTcpSocket* clientSocket = tcpServer->nextPendingConnection();
QString log = QString("[") + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] 客⼾端上线!";
ui->listWidget->addItem(log);
// 2. 通过信号槽, 处理收到请求的情况
connect(clientSocket, &QTcpSocket::readyRead, this, [=]()
{
// a) 读取请求
QString request = clientSocket->readAll();
// b) 根据请求处理响应
const QString& response = process(request);
// c) 把响应写回客⼾端
clientSocket->write(response.toUtf8());
QString log = QString("[") + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] req: " + request + ", resp: " + response;
ui->listWidget->addItem(log);
});
// 3. 通过信号槽, 处理断开连接的情况
connect(clientSocket, &QTcpSocket::disconnected, this, [=]()
{
QString log = QString("[") + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] 客⼾端下线!";
ui->listWidget->addItem(log);
// 删除 clientSocket
clientSocket->deleteLater();
});
}
(4)实现 process 方法,实现根据请求处理响应。
- 由于我们此处是实现回显服务器。所以 process 方法中并没有包含实质性的内容。
QString Widget::process(const QString &request)
{
return request;
}
4.2.3 回显客户端
(1)创建界面。包含⼀个 QLineEdit , QPushButton , QListWidget。
- 先使用水平布局把 QLineEdit 和 QPushButton 放好,并设置这两个控件的垂直方向的 sizePolicy 为 Expanding 。
- 再使用垂直布局把 QListWidget 和上面的水平布局放好。
- 设置垂直布局的 layoutStretch 为 5, 1 (当然这个尺寸比例根据个人喜好微调)。
(2)创建 QTcpSocket 并实例化。
- 修改 widget.h,创建成员。
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
// 新增 QTcpSocket
QTcpSocket* socket;
}
- 修改 widget.cpp,对 QTcpSocket 进行实例化。
- 设置窗口标。
- 实例化 socket 对象 (父元素设为当前控件,会在父元素销毁时被⼀起销毁)。
- 和服务器建立连接。
- 等待并确认连接是否出错。
Widget::Widget(QWidget *parent)
:QWidget(parent)
,ui(new Ui::Widget)
{
ui->setupUi(this);
// 1. 设置窗⼝标题.
this->setWindowTitle("客⼾端");
// 2. 实例化 socket 对象.
socket = new QTcpSocket(this);
// 3. 和服务器建⽴连接.
socket->connectToHost("127.0.0.1", 9090);
// 4. 等待并确认连接是否出错.
if (!socket->waitForConnected())
{
QMessageBox::critical(nullptr, "连接服务器出错!", socket->errorString());
exit(1);
}
}
(3)修改 widget.cpp,给按钮增加点击的 slot 函数,实现发送请求给服务器。
void Widget::on_pushButton_clicked()
{
// 获取输⼊框的内容
const QString& text = ui->lineEdit->text();
// 清空输⼊框内容
ui->lineEdit->setText("");
// 把消息显⽰到界⾯上
ui->listWidget->addItem(QString("客⼾端说: ") + text);
// 发送消息给服务器
socket->write(text.toUtf8());
}
(4)修改 widget.cpp 中的 Widget 构造函数,通过信号槽,处理收到的服务器的响应。
// 处理服务器返回的响应.
connect(socket, &QTcpSocket::readyRead, this, [=]()
{
QString response = socket->readAll();
qDebug() << response;
ui->listWidget->addItem(QString("服务器说: ") + response);
});
(5)先启动服务器,再启动客户端(可以启动多个),最终执行效果:
- 由于我们使用信号槽处理同一个客户端的多个请求,不涉及到循环,也就不会使客户端之间相互影响了。
4.3 HTTP Client
(1)进行 Qt 开发时,和服务器之间的通信很多时候也会用到 HTTP 协议。
- 通过 HTTP 从服务器获取数据。
- 通过 HTTP 向服务器提交数据。
4.3.1 核心 API
(1)关键类主要是三个:QNetworkAccessManager、QNetworkRequest、QNetworkReply。
- QNetworkAccessManager 提供了 HTTP 的核心操作。
方法 | 说明 |
---|---|
get(const QNetworkRequest& ) | 发起⼀个 HTTP GET 请求。返回QNetworkReply 对象。 |
post(const QNetworkRequest& , const QByteArray& ) | 发起⼀个 HTTP POST 请求。返回 QNetworkReply 对象。 |
- QNetworkRequest 表示一个 HTTP 请求(不含 body)。
- 如果需要发送⼀个带有 body 的请求(比如 post),会在QNetworkAccessManager 的 post 方法中通过单独的参数来传入 body。
方法 | 说明 |
---|---|
QNetworkRequest(const QUrl& ) | 通过 URL 构造⼀个 HTTP 请求。 |
setHeader(QNetworkRequest::KnownHeaders header, const QVariant &value) | 设置请求头。 |
- 其中的 QNetworkRequest::KnownHeaders 是一个枚举类型,常用取值:
取值 | 说明 |
---|---|
ContentTypeHeader | 描述 body 的类型。 |
ContentLengthHeader | 描述 body 的长度。 |
LocationHeader | 用于重定向报文中指定重定向地址。(响应中使用,请求用不到) |
CookieHeader | 设置 cookie 。 |
UserAgentHeader | 设置 User-Agent 。 |
- QNetworkReply 表示⼀个 HTTP 响应。这个类同时也是 QIODevice 的子类。
方法 | 说明 |
---|---|
error() | 获取出错状态。 |
errorString() | 获取出错原因的文本。 |
readAll() | 读取响应 body。 |
header(QNetworkRequest::KnownHeaders header) | 读取响应指定 header 的值。 |
(2)此外,QNetworkReply 还有一个重要的信号 finished 会在客户端收到完整的响应数据之后触发。
4.3.2 代码示例
(1)给服务器发送⼀个 GET 请求。创建界面。包含⼀个 QLineEdit、QPushButton。
- 先使用水平布局把 QLineEdit 和 QPushButton 放好,并设置这两个控件的垂直方向的 sizePolicy 为 Expanding。
- 再使用垂直布局把 QPlainTextEdit 和上面的水平布局放好。( QPlainTextEdit 的 readOnly 设为 true )
- 设置垂直布局的 layoutStretch 为 5, 1 (当然这个尺寸比例根据个人喜好微调)。
- 此处建议使用 QPlainTextEdit 而不是 QTextEdit。主要因为 QTextEdit 要进行富文本解析,如果得到的 HTTP 响应体积很大,就会导致界面渲染缓慢甚至被卡住。
(2)修改 widget.h,创建 QNetworkAccessManager 属性。
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_clicked();
private:
Ui::Widget *ui;
// 新增属性
QNetworkAccessManager* manager;
}
(3)修改 widget.cpp,创建实例。
Widget::Widget(QWidget *parent)
:QWidget(parent)
,ui(new Ui::Widget)
{
ui->setupUi(this);
// 实例化属性
manager = new QNetworkAccessManager(this);
}
(4)编写按钮的 slot 函数,实现发送 HTTP 请求功能。
void Widget::on_pushButton_clicked()
{
// 1. 获取到输⼊框中的 URL, 构造 QUrl 对象
QUrl url(ui->lineEdit->text());
// 2. 构造 HTTP 请求对象
QNetworkRequest request(url);
// 3. 发送 GET 请求
QNetworkReply* response = manager->get(request);
// 4. 通过信号槽来处理响应
connect(response, &QNetworkReply::finished, this, [=]()
{
if (response->error() == QNetworkReply::NoError)
{
// 响应正确
QString html(response->readAll());
ui->plainTextEdit->setPlainText(html);
// qDebug() << html;
}
else
{
// 响应出错
ui->plainTextEdit->setPlainText(response->errorString());
}
response->deleteLater();
});
}
- 执行程序,观察效果。
- 发送 POST 请求代码也是类似。使用 manager->post() 即可。
4.4 其他模块
(1)Qt 中还提供了 FTP、DNS、SSL 等网络相关的组件工具。此处不再一 一展开介绍。有需要的可以自行翻阅官方文档学习相关 API 的使用。
5. Qt 音视频
5.1 Qt 音频
(1)在 Qt 中,音频主要是通过 QSound 类来实现。但是需要注意的是QSound 类只支持播放 wav 格式的音频文件。也就是说如果想要添加音频效果,那么首先需要将 非wav格式 的音频文件转换为 wav 格式。通过帮助手册查看 QSound 类如下:
- 注意:使用 QSound 类时,需要添加模块:multimedia。
(2)核心API概览:
API | 说明 |
---|---|
play() | 开始或继续播放当前源 |
(3)示例:
/********************************* SoundTest.pro*********************************/
QT += core gui multimedia //添加⾳频模块
/********************************* widget.cpp*********************************/
#include "widget.h"
#include "ui_widget.h"
#include <QSound> //添加⾳频头⽂件
Widget::Widget(QWidget *parent)
:QWidget(parent)
,ui(new Ui::Widget)
{
ui->setupUi(this);
//实例化对象
QSound *sound = new QSound(":/1.wav",this);
connect(ui->btn,&QPushButton::clicked,[=]()
{
sound->play(); //播放
});
}
Widget::~Widget()
{
delete ui;
}
5.2 Qt 视频
(1)在 Qt 中,视频播放的功能主要是通过 QMediaPlayer类 和 QVideoWidget类 来实现。
- 在使用这两个类时要添加对应的模块 multimedia 和 multimediawidgets 。
(2)核心API概览:
API | 说明 |
---|---|
setMedia() | 设置当前媒体源。 |
setVideoOutput() | 将QVideoWidget视频输出附加到媒体播放器。 如果媒体播放器已经附加了视频输出,将更换⼀个新的。 |
(3)示例:
- 首先在 .pro 文件中添加 multimedia 和 multimediawidgets 两个模块;如下图示:
- 代码如下:
/********************************* widget.h *********************************/
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QHBoxLayout> //⽔平布局
#include <QVBoxLayout> //垂直布局
#include <QVideoWidget> //显⽰视频
#include <QMediaPlayer> //播放声⾳
#include <QPushButton> //按钮
#include <QStyle> //设置图标
#include <QFileDialog> //选择⽂件/⽂件夹
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
public slots:
void chooseVideo();
private:
QMediaPlayer *mediaPlayer;
QVideoWidget *videoWidget;
QVBoxLayout *vbox;
//创建两个按钮:选择视频按钮和开播放按钮
QPushButton *chooseBtn,*playBtn;
};
#endif // WIDGET_H
/********************************* widget.cpp*********************************/
#include "widget.h"
#include <QMediaPlayer>
#include <QSlider>
Widget::Widget(QWidget *parent)
:QWidget(parent)
{
//对象实例化
mediaPlayer = new QMediaPlayer(this);
videoWidget = new QVideoWidget(this);
//设置播放画⾯的窗⼝
videoWidget->setMinimumSize(600,600);
//实例化窗⼝布局---垂直布局
this->vbox = new QVBoxLayout(this);
this->setLayout(this->vbox);
//实例化选择视频按钮
chooseBtn = new QPushButton("选择视频",this);
//实例化播放按钮
playBtn = new QPushButton(this);
//设置图标代替⽂件
playBtn->setIcon(this->style()->standardIcon(QStyle::SP_MediaPlay));
//实例化⼀个⽔平布局,将以上控件放⼊⽔平布局中
QHBoxLayout *hbox = new QHBoxLayout;
//添加控件
hbox->addWidget(chooseBtn);
hbox->addWidget(playBtn);
//将播放窗⼝和⽔平布局都添加到垂直布局中
vbox->addWidget(videoWidget);
//布局中添加布局
vbox->addLayout(hbox);
//将选择视频对应的按钮和槽函数进⾏关联
connect(chooseBtn, &QPushButton::clicked,this, &Widget::chooseVideo);
}
void Widget::chooseVideo()
{
//选择视频,返回⼀个播放视频的名字
QString name = QFileDialog::getSaveFileName(this,"选择视频",".","WMV(*.wmv)");
//设置媒体声⾳
mediaPlayer->setMedia(QUrl(name));
//输出视频画⾯
mediaPlayer->setVideoOutput(videoWidget);
//播放
mediaPlayer->play();
}
Widget::~Widget()
{}
6. Qt 系统相关知识点总结
(1)常用类库与功能模块:
- GUI 开发:
- 核心类:
- QWidget:所有控件的基类。
- QMainWindow:主窗口框架(菜单栏、工具栏、状态栏)。
- QDialog:对话框基类。
- 布局管理:QHBoxLayout、QVBoxLayout、QGridLayout。
- 核心类:
- 网络通信:
- TCP/UDP:QTcpSocket、QUdpSocket。
- HTTP:QNetworkAccessManager、QNetworkRequest。
- 串口与蓝牙:QSerialPort、QBluetoothSocket。
- 文件与 I/O:
- 文件操作:QFile(读写文件)、QSaveFile(安全保存)、QTemporaryFile(临时文件)。
- 内存缓存:QBuffer(基于 QByteArray 的 I/O 设备)。
- 多线程与并发:
- 线程管理:
- QThread:线程基类,重写 run() 方法实现任务。
- QtConcurrent:高阶 API(如 QtConcurrent::run())。
- 线程安全:
- 使用信号槽跨线程通信(自动队列化)。
- QMutex、QReadWriteLock 保护共享资源。
- 线程管理:
(2)Qt 是一个功能全面的跨平台框架,其核心优势在于:
- 信号与槽机制:简化对象间通信。
- 对象树与内存管理:降低资源泄漏风险。
- 丰富的模块支持:覆盖 GUI、网络、数据库等场景。
- 跨平台能力:一套代码适配多系统。