Qt 系统相关
- [1 Qt 事件](#1 Qt 事件)
-
- 1.1事件介绍
- 1.2事件的处理
- [1.3 鼠标事件](#1.3 鼠标事件)
-
- [1.3.1 鼠标单击事件](#1.3.1 鼠标单击事件)
- [1.3.2 鼠标释放事件](#1.3.2 鼠标释放事件)
- [1.3.3 鼠标双击事件](#1.3.3 鼠标双击事件)
- [1.3.4 鼠标移动事件](#1.3.4 鼠标移动事件)
- [1.3.5 鼠标滚轮事件](#1.3.5 鼠标滚轮事件)
- [1.4 按键事件](#1.4 按键事件)
-
- 1.4.1单个按键
- [1.4.2 组合按键](#1.4.2 组合按键)
- [1.5 定时器](#1.5 定时器)
-
- [1.5.1 QTimerEvent 类](#1.5.1 QTimerEvent 类)
- [2 Qt 文件](#2 Qt 文件)
-
- [2.1 Qt 文件概述](#2.1 Qt 文件概述)
- [2.2 输入输出设备类](#2.2 输入输出设备类)
- [2.3 文件读写类](#2.3 文件读写类)
- [2.4 文件和目录信息类](#2.4 文件和目录信息类)
- [3 多线程](#3 多线程)
-
- [3.1 多线程概述](#3.1 多线程概述)
- [3.2 多线程的使用条件](#3.2 多线程的使用条件)
- [3.3 创建线程的方法](#3.3 创建线程的方法)
-
- [3.3.1 使用方法一创建线程](#3.3.1 使用方法一创建线程)
- [3.4 QThread 常用函数](#3.4 QThread 常用函数)
- [3.5 线程同步工具](#3.5 线程同步工具)
-
- [3.5.1 互斥锁](#3.5.1 互斥锁)
- [3.5.2 信号量](#3.5.2 信号量)
- [3.5.3 条件变量](#3.5.3 条件变量)
- [4 Qt 网络](#4 Qt 网络)
-
- [4.1 UDP Socket](#4.1 UDP Socket)
-
- [4.1.1 核心 API 概览](#4.1.1 核心 API 概览)
- [4.1.2 回显服务器](#4.1.2 回显服务器)
- [4.1.3 回显客户端](#4.1.3 回显客户端)
- [4.2 TCP Socket](#4.2 TCP Socket)
-
- [4.2.1 核心 API 概览](#4.2.1 核心 API 概览)
- [4.2.2 回显服务器](#4.2.2 回显服务器)
- [4.2.3 回显客户端](#4.2.3 回显客户端)
- [4.3 HTTP Client](#4.3 HTTP Client)
-
- [4.3.1 核心 API](#4.3.1 核心 API)
- [4.3.2 代码示例](#4.3.2 代码示例)
- [5 Qt 音视频](#5 Qt 音视频)
-
- [5.1 音频](#5.1 音频)
1 Qt 事件
1.1事件介绍
事件是应用程序内部或者外部产生的事情或者动作的统称 。在Qt中使用一个对象来表示一个事件 。所有的Qt事件均继承于抽象类QEvent。事件是由系统或者Qt平台本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一-个相应的事件。-些事件 是在用户操作时发出,如键盘事件、鼠标事件等,另一些事件则是由系统本身自动发出,如定时器事件。常见的Qt事件如下:
常见事件描述
1.2事件的处理
事件处理一般常用的方法为:重写相关的Event函数 。
在Qt中,几乎所有的Event函数都是虚函数,所以可以重新实现。如:在实现鼠标的进入和离开事件时,直接重新实现enterEvent()和leaveEvent()即可。enterEvent() 和leaveEvent() 函数原型如
下:
代码示例:重写 enterEvent 函数
1、新建Qt项目,基类选择QWidget,同时勾选UI界面文件,如下图示;
2、设计UI文件,如下图示;
3、在项目中新添加一-个类: MyLabel;
先选中项目名称QEvent,点击鼠标右键,选择add new...,弹出如下对话框:
4、选择: Choose
弹出如下界面:
5、此时项目中会新添加以下两个文件:
6、在帮助文档中查找对应的内容;
7、点击"显示"之后,出现如下内容:
8、复制enterEvent();,粘贴在项目文件"mylabel.h"中;
9、重写enterEvent()方法;
10、在UI文件中选中Label,右键----提升为...
11、 当点击"提升为..."之后,弹出如下对话框:
12、修改基类
13、执行效果如下:当鼠标进入设计好的标签之后,就会在应用程序输出栏中打印:鼠标进入
1.3 鼠标事件
在Qt中,鼠标事件是用QMouseEvent类来实现的。当在窗口中按下鼠标或者移动鼠标时,都会产生鼠标事件.利用QmouseEvent类可以获取鼠标的哪个键被按下了以及鼠标的当前位置等信息.在QT帮助文档中
查找QMouseEvent类如下图示:
1.3.1 鼠标单击事件
在Qt中,鼠标按下是通过虚函数mousePressEvent()来捕获的。mousePressEvent() 函数原型如
下:
cpp
[virtual protected] void QWidget::mousePressEvent(QMouseEvent * event)
鼠标左右键及滚的表示如下:
Qt::LeftButton鼠标左键
Qt::RightButton鼠标右键
Qt::MidButton鼠标滚轮
代码示例:鼠标单击事件
1、在 widget 中声明鼠标按下事件
在 widget.cpp 中实现 mousePressEvent 函数
cpp
void Widget::mousePressEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
{
qDebug() << "鼠标左键被按下";
}
else if(event->button() == Qt::RightButton)
{
qDebug() << "鼠标右键被按下";
}
else if(event->button() == Qt::MiddleButton)
{
qDebug() << "鼠标中键被按下";
}
}
运行程序,查看效果
1.3.2 鼠标释放事件
鼠标释放事件是通过虚函数mouseReleaseEvent()来捕获的。mouseReleaseEvent() 函数原型如
下:
cpp
[virtual protected] void QWidget::mouseReleaseEvent(QMouseEvent * event)
代码示例:鼠标释放事件
使用方法和 鼠标单击事件 一样
cpp
void Widget::mouseReleaseEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
{
qDebug() << "鼠标左键被释放";
}
else if(event->button() == Qt::RightButton)
{
qDebug() << "鼠标右键被释放";
}
else if(event->button() == Qt::MiddleButton)
{
qDebug() << "鼠标中键被释放";
}
}
1.3.3 鼠标双击事件
鼠标双击事件是通过虚函数: mouseDoubleClickEvent() 来实现的。mouseDoubleClickEvent()
函数原型如下:
cpp
[virtual protected] void QWidget::mouseDoubleClickEvent(QMouseEvent * event)
代码示例:鼠标双击事件
cpp
void Widget::mouseDoubleClickEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
{
qDebug() << "鼠标左键被双击";
}
else if(event->button() == Qt::RightButton)
{
qDebug() << "鼠标右键被双击";
}
else if(event->button() == Qt::MiddleButton)
{
qDebug() << "鼠标中键被双击";
}
}
1.3.4 鼠标移动事件
鼠标移动事件是通过虚函数: mouseMoveEvent() 来实现的。同时为了实时捕获鼠标位置信息,需要通过函数setMouseTracking()来追踪鼠标的位置。mouseMoveEvent()函数原型如下:
cpp
[virtual protected] void QWidget::mouseMoveEvent(QMouseEvent * event)
setMouseTracking()函数原型如下:
cpp
void setMouseTracking(bool enable)
说明:
setMouseTracking()函数默认是false,需要设置为true,才能实时捕获鼠标位置信息
代码示例:鼠标移动事件
1.3.5 鼠标滚轮事件
在Qt中,鼠标滚轮事件是通过QWheelEvent类来实现的。滚轮滑动的距离可以通过angleDelta()函数获
取。angelDelta() 函数原型如下:
cpp
QPoint QWheelEvent::angleDelta() const
其中返回值代表滚轮滑动的距离。正数表示滚轮相对于用户向前滑动,负数表示滚轮相对于用户向后滑动。
cpp
void Widget::wheelEvent(QWheelEvent *event)
{
QPoint angle = event->angleDelta();//每次垂直方向+-120
qDebug() << angle.y();
}
1.4 按键事件
Qt中的按键事件是通过QKeyEvent类来实现的。当键盘上的按键被按下或者被释放时,键盘事件便会触发。在帮助文档中查找QKeyEvent类如下:
查找按键事件中所有的按键类型:在帮助文档中输入: Qt::Key, 如下图:
1.4.1单个按键
示例:当某个按键被按下时,输出:某个按键被按下了;
1、新建项目,在头文件"widget.h"中声明虚函数keyPressEvent();如下图:
2、在"widget.cpp"文件中重写keyPressEvent()虚函数;
1.4.2 组合按键
在Qt文档中搜索: Qt::KeyboardModifier, 如下图示:
Qt::KeyboardModifier中定义了在处理键盘事件时对应的修改键。在Qt中,键盘事件可以与修改键一起使用,以实现一些复杂的交互操作。KeyboardModifier 中修改键的具体描述如下:
示例:
1.5 定时器
Qt中在进行窗口程序的处理过程中,经常要周期性的执行某些操作,或者制作一些动画效果,使用定时器就可以实现。所谓定时器就是在间隔一定时间后, 去执行某一一个任务。定时器在很多场景下都会使用到,如弹窗自动关闭之类的功能等。
Qt中的定时器分为QTimerEvent和QTimer这2个类。
- QTimerEvent类用来描述一个定时器事件。在使用时需要通过startTimer()函数来开启一个定时器,这个函数需要输入一个以毫秒为单位的整数作为参数来表明设定的时间,它返回的整型值代表这个定时器。当定时器溢出时(即定时时间到达)就可以在timerEvent()函数中获取该定时器的编号来进行相关操作。
- QTimer类来实现一个定时器,它提供了更高层次的编程接口,如:可以使用信号和槽,还可以设置只运行一次的定时器。在这篇文章的 4.2 用到了 QTimer ,这里不再赘述常用控件
1.5.1 QTimerEvent 类
示例1:在UI界面上放置两个Label控件,-个让其1秒数字累加一-次,一个让其2秒数字累加一次。
1、新建项目,在UI界面文件放置两个Label控件;
在 widget.h 中声明 timerEvent 函数,并声明两个变量
在 widget .cpp 重写 timerEvent 函数
实现效果如下:
2 Qt 文件
2.1 Qt 文件概述
文件操作是应用程序必不可少的部分。Qt作为一一个通用开发库,提供了跨平台的文件操作能力。Qt提供了很多关于文件的类,通过这些类能够对文件系统进行操作,如文件读写、文件信息获取、文件复制或重命名等。
2.2 输入输出设备类
在Qt中,文件读写的类为QFile。QFile 的父类为QFileDevice,QFileDevice 提供了文件交互操作的底层功能。QFileDevice 的父类是QIODevice, QIODevice 的父类为QObject。
QlODevice是Qt中所有输入输出设备(input/output device,简称I/O设备)的基础类, I/0 设备就是能进行数据输入和输出的设备,例如文件是一种I/0设备,网络通信中的socket是I/0设备,串口、蓝牙等通信接口也是I/O设备,所以它们也是从QIODevice继承来的。Qt 中主要的一-些I/O设备
类的继承关系如下图所示:
上图中各类的说明如下:
- QFile 是用于文件操作和文件数据读写的类,使用QFile可以读写任意格式的文件。
- QSaveFile是用于安全保存文件的类。使用QSaveFile保存文件时,它会先把数据写入一个临时文件,成功提交后才将数据写入最终的文件。如果保存过程中出现错误,临时文件里的数据不会被写入最终文件,这样就能确保最终文件中不会丢失数据或被写入部分数据。在保存比较大的文件或复杂格式的文件时可以使用这个类,例如从网络上下载文件等。
- QTemporaryFile是用于创建临时文件的类。使用函数 QTemporaryFile::open()就能创建一 个文件名唯一的临时文件, 在QTemporaryFile对象被删除时,临时文件被自动删除。
- QTcpSocket 和QUdpSocket 是分别实现了TCP和UDP的类。
- QSerialPort 是实现了串口通信的类, 通过这个类可以实现计算机与 串口设备的通信。
- QBluetoothSocket 是用于蓝牙通信的类。手机和平板计算机等移动设备有蓝牙通信模块,笔记本电脑一般也有蓝牙通信模块。通过QBluetoothSocket类,就可以编写蓝牙通信程。如编程实现笔记本电脑与手机的蓝牙通信。
- QProcess类用于启动外部程序,并且可以给程序传递参数。
- QBuffer以一个QByteArray对象作为数据缓冲区,将QByteArray对象当作一个I/O设备来读写。
2.3 文件读写类
在Qt中,文件的读写主要是通过QFile类来实现。在QFile类中提供了-些用来读写文件的方法。对于文件的操作主要有:
- 读数据: QFile类中提供了多个方法用于读取文件内容;如read()、 readAll()、 readLine()等。
- 写数据: QFile 类中提供了多个方法用于往文件中写内容;如write()、writeData()等。
- 关闭文件:文件使用结束后必须用函数close()关闭文件。
访问一个设备之前,需要使用open()函数打开该设备,而且必须指定正确的打开模式,QlODevice 中
所有的打开模式由QlODevice::OpenMode枚举变量定义,其取值如下:
示例1 : 读取文件内容
新建Qt项目,在UI文件中拖入一个LineEdit, 一个pushButton,一个TextEdit。当点击按钮时,弹出窗口选择要读取的文件,并将读取到的内容在TextEdit中显示;
完成 选取文件 按钮相应的槽函数
cpp
void Widget::on_pushButton_clicked()
{
//通过文件对话框获取到打开的文件路径
QString path = QFileDialog::getOpenFileName(this);
//设置该文件路径到 lineEdit
ui->lineEdit->setText(path);
//创建一个文件对象
QFile file(path);
//只读方式打开文件
file.open(QIODevice::ReadOnly);
//读取文件的内容保存到 str
QString str = file.readAll();
//将文件内容设置到 textEdit
ui->textEdit->setText(str);
//关闭文件
file.close();
}
实现效果如下:
点击选取文件,打开相应的文件,即可把文件的内容显示到 textEdit
示例2 : 写文件
在上述的示例增加一个 写文件操作
cpp
void Widget::on_pushButton_clicked()
{
//通过文件对话框获取到打开的文件路径
QString path = QFileDialog::getOpenFileName(this);
//设置该文件路径到 lineEdit
ui->lineEdit->setText(path);
//创建一个文件对象
QFile file(path);
//以追加的方式写文件
file.open(QIODevice::Append);
file.write("这是示例");
file.close();
//只读方式打开文件
file.open(QIODevice::ReadOnly);
//读取文件的内容保存到 str
QString str = file.readAll();
//将文件内容设置到 textEdit
ui->textEdit->setText(str);
//关闭文件
file.close();
}
在桌面上创建一个 txt ,然后用文件对话框打开
2.4 文件和目录信息类
QFileInfo是Qt提供的一个用于获取文件和目录信息的类,如获取文件名、文件大小、文件修改日期等。QFileInfo类中提供了很多的方法,常用的有:
- isDir()检查该文件是否是目录;
- isExecutable()检查该文件是否是可执行文件;
- fileName()获得文件名;
- completeBaseName()获取完整的文件名;
- suffix()获取文件后缀名;
- completeSuffix()获取完整的文件后缀;
- size()获取文件大小;
- isFile()判断是否为文件;
- fileTime()获取文件创建时间、修改时间、最近访问时间等;
代码示例: 获取文件信息
在 ui 界面上创建一个按钮后,完成对应的槽函数
cpp
void Widget::on_pushButton_clicked()
{
QString path = QFileDialog::getOpenFileName(this);
//文件信息类
QFileInfo fileinfo(path);
//打印相关属性
qDebug() << "文件名字:" << fileinfo.fileName();
qDebug() << "文件后缀:" << fileinfo.suffix();
qDebug() << "文件路径:" << fileinfo.path();
qDebug() << "文件创建时间:" << fileinfo.fileTime(QFileDevice::FileBirthTime);
}
弹出文件对话框后,选择一个文件,即可显示文件的信息
3 多线程
3.1 多线程概述
在Qt中,多线程的处理一般是通过QThread类来实现。QThread 代表-个在应用程序中可以独立控制的线程,也可以和进程中的其他线程共享数据。QThread 对象管理程序中的一个控制线程。
QThread在run()中开始执行。默认情况下,run() 通过调用exec()来启动事件循环,并在线程内运行Qt事件循环。
3.2 多线程的使用条件
在Qt中,多线程常用于比较耗时的任务,或只有通过使用线程执行时才能正常运行的情况。
3.3 创建线程的方法
在Qt中,常用的创建线程的方法如下:
方法一:继承QThread类,重写run()函数;
方法二:继承QObject类,通过moveToThread(thread) ,交给thread执行。
这里只示例方法一
3.3.1 使用方法一创建线程
使用方法一创建线程的步骤:
1.自定义一个类,继承于QThread,并且只有一个线程处理函数(和主线程不是同一个线程),这个线程处理函数主要就是重写父类中的run()函数。
2.线程处理函数里面写入需要执行的复杂数据处理;
3.启动线程不能直接调用run()函数,需要使用对象来调用start()函数实现线程启动;
4.线程处理函数执行结束后可以定义一个信号来告诉主线程;
5.最后关闭线程。
示例:点击开始按钮,显示当前时间并且一直执行
首先新建一个 Qt 项目。设计 ui 界面如下:
新建一个类,继承于 QThread 类
timeThread.h
cpp
#ifndef TIMETHREAD_H
#define TIMETHREAD_H
#include <QThread>
#include <QWidget>
class TimeThread : public QThread
{
Q_OBJECT
public:
explicit TimeThread(QObject *parent = nullptr);
void run();//重写线程任务函数
signals:
void sendTime(QString Time);//声明信号函数
};
#endif // TIMETHREAD_H
timeThread.cpp
cpp
#include "timethread.h"
#include <QTime>
TimeThread::TimeThread(QObject *parent)
: QThread{parent}
{}
void TimeThread::run()
{
while(1)
{
QString time = QTime::currentTime().toString("hh:mm:ss");
emit sendTime(time);//发送信号
sleep(1);//1秒 这个函数是 QThread 里的函数
}
}
在 Qt 中,由于考虑到线程问题,我们要更改界面上的控件,只能在主线程中操作,不能在其它线程中操作。不过我们可以利用子线程给主线程发送一个信号,来告诉主线程收到这个信号后该做什么。
widget.h
cpp
#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();
void showTime(QString time);
private slots:
void on_pushButton_clicked();
private:
Ui::Widget *ui;
TimeThread t;//定义线程对象
};
#endif // WIDGET_H
widget.cpp
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::showTime(QString time)
{
ui->label->setText(time);
}
void Widget::on_pushButton_clicked()
{
t.start();//通过 start 执行 run 函数
}
运行程序,效果如下:
3.4 QThread 常用函数
3.5 线程同步工具
线程并行会导致资源竞争,线程同步工具会解决这些冲突。
实现线程互斥和同步常用的类有:
- 互斥锁: QMutex、 QMutexLocker
- 条件变量: QWaitCondition
- 信号量: QSemaphore
- 读写锁: QReadLocker、 QWriteLocker、 QReadWriteLock
3.5.1 互斥锁
互斥锁是一种保护和防止多个线程同时访问同一对象实例的方法,在Qt中,互斥锁主要是通过QMutex类来处理。
- QMutex
特点: QMutex 是Qt框架提供的互斥锁类,用于保护共享资源的访问,实现线程间的互斥操作。
用途:在多线程环境下,通过互斥锁来控制对共享数据的访问,确保线程安全。
cpp
QMutex mutex;
mutex.lock(); //上锁
//访问共享资源
//...
mutex.unlock(); //解锁
- QMutexLocker
特点: QMutexLocker 是QMutex的辅助类,使用RAII (Resource Acquisition Is Initialization)方式对互斥锁进行上锁和解锁操作。
用途:简化对互斥锁的上锁和解锁操作,避免忘记解锁导致的死锁等问题。
cpp
QMutex mutex;
{
QMutexLocker locker(&mutex); //在作⽤域内⾃动上锁
//访问共享资源
//...
} //在作⽤域结束时⾃动解锁
- QReadWriteLocker、 QReadLocker. QWriteLocker
特点:
QReadWriteLock是读写锁类,用于控制读和写的并发访问。.
QReadLocker用于读操作.上锁,允许多个线程同时读取共享资源。
QWriteLocker用于写操作上锁,只允许一个线程写入共享资源。
用途:在某些情况下,多个线程可以同时读取共享数据,但只有一个线程能够进行写操作。读写锁提供了更高效的并发访问方式。
cpp
QReadWriteLock rwLock;
//在读操作中使⽤读锁
{
QReadLocker locker(&rwLock); //在作⽤域内⾃动上读锁
//读取共享资源
//...
} //在作⽤域结束时⾃动解读锁
//在写操作中使⽤写锁
{
QWriteLocker locker(&rwLock); //在作⽤域内⾃动上写锁
//修改共享资源
//...
} //在作⽤域结束时⾃动解写锁
示例 1 : 加 QMutex 锁
mythread.h
cpp
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QThread>
#include <QWidget>
#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
cpp
#include "mythread.h"
#include <QDebug>
int myThread::num = 0;
QMutex myThread::mutex;
myThread::myThread(QObject *parent)
: QThread{parent}
{}
void myThread::run()
{
while(1)
{
mutex.lock();//加锁
qDebug() << "当前线程:" << this << ", Value : " << this->num++;
mutex.unlock();//解锁
sleep(1);
}
}
widget.h
cpp
#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:
Ui::Widget *ui;
};
#endif // WIDGET_H
widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
#include <mythread.h>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
myThread *t1 = new myThread(this);
myThread *t2 = new myThread(this);
t1->start();
t2->start();
}
Widget::~Widget()
{
delete ui;
}
执行效果:
两个线程使用同一把锁,操作一个数据,数据会被依次打印:0、1、2、3、4 ...
示例 2 :在上述代码的基础上使用 QMutexLocker 锁
cpp
void myThread::run()
{
while(1)
{
// 创建的时候加锁,当QMutexLocker 局部销毁的时候解锁
{
QMutexLocker lock(&this->mutex);
qDebug() << "当前线程:" << this << ", Value : " << this->num++;
}
sleep(1);
}
}
3.5.2 信号量
有时在多线程编程中,需要确保多个线程可以相应的访问一个数量有限的相同资源。例如,运行程序的设备可能是非常有限的内存,因此我们更希望需要大量内存的线程将这一事 实考虑在内,并根据可用的内存数量进行相关操作,多线程编程中类似问题通常用信号量来处理。信号量类似于增强的互斥锁,不仅能完成上锁和解锁操作,而且可以跟踪可用资源的数量。
特点: QSemaphore是Qt框架提供的计数信号量类,用于控制同时访问共享资源的线程数量。
用途:限制并发线程数量,用于解决- -些资源有限的问题。
cpp
QSemaphore semaphore(2); //同时允许两个线程访问共享资源
//在需要访问共享资源的线程中
semaphore.acquire(); //尝试获取信号量,若已满则阻塞
//访问共享资源
//...
semaphore.release(); //释放信号量
//在另⼀个线程中进⾏类似操作
3.5.3 条件变量
在多线程编程中,假设除了等待操作系统正在执行的线程之外,某个线程还必须等待某些条件满足才能执行,这时就会出现问题。这种情况下,线程会很自然地使用锁的机制来阻塞其他线程,因为这只是线程的轮流使用,并且该线程等待某些特定条件,人们会认为需要等待条件的线程,在释放互斥锁或读写锁之后进入了睡眠状态,这样其他线程就可以继续运行。当条件满足时,等待条件的线程将被另一个线程唤醒。
在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();
4 Qt 网络
和多线程类似,Qt为了支持跨平台,对网络编程的API也进行了重新封装。
在进行网络编程之前,需要在项目中的.pro文件中添加network 模块. 添加之后要 手动编译一下项目, 使Qt Creator能够加载对应模块的头文件.
4.1 UDP Socket
4.1.1 核心 API 概览
主要的类有两个. QUdpSocket和QNetworkDatagram
QUdpSocket表示一个UDP的socket文件.
QNetworkDatagram 表示一个 UDP 数据报
4.1.2 回显服务器
创建项目后,在 .pro 文件里添加 network 模块
1)创建界面,包含一个QListWidget用来显示消息
2)创建QUdpSocket| 成员
修改widget.h
cpp
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
QUdpSocket* socket;
};
修改widget.cpp,完成socket后续的初始化
一般来说, 要先连接信号槽,再绑定端口.
如果顺序反过来,可能会出现端口绑定好了之后,请求就过来了.此时还没来得及连接信号槽.那么这个请求就有可能错过了
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
#include <QNetworkDatagram>
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 ,完成处理请求的过程
- 读取请求并解析
- 根据请求计算响应
- 把响应写回到客户端
cpp
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方法中并没有包含实质性的内容.
cpp
QString Widget::process(const QString& request)
{
return request;
}
"根据请求处理响应"是服务器开发中的最核心的步骤. 一个商业服务器程序,这里的逻辑可能是几万行几十万行代码量级的.
此时,服务器程序编写完毕.
但是直接运行还看不出效果.还需要搭配客户端来使用.
4.1.3 回显客户端
1)创建界面.包含- -个QLineEdit ,QPushButton, QListWidget
- 先使用水平布局把QLineEdit和QPushButton 放好,并设置这两个控件的垂直方向的sizePolicy为Expanding
再使用垂直布局把QListWidget 和上面的水平布局放好. - 设置垂直布局的layoutStretch 为5,1 (当然这个尺寸比例根据个人喜好微调).
2)在widget.cpp中,先创建两个全局常量,表示服务器的IP和端口
cpp
//提前定义好服务器的 IP 和 端口
const QString& SERVER_IP = "127.0.0.1";
const quint16 SERVER_PORT = 9090;
3)创建QUdpSocket成员
修改widget.h,定义成员
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QUdpSocket>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
//创建 socket 成员
QUdpSocket* socket;
};
#endif // WIDGET_H
修改widget.cpp,初始化socket
cpp
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//1.设置窗口名字
this->setWindowTitle("客户端");
//2.实例化 socket
socket = new QUdpSocket(this);
}
4)给发送按钮slot函数,实现发送请求.
cpp
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的构造函数,通过信号槽,来处理服务器的响应.
cpp
//通过信号槽,处理服务器返回的数据
connect(socket,&QUdpSocket::readyRead,this,&Widget::processResponse);
void Widget::processResponse()
{
const QNetworkDatagram responseDatagram = socket->receiveDatagram();
QString response = responseDatagram.data();
ui->listWidget->addItem(QString("服务器:") + response);
}
最终执行效果
启动多个客户端都可以正常工作.
4.2 TCP Socket
4.2.1 核心 API 概览
核心类是两个: QTcpServer 和 QTcpSocket
QTcpServer用于监听端口,和获取客户端连接.
QTcpSocket用户客户端和服务器之间的数据交互.
QByteArray用于表示一个字节数组.可以很方便的和QString进行相互转换. 例如:
- 使用QString的构造函数即可把QByteArray转成QString.
- 使用QString的toUtf8函数即可把QString转成QByteArray.
4.2.2 回显服务器
1)创建界面.包含一个QListWidget ,用于显示收到的数据.
2)创建QTcpServer并初始化
修改widget.h,添加QTcpServer指针成员.
cpp
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
// 创建 QTcpServer
QTcpServer* tcpServer;
};
修改widget.cpp,实例化QTcpServer并进行后续初始化操作.
- 设置窗口标题
- 实例化TCP server. (父元素设为当前控件,会在父元素销毁时被一起销毁).
- 通过信号槽,处理客户端建立的新连接.
- 监听端口
cpp
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.
- 通过信号槽,处理收到请求的情况
- 通过信号槽,处理断开连接的情况
cpp
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方法中并没有包含实质性的内容.
此时,服务器程序编写完毕.
但是直接运行还看不出效果.还需要搭配客户端来使用.
4.2.3 回显客户端
1)创建界面.包含-一个QLineEdit , QPushButton, QListWidget
- 先使用水平布局把QLineEdit和QPushButton放好,并设置这两个控件的垂直方向的
sizePolicy为Expanding - 再使用垂直布局把QListWidget和.上面的水平布局放好.
- 设置垂直布局的layoutStretch为5,1 (当然这个尺寸比例根据个人喜好微调).
2)创建
QTcpSocket并实例化
修改widget.h,创建成员.
cpp
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
// 新增 QTcpSocket
QTcpSocket* socket;
};
修改widget.cpp,对QTcpSocket进行实例化.
- 设置窗口标题
- 实例化socket对象(父元素设为当前控件,会在父元素销毁时被-起销毁).
- 和服务器建立连接.
- 等待并确认连接是否出错.
cpp
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函数,实现发送请求给服务器.
cpp
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构造函数,通过信号槽,处理收到的服务器的响应.
cpp
// 处理服务器返回的响应.
connect(socket, &QTcpSocket::readyRead, this, [=]() {
QString response = socket->readAll();
qDebug() << response;
ui->listWidget->addItem(QString("服务器说: ") + response);
});
先启动服务器,再启动客户端(可以启动多个),最终执行效果:
由于我们使用信号槽处理同一个客户端的多个请求,不涉及到循环,也就不会使客户端之间相互影响了.
4.3 HTTP Client
进行Qt开发时,和服务器之间的通信很多时候也会用到HTTP协议.
- 通过HTTP从服务器获取数据.
- 通过HTTP向服务器提交数据.
4.3.1 核心 API
关键类主要是三个. QNetworkAccessManager,QNetworkRequest,QNe tworkReply
QNetworkAccessManager提供了HTTP的核心操作
QNetworkRequest表示一个 HTTP请求(不含body).
如果需要发送一个带有body的请求(此如post),会在QNetworkAccessManager的post方法中通过单独的参数来传入body.
其中的QNetworkRequest: : KnownHeaders是一个枚举类型,常用取值:
QNetworkReply表示一个HTTP响应.这个类同时也是QIODevice的子类
此外,QNetworkReply 还有一个重 要的信号finished 会在客户端收到完整的响应数据之后触发.
4.3.2 代码示例
给服务器发送一个GET请求.
1)创建界面.包含一个QLineEdit, QPushButton
- 先使用水平布局把QLineEdit和QPushButton 放好,并设置这两个控件的垂直方向的sizePolicy为Expanding
- 再使用垂直布局把QPlainTextEdit和上面的水平布局放好. ( QPlainTextEdit的readOnly设为true )
- 设置垂直布局的layoutStretch为5,1 (当然这个尺寸比例根据个人喜好微调).
此处建议使用QPlainTextEdit 而不是QTextEdit .主要因为QTextEdit
要进行富文本解析,如果得到的HTTP响应体积很大,就会导致界面渲染缓慢甚至被卡住.
2)修改widget.h,创建QNetworkAccessManager 属性
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QNetworkAccessManager>
#include <QNetworkReply>
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_pushButton_clicked();
private:
Ui::Widget *ui;
QNetworkAccessManager* manager;
};
#endif // WIDGET_H
3)修改widget.cpp,创建实例
cpp
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
manager = new QNetworkAccessManager(this);
}
4)编写按钮的slot函数,实现发送HTTP请求功能.
cpp
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);
}
else
{
//响应出错
ui->plainTextEdit->setPlainText(response->errorString());
}
response->deleteLater();
});
}
执行程序,观察效果
发送POST请求代码也是类似.使用manager->post() 即可.此处不再演示.
5 Qt 音视频
5.1 音频
在新版本的Qt中,音频主要是通过QSoundEffect类来实现。但是需要注意的是QSoundEffect类只支持播放wav格式的音频文件。也就是说如果想要添加音频效果,那么首先需要将非wav格式的音频文件转换为wav格式。
通过帮助手册查看QQSoundEffect类如下:
使用该类时,需要添加模块 multimedia
核心 API
示例:播放音频
在 ui 界面上创建一个按钮,添加对应的槽函数
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QSoundEffect>
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_pushButton_clicked();
private:
Ui::Widget *ui;
QSoundEffect* sound;
};
#endif // WIDGET_H
cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
sound = new QSoundEffect(this);
//先导入资源文件
sound->setSource(QUrl::fromLocalFile(":/music2.wav"));
sound->setVolume(0.5);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_clicked()
{
sound->play();
}
运行程序,点击按钮可播放声音。