【Qt】系统相关学习--底层逻辑--代码实践

news2024/9/29 16:46:23

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);

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2177327.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何制作小程序商城

在这个瞬息万变的数字时代&#xff0c;每一个商业决策的背后都蕴含着对市场趋势的深刻洞察与把握。随着移动互联网的飞速发展&#xff0c;小程序商城以其便捷性、高效性和低门槛的特点&#xff0c;正逐步成为众多企业商家竞相布局的营销新阵地。今天&#xff0c;就让我们一起深…

sql-server【bcp工具】

目录 1.查看bcp是否可用 2.bcp 命令的基本语法 3.数据导出 4.数据导入 bcp&#xff08;Bulk Copy Program&#xff09;是 SQL Server 提供的一个命令行工具&#xff0c;用于在 SQL Server 实例与用户指定格式的数据文件之间批量复制表或视图数据。bcp 工具非常适合进行大量…

雷池 WAF 如何配置才能正确获取到源 IP

经常有大哥反馈说雷池攻击日志里显示的 IP 有问题。 这里我来讲一下为什么一些情况下雷池显示的攻击 IP 会有问题。 问题说明 默认情况下&#xff0c;雷池会通过 HTTP 连接的 Socket 套接字读取客户端 IP。在雷池作为最外层网管设备的时候这没有问题&#xff0c;雷池获取到的…

康谋分享 | 数据隐私和匿名化:PIPL与GDPR下,如何确保数据合规?(一)

目录 一、自动驾驶数据合规挑战 二、PIPL和GDPR的异同点 1、覆盖范围 2、个人信息定义 3、敏感数据 自动驾驶技术的快速发展伴随着数据隐私保护的严峻挑战。中国《个人信息保护法》&#xff08;PIPL&#xff09;与欧盟《通用数据保护条例》&#xff08;GDPR&#xff09;为…

Unity实战案例全解析:RTS游戏的框选和阵型功能(4)阵型功能

前篇&#xff1a;Unity实战案例全解析&#xff1a;RTS游戏的框选和阵型功能&#xff08;3&#xff09;生成范围检测框 重置框选操作-CSDN博客 本案例来源于unity唐老狮&#xff0c;有兴趣的小伙伴可以去泰克在线观看该课程 我只是对重要功能进行分析和做出笔记分享&#xff0c;…

PCIE XDMA

1 硬件电路 2 XDMA IP说明 2.1 Basic Mode:配置模式,选择 Basic 配置模式就可以了,Advanced 高级配置一般用不到; Lane Width:链路宽度,对于 ZYNQ MPSOC开发板,选择x2; Max Link Speed:最大链路速度,选择 8.0GT/s,即 PCIe 3.0 的传输速率; Refere

ultralytics yolo v8 示例:加载官方模型进行推理

Ultralytics YOLO 是计算机视觉和 ML 领域专业人士的高效工具。 安装 ultralytics 库&#xff1a; pip install ultralytics 实现代码如下&#xff1a; import cv2 from ultralytics import YOLO# 加载预训练的 YOLOv8n 模型 ckpt_dir "./ckpt/" # 模型缓存地址…

3、练习常用的HBase Shell命令+HBase 常用的Java API 及应用实例

目录 &#xff08;一&#xff09;练习常用的HBase Shell命令1、启动HBase2、练习shell命令create scan list describe alterputgetdeletedrop 关于NoSQL数据库中的列族和列3、关闭hbase服务 &#xff08;二&#xff09;HBase 常用的Java API 及应用实例1、启动hbase服务2、启动…

C++版本更新历史

前言 C语言发展至今已经迭代了很多版本&#xff0c;而在不同环境中编写代码时经常看到C标准的设定&#xff0c;比如 Leetcode 中可以看到版本信息&#xff1a; 这说明Leetcode已经支持最新C23标准了&#xff0c;但某些环境并不一定支持这些语法&#xff0c;如果不清楚使用的语法…

提示词工程 (Prompt Engineering) 最佳实践

prompt Engineering 概念解析 提示工程是一门较新的学科&#xff0c;关注提示词开发和优化&#xff0c;帮助用户将大语言模型&#xff08;Large Language Model, LLM&#xff09;用于各场景和研究领域。研究人员可利用提示工程来提升大语言模型处理复杂任务场景的能力&#xf…

