《QT学习第四篇:常见事件与UDP、TCP、文件系统、(锁、信号量、条件变量》

**前引:**Qt框架中的核心编程技术,主要包括:1)事件处理机制,详细讲解了鼠标、键盘、窗口等各类事件的处理方法;2)文件操作,涵盖QFile的读写操作和文件对话框使用;3)多线程编程,包括线程创建、锁机制、条件变量和信号量;4)网络编程,重点阐述了UDP/TCP服务端和客户端的实现流程,以及HTTP客户端的开发方法。文章通过具体代码示例,展示了Qt在GUI事件响应、文件I/O、并发控制和网络通信等方面的强大功能,为Qt开发者提供了全面的技术参考!

目录

一、事件

(1)介绍

(2)处理

二、鼠标事件

(1)进入/退出

(2)鼠标点击位置

(3)点击左键/右键

(4)双击左键/右键

(5)释放鼠标(左/右)

(6)针对全局:鼠标事件

(7)鼠标按住移动事件

三、键盘事件

四、滚轮事件

五、窗口移动/大小事件

六、QFile文件操作

(1)初始化文件对象

(2)判断文件存在与否

(3)打开文件

(4)读取文件

(5)关闭文件

(6)写入文件

七、文件系统

(1)打开

(2)保存

八、线程

(1)继承(不推荐)

(2)QueuedConnection(推荐)

九、锁、条件变量、信号量

(1)锁

(2)条件变量

(3)信号量

十、UDP服务端

(1)创建套接字

(2)绑定端口和IP

(3)响应数据事件

(4)拿到客户端数据包

(5)回发客户端数据包

十一、UDP客户端

(1)目标端口和IP

(2)创建套接字

(3)事件驱动

(4)发送数据包给服务端

(5)拿到服务端数据包

十二、TCP服务端

(1)创建套接字

(2)绑定且监听

(3)响应新连接

(4)与客户端通信

(1)拿到客户端

(2)拿到客户端数据

(3)客户端断开请求

十三、TCP客户端

(1)创建套接字

(2)发起请求

(3)拿到服务端响应

(4)发送数据给服务端

十四、HTTP客户端

(1)构造HTTP管理器

(2)构造URL

(3)构建请求对象

(4)发送请求

(5)信号驱动

(6)效果展示​编辑


一、事件

(1)介绍

信号属于事件的一种,信号是应用层(比如按钮点击、移动),事件是系统级比如(鼠标、键盘)

在Qt中使⽤⼀个对象来表⽰⼀个事件。所 有的Qt事件均继承于抽象类 QEvent,常见事件如下:

(2)处理

事件由系统 / Qt 内核产生, 无须完成绑定,一般这些事件都是默认没有直观的显示效果的,需要使用C++的多态对这些事件函数进行重写,再在 ui 或者 代码中使用自定义的类,调用自己的方法

(为何无须绑定?因为是内核产生,QLabel这些控件只是事件的接收者)

二、鼠标事件

(1)进入/退出

事件描述:鼠标进入这个控件,或者退出这个控件就会产生这个事件,以QLabel为例

获取事件:enterEvent(进入)、leveEvent(离开)

例如:先自定义类继承QLabel,然后在 ui 中选择这个基类,右键提升为自定义类,完成多态调用

效果如下:

(2)鼠标点击位置

事件描述:鼠标点击这个控件的某个位置,可以唤醒事件

获取事件:mousePressEvent,通过x()和y()获取坐标(记得先重写,然后提升为自定义类)

第一种位置:以这个控件左上角为原点

第二种:用 globalX() 和 globalY() 获取坐标,以整个电脑界面左上角为坐标原点

(3)点击左键/右键

事件描述:鼠标点击了左键还是右键

获取事件:mousePressEvent(),通过 ->button 判断

例如:

(4)双击左键/右键

事件描述:鼠标双击了左键还是右键(注意:双击同时会触发单词点击事件)

获取事件:mouseDoubleClickEvent(),通过 ->button 判断

例如:

(5)释放鼠标(左/右)

事件描述:鼠标点击之后释放

获取事件:mouseReleaseEvent()

例如:

(6)针对全局:鼠标事件

我们知道事件是由 内核 产生,QWIdget也只是接收,更何况QLabel这些只是继承了QWIdget,所以,我们在QWIdget中使用鼠标信号也可以!但是需要用 setMouseTracking 打开开关(吃资源)

例如:

复制代码
(7)鼠标按住移动事件

