Qt系统相关
- 1、Qt事件
- 2、Qt文件
- 3、Qt多线程
- 4、Qt网络
-
- 4.1、QUdpSocket
- [4.2、QTcpServer && QTcpSocket](#4.2、QTcpServer && QTcpSocket)
- 4.3、HttpClient
- 5、Qt音视频
虽然Qt是跨平台的C++开发框架,但是Qt的很多能力都是操作系统提供的,只不过Qt封装了系统的API。下面我们主要要涉及的是:事件、文件操作、多线程编程、网络编程、多媒体。
1、Qt事件
1.1、事件介绍
用户进行的各种操作可能产生信号,所以可以给某个信号绑定槽函数,当信号触发时就能够自动执行对应的槽函数了。事件和信号非常相似,用户进行各种操作也会产生事件 ,程序员同样可以给事件关联上处理函数,当事件触发就可以执行对应的代码。
事件本身是操作系统提供的机制,Qt同样把操作系统的事件机制进行了封装,拿到了Qt中。但是由于事件本身的代码编写起来不是很方便,所以Qt又对事件做了进一步的封装,就得到了信号槽。信号槽就是对事件的进一步封装,事件是信号槽的底层机制。
实际Qt开发过程中,绝大部分和用户之间进行的交互都是通过信号槽来完成的。有些特殊情况下信号槽不一定能搞定。比如某个用户的行为Qt没有提供对应的信号。此时就需要通过重写事件处理函数来手动处理事件的响应逻辑。
事件是应用程序内部或者外部产生的事情或者动作的统称。在 Qt 中使用一个对象来表示一个事件。所有的Qt事件均继承于抽象类QEvent。事件是由系统或者Qt平台本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件是在用户操作时发出,如键盘事件、鼠标事件等,另一些事件则是由系统本身自动发出,如定时器事件。常见的Qt事件如下:


1.2、enter/leaveEvent
让一段代码和某个事件关联起来,当事件触发的时候就会执行这段代码。之前信号槽是直接通过connect来完成关联的。但是对于事件不太一样,事件这里需要让当前的类重写某个事件处理函数。这样就实现了多态,后续事件触发就会执行到当前类重写的虚函数。

下面实现一个样例:创建一个QLabel,然后重写鼠标进入QLabel和离开QLabel的事件,当鼠标进入或离开通过qDebug()输出调试信息。
首先创建一个QLabel,然后为了方便测试,设置右侧属性的frameShape为Box。

接着我们创建一个Label类继承QLabel。

接着我们补充一下.h和.cpp的文件,修改一下构造函数,并重写enterEvent和leaveEvent。

接着还有一个问题,就是我们前面创建的QLabel是通过图形化界面的方式创建出来的,但是应该用的是我们自定义的Label才行,因为我们需要继承然后重写上面两个函数。这里有个方法可以将界面上的QLabel变成Label类。

接着运行程序进行测试,发现输出了信息,说明当前的enterEvent和leaveEvent已经被捕获到了。

前面通过geometry写了一个表白的样例,当鼠标点击拒绝的时候触发clicked信号,执行槽函数将按钮移走。这时候我们就可以通过重写enterEvent事件,当鼠标进入QPushButton内部直接通过move函数将按钮移走。
1.3、鼠标单击事件mousePressEvent
在Qt中,鼠标按下是通过虚函数mousePressEvent()来捕获的。mousePressEvent()函数原型如下:

接着实现一个新的样例:创建一个QLabel,当鼠标在QLabel里面点击可以获取到鼠标的位置信息。
这个可以通过重写mousePressEvent来实现,参数QMouseEvent里面就包含了鼠标的位置信息。这个函数按下左键、右键、鼠标滚轮、甚至侧边的前进后退键都能触发。
还是和之前一样的流程创建一个QLabel,然后自定义Label重写mousePressEvent函数。

重写了之后将QLabel提升为Label即可进行测试。

1.4、鼠标释放事件mouseReleaseEvent
鼠标释放事件是通过虚函数 mouseReleaseEvent()来捕获的。mouseReleaseEvent()函数原型如下:


clicked这样的信号,就相当于是一次鼠标按下事件和一次鼠标释放事件。
1.5、鼠标双击事件mouseDoubleClickEvent
鼠标双击事件是通过虚函数:mouseDoubleClickEvent()来实现的。mouseDoubleClickEvent()函数原型如下:


测试结果如下图所示:

注意:第二次按下时才能识别是双击。有的程序可能是单击有一些逻辑,双击有另一些逻辑。如果我们双击就会触发单击事件,可以就会出BUG。
1.6、鼠标移动事件mouseMoveEvent
鼠标移动事件是通过虚函数:mouseMoveEvent()来实现的。同时为了实时捕获鼠标位置信息,需要通过函数setMouseTracking()来追踪鼠标的位置。mouseMoveEvent()函数原型如下:

前面重写鼠标事件时,都是在自定义的Label中完成的,鼠标只有在Label范围内才能触发事件。我们也可以把这些操作直接放到Widget中,这样鼠标在整个窗口范围内都能触发事件。下面创建一个新的项目来实现:

但是直接重写mouseMoveEvent是看不到输出的信息的。这是因为鼠标移动不同于鼠标按下,随便移动一下鼠标会产生大量的鼠标移动事件,当你捕获事件的时候,尤其是在这里进行一些复杂的逻辑,程序负担就会很重,很容易卡顿。Qt为了保证程序的流畅性,默认情况下不会对鼠标移动进行追踪,鼠标移动的时候不会调用mouseMoveEvent。除非显示告诉Qt要追踪鼠标的位置。
因此我们需要在构造函数中调用setMouseTracking,把鼠标追踪开启。
1.7、滚轮事件wheelEvent
鼠标滚轮事件是通过虚函数:wheelEvent()来实现的。mouseMoveEvent()函数原型如下:

QWheelEvent里面有一个delta函数可以获取到这次事件鼠标滚轮滚动了多远。
重新创建一个项目继承QWidget,然后直接在Widge中重写事件函数。

再比如我们可以在Widget类中定义一个变量,然后每次执行wheelEvent的时候,计算出每次滚动的距离,然后加到这个变量上,这样就能计算出总的滚动距离。
1.8、按键事件keyPressEvent
想要获取到用户的按键,可以通过QShortcut来实现,这是信号槽对事件封装过的获取键盘按键的方式。站在更底层的角度,也可以通过事件机制来获取用户按键,通过重写keyPressEvent实现,函数原型如下:

下面创建一个新项目继承自QWidget,然后在Widget类中重写keyPressEvent函数。

这里我们范围是在整个窗口中,但是我们需要让焦点处于这个窗口按下按键才有事件触发。另外有时候我们可能按下的是组合键,搭配组合键的功能键比如ctrl就单独拎出来了,需要用modifiers获取然后进行判断。
1.9、定时器事件timerEvent
前面介绍过定时器QTimer的用法,QTimer背后是QTimerEvent定时器事件进行支撑的。QObject提供了一个timerEvent函数,需要搭配两个函数来使用:startTimer启动定时器和killTimer关闭定时器。timerEvent函数原型如下:

下面直接实现一个样例:创建一个QLCDNumber,设置初始值为0,然后通过startTimer启动定时器。通过timerEvent事件来修改QLCDNumber的值,最后通过killTimer关闭定时器。

说明:
1、在构造函数中通过startTimer启动定时器,参数为定时器的事件间隔,单位为毫秒,此处1000毫秒就表示每个1秒触发一次定时器事件。并且该函数有一个返回值timerId,用来表示定时器的,类似于Linux中的文件描述符fd。
2、需要将timerId保存下来,后续timerEvent触发的时候判断是否是想要的定时器触发,因为可能启动多个定时器。
3、使用timerEvent比QTimer还是要复杂一些,需要手动管理timerId,还需要区分是哪个定时器触发了eventTimer。所以后续直接使用QTimer即可。
1.10、窗口相关事件
主要有两个事件:moveEvent窗口移动事件和resizeEvent窗口大小改变事件。函数原型如下:


QMoveEvent可以通过oldPos和pos获取老的坐标和新的坐标。


QResizeEvent可以通过oldSize和size获取旧的和新的尺寸。
下面直接实现样例:

1.11、事件分发器
在Qt中,事件分发器(Event Dispatcher) 是一个核心概念,用于处理GUI应用程序中的事件。事件分发器负责将事件从一个对象传递到另一个对象,直到事件被处理或被取消。每个继承自QObject类或QObject类本身都可以在本类中重写bool event(QEvent *e) 函数,来实现相关事件的捕获和拦截。
在Qt中,我们发送的事件都是传给了QObject对象,更具体点是传给了QObject对象的event()函数。所有的事件都会进入到这个函数里面,那么我们处理事件就要重写这个event()函数。event()函数本身不会去处理事件,而是根据事件类型(type值)调用不同的事件处理函数。事件分发器就是工作在应用程序向下分发事件的过程中,如下图:

如上图,事件分发器用于分发事件。在此过程中,事件分发器也可以做拦截操作。事件分发器主要是通过bool event(QEvent *e) 函数来实现。其返回值为布尔类型,若为 ture,代表拦截,不向下分发。
通俗的说可以总结为以下两点:
1、在Qt中,事件触发后不会直接调用重写的具体事件处理函数,而是先进入event(QEvent*) 这一统一入口。
Qt默认实现的event()会根据event->type()判断事件类型,并自动调用对应的具体事件处理函数(如mousePressEvent、keyPressEvent 等)。
当我们重写了某个具体事件处理函数时,Qt会在event()中自动调用我们重写的版本。同时,一次事件只对应一个QEvent对象,也只会触发一次event()调用;如果有多个事件发生,就会多次进入event(),每次处理一个事件。
2、我们也可以重写event()函数,在其中通过event->type()判断当前触发的事件。
对于我们关心的事件,可以在event()中自行处理并返回true,表示事件已被处理,从而实现拦截。
对于不关心的事件,调用并返回父类的event(),由Qt默认的事件分发机制继续处理这些事件。
通过这种方式,可以在event()中实现对事件的选择性拦截与放行。
下面通过代码实现一个样例:创建一个新项目,然后在Widget类中重写event和mousePressEvent函数,然后在event中拦截该事件处理函数。


我们重写了mousePressEvent函数和event函数,在event里面我们对鼠标按下事件做了拦截,因此当我们在窗口中按下鼠标左键,事件触发先进入event,然后被if语句拦截了直接返回了,不会在执行mousePressEvent了。如果我们再重写其他事件,当其他事件触发也会进入event,但是event并没有对其他事件做拦截,所以最终会调用父类的event函数进行默认的处理,父类的event函数就是根据我们重写的事件处理函数,然后对type()进行判断直接调用重写的虚函数。
1.12、事件过滤器
在 Qt 中,经常会遇到这样的需求:一个对象想要拦截或修改另一个对象的事件。虽然我们可以通过重写 event()来做到这一点,但由于event()是protected的,这意味着我们必须继承目标对象的类;如果控件很多,就需要写很多子类,而且重写event()还容易出错。为了解决这个问题,Qt 提供了事件过滤器机制,允许我们在不修改目标对象的前提下,对其事件进行拦截或处理。
事件过滤器是在应用程序分发到 event事件分发器 之前,再做一次更高级的拦截。如下图示:

事件过滤器的一般使用步骤:
1、安装事件过滤器installEventFilter;
2、重写事件过滤器函数:eventfilter() 。
总结如下三点:
1、在Qt中,事件在进入目标对象的event(QEvent*)函数之前,会先经过事件过滤器(eventFilter(QObject*, QEvent*))。事件过滤器允许一个对象在不继承目标对象的前提下,提前查看、处理或拦截另一个对象的事件。
当事件触发时,Qt会依次调用已安装的事件过滤器的eventFilter()函数;若某个事件过滤器返回true,表示该事件已被处理,事件将不再继续传递,也不会进入目标对象的event()函数。每一个事件同样只对应一次eventFilter()调用,若发生多个事件,则会多次调用 eventFilter(),每次处理一个事件。
2、我们可以通过installEventFilter()为某个对象安装事件过滤器,在事件过滤器的eventFilter()函数中通过event->type() 判断事件类型。
对于我们关心的事件,可以在eventFilter()中自行处理并返回true,从而实现对事件的拦截;
对于不关心的事件,返回false或调用父类的eventFilter(),事件将继续传递,进入目标对象的event()函数,并由Qt默认的事件分发机制进一步处理。
通过事件过滤器机制,可以在不修改目标对象、不重写其event()函数的情况下,实现对事件的统一监听、修改或拦截。
3、当事件过滤器安装在某个父对象上时,该父对象自身的事件,以及它所有子对象的事件,都会在进入各自的event()之前,先进入该父对象的eventFilter()。
下面实现一个样例:创建一个新项目,创建一个QLabel类,然后创建一个Label类继承自QLabel,在Label类中重写mousePressEvent和event,在函数中分别通过qDebug输出信息。接着在父元素Widget中安装事件过滤器并重写eventFilter,判断条件输出信息。
最终我们运行程序触发事件,看看输出的信息是属于哪个函数内部,如果是eventFilter,说明实现了事件过滤的功能,事件不会再进入到event中。

首先实现Label类,然后将QLabel提升为Label。

在Widget中重写eventFilter并监听子控件。

运行结果显示在eventFilter输出了调试信息,说明eventFilter已经拦截了鼠标点击事件,后续不会再进入event了。
2、Qt文件
文件操作是应用程序必不可少的部分。Qt作为一个通用开发库,提供了跨平台的文件操作能力。Qt提供了很多关于文件的类,通过这些类能够对文件系统进行操作,如文件读写、文件信息获取、文件复制或重命名等。
2.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设备类的继承关系如下图所示:

- QFile是用于文件操作和文件数据读写的类,使用QFile可以读写任意格式的文件。
- QSaveFile是用于安全保存文件的类。使用QSaveFile保存文件时,它会先把数据写入一个临时文件,成功提交后才将数据写入最终的文件。如果保存过程中出现错误,临时文件里的数据不会被写入最终文件,这样就能确保最终文件中不会丢失数据或被写入部分数据。 在保存比较大的文件或复杂格式的文件时可以使用这个类,例如从网络上下载文件等。
- QTemporaryFile是用于创建临时文件的类。使用函数QTemporaryFile::open()就能创建一个文件名唯一的临时文件,在QTemporaryFile对象被删除时,临时文件被自动删除。
- QTcpSocket和QUdpSocket是分别实现了TCP和UDP的类。
- QSerialPort是实现了串口通信的类,通过这个类可以实现计算机与串口设备的通信。
- QBluetoothSocket是用于蓝牙通信的类。手机和平板计算机等移动设备有蓝牙通信模块,笔记本电脑一般也有蓝牙通信模块。通过QBluetoothSocket类,就可以编写蓝牙通信程序。如编程实现笔记本电脑与手机的蓝牙通信。
- QProcess类用于启动外部程序,并且可以给程序传递参数。相当于是对fork/exec操作的封装。
- QBuffer以一个QByteArray对象作为数据缓冲区,将QByteArray对象当作一个I/O设备来读写。
2.2、QFile
首先对于QFile的构造函数需要传入一个文件的路径和一个父元素指针。

1、打开文件,open函数,

可以看到上面两个open函数的第一个参数,一个是搭配C语言来打开,另一个是搭配文件描述符fd来打开,不过会麻烦一些。

常用的是上面这个从父类继承的open函数,mode表示打开的方式,可以设置只读、只写、追加写等等打开方式。当构造函数指定路径之后,直接通过这个函数打开即可。枚举类型如下图所示。

2、读数据:QFile类中提供了多个方法用于读取文件内容;如 read()、readAll()、readLine()等。

QByteArray是Qt针对于字符数组又做了一层封装。QByteArray可以很方便的转换成QString。
3、写数据:QFile类中提供了多个方法用于往文件中写内容;如 write()、writeData()等。

4、关闭文件:文件使用结束后必须用函数close()关闭文件。

关闭的本质是要是要释放文件描述符表中的表项。文件描述符是存在上限的,如果一直打开不关闭就会使文件描述符占满,后续就无法打开文件了。
下面实现一个样例:创建一个菜单栏,有一个文件菜单,里面有打开和保存两个菜单项,然后中心区域一个QPlainTextEdit,当打开文件的时候可以把文件的内容读取到编辑框中,当保存文件的时候可以把编辑框中的内容保存到文件中。同时下方状态栏会显示文件的路径。
cpp
// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QPlainTextEdit>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
void handle1();
void handle2();
private:
Ui::MainWindow *ui;
QPlainTextEdit* edit;
};
#endif // MAINWINDOW_H
cpp
// mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QFileDialog>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
// 创建菜单栏
QMenuBar* menuBar = new QMenuBar();
this->setMenuBar(menuBar);
// 创建菜单
QMenu* menu = new QMenu("文件");
menuBar->addMenu(menu);
// 创建菜单项
QAction* action1 = new QAction("打开");
QAction* action2 = new QAction("保存");
action1->setIcon(QIcon(":/open.png"));
action2->setIcon(QIcon(":/save.png"));
menu->addAction(action1);
menu->addAction(action2);
// 创建纯文本编辑框
edit = new QPlainTextEdit();
this->setCentralWidget(edit);
// 连接信号槽
connect(action1, &QAction::triggered, this, &MainWindow::handle1);
connect(action2, &QAction::triggered, this, &MainWindow::handle2);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::handle1()
{
// 1.用户选择文件
QString path = QFileDialog::getOpenFileName(this);
// 2.把文件名显示到状态栏中
QStatusBar* statusBar = this->statusBar();
statusBar->showMessage(path);
// 3.打开文件
QFile file(path);
bool ret = file.open(QFile::ReadOnly);
if (!ret) {
statusBar->showMessage(path + "打开失败!");
return;
}
// 4.读取文件
QString text = file.readAll();
// 5.关闭文件
file.close();
// 6.将读取的数据在编辑框上显示
edit->setPlainText(text);
}
void MainWindow::handle2()
{
// 1.先弹出保存文件对话框
QString path = QFileDialog::getSaveFileName(this);
// 2.把文件名显示到状态栏中
QStatusBar* statusBar = this->statusBar();
statusBar->showMessage(path);
// 3.打开文件
QFile file(path);
bool ret = file.open(QFile::WriteOnly);
if (!ret) {
statusBar->showMessage(path + "打开失败!");
return;
}
// 4.写入文件
const QString& text = edit->toPlainText();
file.write(text.toUtf8());
// 5.关闭文件
file.close();
}
说明:
1、readAll函数的返回值是一个QByteArray,但是我们用了QString去接收,这是因为QString里面提供了QByteArray构造QString的构造函数。所以支持QByteArray转换成QString。
2、读取数据的时候用QString接收要保证打开的文件是文本文件,如果是二进制数据QString就不合适了。
3、write函数的参数也是QByteArray,这里可以通过QString提供的toUtf8()函数进行转换。
2.3、QFileInfo
**QFileInfo是Qt提供的一个用于获取文件和目录信息的类,如获取文件名、文件大小、文件修改日期等。QFileInfo类中提供了很多的方法,常用的有: **