Jetson nano (4GB版本)跑yolov8n(TensorRT 加速)网络

大家好&#xff0c;我是王硕&#xff0c;项目原因需要在Jetson nano平台上跑yolov8s &#xff0c;需要使用TensorRt加速&#xff0c;看了网上的教程&#xff0c;写的太差了&#xff0c;资料零零散散的&#xff0c;故详细介绍一下步骤。 如果想使用jetson Nano平台部署yolov8&a…

Selenium与数据库结合:数据爬取与存储的技术实践

目录 一、Selenium与数据库结合的基础概念 1.1 Selenium简介 1.2 数据库简介 1.3 Selenium与数据库结合的优势 二、Selenium爬取数据的基本步骤 2.1 环境准备 2.2 编写爬虫代码 2.3 数据提取 2.4 异常处理 三、数据存储到数据库 3.1 数据库连接 3.2 数据存储 3.3 …

windows10录屏工具,四款新手必备软件!

今天要和大家聊的是-——win10的录屏工具。在Win10电脑上&#xff0c;那些让我们爱不释手的录屏神器有很多&#xff0c;不管是哪个行业的人&#xff0c;录屏软件简直是日常工作的得力助手&#xff0c;比如说对于程序员来说&#xff0c;不管是分享代码教程、记录bug复现&#xf…

RKLLM部署

RKLLM 写在前面&#xff1a;建议去阅读官方提供的RKLLM doc&#xff0c;本文基于官方的RKLLM doc制作而成&#xff08;没有将flask相关内容添加进来&#xff09;&#xff0c;仅仅添加了完整流程的执行过程截图和在做这以流程过程中遇到的问题 RKLLM可以帮助用户快速将人工智能…

【vite】搭建完整项目流程、项目配置

文章目录 完整项目源码项目技术栈&#xff1a;项目地址&#xff1a; 一、创建项目二、安装scss三、安装路由router四、项目配置 参考文章&#xff1a;vite搭建完整项目 完整项目源码 觉得创建太麻烦就直接从github克隆吧&#xff0c;https://github.com/fruge365/vite-common…

金智维KRPA之Excel自动化

Excel自动化操作概述 Excel自动化主要用于帮助各种类型的企业用户实现Excel数据处理自动化&#xff0c;Excel自动化是可以从单元格、列、行或范围中读取数据&#xff0c;向其他电子表格或工作簿写入数据等活动。 通过相关命令&#xff0c;还可以对数据进行排序、进行格式…

开发者如何自主绑定和解除小程序和公众号长期/短期运营者微信号?

开发者如何自主绑定和解除小程序和公众号长期/短期运营者微信号&#xff1f; 1、什么是长期/短期运营者微信号&#xff1f; 为了让更多人管理公众号更方便与安全&#xff0c;每个公众号可由管理员添加绑定5个长期运营者微信号、20个短期运营者微信号&#xff0c;运营者微信号…

【sw2024】solidworks2024双击setup.exe无反应管理员运行也没反应解决方法

第一步 官网下载xxclean&#xff0c;打开xxclean最新版本&#xff0c;登录。官网xxclean.com或者自己浏览器搜。 第二步 点击扩展功能&#xff0c;点击 运行sw2024安装程序无反应 右边的开始 第三步 进度百分之百之后去双击setup就有界面了。

Tusi.Art:AI模型,comfyui工作流,一键同款!

前言 Tusi.Art&#xff1a;AI模型分享与在线运行平台的革命性体验 在当今迅速发展的AI绘图领域&#xff0c;模型的分享与应用变得尤为重要。Tusi.Art 就是这样一个平台&#xff0c;为用户提供了分享和在线运行 AI 模型的便捷服务。不仅如此&#xff0c;平台还具备强大的功能&…

现代易货交易:重塑价值交换的新趋势与未来展望“

在当今社会&#xff0c;随着经济的快速发展&#xff0c;一种新兴的交易方式——现代易货交易&#xff0c;正逐渐受到市场的青睐。这种模式不仅对传统的物品交换方式进行了现代化的改进&#xff0c;而且体现了对物品价值的重新评估和交换方法的创新。那么&#xff0c;什么是现代…