自学内容网 自学内容网

【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() 以指定模式(只读、只写等)打开文件。
  • 读取文件:使用 QTextStreamQDataStream 读取文件内容。
  • 写入文件:使用 QTextStreamQDataStream 向文件中写入数据。
  • 关闭文件:使用 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)!