下面实现一个样例:提供一个按钮,当点击按钮的时候弹出获取文件名的对话框,然后根据获取的路径构造QFileInfo对象,然后拿到文件的属性信息。

3、Qt多线程
3.1、QThread
Qt多线程和Linux中的线程本质是一个东西,Linux学过的多线程API是由pthread库提供的。那么Qt也针对了不同系统的线程API进行了封装。
在Qt中,用QThread来表示线程。想要创建线程需要创建出这样类的实例,创建线程的时候我们都知道要指明线程的入口函数,在Qt中,我们是通过创建一个QThread的子类,然后重写其中的run函数,实现C++中的多态,从而达到指定入口函数的目的。而C++标准库中的std::thread就是通过指明回调函数的方式,因为有的大佬认为C++多态会带来运行时的额外开销(需要多一步查找虚函数)。
下面是QThread常用的API:

下面实现一个样例:之前基于定时器实现了倒计时这样的功能,现在我们通过创建一个新线程来实现,新线程循环10次,每次sleep 1秒钟,然后通过一个自定义的信号来通知主线程,然后主线程对控件上的数值进行修改。

首先是继承QThread实现一个Thread类,然后重写run方法。同时前面说过,由于线程安全问题,Qt不允许多个线程同时对界面进行修改,所以Qt直接禁止了其他线程修改界面,只能通过主线程来修改。因此此处我们在提供一个notify信号,每次循环内部休眠1秒之后就触发信号。这样在主线程中就能收到信号,然后再去执行对应的槽函数修改value。