事件:

cpp 复制代码
void mouseMoveEvent(QMouseEvent *event);

globalPos():相较于屏幕的坐标

mapFromGlobal(event->globalPos()):相较于控件的坐标,返回QPoint

cursorForPosition(QPoint):鼠标相较于文本的位置

三、键盘事件

事件描述:键盘输入了某个字符,会发生对应事件来定位到具体的内容

获取事件:mouseDoubleClickEvent(),通过 ->button 判断

例如:(注意:先判断组合键,否则可能直接拦截在 单个键这里)

四、滚轮事件

事件描述:滚轮的滑动会产生对应事件通知

获取事件:WheelEvent()

例如:

五、窗口移动/大小事件

事件描述:根据窗口是否移动,以及大小是否改变触发事件

获取事件:moveEvent(QMoveEvent *event)移动和resizeEvent(QResizeEvent *event)大小

例如:

六、QFile文件操作

(1)初始化文件对象

参数:要打开的文件的绝对路径或者相对路径

cpp 复制代码
QFile::QFile(const QString &fileName);

例如:

cpp 复制代码
QFile f1("test.txt");
(2)判断文件存在与否

参数接口:bool exists()

例如:

cpp 复制代码
if (!file.exists()) 
{
    qDebug() << "文件不存在";
    return;
}
(3)打开文件
cpp 复制代码
bool QFile::open(OpenMode mode);

参数:打开的模式

  • QIODevice::ReadOnly:只读模式(除此,下面三个模式都是文件不存在就创建)
  • QIODevice::Text:文本文件模式(按字符 / 行解析)
  • QIODevice::Append:打开追加
  • QIODevice::WriteOnly:打开追加

一般覆盖式写入是:QIODevice::WriteOnly | QIODevice::Text

一般追加式写入是:QIODevice::Append | QIODevice::Text

返回值: 打开成功返回true,失败返回false

例如:

cpp 复制代码
f1.open(QIODevice::ReadOnly | QIODevice::Text);
(4)读取文件
cpp 复制代码
//文本读取
QTextStream::readAll()

或者

//二进制读取
QFile::readAll()

解释:

文本读取是 "字节→字符串" 解析

二进制读取是 "字节原样保留",需要手动转字符串

例如:

cpp 复制代码
QString text = QTextStream(&f1).readAll();

或者

QByteArray bytes = f2.readAll();
(5)关闭文件
cpp 复制代码
void QFile::close();

例如:

cpp 复制代码
f1.close();
(6)写入文件
cpp 复制代码
QTextStream out();

例如:

cpp 复制代码
QTextStream out(&file);
out << "\n追加的内容";

七、文件系统

(1)打开
cpp 复制代码
QFileDialog::getOpenFileName( )

返回值:返回这个打开文件的完整路径

例如:

cpp 复制代码
QString filePath = QFileDialog::getOpenFileName(this, "窗口标题");
(2)保存
cpp 复制代码
QFileDialog::getSaveFileName ( )

返回值:返回这个保存文件的完整路径

例如:

cpp 复制代码
QString savePath = QFileDialog::getSaveFileName(this, "窗口标题");

八、线程

(1)继承(不推荐)

void run()override:线程的执行方法,需要继承QThread重写

start():启动线程

例如:创建一个计时器,让线程去发信号,主线程去修改窗口

先继承QThread,重写 run(),然后自定义信号,用于通知

然后在WIdget中创建一个线程,绑定信号和槽函数即可

(2)QueuedConnection(推荐)

注意:主类通过信号触发的对应槽函数会在线程中执行,但是模块类里面的自己调用不会

直接调用函数: 谁调的,就在谁的线程执行,跟对象属于哪个线程无关,就需要手动添加

第一步:理解"线程"在 Qt 里到底是什么

你可以把线程想象成一个流水线工人。你的程序默认只有一个工人(主线程),他要干所有事:刷新界面、处理按钮点击、读串口、写文件......

如果写文件很慢(比如要1秒),这1秒里工人被占着,界面就卡死了。

所以你想多雇几个工人(子线程),让他们分工。

第二步:QThread 是什么

QThread 就是一个工人。但工人不会自己找活干,他需要一个任务清单(事件循环)

复制代码
QThread* thread = new QThread;
thread->start();  // 工人上岗了,开始等任务

start() 之后,这个工人就一直站在那等任务来。你不往他的任务清单里塞东西,他就一直等

第三步:moveToThread 是什么

复制代码
m_dataStorage->moveToThread(thread);

意思:把 m_dataStorage 这个对象"绑定"到那个工人身上。

从此以后,所有通过信号槽发给 m_dataStorage 的任务,都会被塞到那个工人的任务清单里。

打个比方:

复制代码
moveToThread 之前:
    m_dataStorage 归主线程工人管
    有活来了 → 主线程工人亲自干

moveToThread 之后:
    m_dataStorage 归子线程工人管
    有活来了 → 塞到子线程工人的任务清单 → 子线程工人干

第四步:信号槽怎么配合的

现有的连接:

复制代码
connect(m_dataParser, &DataParserCache::newDataParsed,
        m_dataStorage, &DataStorageManager::onNewDataParsed);

这行代码完全不需要改。Qt 会自动判断:

复制代码
m_dataParser 在哪个线程? → 主线程
m_dataStorage 在哪个线程?→ 子线程(因为moveToThread了)

不在同一个线程!→ 自动用 QueuedConnection

QueuedConnection的工作方式是这样的:

复制代码
1. 主线程里 m_dataParser 发出 newDataParsed 信号
2. Qt 说:"接收方 m_dataStorage 在子线程,不能直接调用"
3. Qt 把这次调用打包成一个"任务"
4. 任务被投递到子线程的任务清单里
5. 主线程立刻继续干别的事(不等!不卡!)
6. 子线程工人从任务清单取出任务
7. 子线程工人执行 onNewDataParsed()
8. 写文件在子线程完成,主线程完全不受影响

第五步:为什么不能传 parent

复制代码
// 错误
m_dataStorage = new DataStorageManager(this);
m_dataStorage->moveToThread(thread);  // 会报错!

// 正确
m_dataStorage = new DataStorageManager;  // 不传 parent
m_dataStorage->moveToThread(thread);     // OK

Qt 的规则是:子对象必须和父对象在同一个线程。

如果你传了 this(主窗口)作为 parent,那 m_dataStorage 是主窗口的子对象,你再把它移到别的线程,Qt 不允许,因为父子不在同一线程了

不传 parent 的话,谁来管它的生命周期?用这行:

复制代码
connect(thread, &QThread::finished, m_dataStorage, &QObject::deleteLater);

意思是:线程停止时,自动删除 m_dataStorage。这样就不会内存泄漏。

第六步:Timer 为什么要特殊处理

复制代码
// 构造函数里
m_flushTimer = new QTimer(this);  // Timer 被创建了,此刻还在主线程
m_flushTimer->start();            // Timer 在主线程开始计时

// 然后
m_dataStorage->moveToThread(thread);  // 对象移走了
// 但 Timer 的"心跳"还在主线程!Timer 触发的槽也在主线程跑!

Timer 有个特殊规定:它在哪个线程被 start(),就在哪个线程 tick。

所以要确保 Timer 是在子线程里被 start 的

复制代码
// 构造函数里只创建,不start
m_flushTimer = new QTimer(this);

// 主类里连接线程启动信号
connect(thread, &QThread::started, m_dataStorage, &DataStorageManager::startWork);

// startWork() 槽
void DataStorageManager::startWork()
{
    m_flushTimer->start();  // 这行代码在子线程里执行!Timer就属于子线程了
}

为什么 startWork() 会在子线程执行?因为 m_dataStorage 已经被 moveToThread 了,thread->started 信号连接到 m_dataStorage->startWork(),跨线程,自动队列连接,所以 startWork() 在子线程里执行。

第七步:去掉线程为什么这么简单

如果以后不想用线程了:

复制代码
// 删掉这几行
m_storageThread = new QThread(this);
m_dataStorage->moveToThread(m_storageThread);
connect(m_storageThread, &QThread::finished, m_dataStorage, &QObject::deleteLater);
connect(m_storageThread, &QThread::started, m_dataStorage, &DataStorageManager::startWork);
m_storageThread->start();

// 把 new DataStorageManager 改回 new DataStorageManager(this)

之后所有 connect 不用改。因为 m_dataParserm_dataStorage 都在主线程,Qt 自动用 DirectConnection,槽函数直接在主线程执行。业务逻辑完全不变。

九、锁、条件变量、信号量

(1)锁

锁:很简单,从并行到串行访问,中间只有一个执行流修改资源

接口:

创建锁资源:QMutex mutex;

获取锁:mutex.lock();

释放锁:mutex.unlock();

(2)条件变量

条件变量:不满足条件时形成阻塞式的等待(会形成类似一个队列,有顺序),期间释放锁

(条件变量属于共享资源,必须要在锁的基础上使用)

为什么要释放锁?

假设你要洗澡,水没烧开,你如果不释放锁,那么就没有人去烧水,也就是没有线程去修改这个资源状态,你就永远进不去:只有你发现条件不满足,释放锁,让其他线程去执行生产,你才有资源

(否则你一直拿着锁,而锁又是生产和消费共享的,会导致单方没有锁形成死锁问题)

如何看待条件变量带来的性能提升?

如果没有条件变量,那么就会循环执行竞争锁->释放锁,导致CPU空转

而有了条件变量,就会阻塞到那里,不会执行空转

为什么需要循环判断?

因为可能存在虚假唤醒,比如线程调度器的随机唤醒机制

接口:

QWaitCondition cond:创建条件变量

cond.wait(&mutex(锁资源)):等待条件

cond.wakeOne():唤醒条件

(3)信号量

信号量:只控制并发数,也是一个共享资源,需要在锁的基础上使用

接口:

QSemaphore sem(int num):创建信号量资源

sem.acquire():信号量-1

sem.release():信号量+1

十、UDP服务端

(1)创建套接字

接口类:QUdpSocket

作用:创建一个 UDP 套接字对象

例如:

cpp 复制代码
QUdpSocket *socket = new QUdpSocket(this);
(2)绑定端口和IP

接口:bind

作用:将 UDP 套接字绑定到本地地址和端口,监听这个地址和端口

参数:

  • QHostAddress::Any:绑定到所有本地网络接口(即 0.0.0.0)
  • 9090:要监听的端口号

返回值:true 表示绑定成功,false 表示失败

例如:

cpp 复制代码
socket->bind(QHostAddress::Any, 9090);
(3)响应数据事件

例如:

cpp 复制代码
connect(socket, &QUdpSocket::readyRead, this, &Widget::processRequest);

信号 QUdpSocket::readyRead:当 socket 收到 UDP 有数据可读时,Qt 会自动发出这个信号,这 个槽函数是自定义的(作用类似 Linux的 IO的多路复用)

(4)拿到客户端数据包

接口:socket->receiveDatagram()

作用:一次性读取完整 UDP 数据报,封装成 QNetworkDatagram 对象,包含:

  • 原始数据(.data()
  • 客户端 IP(.senderAddress()
  • 客户端端口(.senderPort()

例如:

cpp 复制代码
//获取数据包
const QNetworkDatagram& requestDatagram = socket->receiveDatagram();
//拿到原始数据
QString request = requestDatagram.data();
(5)回发客户端数据包

需要单独写一个自定义函数来完成回发给对方的内容,假设已经全部放在QString response里面

先把要发送的数据进行打包:发送的QString转为UDP序列化、发送的IP、发送的端口

发送:调用 socket->writeDatagram()完成发送

例如:

cpp 复制代码
//构造数据包
QNetworkDatagram responseDatagram(response.toUtf8(), requestDatagram.senderAddress(), requestDatagram.senderPort());
//发送
socket->writeDatagram(responseDatagram);

十一、UDP客户端

如何同时启动多个客户端?右键->客户端的 pro 文件在Explore显示->找到对应的build文件,在Debug中找到 .exe 文件,直接启动即可

(1)目标端口和IP

一般是定义两个字符串,然后在构建数据包时转化一下即可:

(2)创建套接字

接口类:QUdpSocket

作用:创建一个 UDP 套接字对象

例如:

cpp 复制代码
QUdpSocket *socket = new QUdpSocket(this);
(3)事件驱动

作用:当服务端有数据发过来时触发这个自定义函数

信号:QUdpSocket::readyRead

例如:

cpp 复制代码
connect(udpSocket, &QUdpSocket::readyRead, this, &UdpClient::handleServerResponse);
(4)发送数据包给服务端

参数:

  • response:要发送的内容(记得序列化)
  • serverIP:目标IP
  • serverPORT:目标端口

例如:

cpp 复制代码
//构造数据包
QNetworkDatagram responseDatagram(response.toUtf8(), QHostAddress(serverIP), serverPORT);
//发送
udpSocket->writeDatagram(responseDatagram);
(5)拿到服务端数据包

接口:udpSocket->receiveDatagram()

作用:一次性读取完整 UDP 数据报,封装成 QNetworkDatagram 对象,包含:

  • 原始数据(.data()
  • 客户端 IP(.senderAddress()
  • 客户端端口(.senderPort()

例如:

cpp 复制代码
//获取数据包
const QNetworkDatagram& requestDatagram = udpSocket->receiveDatagram();
//拿到原始数据
QString request = requestDatagram.data();

十二、TCP服务端

(1)创建套接字

接口类:QTcpServer

作用:创建一个 TCP 套接字对象

例如:

cpp 复制代码
QTcpServer* tcp_server = new QTcpServer(this);
(2)绑定且监听

接口类:tcp_server->listen

作用:绑定IP端口+监听(合二为一)

参数:

  • QHostAddress::Any:绑定到所有本地网络接口(即 0.0.0.0)
  • 9090:要监听的端口号

返回值:true 表示绑定成功,false 表示失败

(可以通过 tcp_server->errorString() 拿到错误原因)

例如:

cpp 复制代码
bool result = tcp_server->listen(QHostAddress::Any,9090);
(3)响应新连接

例如:

cpp 复制代码
connect(tcp_server,&QTcpServer::newConnection,this,&Widget::discover_connect);

信号QTcpServer::newConnection:有新的客户端连接这个服务端时触发这个信号(不是有数据)

(4)与客户端通信
(1)拿到客户端

此时代表有客户端连接,先拿到这个连接(相当于 Linux 的 accept):

接口:tcp_server->nextPendingConnection(),返回类型:QTcpSocket类

例如:

cpp 复制代码
QTcpSocket* client_socket = tcp_server->nextPendingConnection();
(2)拿到客户端数据

客户端发起数据时会触发信号,通过槽函数或者Lambda表达式完成信息的获取:

信号:QTcpSocket::readyRead(当这个客户端发起了数据时触发)

拿到数据:client_socket->readAll()

回发数据:client_socket->write(记得转为 toUtf8())

拿到这个客户端地址:client_socket->peerAddress().toString();

(3)客户端断开请求

客户端如果断开请求,会触发信号,可以通过Lambda或者槽函数完成操作:

信号:QTcpSocket::disconnected

(注意:这个 client_socket 对象是需要自己手动释放的 client_socket->deleteLater()

十三、TCP客户端

如果想同时启动多个客户端,请参考:"UDP客户端"

(1)创建套接字

接口:QTcpSocket

例如:

cpp 复制代码
QTcpSocket* tcp_client = new QTcpSocket(this);
(2)发起请求

接口1:tcp_client->connectToHost

接口2:tcp_client->waitForConnected()(QT里连接(三次握手)是非阻塞的,需要手动等待)

返回值bool:true代表成功,false代表失败

例如:

(3)拿到服务端响应

服务端发来数据时会触发信号:

信号:QTcpSocket::readyRead

读取内容:tcp_client->readAll( )

(4)发送数据给服务端

发送内容给服务端:tcp_client->write()

例如:

十四、HTTP客户端

(1)构造HTTP管理器

类:QNetworkAccessManager

例如:

cpp 复制代码
QNetworkAccessManager *http_manager = new QNetworkAccessManager(this);
(2)构造URL

类:QUrl(需要将字符串转化为 URL)

例如:

cpp 复制代码
QUrl url(ui->lineEdit->text());

//等价于

QUrl url("http://127.0.0.1:8080/index");
(3)构建请求对象

类:QNetworkRequest(根据URL构建请求头、请求体------>也就是整个请求对象)

例如:

cpp 复制代码
QNetworkRequest request(url);
(4)发送请求

接口:manger->get(request)(根据对象进行发送请求),返回的结果类QNetworkReply

例如:

cpp 复制代码
QNetworkReply* response = manger->get(request);
(5)信号驱动

上面你已经给对面发送了请求,现在根据信号来看对方是否有数据可读:

信号:QNetworkReply::finished

根据请求结果来判断回复结果:

复制代码
如果:response->error()==QNetworkReply::NoError  说明对方正常响应
(6)效果展示
相关推荐
hef2888 小时前
NASM工具怎么用 汇编转机器码实战教程
汇编
isyangli_blog9 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb2008119 小时前
FastAPI APIRouter
开发语言·python
Benszen9 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆9 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木9 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
杨充10 小时前
1.3 浮点型数据设计灵魂
开发语言·python·算法
噜噜噜阿鲁~10 小时前
python学习笔记 | 11.3、面向对象高级编程-多重继承
java·开发语言
basketball61610 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang