【Qt】系统相关学习--底层逻辑--代码实践
Qt事件
基本概念
理解Qt事件
事件是用户与应用程序之间交互的基础。它允许应用程序对用户的输入做出响应,例如鼠标点击一下又或者用户键盘输入相应内容。也就是说每一次用户与应用程序交互的时候,都会产生一个事件,然后传递给相应的控件或者窗口来进行处理。
关于Qt事件中重要概念理解
- 事件对象:在Qt中每个事件都是一个QEvent类或者其子类的一个对象,其中包含了事件的类型和相关数据(这个可以理解成一个大类,比如鼠标相关事件、键盘相关事件)
- 事件类型: 定义了事件的种类,每个事件类型对应一个整数值(也就是具体的事件,比如键盘或者鼠标按下)
- 事件循环:Qt使用事件循环来管理和调度事件,也就是应用程序启动后,Qt的事件循环会持续运行,等待用户的输入或者系统信号,然后将这些事件分发给相应的对象进行处理
- 事件传递:事件发生的时候,Qt会将该事件分发给合适的接受者;注意,如果事件是从最前端传递的,那么需要对象向上层对象传递,直到找到一个处理该事件的对象为止
- 事件处理函数:也就是用来处理事件发生的函数
信号和槽与事件的区别
- 信号和槽:Qt提供的一种高层次的通信方式,主要在与对象之间的交互,比如按钮点击触发了某个操作
- 事件处理:是一种更为底层的机制,处理更加细化的用户输入、窗口系统事件等,通常事件用于处理特定的用户输入,而信号和槽用于更广泛的对象间通信
常见的事件类型
- 鼠标事件:鼠标的各种移动,或者按下松开
- 键盘事件:按键类型、按键按下和松开
- 定时器事件:定时时间到达
- 进入离开事件:鼠标的进入和离开
- 滚轮事件:鼠标滚轮的滚动
- 绘屏事件:重新绘制屏幕的某些部分
- 显示隐藏事件:窗口的显示和隐藏
- 移动事件:窗口位置拜年话
- 大小改变事件:窗口大小的改变
- 焦点事件:键盘焦点的移动
事件处理
重写Event函数
因为在Qt中的Event基本都是虚函数,所以提供了重新实现接口。通过该方法可以对函数进行重写,然后处理特定的事件。
逻辑理解
创建一个新类,然后在这个新类中继承QLabel,然后对其事件进行重写。然后找到Label标签页,将其提升为继承自己类,然后当事件触发的时候,执行的便是自己定义的函数内容。
重写enterEvent()函数
- 创建新的类MyLabel继承自QLabel,也就是说这个新类是拥有与QLabel类相同的所有功能,也就可以重写其中的函数(QLabel中事件有大量的虚函数,随时都可以对其进行重写)
- 重写enterEvent()函数,通过重写鼠标进入的时候,触发的动作,从而实现鼠标一进入就打印文字的效果
- UI中创建QLabel并关联新类,也就是之前关联的是QLabel,现在则让其关联MyLabel,这样也就最终实现了自定义的功能
鼠标事件的实现
鼠标移动事件
- 默认情况下,鼠标移动事件只有当鼠标按下的时候才会被捕捉,但是打开鼠标追踪(setMousetracking)就可以实现那在该窗口内部移动就触发鼠标移动事件
- 函数原型分析
-
mouseMoveEvent(QMouseEvent* event)
:这是QWidget
类中的一个虚函数,用来处理鼠标移动事件。通过重写这个函数,可以自定义鼠标移动时的行为 -
setMouseTracking(bool enable)
:该函数用于启用或禁用鼠标追踪。当参数为true
时,鼠标移动时,即使未按下,也会触发mouseMoveEvent()
;当参数为false
时,只有按下鼠标时,才能触发mouseMoveEvent()
-
滚轮事件
实现过程分析
- 重写wheelEvent()函数,然后通过ddelta()获取滚轮滚动的距离
- 根据滚轮的方向计算其方位然后打印位置数据
定时器
QTimerEvent类实现定时器
- 启动两个定时器,时间间隔分别是1秒和2秒
- 哪个定时器超时,就更新对应哪个标签页中的数字内容
QTimer类
- 创建定时器:使用QTimer类创建一个定时器对象time,同时将定时器挂到当前窗口对象上
- 启动定时器:通过按钮1,启动定时器然后设定超时1秒,每秒触发一次定时器
- 定时器超时处理:每次定时器超时的时候,显示num的数值,同时数值自增,同时让这个数值同步在Label上显示
- 停止定时器:通过按钮2和定时器停止信号关联,从而达到关闭定时器功能
补充说明QTimer类中部分功能
- 指定时间间隔后发送一个timeout()信号
- 定时器可以设置为单次触发或者重复触发
- 定时器可以通过start()方法启动,同样可以通过stop方法停止
事件分发器
事件分发器理解
Qt中事件分发器负责将发生的各种事件(比如鼠标点击、键盘按下等事件)从系统传递给Qt对象,直到事件被处理或者被忽略,每个继承自QObject的类的对象都是可以接收并处理事件,Qt框架也会自动调用响应的函数对其进行处理。
事件分发,也就是从系统中捕获所有的事件,并将事件传递给合适的QObject对象来进行处理,每个继承QObject又可以通过重写函数的方式对该事件进行处理。
工作原理理解
上述描述中所产生的事件,会被应用程序中的event()函数捕获,然后针对事件的类型进行处理。
类比:可以简单的理解成餐厅中服务员处理顾客的请求。餐厅中的顾客就是事件,顾客会提出各种请求,这也就对应着鼠标点击等各种请求。服务员就是事件分发器,处理顾客的各种事件,服务员会根据顾客提出不同请求类型,去调用不同的处理函数去处理相应事件,如果请求难以达成,会进行忽略或者拦截。
代码逻辑
- event函数:通用事件处理函数,用于捕捉并处理所有类型的事件,其内部通过检查事件类型
- mouseressEvent函数:专门用于处理鼠标按下事件,主要用于处理更具体鼠标交互
- 时间拦截与传递:event函数中,如果已经返回true,则表明事件已经被处理了,不会再传递给其他函数,否则会继续传递给mouseressEvent继续处理
事件过滤器
基本概念了解
Qt的事件过滤器允许拦截、查看和处理对象的事件,一般情况下Qt的事件是通过对象的event()函数处理的,但是通过事件过滤器,可以在事件到达目标文件之前,相对器进行捕获和处理。
- 处理特定对象的事件:如果想要捕捉特定对象的事件(比如鼠标点击或者键盘输入事件),就可以通过事件过滤器来实现
- 改变事件的行为:可以在事件到达目标对象之前,先对其进行处理,既可以修改事件的默认行为,也可以通过事件过滤器组织事件到达对象
- 调试或者是监控事件:事件过滤器可以用来调试或者监控某些事件,从而查看对象接收的事件类型和信息
事件过滤器实现机制分析
如果一个对象安装了事件过滤器,所有传递给该对象的事件都会先经过事件过滤器,过滤器会检查或者修改事件,甚至可以决定函是否将该事件传递给目标对象。
- installEventFilter(QObject*filterobj):将一个过滤器对象安装到目标对象上,这样目标对象的事件就会先传递给过滤器独享先处理
- 定义一个类并重写eventFilter()函数:这样该函数就会拦截所有传递到目标对象的事件,因此可以通过这个函数中编写逻辑来处理事件
Qt文件
基本概念
Qt中文件指的是程序与文件系统进行交互的实体,也就是用于处理磁盘上文件数据。与标准的C++文件操作相比,QFile是更高层的抽象,并且与信号和槽机制以及跨平台机制相结合。
QFile继承自QIODevice,这也就意味着Qt中的文件不仅仅是简单的读写对象,还可以作为流式设备(文件、网络等),并且可以与Qt的其他QIODevice类型对象使用相同的接口操作。
支持多种文件操作模式,比如只读、只写、读写、追加等模式。
Qt文件相关类与组件简述
- QFile:Qt中的基本文件操作类,用于表示一个文件,可以进行文件的打开、读取、写入、关闭等,注意其不仅可以处理文本文件还可以处理二进制文件
- QFileInFo:文件的元数据信息(文件大小、修改时间、权限等),并且支持跨平台操作
- QTextStream:主要用于处理文本文件,通过该类可以方便的进行文本逐行读取或者写入
- QDataStream:用于处理二进制文件的流对象,可以将数据以二进制格式读写到文件中,避免了文本编码问题
- QDir:提供了目录操作功能,也就是列出目录文件、创建或者删除目录、改变工作目录等
- QIODevice:该类时QFile所继承的基础类,提供了通用的输入/输出设备的接口,其他IO类例如QTCcpSocket也是继承QIODevice的
Qt文件常见的操作总结
- 打开文件:使用
QFile::open()
以指定模式(只读、只写等)打开文件。 - 读取文件:使用
QTextStream
或QDataStream
读取文件内容。 - 写入文件:使用
QTextStream
或QDataStream
向文件中写入数据。 - 关闭文件:使用
QFile::close()
关闭文件。 - 检查文件存在:使用
QFile::exists()
检查文件是否存在。 - 获取文件信息:使用
QFileInfo
获取文件的大小、创建时间、修改时间等信息。
输入输出设备类
Qt的文件读写类是QFile,QFile发父类是QFileDevice,QFileDevice主要就是文件相应的底层操作。上述类的最顶层是QIODevice,QIODevice的父类就是的QOBject了。
主要类的功能总结
- QFile:负责处理文件的读写
- QFileDevice:文件交互的底层功能
- QIODevice:代表所有输入输出的设备的基类,统一了I/O设备的输入输出操作
- QOBject:Qt的核心类,提供信号与槽、元对象等机制
具体实现使用
创建并打开文件
创建一个QFile实例对象,然后调用其open()方法打开文件,可以自由的选择打开方式
#include <QFile>
#include <QTextStream>
#include <QDebug>
int main() {
// 1. 创建 QFile 对象,传入文件路径
QFile file("example.txt");
// 2. 尝试以只读模式打开文件
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "无法打开文件!";
return 1; // 失败返回
}
// 文件成功打开,可以进行读写操作
}
读取文件内容
文件打开成功后,就可以使用QTextStream或者直接使用QFile提供的读写函数进行读取
QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine(); // 逐行读取文本内容
qDebug() << line;
}
写入文件内容
首先是需要打开QFile的写入模式,然后使用QTextStream或者Write()方法写入内容,写入的时候会覆盖原有的内容
QFile file("example.txt");
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qDebug() << "无法打开文件进行写入!";
return 1;
}
QTextStream out(&file);
out << "Hello, World!\n"; // 写入文本
关闭文件
file.close(); // 关闭文件
QFile高级功能
- 临时文件操作:通过 QTemporaryFile 创建临时文件,这些文件可以在操作完成后自动删除
- 安全保存文件:使用 QSaveFile 确保文件写入过程中不会丢失数据(尤其在写入失败时,避免数据损坏)
- 缓冲区操作:通过 QBuffer,可以将内存中的数据当作文件进行操作
- 设备操作:除了操作文件,QIODevice 还支持如网络套接字(QTcpSocketQUdpSocket)、串口(QSerialPort)、蓝牙设备等 I/O 设备
文件读写类
对于文件的基本操作方法总结
打开方式总结
- QIODeviceBase::ReadOnly:用于只读操作
- QIODeviceBase::WriteOnly | QIODeviceBase::Append:用于在文件末尾追加数据
- QIODeviceBase::ReadWrite:允许同时进行读写操作
- QIODeviceBase::Truncate:用于清空文件并重新写入数据
读取文件到客户端
实现逻辑,先创建一个文件类,然后打开特定路径的文件显示到客户端中即可
#include "widget.h"
#include "./ui_widget.h"
#include<QFileDialog>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
connect(ui->pushButton,&QPushButton::clicked,[=](){
QString path = QFileDialog::getOpenFileName(this,"打开文件","C:\\Users");
ui->lineEdit->setText(path);
QFile file(path);
file.open(QIODevice::ReadOnly);
QString str = file.readAll();
ui->textEdit->setText(str);
file.close();
});
}
Widget::~Widget()
{
delete ui;
}
写入文件逻辑的实现
文件和目录信息类
QFileInfo类中提供了获取文件和目录的一些常用方法,其中的允许我们获取文件名、大小、修改时间、路径等信息
核心方法
- isDir():检查给定的路径是否是一个目录
- isExecutable():检查文件是否为可执行文件,用于检查应用程序或者二进制文件是否可以执行
- fileName():返回文件的名称,一般用于显示或者处理文件列表
- completeBaseName():获取文件的完整基本名,适用于获取文件名而不包括后缀的情况
- suffix():获取文件的后缀
- completeSuffix():获取文件完整的后缀
- size():返回文件大小
- isFile():判断目标是否为文件
- fileTime:获取文件的创建事件、修改时间、最近访问时间等
Qt多线程
实现逻辑
QThread底层实现分析
基本执行逻辑分析
- 线程创建,QThread通过调用底层API来创建线程
- 事件循环,Qt对事件循环机制进行了底层封装。在代码中如果想要使用定时器、信号与槽机制等需要调用exec()启动事件循环机制(下文代码简单表示)
- 信号与槽的线程间通信
- 如果信号与槽位于不同线程中的话,Qt会自动使用Queued Connection队列连接
- 信号发射的时候,参数会被复制并放入到目标线程的事件队列中(这样不会打断目标线程的执行),槽函数在目标线程的事件循环中被调用
QObject 的线程亲和性理解
- 理解:每个QObject对象都有一个关联的线程,该对象只在创建它的线程中执行,同时还可以使用moveToThread()方法,将一个QObject对象移动到另一个线程中执行
- 一个线程中是可以存在多个QObject对象,这些对象的槽函数和事件处理机制也会在该线程中执行
- 一个线程存在多个QObject对象的时候,需要确保跨线程的数据共享时的线程安全问题,也就是说多个对象在其他线程中共享数据的时候,需要一定的安全机制,确保数据一致性
- UI对象必须在主线程中创建和使用
- 不要在不同线程中直接访问QObject的成员变量和方法,只有在线程安全的时候才可以
QThreadPool和QRunnable的底层实现
- QThreadPool:管理一堆线程池,线程池的数量对应着CPU的内核数
- QRunnable:表示一个可运行的任务,QThreadPool从任务队列中取出任务,然后放到空闲的线程中执行
Qt Concurrent 模块
该模块时基于QThreadPool实现的,提供了高层次的API,封装了线程管理和同步,也就是负责管理线程的创建和销毁,使用Future和Watcher模式可以跟踪异步计算的结果和状态。
线程使用
使用QThread类
直接继承QTread类,然后重写run()方法
可以将耗时的操作交给创建的线程做,也是将其逻辑写入到run方法中,但是在run方法中尽量不要操作UI元素,因为两者是属于不同线程的,容易造成错误。
class MyThread : public QThread {
Q_OBJECT
protected:
void run() override {
// 在线程中执行的代码
}
};
//具体使用(创建一个线程,执行run方法)
MyThread *thread = new MyThread();
thread->start();
使用工作对象
通过将工作对象移动到新线程中,并通过信号与槽机制管理线程的运行和终止,逻辑实现如下。
- 线程和工作对象(也就是子线程需要执行的任务)
- Worker开始是属于主线程的,因为刚创建的时候是在主线程中运行
- 将工作对象移动到新线程
- 也就是将工作任务交给新线程中执行,该步骤就是为了保证工作在新线程中执行
- 信号与槽连接
- 信号槽1:thread线程启动后,发射started()向后,然后触发worker中的dowork()函数(注意,此时worker已经移动到thread中,所以dowork()函数会在新线程中执行)
- 信号槽2:worker完成任务后,会发射workFinished()向后,这个信号是连接到thread中的quit()槽函数,表示线程的事件循环应当退出,所以线程在工作完成后会自动退出
- 信号槽3:thread线程完成后,会发射结束信号,当接收到该信号的时候,就调用对应槽函数确保线程的生命周期安全的结束
- 启动线程
- 线程退出与对象销毁
使用该方式优点总结,首先其是借助Qt中信号与槽机制实现不同线程之间通信,以及借助信号与槽机制实现了对线程以及工作对象生命周期的管理,从而最大的限度的避免了线程安全问题
class Worker : public QObject {
Q_OBJECT
public slots:
void doWork() {
// 执行耗时操作
emit workFinished();
}
signals:
void workFinished();
};
// 在主线程中:
QThread *thread = new QThread;
Worker *worker = new Worker();
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Worker::doWork);
connect(worker, &Worker::workFinished, thread, &QThread::quit);
connect(thread, &QThread::finished, worker, &QObject::deleteLater);
thread->start();
使用QRunnable和QThreadPool
QRunnabled接口类,封装需要在线程池中需要执行的任务
class MyTask : public QRunnable {
public:
void run() override {
// 执行任务
}
};
QThreadPool,管理一组工作线程,这些线程就是负责执行QRunnable的任务
QThreadPool *threadPool = QThreadPool::globalInstance();
MyTask *task = new MyTask();
threadPool->start(task);
使用Qt Concurrent模块
QtConcurrent::run(),主要用于新线程中运行一个函数或者函数对象
QtConcurrent::run([](){
// 在线程中执行的代码
});
Connect第五个参数
- Qt::AutoConnection:信号和槽同一线程中,Qt::DirectConnection同步调用槽函数;不同线程则Qt::QueuedConnectio,将信号放入事件队列中,槽函数在接收线程的事件中循环执行
- Qt::DirectConnection:信号发出时槽函数就立刻执行,适合同一线程实时响应的情况
- Qt::QueuedConnection:异步调用,信号发出后程序会继续执行,不会等待槽函数执行完毕,一般适用于不同线程的情况
- Qt::BlockingQueuedConnection:信号会插入到接收线程的事件队列中,但是信号发出线程会出现阻塞,直到槽函数运行执行完成后才执行
- Qt::UniqueConnection:防止重复连接,使用该类型从而确保信号和槽之间只建立一次连接诶,如果信号和槽之前已经连接过,再次连接的时候会无效
// 使用 Qt::AutoConnection(默认)
connect(sender, &Sender::signal, receiver, &Receiver::slot);
// 指定使用 Qt::DirectConnection
connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::DirectConnection);
// 指定使用 Qt::QueuedConnection
connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::QueuedConnection);
// 使用 Qt::BlockingQueuedConnection
connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::BlockingQueuedConnection);
// 防止重复连接,使用 Qt::UniqueConnection
connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::UniqueConnection);
Qt线程编码实现
实现逻辑梳理
- 定义继承自QThread的类,重写run()函数
- 通过信号与槽机制,将子线程的结果传递给主线程
- 使用start()启动线程,并通过信号槽来进行跨线程通信,避免直接在子线程中操作UI
具体实现
线程安全
方法概述
线程安全的方法总体还是Linux网络编程的一些方法
- 互斥锁:用于确保同一时间只有一个线程可以访问共享资源
- 条件变量:用于线程同步,允许一个线程等待特定条件
- 信号量:用于控制线程对有限资源的访问,可以允许多个线程同时访问多个资源
- 读写锁:允许多个线程同时读,但写的时候只有一个线程可以访问,适合读多写少的场景
互斥锁
QMutex:用于确保任何时刻都只有一个线程可以访问共享资源,其他线程必须等待直到锁释放
QMutex mutex;
mutex.lock(); // 锁定
// 访问共享资源
mutex.unlock(); // 解锁
QMytexLocker:较为方便的一个类,其会在作用域结束的时候自动解锁互斥锁
QMutex mutex;
{
QMutexLocker locker(&mutex);
// 访问共享资源
} // 离开作用域时自动解锁
条件变量
QWaitCondition:主要用于线程同步,一般和互斥锁结合使用,允许一个线程等待条件满足的时候继续执行。一般是一个线程调用wait()方法等待某个条件,然后另一个线程通过wakeone()或者wakeAll()通知等待的线程
QMutex mutex;
QWaitCondition condition;
mutex.lock();
condition.wait(&mutex); // 等待条件满足
mutex.unlock();
// 在另一个线程中唤醒等待的线程
mutex.lock();
condition.wakeOne(); // 唤醒一个等待的线程
mutex.unlock();
信号量
QSemaphore:信号量就是用来控制一组共享资源的访问,可以允许多个线程同时访问有限数量的资源,线程可以通过acquire()获取资源,通过release()方法释放资源
QSemaphore semaphore(3); // 信号量允许同时访问3个资源
semaphore.acquire(); // 获取资源
// 访问资源
semaphore.release(); // 释放资源
读写锁
QReadWriteLock:允许多个线程同时读的锁,但是只允许一个线程写。QReadLocker,用于锁定读取访问,允许多个线程同时读取。QWriteLocker则是用于锁定写入访问,确保只有一个线程在写入的时候访问资源
QReadWriteLock lock;
// 读锁
{
QReadLocker locker(&lock);
// 多个线程可以同时读取资源
}
// 写锁
{
QWriteLocker locker(&lock);
// 只有一个线程可以写入资源
}
Qt网络
Qt网络接口封装逻辑
事件驱动与信号槽机制
简单总结说Qt网络模块就是基于事件驱动模型,利用信号槽机制实现的异步非阻塞的网络通信
事件循环
- Qt的应用程序中有一个主事件循环,也就是QCoreApplication::exec(),专门用于处理信号
- 注意:Qt中的网络模块会将套接字的读写事件(例如数据到达、发送数据)转换为Qt事件,然后在事件循环中处理
- exec()函数简要了解
- 函数基本作用:该函数进入主事件循环后,会一直执行,直到调用exit()或者quit()的时候,其主要功能就是不断的接收来自系统的事件(例如鼠标点击、按钮事件等),然后分配这些事件到应用程序中的窗口空间中,让其处理具体的事件
- 事件循环:Qt中的事件循环与服务器其中的事件循环类似,不断的检查事件,发现就绪事件后对其事件进行处理
// 基本使用方法示例
#include <QCoreApplication>
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
// 应用程序的初始化代码
return app.exec(); // 进入事件循环,直到调用 exit()/quit() 才会退出
}
信号与槽
- 当网络事件发生后(例如当数据可读的时候),Qt就会发出相应的信号
- 代码中可以写专门处理这些向后的槽函数,然后在槽函数中处理数据
异步非阻塞
- Qt中的网络的操作是不会阻塞主线程的,数据的发送和接收都是异步的,这样就确保了应用程序的界面响应不会因为网络操作而出现卡顿
理解Qt对底层网络I/O操作封装逻辑
Qt网络编程信号运行逻辑
- 事件监听,Qt底层使用epoll机制检测某个网络套接字状态是否发生了变化,例如套接字是否变成了可读状态等
- Qt内部封装事件处理,通过内部机制对套接字状态进行监控,然后在事件循环中检测到状态变化后,会将其转换为一个Qt事件,这个事件会放入到事件队列中等待被处理
- 信号发出,当事件循环处理到该Qt事件的时候,会通知QTCPSocket或者QUdpSocket实例,该实例内部逻辑又会触发readyRead()信号
- 信号与槽函数处理,设计槽函数,然后对其接收到的信号进行处理
封装逻辑
应用程序层
负责处理具体的任务,不需要关注底层的具体实现细节,只需要调用顶层的接口即可,流入发送HTTP请求处理接收的数据等
QTcpSocket *socket = new QTcpSocket(this);
socket->connectToHost("example.com", 80);
connect(socket, &QTcpSocket::readyRead, this, &MyClient::onReadyRead);
UDP Socket
UDP 接口
QUdpSocket类API分析
bind(const QHostAddress&, quint16)
- 作用:绑定指定的IP地址和端口号,也就是说发送该IP地址和端口号的数据包都是由这个Socket来接收
- 底层实现:其底层就是对C语言中的Bind 系统调用进行了简单封装
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY; // 绑定所有本地IP
addr.sin_port = htons(12345); // 绑定端口
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
receiveDatagram()
- 作用:从UDP套接字中接收一个UDP数据报并返回一个QNetworkDatagram对象,这个方法从套接字缓冲区中读取到达的数据
- 底层实现:使用recvfrom()函数简单的封装
char buffer[1024];
struct sockaddr_in sender_addr;
socklen_t sender_len = sizeof(sender_addr);
int recv_len = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&sender_addr, &sender_len);
writeDatagram(const QNetworkDatagram&)
- 作用:发送一个UDP数据报到指定的目标,这个方法主要用于无连接传输,发送方不需要建立连接,只需要将数据发送到目标IP和端口即可
- 底层:sendto()的封装,主要就是用于UDP套接字发送数据
struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(12345);
inet_pton(AF_INET, "192.168.1.1", &dest_addr.sin_addr);
sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr*)&dest_addr, sizeof(dest_addr));
readyRead
- 作用:这是UDPSocket的一个信号,当有数据到达并且可以读取的时候,这个信号就会被触发,此时就通过槽函数对其进行处理,实时处理接收到的数据
- 底层封装的类似于多路复用接口
QNetworkDatagram数据报API
QNetworkDatagram(const QByteArray&, const QHostAddress&, quint16) (构造函数)
- 作用:用于创建一个UDP数据报
- QByteArray:数据的内容
- QHostAddress:目标IP地址
- quint16:目标端口号
data() (方法)
- 作用:获取数据报内部的数据,然后返回QByteArray类型
- 主要就是通过调用该方法,获取数据内容
senderAddress() (方法)
- 作用:获取UDP数据报发送方的IP地址,当接收到一个UDP数据报的时候,这个方法就会返回发送方的IP地址
QHostAddress senderIp = datagram.senderAddress();
qDebug() << "Sender IP:" << senderIp.toString();
senderPort() (方法)
- 作用:获取UDP数据报发送方的端口号,返回的是发送方的端口号,用于标识数据是从哪个端口发送过来的
quint16 senderPort = datagram.senderPort();
qDebug() << "Sender port:" << senderPort;
UDP 回显服务器
依据其官方文档添加其CMake语句,以便可以正常使用其网络接口
信号槽连接要先于端口号绑定
信号槽绑定只是预先设定了事件处理机制,并不会主动触发事件,所以不会影响后续请求,如果先绑定了端口号,那么此时请求可能就会到来,但是事件处理机制还没有完善,此时就会出现错误。
处理请求逻辑分析
- 读取请求数据包(使用QNetworkDatagram对象实例--下文介绍该类的使用)
- 处理请求,生成响应,借助process函数处理请求
- 构建并发送响应数据包
QNetworkDatagram
参数说明:发送的数据+数据报的目标IP地址+目标地址的端口号
作用总结:通过构造函数创建一个包含数据、目标地址和端口号的UDP数据报
QByteArray data = "Hello, World!";
QHostAddress destAddress("192.168.1.10");
quint16 port = 12345;
// 创建一个数据报并指定目标地址和端口
QNetworkDatagram datagram(data, destAddress, port);
// 通过 socket 发送数据
socket->writeDatagram(datagram);
服务端代码
// widget.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);
//创建Socket实例对象
socket = new QUdpSocket(this);
//设置窗口标题
this->setWindowTitle("服务器");
//连接信号槽,当Socket发出readyRead信号的时候,则用对应函数进行处理
connect(socket,&QUdpSocket::readyRead,this,&Widget::processRequest);
//绑定端口号(绑定成功返回true,绑定失败返回false)
bool ret = socket->bind(QHostAddress::Any,9099);
if(!ret){
QMessageBox::critical(this,"服务器启动出现错误",socket->errorString());
return;
}
}
Widget::~Widget()
{
delete ui;
}
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);
}
QString Widget::process(const QString &request)
{
return request;
}
// widget.h
#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;
QUdpSocket*socket;
//处理信号的逻辑
void processRequest();
QString process(const QString&request);
};
#endif // WIDGET_H
UDP 回显客户端
发送按钮(向服务端发送数据)
首先是获取输入框中的内容,然后构造并发送请求数据,最后将信息写入到页面上,最后清空输入框即可。
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("");
}
UDP通信逻辑
Socket初始化,然后使用信号槽连接Socket和可读数据信号的处理函数。处理函数的响应逻辑就是获取响应数据,然后将响应的信息放到显示器上。
void Widget::processResponse()
{
//基本逻辑:读取响应数据然后将响应数据放到界面上即可
//1. 获取响应数据
const QNetworkDatagram&responseDatagram = socket->receiveDatagram();
QString response = responseDatagram.data();
//2. 将响应信息放到显示界面上
ui->listWidget->addItem("服务器:"+response);
}
代码总览
// widget.cpp
#include "widget.h"
#include "./ui_widget.h"
#include<QNetworkDatagram>
// 服务器IP地址和端口号
const QString&SERVER_IP = "127.0.0.1";
const quint16 SERVER_PORT = 9099;
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//设置窗口名
this->setWindowTitle("客户端");
//Socket实例化
socket = new QUdpSocket(this);
//信号槽处理放服务器返回的数据
connect(socket,&QUdpSocket::readyRead,this, &Widget::processResponse);
}
Widget::~Widget()
{
delete ui;
}
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("");
}
void Widget::processResponse()
{
//基本逻辑:读取响应数据然后将响应数据放到界面上即可
//1. 获取响应数据
const QNetworkDatagram&responseDatagram = socket->receiveDatagram();
QString response = responseDatagram.data();
//2. 将响应信息放到显示界面上
ui->listWidget->addItem("服务器:"+response);
}
// Widget.h
#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 slots:
void on_pushButton_clicked();
//请求处理函数
void processResponse();
private:
Ui::Widget *ui;
// Socket
QUdpSocket*socket;
};
#endif // WIDGET_H
总体逻辑梳理
TCP Socket
TCP 接口
QTcpServer
listen(const QHostAddress&, quint16 port)
底层调用的就是系统级别的bind()和listen(),bind将服务器的IP地址和端口号绑定到一个Socket对象上,listen则是让该Socket进入监听状态,客户端有请求的时候会通知服务器
int serverSocket = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP socket
bind(serverSocket, (struct sockaddr*)&address, sizeof(address)); // 绑定IP和端口
listen(serverSocket, backlog); // 监听连接
nextPendingConnection()
底层使用accept()系统调用,也就是当有客户端连接的时候,accept()被调用,生成一个新的socket用于客户端进行通信,返回这个socket的描述符。
newConnection
触发Qt的事件循环机制,在新连接到来后,系统通知Qt的事件循环,然后newConnection信号会被发射,然后该线程就可以继续去处理新连接
QTcpSocket
readAll()
其底层是调用read()或者recv()系统调用,从socket的接收缓冲区中读取数据,然后返回给调用者。Qt使用QByteArray对象存储读取到的数据
write(const QByteArray& data)
write()或者send()系统调用,也就是将QByteArray中的字节数据通过socket发送给对方
deleteLater()
将socket标记为无效,并等待事件循环结束的时候释放资源即可,因为该函数本质上是Qt的一种机制,主要用于延迟删除对象,避免在事件处理过程中立即删除而导致程序崩溃的情况
readRead
类似于select()机制,Qt的事件循环会检测socket的读事件,当socket中有数据可读的时候,会发出reeayRead信号,通知上层应用可以调用readAll()读取数据
disconnected
当socket的连接断开的时候,Qt通过事件循环检测到断开事件,发出disconnected信号通知上层
TCP 回显服务器
实现逻辑分析
创建QTcpServer然后初始化
- 设置服务端窗口标题为“服务器”
- 实例化QTcpServer:实例出来一个tcpserver对象
- 监听端口,通过listen()方法,监听指定的端口,等待客户端连接请求
处理客户端连接
处理客户端请求和响应
断开连接(通过信号与槽机制)
TCP 回显客户端
基本逻辑梳理,客户端与服务端建立连接,然后客户端发送消息给服务器,服务器返回消息后,客户端解析消息,最终回显到显示框中。其中的连接处理都是通过信号与槽机制实现的。
// 客户端:Widget.cpp
#include "widget.h"
#include "./ui_widget.h"
#include<QMessageBox>
#include<QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//设置窗口标题
this->setWindowTitle("客户端");
//实例化Socket
socket = new QTcpSocket(this);
//与服务器进行连接
socket->connectToHost("127.0.0.1",9090);
//判断连接是否成功
if(!socket->waitForConnected()){
QMessageBox::critical(nullptr,"连接服务器出错!",socket->errorString());
exit(1);
}
connect(socket,&QTcpSocket::readyRead,this,[=](){
QString response = socket->readAll();
qDebug()<<response;
ui->listWidget->addItem(QString("服务器说:")+response);
});
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_clicked()
{
//获取输入框的内容
const QString&text = ui->lineEdit->text();
//清空输入框
ui->lineEdit->setText("");
//消息显示到界面上
ui->listWidget->addItem(QString("客户端说:")+text);
//发送消息给服务器
socket->write(text.toUtf8());
}
// 客户端 widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QTcpSocket>
#include <QTcpServer>
#include<QString>
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;
//QTcpSocket
QTcpSocket*socket;
};
#endif // WIDGET_H
//服务端 Widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QTcpSocket>
#include <QTcpServer>
#include<QString>
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;
//创建QTcpServer
QTcpServer*tcpserver;
//处理连接
void processConnection();
//处理请求
QString process(const QString&request);
};
#endif // WIDGET_H
// 服务端Widget.cpp
#include "widget.h"
#include "./ui_widget.h"
#include<QMessageBox>
#include<QString>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//设置窗口标题
this->setWindowTitle("服务器");
//实例化Tcp Server
tcpserver = new QTcpServer(this);
//简历信号槽,处理客户端建立的新连接
connect(tcpserver,&QTcpServer::newConnection,this,&Widget::processConnection);
//监听端口
bool ret = tcpserver->listen(QHostAddress::Any,9090);
if(!ret)
{
QMessageBox::critical(nullptr,"服务器启动失败",tcpserver->errorString());
exit(1);
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::processConnection()
{
//获取新连接的socket
QTcpSocket*clientSocket = tcpserver->nextPendingConnection();
QString log = QString("[") + clientSocket->peerAddress().toString()+":" + QString::number(clientSocket->peerPort())
+ "] 客户端上限";
ui->listWidget->addItem(log);
//信号槽:处理收到的请求
connect(clientSocket,&QTcpSocket::readyRead,this,[=](){
//读取请求
QString request = clientSocket->readAll();
//根据请求构建响应
const QString&response = process(request);
//响应写回客户端
clientSocket->write(response.toUtf8());
QString log = QString("[") + clientSocket->peerAddress().toString()+":" + QString::number(clientSocket->peerPort())
+"]req:"+request+"resq:"+response;
ui->listWidget->addItem(log);
});
//信号槽处理断开连接的情况
connect(clientSocket,&QTcpSocket::disconnected,this,[=](){
QString log = QString("[") + clientSocket->peerAddress().toString()+":" + QString::number(clientSocket->peerPort())
+ "] 客户端下线";
ui->listWidget->addItem(log);
clientSocket->deleteLater();
});
}
QString Widget::process(const QString &request)
{
return request;
}
HTTP Client
HTTP接口
QNetworkAccessManager
提供发送HTTP请求的一个类,可以执行GET、POST、PUT等常规操作,负责管理网络通信的生命周期,只叙述其重要常见的方法。
- get(const QNetworkRequest&):发送一个HTTP GET请求,返回一个QNetworkReply对象,用于处理响应
- post(const QNetworkRequest&, const QByteArray&):发送一个POST请求,返回如上
QNetworkRequest
表示一个HTTP请求(不包括请求体),其是通过URL来指定请求目的地,并允许设置请求头
- QNetworkRequest(const QUrl&):通过URL构造一个HTTP请求
- setHeader(QNetworkRequest::KnownHeaders, const QVariant&):设置请求的头部信息,经常用来指内容类型长度等
- QNetworkRequest::KnownHeaders:枚举类
ContentTypeHeader
:描述body的类型(如application/json
)。ContentLengthHeader
:描述body的长度。UserAgentHeader
:设置客户端的User-Agent信息。CookieHeader
:设置cookie信息
QNetworkReply
该类表示一个HTTP响应,其也是QIODevice的子类,可以像处理文件一样处理响应中的数据,主要用于获取HTTP响应状态、响应头、数据体
-
error()
获取请求过程中发生的错误状态 -
errorString()
获取错误原因的详细描述文本 -
readAll()
读取响应的body部分(即服务器返回的内容) -
header(QNetworkRequest::KnownHeaders)
获取指定响应头的值
实现基本逻辑(代码事例理解)
#include <QCoreApplication>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QDebug>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 创建 QNetworkAccessManager 实例
QNetworkAccessManager manager;
// 发送 GET 请求
QNetworkRequest request(QUrl("http://jsonplaceholder.typicode.com/posts/1"));
QNetworkReply* reply = manager.get(request);
// 处理响应
QObject::connect(&manager, &QNetworkAccessManager::finished, [&](QNetworkReply* reply) {
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
qDebug() << "Response:" << responseData;
} else {
qDebug() << "Error:" << reply->errorString();
}
reply->deleteLater(); // 清理Reply对象
});
return a.exec();
}
Qt音视频
Qt音频
Qt中的音频播放实现主要就是通过QSound类来实现的,这个类支持用于播放简单的音效,但是只支持.WAV格式的音频文件,如果想要支持其他的音频文件,则需要使用相应的处理库,比如QMediaPlayer
QSound类
核心方法就是使用play()开始播放音频,使用stop()方法关闭音频方法
QSound *sound = new QSound(":/1.wav", this);
sound->play();
不要忘记修改相应的构建文件以及引入头文件
Qt视频
QMediaPlayer
类
这个类是Qt的多媒体播放类,支持音视频文件和流媒体播放,可以控制媒体的运行
- setMedia(const QMediaContent& media):设置播放文件的路径,可以是本地路径也可以是网络路径
- play():开始或者继续播放当前设置的媒体
QVideoWidget
类
主要用于视频显示的控件,可以和QMediaPlayer结合使用,然后将视频输出到QVideoWidget中,其也是继承自QWidget
- show():显示QVideoWidget,从而使得视频画面可以正常在UI中显示
- setFullScreen(bool):设置全屏显示视频
videoWidget->show();
videoWidget->setFullScreen(true);
原文地址:https://blog.csdn.net/gma999/article/details/142437877
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!