然后我们在Widget类中实现notify信号对应的槽函数handle,并声明一个Thread对象。接着在构造函数中连接信号槽,然后启动线程即可。后续线程开始执行,每隔一秒钟就会触发一次信号,这时候主线程执行以下handle函数的逻辑即可实现倒计时功能。
之前学习的多线程主要是站在服务器开发的角度来看待的。当时谈到的多线程,最主要目的是充分利用CPU多核资源。对于客户端来说,多线程仍然很有意义,但是侧重点就不同了,客户端多线程主要还是用于执行一些耗时的等待IO的操作,避免主线程卡死,避免对用户造成一些不良体验。比如客户端要上传/下载一个很大的文件,可能需要20分钟,这种密集的IO会使程序阻塞、挂起,一旦进程被挂起了意味着用户进行任何的操作都不会有响应。所以使用多线程来处理这种密集的IO操作,要挂起也是挂起新的线程,主线程继续事件循环处理用户的各种操作,此时主线程仍然可以响应用户的操作。

3.2、QMutex
互斥锁是一种保护和防止多个线程同时访问同一对象实例的方法,在Qt中,互斥锁主要是通过QMutex类来处理。


这里的QMutex和QMutexLocker就类似于std::mutex和std::lock_guard。
下面我们就通过一个最经典的样例来演示:创建两个线程,这两个线程分别对同一个变量进行++操作,最后看结果是否符合我们的预期。

