QT系统相关
程序是运行在操作系统上的,需要操作系统给我们提供支撑,Qt是跨平台的C++开发框架,Qt封装了操作系统API,所以需要学习Qt系统提供的API
1.Qt中的事件,是一个图形化界面中用户操作和程序交互的一个核心机制,Qt中的事件是对系统中的事件的一个封装。
2.Qt中的文件操作,C++标准库也提供了文件操作,但是Qt提供的文件操作更加丰富。
3.Qt中的多线程编程
4.Qt中的网络编程
5.Qt中的多媒体,例如视频和音频的播放
1.Qt中的事件
信号槽,用户进行的各种操作,就有可能会产生出信号,可以给莫个信号指定槽函数,当信号触发时,就能够自动的执行到对应的槽函数。
事件非常类似,用户进行的各种操作,也会产生事件,程序员同样可以给事件关联上处理函数(处理的逻辑),当事件触发的时候,就能够执行到对应的代码。
事件本身就是操作系统提供的机制,Qt也同样把操作系统事件机制进行了封装,拿到了Qt中,但是由于时间对应的代码编写起来不是很方便,Qt对于事件机制又进行了进一步的封装,就得到了信号槽。
实际Qt开发程序过程中,绝大部分和用户之间的交互都是通过“信号槽”来完成的,有些特殊情况下,信号槽不一定能够搞定,用户的某个动作行为,Qt没有提供对应的信号,此时就需要通过重写事件处理函数的形式,来手动处理事件的响应逻辑。常见的 Qt 事件如下:
常见事件描述:
鼠标事件 | 鼠标左键、鼠标右键、鼠标滚轮,鼠标的移动,鼠标按键的按下和松开 |
---|---|
键盘事件 | 按键类型、按键按下、按键松开 |
定时器事件 | 定时时间到达 |
进入离开事件 | 鼠标的进入和离开 |
滚轮事件 | 鼠标滚轮滚动 |
绘屏事件 | 重绘屏幕的某些部分 |
显示隐藏事件 | 窗口的显示和隐藏 |
移动事件 | 窗口位置的变化 |
窗口事件 | 是否为当前窗口 |
大小改变事件 | 窗口大小改变 |
焦点事件 | 键盘焦点移动 |
拖拽事件 | 用鼠标进行拖拽 |
2.Qt中的文件操作
QIODevice 的父类为 QObject 。QIODevice 是 Qt 中所有输入输出设备(input/output device,简称 I/O 设备)的基础类,I/O 设备就是能进行数据输入和输出的设备,例如文件是一种 I/O 设备,网络通信中的 socket 是 I/O 设备, 串口、蓝牙等通信接口也是 I/O 设备,所以它们也是从 QIODevice 继承来的。Qt 中主要的一些 I/O 设备类的继承关系如下图所示:
C和C++都提供了自己的文件操作,但是在编写Qt程序的时候,更推荐使用Qt自己提供的这套文件操作,和QString等Qt内置的类可以很好的配合。
2.1文件读写
构造文件对象
打开文件
使用以下两种方法,需要文件路径和文件描述符,使用起来比较麻烦
在构造函数时指定了文件路径,就可以使用如下版本的open。
QIODevice::NotOpen | 没有打开设备 |
---|---|
QIODevice::ReadOnly | 以只读方式打开设备 |
QIODevice::WriteOnly | 以只写方式打开设备 |
QIODevice::ReadWrite | 以读写方式打开设备 |
QIODevice::Append | 以追加方式打开设备,数据将写到文件末尾 |
QIODevice::Truncate | 每次打开文件后重写文件内容,原内容将被删除 |
QIODevice::Text | 在读文件时,行尾终止符会被转换为 ‘\n’;当写入文件时,行尾终止符会被转换为本地编码。如 Win32上为’\r\n’; |
QIODevice::Unbuffered | 无缓冲形式打开文件,绕过设备中的任何缓冲区 |
QIODevice::NewOnly | 文件存在则打开失败,不存在则创建文件 |
读操作 read/readLine/readAll
写操作
关闭文件
代码示例:
QPlainTextEdit是一个功能强大、易于使用的纯文本编辑器/查看器。它使用与QTextEdit相同的技术和概念,但是为纯文本的处理进行了优化,因此更适合处理大型纯文本文档。QPlainTextEdit不提供富文本编辑功能,如字体、颜色、大小等的格式化,而是专注于纯文本的编辑和显示。
2.2文件和目录信息类
QFileInfo 是 Qt 提供的一个用于获取文件和目录信息的类,如获取文件名、文件大小、文件修改日期等。QFileInfo类中提供了很多的方法,常用的有:
- isDir() 检查该文件是否是目录;
- isExecutable() 检查该文件是否是可执行文件;
- fileName() 获得文件名;
- completeBaseName() 获取完整的文件名;
- suffix() 获取文件后缀名;
- completeSuffix() 获取完整的文件后缀;
- size() 获取文件大小;
- isFile() 判断是否为文件;
- fileTime() 获取文件创建时间、修改时间、最近访问时间等;
3.Qt多线程
3.1线程的使用
QThread常用API
run() | 子类重写父类的run函数 |
---|---|
start() | 这个函数是真正调用系统的API创建线程,新的线程创建出来之后自然就会执行run函数 |
currentThread() | 返回线程对象的指针 |
isRunning() | 如果线程正在运行则返回true,否则返回false |
sleep()/msleep()/usleep() | 设置线程休眠时间,单位为秒 / 毫秒 / 微秒 |
wait() | 阻塞线程,直到满足以下任何一个条件: 与此 QThread 对象关联的线程已经完成执行(即当它从run()返回时)。如果线程已经完成,这个函数将返回 true。如果线程尚未启动,它也返回 true。已经过了几毫秒。如果时间是 ULONG_MAX(默认值),那么等待永远不会超时(线程必须从run()返回)。如果等待超时,此函数将返回 false。这提供了与 POSIX pthread_join() 函数类似的功能 |
terminate() | 终止线程的执行。线程可以立即终止,也可以不立即终止,这取决于操作系统的调度策略。在terminate() 之后使用 QThread::wait() 来确保 |
finished() | 当线程结束时会发出该信号,可以通过该信号来实现线程的清理工作 |
代码示例:多线程版的计时器
在多线程中,由于对界面上控件的修改存在着线程安全的问题,Qt规定只能在主线中修改控件的状态。
在客户端中多线程的意义
在服务器开发中使用多线程的目的,是为了充分利用多核CPU的计算资源,尤其在服务器端一般是双路CPU(双路CPU是一个主板上面有两个CPU)。
在客户端多线程仍然非常有意义,但是侧重点就不同了。对于普通用户来说,“使用体验”是一个非常重要的话题。现如今普通用户的家用PC上也是多核CPU,但客户端上的程序一般不会使用多线程把CPU计算资源吃完,就算是做很重量级的计算,也不会把CPU计算资源利用完,一旦利用完了,此时系统会很卡,用户的体验会很差。
客户端的多线程最主要不是为了提高程序速度,相比之下,客户端中的多线程,主要是用于,通过多线程的方式,执行一些耗时的等待IO操作,避免主线程被卡死,避免对用户造成不好的体验。例如:
客户端经常会和服务器进行网络通信,比如客户端要上传/下载一个很大的文件,传输需要消耗很久,还有代码中持续不断的进行QFile.write,这种就是一个密集的IO操作,这种密集IO就会使程序被系统阻塞,然后被挂起。一旦程序都被挂起了,此时意味着,用户进行的各种操作,程序都无法响应。在生活中我们使用软件的过程中会遇到这样的场景,Windows提示你这个窗口不能响应,是否要强制结束,这种现象就是程序被挂起来了。
最好的做法,就是使用单独的线程,来处理这种密集的IO操作,主线程负责事件循环、负责处理用户的各项操作,如果被挂,挂起也是只挂起这个新的线程,主线程并不会受到影响,主线程仍然可以继续工作,继续响应用户的各种操作。
3.2线程安全问题
实现线程互斥和同步常用的类有:
- 互斥锁:QMutex、QMutexLocker
- 条件变量:QWaitCondition
- 信号量:QSemaphore
- 读写锁:QReadLocker、QWriteLocker、QReadWriteLock
3.2.1互斥锁
解决线程安全问题常用的就是加锁,就是把多线程要访问的公共资源,通过锁保护起来,借助这个锁把并发执行=》串行执行,在Linux中提供mutex互斥量来作为锁,到C++11开始把锁融入到标准库中std::mutex,把系统中的锁进行封装,Qt同样提供了锁,来对系统中的锁进行封装QMutex。
案例:对一个变量,通过两个线程循环对变量进行加加
多个线程进行加锁的对象,得是同一个锁对象。不同锁对象,就不会产生锁的互斥,也就无法把并发执行=》串行执行,也就无法解决上述问题。
在实际开发中,加锁之后,涉及到的逻辑可能很复杂,可能会忘记释放锁。使用QMutexLocker对互斥锁进行智能管理。
3.2.2读写锁
QReadWriteLocker、QReadLocker、QWriteLocker
特点:
- QReadWriteLock 是读写锁类,用于控制读和写的并发访问。
- QReadLocker 用于读操作上锁,只允许一个线程读取共享资源。
- QWriteLocker 用于写操作上锁,只允许一个线程写入共享资源。
用途:在某些情况下,多个线程可以同时读取共享数据,但只有一个线程能够进行写操作。读写锁提供了更高效的并发访问方式。
QReadWriteLock rwLock;
//在读操作中使用读锁
{QReadLocker locker(&rwLock); //在作用域内自动上读锁//读取共享资源//...
} //在作用域结束时自动解读锁//在写操作中使用写锁
{QWriteLocker locker(&rwLock); //在作用域内自动上写锁//修改共享资源//...
}//在作用域结束时自动解写锁
3.2.3条件变量
在多线程编程中,假设除了等待操作系统正在执行的线程之外,某个线程还必须等待某些条件满足才能执行,这时就会出现问题。这种情况下,线程会很自然地使用锁的机制来阻塞其他线程,因为这只是线程的轮流使用,并且该线程等待某些特定条件,人们会认为需要等待条件的线程,在释放互斥锁或读写锁之后进入了睡眠状态,这样其他线程就可以继续运行。当条件满足时,等待条件的线程将被另一个线程唤醒。在 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.2.4 信号量
有时在多线程编程中,需要确保多个线程可以相应的访问一个数量有限的相同资源。例如,运行程序的设备可能是非常有限的内存,因此我们更希望需要大量内存的线程将这一事实考虑在内,并根据可用的内存数量进行相关操作,多线程编程中类似问题通常用信号量来处理。信号量类似于增强的互斥锁,不仅能完成上锁和解锁操作,而且可以跟踪可用资源的数量。
特点:QSemaphore 是 Qt 框架提供的计数信号量类,用于控制同时访问共享资源的线程数量。
用途:限制并发线程数量,用于解决一些资源有限的问题。
QSemaphore semaphore(2); //同时允许两个线程访问共享资源
//在需要访问共享资源的线程中
semaphore.acquire(); //尝试获取信号量,若已满则阻塞
//访问共享资源
//...
semaphore.release(); //释放信号量
//在另一个线程中进行类似操作
4.Qt中的网络
和多线程类似, Qt 为了支持跨平台, 对网络编程的 API 也进行了重新封装。接下来介绍 Qt 的网络相关的 API 的使用。实际 Qt 开发中进行网络编程, 也不一定使用 Qt 封装的网络 API, 也有一定可能使用的是系统原生 API 或者其他第三方框架的 API。
进行网络编程的时候,本质上是在编写应用层代码,需要传输层提供支持,而传输层操作系统已经实现好了,并给我们提供了相关接口。传输层最核心的协议有UDP和TCP,UDP无连接不可靠传输面向数据报全双工,TCP有连接可靠传输面向字节流全双工,操作系统给我们提供了两套Socket API。
在使用Qt中的网络API之前, 需要在项目中的.pro 文件中添加 network 模块.添加之后要手动编译一下项目, 使 Qt Creator 能够加载对应模块的头文件。之前学过的各种控件,各种内容,都包含在QtCore模块中(该模块默认就被添加了),与之对应的Qt还提供了其他模块,要想使用相关内容,就需要先添加对应的模块。
为什么Qt要分模块?
Qt框架本身是非常庞大的,如果把Qt中所有的功能放到一起,即使写一个简单的hello world程序,生成的可执行程序也非常庞大,这里包含了大量没有使用的功能。Qt还有一个典型的应用场景,就是应用在嵌入式的系统上,嵌入式系统的配置比较低,一个简单的程序,却非常的大,在嵌入式系统上可能无法运行。
Qt提供了静态库的版本,还提供了动态库的版本,如果使用的是静态库版本,那在.pro工程文件中添加的模块在编译时会被全都添加到.exe文件中。
4.1UDP
4.1.1 核心 API
主要的类有两个QUdpSocket 和 QNetworkDatagram(数据报,每次发送和接受都必须是一个完整的数据报)
QUdpSocket 表示一个 UDP 的 socket 文件
名称 | 类型 | 说明 | 对标原生API |
---|---|---|---|
bind( const QHostAddress&, quint16) | 方法 | 绑定指定的端口号 | bind |
receiveDatagram() | 方法 | 返回QNetworkDatagram读取一个 UDP 数据报 | recvfrom |
writeDatagram( const QNetworkDatagram&) | 方法 | 发送一个 UDP 数据报 | sendto |
readyRead | 信号 | 在收到数据并准备就绪后触发. | 无 (类似于 IO 多路复用的通知机制) |
系统原生的API接口中recvfrom是阻塞IO,当客户端一直没有发送数据,recvfrom会一直阻塞,直到有请求。基于阻塞模型来实现的服务器并不是很好,一阻塞就会导致整个线程卡在这里,其他什么操作也做不了,如果利用上这一时间做一些其他的操作可能会更好,Qt就利用了信号和槽机制。
在QUdpSocket类中就提供了readRead信号,当socket收到请求的时候,就会触发readyRead ,在对应的槽函数中实现读取请求操作。
QNetworkDatagram 表示一个 UDP 数据报
名称 | 类型 | 说明 | 对标原生 API |
---|---|---|---|
QNetworkDatagram (const QByteArray&, const QHostAddress& , quint16 ) | 构造函数 | 通过 QByteArray , 目标 IP 地址,目标端口号 构造一个 UDP 数据报,通常用于发送数据时 | 无 |
data() | 方法 | 获取数据报内部持有的数据,返回QByteArray | 无 |
senderAddress() | 方法 | 获取数据报中包含的对端的 IP 地址 | 无,recvfrom 包含了该功能 |
senderPort() | 方法 | 获取数据报中包含的对端的端口号 | 无,recvfrom 包含了该功能 |
在构造QUdpSocket对象时可以指定一个父对象
4.1.2回显服务器
“根据请求处理响应” 是服务器开发中的最核心的步骤,一个商业服务器程序,这里的逻辑可能是几万行几十万行代码量级的。
此时,服务器程序编写完毕,但是直接运行还看不出效果. 还需要搭配客户端来使用。
4.1.3回显客户端
- 创建界面,含一个 QLineEdit , QPushButton , QListWidget
- 先使用水平布局把 QLineEdit 和 QPushButton 放好, 并设置这两个控件的垂直方向的izePolicy 为 Expanding
- 再使用垂直布局把 QListWidget 和上面的水平布局放好.
- 设置垂直布局的 layoutStretch 为 5, 1 (当然这个尺寸比例根据个人喜好微调)
- 在 mainwindow.cpp 中, 先创建两个全局常量, 表示服务器的 IP 和 端口
// 提前定义好服务器的 IP 和 端口
const QString& SERVER_IP = "127.0.0.1";
const quint16 SERVER_PORT = 9090;
- 创建 QUdpSocket 成员
修改 mainwindow.h 定义成员
修改mainwindow.cpp, 初始化 socket
- 给发送按钮 slot 函数,实现发送请求
- 通过信号槽,来处理服务器的响应
最终执行效果
启动多个客户端都可以正常工作
4.2TCP
4.2.1 核心 API
核心类是两个: QTcpServer 和 QTcpSocket
QTcpServer 用于监听端口和获取客户端连接
名称 | 类型 | 说明 | 对标原生 API |
---|---|---|---|
listen(const QHostAddress&, quint16 port) | 方法 | 绑定指定的地址和端口号,并开始监听 | bind 和 listen |
nextPendingConnection() | 方法 | 从系统中获取到一个已经建立好的返回一个 QTcpSocket,表示这个tcp 连接,客户端的连接通过这个 socket 对象完成和客户端之间的通信 | accept |
newConnection | 信号 | 有新的客户端建立连接好之后触发 | 无 (但是类似于 IO 多路复用中的通知机制) |
QTcpSocket 用户客户端和服务器之间的数据交互
名称 | 类型 | 说明 | 对标原生 API |
---|---|---|---|
readAll() | 方法 | 读取当前接收缓冲区中的所有数据,返回 QByteArray 对象 | read |
write(const QByteArray& ) | 方法 | 把数据写入 socket 中 | write |
readyRead | 信号 | 有数据到达并准备就绪时触发 | 无 (但是类似于 IO 多路复用中的通知机制) |
deleteLater | 方法 | 暂时把 socket 对象标记为无效,Qt会在下个事件循环中析构释放该对象 | 无 (但是类似于 “半自动化的垃圾回收”) |
disconnected | 信号 | 连接断开时触发 | 无 (但是类似于 IO 多路复用中的通知机制) |
QByteArray 用于表示一个字节数组,可以很方便的和 QString 进行相互转换
- 使用 QString 的构造函数即可把 QByteArray 转成 QString
- 使用 QString 的 toUtf8 函数即可把 QString 转成 QByteArray
4.2.2 TCP回显服务器(请求的内容是什么就回显什么内容)
4.2.3TCP回显客户端
先启动服务器, 再启动客户端(可以启动多个),最终执行效果
4.3HTTP
4.3.1 核心 API
关键类主要是三个,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) | 设置请求头 |
- 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 的值 |
此外,QNetworkReply 还有一个重要的信号 finished 会在客户端收到完整的响应数据之后触发
4.3.2代码示例
给服务器发送一个GET请求
- 先来构建界面
将水平布局中的两个控件的垂直策略改为充满
此处建议使用 QPlainTextEdit 而不是 QTextEdit,主要因为 QTextEdit是支持HTML解析的,会对HTML进行解析和渲染的,如果得到的 HTTP 响应体积很大,就会导致界面渲染缓慢甚至被卡住。
- 编写代码
访问百度的网页
在实际开发中HTTP Client也不一定得到的非得是HTML,更多的是客户端和服务器开发约定好交互的数据格式。按照约定好的格式,客户端拿到之后,进行解析,并显示到界面上。
4.4 其他模块
Qt 中还提供了 FTP, DNS, SSL 等网络相关的组件工具,需要时可以翻阅官方文档学习相关 API 的使用
5.Qt中音视频
5.1播放声音
在 Qt 中,音频主要是通过 QSound 类来实现。但是需要注意的是 QSound 类只支持播放 wav 格式的音频文件。也就是说如果想要添加音频效果,那么首先需要将 非wav格式 的音频文件转换为 wav 格式。使用 QSound 类时,需要添加模块:multimedia。
play() | 开始或继续播放当前源。 |
---|
代码示例:
5.2播放视频
5.2.1核心API
在 Qt 中,视频播放的功能主要是通过 QMediaPlayer类 和 QVideoWidget类 来实现。在使用这两个类时要添加对应的模块 multimedia 和 multimediawidgets。
setMedia() | 设置当前媒体源。 |
---|---|
setVideoOutput() | 将QVideoWidget视频输出附加到媒体播放器。如果媒体播放器已经附加了视频输出,将更换一个新的。 |
首先在 .pro 文件中添加 multimedia 和 multimediawidgets 两个模块;如下图示: