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

