一、Qt 事件
1、事件介绍
事件是应用程序内部或者外部产生的事情或者动作的统称。在 Qt 中使用一个对象来表示一个事件。所有的 Qt 事件均继承于抽象类 QEvent。事件是由系统或者 Qt 平台本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件是在用户操作时发出,如键盘事件、鼠标事件等,另一些事件则是由系统本身自动发出,如定时器事件。
常见的 Qt 事件如下:
不同场景下,要关注的点不一样。这些事件的子类中就会包含一些对应的不同属性。
常见事件描述:
2、事件的处理
事件处理一般常用的方法为:重写相关的 Event 函数。
在 Qt 中,几乎所有的 Event 函数都是虚函数,所以可以重新实现。比如:在实现鼠标的进入和离开事件时,直接重新实现 enterEvent() 和 leaveEvent() 即可。enterEvent() 和 leaveEvent() 函数原型如下:
【示例一】
(1)新建 Qt 项目
基类选择 QWidget,同时勾选 UI 界面文件:
(2)设计 UI 文件
(3)在项目中新添加一个类
先选中项目名称 QEvent,点击鼠标右键,选择 add new ... ,弹出如下对话框:
(4)定义类名并选择基类
(5)此时项目中会新添加以下两个文件
(6)在帮助文档中查找对应的内容
(7)点击 “显示” 之后
(8)复制 enterEvent(),粘贴在项目文件 "label.h" 中
要想重写父类的函数就需要确保写的函数名字和函数的参数列表完全一致(形参名无所谓)。
(9)重写 enterEvent() 和 leaveEvent() 方法
(10)在 UI 文件中选中 Label,右键 ——> 提升为...
(11)当点击 “提升为...” 之后
通过 “提升为...” 这样的方式就可以把 Qt Designer 中托上去的控件的类型转换成自定义的控件类型。
(12)修改基类
(13)执行效果
当鼠标进入设计好的标签之后,就会在应用程序输出栏中打印:enterEvent;鼠标移出设计好的标签之后,就会在应用程序输出栏中打印:leaveEvent。
此时就说明当前的 enterEvent 和 leaveEvent 这两个事件就被我们给捕获到了。
【示例二 —— 当鼠标点击时,获取对应的坐标值】
(1)在 label.h 中声明 mousePressEvent() 方法
(2)在 label.cpp 中重写 mousePressEvent() 方法
鼠标左键点击时,打印对应的坐标值,鼠标右键点击时,打印基于屏幕的坐标。
实现效果如下:
3、键盘按键事件
Qt 中的按键事件是通过 QKeyEvent 类来实现的。当键盘上的按键被按下或者被释放时,键盘事件便会触发。
在帮助文档中查找 QKeyEvent 类如下:
查找按键事件中所有的按键类型:在帮助文档中输入:Qt::Key,如下图:
(1)单个按键
示例:当某个按键被按下时,输出:某个按键被按下了;
A. 新建项目,在头文件 "widget.h" 中声明虚函数 keyPressEvent();如下图:
B. 在 "widget.cpp" 文件中重写 keyPressEvent() 虚函数
(2)组合按键
在 Qt 助手中搜索:Qt::KeyboardModifier,如下图示:
Qt::KeyboardModifier 中定义了在处理键盘事件时对应的修改键。在 Qt 中,键盘事件可以与修改键以起使用,以实现一些复杂的交互操作。KeyboardModifier 中修改键的具体描述如下:
示例:
4、鼠标事件
在 Qt 中,鼠标事件是用 QMouseEvent 类来实现的。当在窗口中按下鼠标或者移动鼠标时,都会产生鼠标事件。
利用 QMouseEvent 类可以获取鼠标的哪个键被按下了以及鼠标的当前位置等信息。在 Qt 帮助文档中查找 QMouseEvent 类如下图示:
(1)鼠标单击事件
在 Qt 中,鼠标按下是通过虚函数 mousePressEvent() 来捕获的。mousePressEvent() 函数原型如下:
[ virtual protected] void QWidget:: mousePressEvent (QMouseEvent * event )
鼠标左右键及滚的表示如下:
- Qt::LeftButton 鼠标左键
- Qt::RightButton 鼠标右键
- Qt::MidButton 鼠标滚轮
【鼠标左键】
A. 在 "widget.h" 头文件中声明鼠标按下事件
B. 在 "widget.cpp" 文件中重新实现 mousePressEvent() 函数
实现效果如下:
【鼠标右键】
实现效果如下:
【鼠标滚轮】
实现效果如下:
(2)鼠标释放事件
鼠标释放事件是通过虚函数 mouseReleaseEvent() 来捕获的。mouseReleaseEvent() 函数原型如下:
[ virtual protected] void QWidget:: mouseReleaseEvent (QMouseEvent * event )
示例:
执行效果如下:
(3)鼠标双击事件
鼠标双击事件是通过虚函数:mouseDoubleClickEvent() 来实现的。mouseDoubleClickEvent() 函数原型如下:
[ virtual protected] void QWidget:: mouseDoubleClickEvent (QMouseEvent * event )
示例:鼠标左键双击
执行效果如下:
(4)鼠标移动事件
鼠标移动事件是通过虚函数:mouseMoveEvent() 来实现的。同时为了实时捕获鼠标位置信息,需要通过函数 setMouseTracking() 来追踪鼠标的位置。mouseMoveEvent()函数原型如下:
[ virtual protected] void QWidget:: mouseMoveEvent (QMouseEvent * event )
setMouseTracking() 函数原型如下:
void setMouseTracking( bool enable )
说明:setMouseTracking() 函数默认是 false,需要设置为 true,才能实时捕获鼠标位置信息。否则只有当鼠标按下时才能捕获其位置信息。
示例:
执行效果:
(5)滚轮事件
在 Qt 中,鼠标滚轮事件是通过 QWheelEvent 类来实现的。滚轮滑动的距离可以通过 delta() 函数获取。delta() 函数原型如下:
int QGraphicsSceneWheelEvent:: delta() const
其中返回值代表滚轮滑动的距离。正数表示滚轮相对于用户向前滑动,负数表示滚轮相对于用户向后滑动。
示例:
执行效果如下:
5、定时器
Qt 中在进行窗口程序的处理过程中,经常要周期性的执行某些操作,或者制作一些动画效果,使用定时器就可以实现。所谓定时器就是在间隔一定时间后,去执行某一个任务。定时器在很多场景下都会使用到,如弹窗自动关闭之类的功能等。
Qt 中的定时器分为 QTimerEvent 和 QTimer 这 2 个类。
-
QTimerEvent 类用来描述一个定时器事件。在使用时需要通过 startTimer() 函数来开启⼀个定时器,这个函数需要输入一个以毫秒为单位的整数作为参数来表明设定的时间,它返回的整型值代表这个定时器。当定时器溢出时(即定时时间到达)就可以在 timerEvent() 函数中获取该定时器的编号来进行相关操作。
-
QTimer 类来实现一个定时器,它提供了更高层次的编程接口,如:可以使用信号和槽,还可以设置只运行一次的定时器。
(1)QTimerEvent 类
【示例 1】在 UI 界面上放置一个 LCD Number 控件,让其 10 秒数字不断递减到 0,相当于倒计时。
A. 新建项目,在 UI 界面文件放置一个 LCD Number 控件
B. 在 "widget.h" 头文件中声明 timerEvent() 函数,并定义一个整型变量
C. 在 "widget.cpp" 文件中重写 timerEvent() 函数
实现效果如下:
(2)QTimer 类
示例:在 UI 界面放置一个 Label 标签,两个按钮,分别是 “开始” 和 “停止” ,当点击 “开始” 按钮时,开始每隔 1 秒计数一次,点击 “停止” 按钮时,暂停计数。
A. 设计 UI 界面
B. 在 "widget.cpp" 文件中实现对应功能
实现效果如下:
(3)获取系统日期及时间
在 Qt 中,获取系统的日期及实时时间可以通过 QTimer 类和 QDateTime 类。
QDateTime 类提供了字符串格式的时间。字符串形式的时间输出格式由 toString() 方法中的 format 参数列表决定,可⽤的参数列表如下:
【获取系统日期及实时时间】
A. 设计 UI 界面文件
放置一个 Label 控件,用来显示日期及时间,放置两个按钮:“开始” 和 “停止”
B. 在 "widget.h" 头文件中声明更新时间的槽函数
C. 在 "widget.cpp" 文件中实现对应功能
实现效果如下:
6、事件分发器
(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 可以查看其所包括的事件类型,如下图示:
示例:
A. 在 "widget.h" 头文件中声明鼠标点击事件和事件分发器
如下图示:
B. 在 "widget.cpp" 文件中实现鼠标点击事件和拦截事件
执行结果如下:
7、事件过滤器
在 Qt 中,一个对象可能经常要查看或拦截另外一个对象的事件,如对话框想要拦截按键事件,不让别的组件接收到,或者修改按键的默认值等。通过上面的学习,我们已经知道,Qt 创建了 QEvent 事件对象之后,会调用 QObject 的 event() 函数 处理事件的分发。显然,我们可以在 event() 函数中实现拦截的操作。由于 event() 函数是 protected 的,因此,需要继承已有类。如果组件很多,就需要重写很多个 event() 函数。这当然相当⿇烦,更不用说重写 event() 函数还得小心一堆问题。好在 Qt 提供了另外一种机制来达到这一目的:事件过滤器。
事件过滤器是在应用程序分发到 event 事件分发器 之前,再做一次更高级的拦截。
如下图示:
事件过滤器的一般使用步骤:
- 安装事件过滤器;
- 重写事件过滤器函数:eventfilter() 。
示例:
(1)新建 Qt 项目
基类选择 QWidget,同时勾选 UI 界面文件,如下图示:
(2)设计 UI 文件
(3)在项目新添加⼀个类:MyLabel
先选中项目名称 QEvent,点击鼠标右键,选择 add new ... ,弹出如下对话框:
(4)选择:Choose ...
弹出如下界面:
(5)此时项目中会新添加以下两个文件
(6)在 UI 文件中选中 Label,右键 ——> 提升为...
(7)当点击 "提升为... " 之后,弹出对话框
(8)在 "mylabel.h" 中声明鼠标点击事件和事件分发器
(9)在 "mylabel.cpp" 文件中实现鼠标点击事件和事件分发器
(10)在 "widget.h" 头文件中声明事件过滤器函数
(11)在 "widget.cpp" 文件中实现事件过滤器的两个步骤
执行结果如下所示:
二、Qt 文件
1、Qt 文件概述
文件操作是应用程序必不可少的部分。Qt 作为一个通用开发库,提供了跨平台的文件操作能力。Qt 提供了很多关于文件的类,通过这些类能够对文件系统进行操作,如文件读写、文件信息获取、文件复制或重命名等。
2、输入输出设备类
在 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 类用于启动外部程序,并且可以给程序传递参数。
- QBuffer 以⼀个 QByteArray 对象作为数据缓冲区,将 QByteArray 对象当作⼀个 I/O 设备来读写。
3、文件读写类
- 读数据:QFile 类中提供了多个方法用于读取文件内容;如 read()、readAll()、readLine() 等。
- 写数据:QFile 类中提供了多个方法用于往文件中写内容;如 write()、writeData() 等。
- 关闭文件:文件使用结束后必须⽤函数 close() 关闭文件。
访问一个设备之前,需要使用 open()函数 打开该设备,而且必须指定正确的打开模式,QIODevice 中所有的打开模式由 QIODevice::OpenMode 枚举变量定义,其取值如下:
【打开(读)文件 & 保存(写)文件】
(1)在 "mainwindow.h" 文件中声明 handleAction1 和handleAction2 方法
(2)在 "mainwindow.cpp" 文件中实现对应功能
实现效果如下:
4、文件和目录信息类
QFileInfo 是 Qt 提供的一个用于获取文件和目录信息的类,如获取文件名、文件大小、文件修改日期等。类似于这样的功能,在 C/C++ 标准库中原先是没有的,所以要想使用类似的功能往往需要使用系统 API。后来,C++17 引入了一个模块:filesystem,将上述内容纳入其中。
QFileInfo 类中提供了很多的方法,常用的有:
- isDir() 检查该文件是否是目录
- isExecutable() 检查该文件是否是可执行文件
- fileName() 获得文件名
- completeBaseName() 获取完整的文件名
- suffix() 获取文件后缀名
- completeSuffix() 获取完整的文件后缀
- size() 获取文件大小
- isFile() 判断是否为文件
- fileTime() 获取文件创建时间、修改时间、最近访问时间等
示例:
在 "widget.cpp" 文件中添加如下代码:
实现效果如下:
三、Qt 多线程
1、Qt 多线程概述
在 Qt 中,多线程的处理一般是通过 QThread 类来实现。
QThread 代表一个在应用程序中可以独立控制的线程,也可以和进程中的其他线程共享数据。
QThread 对象管理程序中的一个控制线程。
2、QThread 常用 API
3、使用线程
创建线程的步骤:
- 自定义一个类,继承于 QThread,并且只有一个线程处理函数(和主线程不是同⼀个线程),这个线程处理函数主要就是重写父类中的 run() 函数。
- 线程处理函数里面写入需要执行的复杂数据处理。
- 启动线程不能直接调用 run() 函数,需要使用对象来调用 start() 函数实现线程启动。
- 线程处理函数执行结束后可以定义⼀个信号来告诉主线程。
- 最后关闭线程。
(1)首先新建 Qt 项目
设计 UI 界面如下:
(2)新建一个类,继承于 QThread 类
程序如下:
执行效果:
说明:
- 线程函数内部不允许操作 UI 图形界面,⼀般用数据处理。
- connect() 函数第五个参数表示的为连接的方式,且只有在多线程的时候才意义。
connect() 函数第五个参数为 Qt::ConnectionType,用于指定信号和槽的连接类型。同时影响信号的传递方式和槽函数的执行顺序。
Qt::ConnectionType 提供了以下五种方式:
4、线程安全
实现线程互斥和同步常用的类有:
- 互斥锁:QMutex、QMutexLocker
- 条件变量:QWaitCondition
- 信号量:QSemaphore
- 读写锁:QReadLocker、QWriteLocker、QReadWriteLock
(1)互斥锁
互斥锁是⼀种保护和防止多个线程同时访问同一对象实例的方法。在 Qt 中,互斥锁主要是通过 QMutex 类来处理。
A. QMutex
a. 特点
QMutex 是 Qt 框架提供的互斥锁类,用于保护共享资源的访问,实现线程间的互斥操作。
b. 用途
在多线程环境下,通过互斥锁来控制对共享数据的访问,确保线程安全。
QMutex mutex;
mutex.lock(); //上锁
//访问共享资源
//...
mutex.unlock(); //解锁
B. QMutexLocker
a. 特点
QMutexLocker 是 QMutex 的辅助类,使用 RAII(Resource Acquisition Is Initialization)方式对互斥锁进行上锁和解锁操作。
b. 用途
简化对互斥锁的上锁和解锁操作,避免忘记解锁导致的死锁等问题。
QMutex mutex;
{
QMutexLocker locker(&mutex); //在作⽤域内⾃动上锁
//访问共享资源
//...
} //在作⽤域结束时⾃动解锁
C. QReadWriteLocker、QReadLocker、QWriteLocker
a. 特点
-
QReadWriteLock 是读写锁类,用于控制读和写的并发访问。
-
QReadLocker 用于读操作上锁,允许多个线程同时读取共享资源。
-
QWriteLocker 用于写操作上锁,只允许一个线程写入共享资源。
b. 用途
在某些情况下,多个线程可以同时读取共享数据,但只有一个线程能够进行写操作。读写锁提供了更高效的并发访问方式。
QReadWriteLock rwLock;
//在读操作中使⽤读锁
{
QReadLocker locker(&rwLock); //在作⽤域内⾃动上读锁
//读取共享资源
//...
} //在作⽤域结束时⾃动解读锁
//在写操作中使⽤写锁
{
QWriteLocker locker(&rwLock); //在作⽤域内⾃动上写锁
//修改共享资源
//...
} //在作⽤域结束时⾃动解写锁
【示例1】
执行效果:
两个线程使用一把锁:
【在示例 1 的基础上使用 QMutexLocker 锁】
执行效果如下:
Qt 的锁和 C++ 标准库中的锁,本质上都是封装的系统提供的锁。编写多线程程序时,可以使用 Qt 的锁,也可以使用 C++ 的锁。
C++ 的锁能否锁 Qt 的线程呢?
可以,但不建议混着使用。
(2)条件变量
多个线程之间的调度是无序的,为了能够一定程度的干预线程之间的执行顺序,引入了条件变量。
在多线程编程中,假设除了等待操作系统正在执行的线程之外,某个线程还必须等待某些条件满足才能执行,这时就会出现问题。这种情况下,线程会很自然地使用锁的机制来阻塞其他线程,因为这只是线程的轮流使用,并且该线程等待某些特定条件,人们会认为需要等待条件的线程,在释放互斥锁或读写锁之后进入了睡眠状态,这样其他线程就可以继续运行。当条件满足时,等待条件的线程将被另一个线程唤醒。在 Qt 中,专门提供了 QWaitCondition 类来解决像上述这样的问题。
A. 特点
QWaitCondition 是 Qt 框架提供的条件变量类,用于线程之间的消息通信和同步。
B. 用途
在某个条件满足时等待或唤醒线程,用于线程的同步和协调。
QMutex mutex;
QWaitCondition condition;
//在等待线程中
mutex.lock();
//检查条件是否满⾜,若不满⾜则等待
while (!conditionFullfilled())
{
condition.wait(&mutex); //等待条件满⾜并释放锁
}
//条件满⾜后继续执⾏
//...
mutex.unlock();
//在改变条件的线程中
mutex.lock();
//改变条件
changeCondition();
condition.wakeAll(); //唤醒等待的线程
mutex.unlock();
(3)信号量
有时在多线程编程中,需要确保多个线程可以相应的访问一个数量有限的相同资源。例如,运行程序的设备可能是非常有限的内存,因此我们更希望需要大量内存的线程将这⼀事实考虑在内,并根据可用的内存数量进行相关操作,多线程编程中类似问题通常用信号量来处理。
信号量类似于增强的互斥锁,不仅能完成上锁和解锁操作,而且可以跟踪可用资源的数量。
A. 特点
QSemaphore 是 Qt 框架提供的计数信号量类,⽤于控制同时访问共享资源的线程数量。
B. 用途
限制并发线程数量,用于解决⼀些资源有限的问题。
QSemaphore semaphore(2); //同时允许两个线程访问共享资源
//在需要访问共享资源的线程中
semaphore.acquire(); //尝试获取信号量,若已满则阻塞
//访问共享资源
//...
semaphore.release(); //释放信号量
//在另⼀个线程中进⾏类似操作
四、Qt 网络
和多线程类似,Qt 为了支持跨平台, 对网络编程的 API 也进行了重新封装。
实际 Qt 开发中进行网络编程,也不一定使用 Qt 封装的网络 API,也有一定可能使用的是系统原生 API 或者其他第三方框架的 API。
在进行网络编程之前,需要在项目中的 .pro 文件中添加 network 模块。添加之后要手动编译一下项目,使 Qt Creator 能够加载对应模块的头文件。
为什么 Qt 要划分出这些模块呢?
Qt 本身是一个非常庞大,包罗万象的框架。如果把所有的 Qt 的功能都放到一起,既是我们就只是写一个简单的 hello world,那此时生成的可执行程序也会非常庞大(就包含了大量并没有使用的功能)。
模块化处理:
- 其它的功能分别封装成不同的模块。
- 默认情况下,这些额外的模块不会参与编译。
- 需要在 .pro 文件中引入对应的模块,才能把对于功能给编译加载进来。
1、UDP Socket
(1)核心 API 概览
主要的类有两个:QUdpSocket 和 QNetworkDatagram
QUdpSocket 表示一个 UDP 的 socket 文件。
QNetworkDatagram 表示一个 UDP 数据报。
(2)回显服务器
A. 创建界面,包含一个 QListWidget 用来显示消息
B. 创建 QUdpSocket 成员
不能直接添加头文件,否则无法编译通过,需要先添加网络模块。
a. 修改 widget.h
b. 修改 widget.cpp,完成 socket 后续的初始化
⼀般来说,要先连接信号槽,再绑定端口。如果顺序反过来,可能会出现端口绑定好了之后,请求就过来了,此时还没来得及连接信号槽,那么这个请求就有可能错过了。
C. 实现 processRequest,完成处理请求的过程
- 读取请求并解析
- 根据请求计算响应
- 把响应写回到客户端
D. 实现 process 函数
由于我们此处是实现回显服务器,所以 process 方法中并没有包含实质性的内容。
此时,服务器程序编写完毕,但是直接运行还看不出效果,还需要搭配客户端来使用。
(3)回显客户端
A. 创建界面,包含一个 QLineEdit、QPushButton、QListWidget
- 先使用水平布局把 QLineEdit 和 QPushButton 放好,并设置这两个控件的垂直方向的 sizePolicy 为 Expanding。
- 再使用垂直布局把 QListWidget 和上面的水平布局放好。
- 设置垂直布局的 layoutStretch 为 5,1(这个尺寸比例可以根据个人喜好微调)。
B. 在 widget.cpp 中,先创建两个全局常量,表示服务器的 IP 和端口
端口到本质上是一个 2 字节的无符号整数。
quint16:本质上就是一个 unsigned short(虽然 short 通常都是 2 个字节,但是 C++ 标准中没有明确规定这一点,只是说 short 不应该少于 2 个字节)。
C. 创建 QUdpSocket 成员
a. 修改 widget.h,定义成员
b. 修改 widget.cpp,初始化 socket
D. 给发送按钮 slot 函数,实现发送请求
E. 再次修改 Widget 的构造函数,通过信号槽来处理服务器的响应
a. 最终执行效果
客户端服务器测试的基本原则:一定是先启动服务器,后启动客户端。
启动多个客户端都可以正常工作,但是不能在界面选择直接运行,否则会覆盖上一个客户端。
2、TCP Socket
(1)核心 API 概览
核心类是两个:QTcpServer 和 QTcpSocket。
QTcpServer 用于监听端口,和获取客户端连接。
QTcpSocket 用户客户端和服务器之间的数据交互。
QByteArray 用于表示一个字节数组,可以很方便的和 QString 进行相互转换。
- 使用 QString 的构造函数即可把 QByteArray 转成 QString
- 使用 QString 的 toUtf8 函数即可把 QString 转成 QByteArray
(2)回显服务器
A. 创建界面,包含一个 QListWidget,用于显示收到的数据
B. 创建 QTcpServer 并初始化
a. 修改 widget.h,添加 QTcpServer 指针成员
b. 修改 widget.cpp,实例化 QTcpServer 并进行后续初始化操作
- 设置窗口标题
- 实例化 TCP server(父元素设为当前控件,会在父元素销毁时被一起销毁)
- 通过信号槽,处理客户端建立的新连接
- 监听端口
C. 继续修改 widget.cpp,实现处理连接的具体方法 processConnection
- 获取到新的连接对应的 socket
- 通过信号槽,处理收到请求的情况
- 通过信号槽,处理断开连接的情况
上述代码其实不够严谨,但在这里作为回显服务器已经够了。实际在使用 TCP 的过程中,TCP 是面向字节流的,一个完整的请求可能会分成多段字节数组进行传输。虽然 TCP 已经帮我们处理了很多棘手的问题,但是 TCP 本身并不负责区分从哪里到哪里是一个完整的应用层数据(粘包问题)。更严谨的做法:每次收到的数据都给它放到一个字节数组缓冲区中,并且提前约定好应用层协议的格式(分隔符 / 长度 / 其他办法),再按照协议格式对缓冲区数据进行更细致的解析处理。
D. 实现 process 方法,实现根据请求处理响应
由于我们此处是实现回显服务器,所以 process 方法中并没有包含实质性的内容。
此时,服务器程序编写完毕,但是直接运行还看不出效果,还需要搭配客户端来使用。
(3)回显客户端
A. 创建界面,包含一个 QLineEdit、QPushButton、QListWidget
- 先使用水平布局把 QLineEdit 和 QPushButton 放好,并设置这两个控件的垂直方向的 sizePolicy 为 Expanding
- 再使用垂直布局把 QListWidget 和上面的水平布局放好
- 设置垂直布局的 layoutStretch 为 5,1(这个尺寸比例可以根据个人喜好微调)
B. 创建 QTcpSocket 并实例化
a. 修改 widget.h,创建成员
b. 修改 widget.cpp,对 QTcpSocket 进行实例化
- 设置窗口标题
- 实例化 socket 对象(父元素设为当前控件,会在父元素销毁时被一起销毁)
- 和服务器建立连接
- 等待并确认连接是否出错
C. 修改 widget.cpp,给按钮增加点击的 slot 函数,实现发送请求给服务器
D. 修改 widget.cpp 中的 Widget 构造函数,通过信号槽处理收到的服务器的响应
先启动服务器,再启动客户端(可以启动多个),最终执行效果:
由于我们使用信号槽处理同一个客户端的多个请求,不涉及到循环,也就不会使客户端之间相互影响了。
3、HTTP Client
进行 Qt 开发时,和服务器之间的通信很多时候也会用到 HTTP 协议。
- 通过 HTTP 从服务器获取数据
- 通过 HTTP 向服务器提交数据
(1)核心 API
关键类主要是三个:QNetworkAccessManager、QNetworkRequest、QNetworkReply。
QNetworkAccessManager 提供了 HTTP 的核心操作:
QNetworkRequest 表示一个 HTTP 请求(不含 body)。
如果需要发送一个带有 body 的请求(比如 post),会在 QNetworkAccessManager 的 post 方法中通过单独的参数来传入 body。
其中的 QNetworkRequest::KnownHeaders 是一个枚举类型,常用取值:
QNetworkReply 表示一个 HTTP 响应,这个类同时也是 QIODevice 的子类。
此外,QNetworkReply 还有一个重要的信号 finished 会在客户端收到完整的响应数据之后触发。
(2)给服务器发送一个 GET 请求
A. 创建界面,包含一个 QLineEdit、QPushButton
- 先使用水平布局把 QLineEdit 和 QPushButton 放好,并设置这两个控件的垂直方向的 sizePolicy 为 Expanding。
- 再使用垂直布局把 QPlainTextEdit 和上面的水平布局放好(QPlainTextEdit 的 readOnly 设为 true)。
- 设置垂直布局的 layoutStretch 为 5,1(这个尺寸比例可以根据个人喜好微调)。
注意:此处建议使用 QPlainTextEdit,而不是 QTextEdit。主要是因为 QTextEdit 要进行富文本解析,最终显示的结果就不是原始的 HTML 了,如果得到的 HTTP 响应体积很大,会导致界面渲染缓慢甚至被卡住。
B. 修改 widget.h,创建 QNetworkAccessManager 属性
C. 修改 widget.cpp,创建实例
D. 编写按钮的 slot 函数,实现发送 HTTP 请求功能
执行程序,观察效果:
发送 POST 请求代码也是类似,使用 manager->post() 即可。
4、其他模块
Qt 中还提供了 FTP、DNS、SSL 等网络相关的组件工具。
五、Qt 音视频
1、Qt 音频
在 Qt 中,音频主要是通过 QSound 类来实现。但是需要注意的是 QSound 类只⽀持播放 wav 格式的音频文件。也就是说如果想要添加音频效果,那么首先需要将非 wav 格式的音频文件转换为 wav 格式。
通过帮助手册查看 QSound 类如下:
注意:使用 QSound 类时,需要添加模块: multimedia 。
(1)核心 API 概览
(2)示例
2、Qt 视频
在 Qt 中,视频播放的功能主要是通过 QMediaPlayer 类和 QVideoWidget 类来实现。在使用这两个类时要添加对应的模块 multimedia 和 multimediawidgets。
(1)核心 API 概览
(2)示例
首先在 .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()
{
}