首先是继承QThread类然后重写run函数,同时我们需要在两个Thread对象里面访问同一个变量,所以定义了一个静态的count,也定义一把锁后面使用。然后run函数里面就是不加以保护直接对count++操作。

接着在Widget类中声明这两个线程,然后构造函数中启动线程,并通过wait函数阻塞等待线程执行完毕。最后输出结果。我们发现正常结果应该是十万,但是很明显由于没有对count进行保护,所以导致数据不一致。

所以我们修改代码,在count++之前先加锁。当然也可以直接手动加锁然后再解锁,但是如果代码中间直接返回或者抛异常,再或者忘记释放锁了就会有问题,所以一般我们都会用RAII的思想,就类似std::lock_guard,通过构造函数自动加锁,出了作用域析构函数自定解锁。
接着是读写锁:

cpp
QReadWriteLock rwLock;
//在读操作中使⽤读锁
{
QReadLocker locker(&rwLock); //在作⽤域内⾃动上读锁
//读取共享资源
//...
} //在作⽤域结束时⾃动解读锁
//在写操作中使⽤写锁
{
QWriteLocker locker(&rwLock); //在作⽤域内⾃动上写锁
//修改共享资源
//...
} //在作⽤域结束时⾃动解写锁
3.3、QWaitCondition
在多线程编程中,假设除了等待操作系统正在执行的线程之外,某个线程还必须等待某些条件满足才能执行,这时就会出现问题。这种情况下,线程会很自然地使用锁的机制来阻塞其他线程,因为这只是线程的轮流使用,并且该线程等待某些特定条件,人们会认为需要等待条件的线程,在释放互斥锁或读写锁之后进入了睡眠状态,这样其他线程就可以继续运行。当条件满足时,等待条件的线程将被另一个线程唤醒。
在Qt中,专门提供了QWaitCondition类 来解决像上述这样的问题。
特点:QWaitCondition是Qt框架提供的条件变量类,用于线程之间的消息通信和同步。
用途:在某个条件满足时等待或唤醒线程,用于线程的同步和协调。
cpp
QMutex mutex;
QWaitCondition condition;
//在等待线程中
mutex.lock();
//检查条件是否满⾜,若不满⾜则等待
while (!conditionFullfilled())
{
condition.wait(&mutex); //等待条件满⾜并释放锁
}
//条件满⾜后继续执⾏,执行结束后解锁
mutex.unlock();
//在改变条件的线程中
mutex.lock();
//改变条件
changeCondition();
condition.wakeAll(); //唤醒等待的线程
mutex.unlock();
这里就再说明一下:
1、条件变量相当于是锁+等待队列,然后需要判断条件,而判断条件就是访问临界资源所以需要加锁,另外如果条件不满足需要到对应的条件变量下等待,此时需要先释放锁再去等待,所以通过这两方面来说,必须先获取到锁。
2、判断条件写成while循环是为了防止伪唤醒。比如这里用的是if语句判断,然后一次唤醒了多个线程,但是实际上只有一个资源,第一个线程拿到锁消费了之后就会释放锁,此时要是再被唤醒的线程拿到锁,由于是if所以不会再次判断是否满足条件,直接会往下执行消费,那么由于前面只剩下的一个资源已经被第一个线程消费了,所以此时就会出现问题,因此需要用while循环来防止伪唤醒。
3.4、QSemaphore
有时在多线程编程中,需要确保多个线程可以相应的访问一个数量有限的相同资源。例如,运行程序的设备可能是非常有限的内存,因此我们更希望需要大量内存的线程将这一事实考虑在内,并根据可用的内存数量进行相关操作,多线程编程中类似问题通常用信号量来处理。信号量类似于增强的互斥锁,不仅能完成上锁和解锁操作,而且可以跟踪可用资源的数量。
特点:QSemaphore是Qt框架提供的计数信号量类,用于控制同时访问共享资源的线程数量。
用途:限制并发线程数量,用于解决一些资源有限的问题。
cpp
QSemaphore semaphore(2); //同时允许两个线程访问共享资源
//在需要访问共享资源的线程中
semaphore.acquire(); //尝试获取信号量,若已满则阻塞
//访问共享资源
//...
semaphore.release(); //释放信号量
//在另⼀个线程中进⾏类似操作
4、Qt网络
网络变成是操作系统提供的一套API(socket API),很遗憾C++标准库并没有提供网络库,之前写的是Linux平台下的系统接口。那么使用Qt的网络库需要现在.pro文件中加入network模块,我们之前学习的各种控件都是包含在QtCore模块中的。
那么Qt为什么要划分模块呢?因为Qt本身是一个非常庞大的框架,如果把所有的Qt功能都放到一起,那么就算写一个简单的hell world生成的课程行程序也会很大,但是包含了很多我们没有用的功能。而且如果是在嵌入式系统上内存就更小了。所以需要模块化处理,这样需要用到什么就引入进来,只有引入进来的模块才会参与编译。
4.1、QUdpSocket
主要的类有QUdpSocket和QNetworkDatagram。


QNetworkDatagram表示udp数据报,当我们读取数据或者写数据都是通过这个参数来实现的。另外我们发现QUdpSocket有一个readyRead信号,我们Linux系统的接口默认读取数据或者写数据是阻塞的,所以如果对方没有发送数据我们就得阻塞住一直等,但是Qt采用信号驱动的方式,当socket接收到数据就会触发readyRead这个信号,此时我们就可以在对应的槽函数中进行处理。
因此基于信号槽,我们就天然的达成了事件驱动这样的网络编程方式。
下面我们实现一个UDP回显服务器和客户端,服务器接受客户端发送的数据,然后直接原样写回去。
1、首先创建一个新项目,然后在.pro文件中引入network模块。并编辑ui文件添加一个QListWidget,用来显示通信的信息。


2、在Widget类中声明一个QUdpSocket指针变量,后续在构造函数中实例化。然后提供一个槽函数还有一个处理数据的函数。

2.1、这里最重要的点就是在构造函数中需要先连接信号槽然后再进行绑定。因为如果先完成绑定然后再连接信号槽,这样就有可能在完成绑定之后连接信号槽之前就有客户端请求,此时可能就读不到数据了。
2.2、另外我们也需要对绑定的返回值进行判断,因为端口可能被占用了,所以是有可能绑定失败的,我们需要对返回的bool类型判断,如果绑定失败我们就通过QMessageBox弹出一个对话框信息。
2.3、QNetworkDatagram里面的data函数可以获取到数据,但是返回的是QByteArray,所以上面的代码实际上是QString支持了QByteArray的构造函数,所以支持了转换。
2.4、客户端的信息,也就是源IP地址和源端口,可以通过QNetwrokDatagram获取。
2.5、我们只是为了实现一个UDP的回显服务器,所以这里的process函数直接将request返回了,如果是一个商业服务器,这里的处理逻辑可能是很复杂的。
3、创建客户端项目,Qt Creator是支持同时打开多个项目的,所以可以直接创建一个新项目。同时记得给新项目添加network模块。

3.1、虽然打开了两个项目,但是我们仔细看会发现我们新建的UdpClient项目的字体是更粗的。这时候运行项目的时候就是编译构建UdpClient。那如果我们需要修改为UdpServer,运行的时候就运行UdpServer这个项目,需要对着项目右击,修改为活动项目。

3.2、如果我们想直接运行UdpServer这个项目,也可以右击项目,直接点击运行。如下:

4、先给客户端添加一些控件,如下图所示,这样就可以在QListWidget中添加我们发送的内容和服务器回复的内容。然后在输入框可以输入我们想要发送的内容点击发送按钮进行发送。

5、在客户端的Widget类也是添加一个指针变量、槽函数、处理函数。在构造函数中创建QUdpSocket对象并连接信号槽。当用户点击时发送数据并添加到QListWidget中,当收到数据就转换成QString添加到QList中。

6、下面进行测试,我们启动一个服务器,然后找到客户端构建的临时文件build-xxx,然后找到debug目录下的可执行程序,我们运行两个客户端。然后进行通信。

可以看到多个客户端和服务器进行通信,客户端的ip地址相同但是端口不同。
问题一:能否把现在写的UDP服务器放到Linux服务器上运行呢?
大概率是不行的,这取决于你的Linux服务器是否安装了图形化界面。Qt程序需要依赖图形化界面来运行,而Linux服务器一般是命令行的,没有图形化界面,所以需要手动安装图形化界面才能运行。
但其实作为一个服务器本身就是不需要图形化界面的,此处只是为了演示,也不可能用Qt来写服务器。
问题二:能否用现在写的UDP客户端去连之前在Linux平台上写的UDP服务器呢?
这是完全没问题的,因为网络协议是共通的,底层都是一样的,就像你可以用windows系统的socket去连接Linux服务器。
4.2、QTcpServer && QTcpSocket
核心类是两个:QTcpServer和QTcpSocket。


下面实现TCP回显服务器和客户端,那么界面设计还是类似UDP那样。
1、创建TcpServer项目,引入network模块,添加QListWidget控件,实现代码逻辑。

声明一个QTcpServer指针,在构造函数中创建实例,然后先连接信号槽再进行绑定监听,这里和前面UDP是一样的。然后当建立连接后可以获取连接会触发newConnection信号,在槽函数中获取到QTcpSocket,然后设置两个信号槽进行处理即可。这里为了方便我们直接用lambda表达式。
问题1:上面的读取数据的代码其实并不完善,我们知道TCP是面向字节流的,所以可能存在粘包问题,所以我们需要自己约定好应用层协议格式来处理粘包问题,不过我们这里只是为了演示API,所以写的简单一些。
问题2:每次获取连接都有一个QTcpSocket对象,如果客户端断开连接我们不释放,就会导致clientSocket越来越多,一方面会占用内存导致内存泄漏,另一方面会占用文件描述符表导致文件描述符泄漏。由于文件描述符是有上限的,所以这个问题就更严重了。因此我们要在客户端断开连接的时候释放掉QTcpSocket。这里我们可以直接在最后的时候delete,但是不建议这么写,主要有下面两个理由:一个是我们必须保证delete的时候是在最后一步,不然如果delete之后又继续使用clientSocket就会出错。另一个是我们不一定能执行到最后一步的delete,万一提前返回了或者抛出异常了,因此我们用QTcpServer提供的函数deleteLater()。这个操作并不会立即销毁QTcpServer,而是告诉Qt在下一轮事件循环中在进行上述销毁操作。
2、创建TcpClient项目实现客户端。

首先还是实现客户端的界面,这里我们用一个水平布局管理器管理QLineEdit和QPushButton,然后再和QListWidget加入到垂直布局管理器中。设置垂直布局管理器的拉伸系数为5:1,然后设置QLineEdit和QPushButton的垂直拉伸策略为Expanding。
3、构造函数中连接readyrRead信号,然后发起连接,这里需要注意connectToHost是非阻塞的,之前学的LinuxAPI是阻塞的,等待三次握手完成才会返回,所以这里我们还调用了waitForConnected等待连接完成返回。

4、下面进行测试,启动一个服务端,启动三个客户端。

之前在写LinuxTcpSocket的时候,我们的服务器是只能一对一处理单个客户端的,只有等这个客户端断开连接了,此时服务器才能继续accept获取下一个连接进行通信。如果想要实现服务器能够处理多个客户端,同时跟多个客户端通信,需要创建多进程或多线程,再或者可以使用IO多路复用。但是在Qt这里我们发现当前的TCP服务器就可以直接跟多个客户端进行通信。
之前的问题本质在于双重循环,外层循环accept获取连接,内层循环跟客户端通信,所以客户端不断开那么服务器就无法获取新连接。引入多线程本质就是将双重循环化简成两个独立的循环。Qt程序一个循环都没写,因为Qt是基于信号槽来驱动的,很好的简化了程序。
4.3、HttpClient
进行Qt开发时,和服务器之间的通信很多时候也会用到HTTP协议。Qt也提供HTTP客户端,但没有提供HTTP服务器,因为Qt本身就是客户端开发框架。
关键类主要是三个:QNetworkAccessManager,QNetworkRequest,QNetworkReply。




QVariant是一个类型可变的值,类似于C语言中的void*。
下面实现样例:界面布局我们就类似前面的客户端布局,提供一个QPlaintTextEdit、QLineEdit和一个发送按钮。然后这里治所用QPlaintTextEdit是因为这个是纯文本的形式,如果用QTextEdit这是天然支持HTML格式的,所以会对HTML进行渲染,就无法看到原始的数据了。而且QTextEdit背后做了很多工作,如果HTML比较大就会卡顿。因为我们要向网站发起一个HTTP请求获取HTML数据。

点击按钮后就是获取URL,然后通过URL构造HTTP请求,接着发送请求,然后获取响应并处理,销毁响应数据即可。这里的get方法只是负责发出去请求,并不负责等待响应回来,所以我们需要连接QNetworkReply里面的finished信号。
5、Qt音视频
5.1、Qt音频
在Qt中,音频主要是通过QSound类来实现。但是需要注意的是QSound类只支持播放wav格式的音频文件。也就是说如果想要添加音频效果,那么首先需要将非wav格式的音频文件转换为wav格式。

下面实现一个简单样例,点击按钮可以触发一个音频播放。那么我们需要先到网上找一个简单的wav音频文件。接着我们也需要引入音频文件,引入方式就是通过qrc引入。

那么这边我是自己准备了一个叮一声的音频文件。

然后通过文件路径构造一个QSound对象,然后通过play函数进行播放。
5.2、Qt视频
在Qt中,视频播放的功能主要是通过QMediaPlayer类和QVideoWidget类来实现。在使用这两个类时要添加对应的模块multimedia和multimediawidgets。
核心API如下:

这里就不再演示,有兴趣自行了解。