《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继承不推荐2QueuedConnection推荐九、锁、条件变量、信号量1锁2条件变量3信号量十、UDP服务端1创建套接字2绑定端口和IP3响应数据事件4拿到客户端数据包5回发客户端数据包十一、UDP客户端1目标端口和IP2创建套接字3事件驱动4发送数据包给服务端5拿到服务端数据包十二、TCP服务端1创建套接字2绑定且监听3响应新连接4与客户端通信1拿到客户端2拿到客户端数据3客户端断开请求十三、TCP客户端1创建套接字2发起请求3拿到服务端响应4发送数据给服务端十四、HTTP客户端1构造HTTP管理器2构造URL3构建请求对象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鼠标按住移动事件事件void mouseMoveEvent(QMouseEvent *event);globalPos()相较于屏幕的坐标mapFromGlobal(event-globalPos())相较于控件的坐标返回QPointcursorForPosition(QPoint)鼠标相较于文本的位置三、键盘事件事件描述键盘输入了某个字符会发生对应事件来定位到具体的内容获取事件mouseDoubleClickEvent通过 -button 判断例如注意先判断组合键否则可能直接拦截在 单个键这里四、滚轮事件事件描述滚轮的滑动会产生对应事件通知获取事件WheelEvent例如五、窗口移动/大小事件事件描述根据窗口是否移动以及大小是否改变触发事件获取事件moveEventQMoveEvent *event移动和resizeEvent(QResizeEvent *event)大小例如六、QFile文件操作1初始化文件对象参数要打开的文件的绝对路径或者相对路径QFile::QFile(const QString fileName);例如QFile f1(test.txt);2判断文件存在与否参数接口bool exists例如if (!file.exists()) { qDebug() 文件不存在; return; }3打开文件bool QFile::open(OpenMode mode);参数打开的模式QIODevice::ReadOnly只读模式除此下面三个模式都是文件不存在就创建QIODevice::Text文本文件模式按字符 / 行解析QIODevice::Append打开追加QIODevice::WriteOnly打开追加一般覆盖式写入是QIODevice::WriteOnly | QIODevice::Text一般追加式写入是QIODevice::Append | QIODevice::Text返回值 打开成功返回true失败返回false例如f1.open(QIODevice::ReadOnly | QIODevice::Text);4读取文件//文本读取 QTextStream::readAll() 或者 //二进制读取 QFile::readAll()解释文本读取是 “字节→字符串” 解析二进制读取是 “字节原样保留”需要手动转字符串例如QString text QTextStream(f1).readAll(); 或者 QByteArray bytes f2.readAll();5关闭文件void QFile::close();例如f1.close();6写入文件QTextStream out();例如QTextStream out(file); out \n追加的内容;七、文件系统1打开QFileDialog::getOpenFileName( )返回值返回这个打开文件的完整路径例如QString filePath QFileDialog::getOpenFileName(this, 窗口标题);2保存QFileDialog::getSaveFileName ( )返回值返回这个保存文件的完整路径例如QString savePath QFileDialog::getSaveFileName(this, 窗口标题);八、线程1继承不推荐void run()override线程的执行方法需要继承QThread重写start启动线程例如创建一个计时器让线程去发信号主线程去修改窗口先继承QThread重写 run然后自定义信号用于通知然后在WIdget中创建一个线程绑定信号和槽函数即可2QueuedConnection推荐注意主类通过信号触发的对应槽函数会在线程中执行但是模块类里面的自己调用不会直接调用函数谁调的就在谁的线程执行跟对象属于哪个线程无关就需要手动添加第一步理解线程在 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了 不在同一个线程→ 自动用 QueuedConnectionQueuedConnection的工作方式是这样的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); // OKQt 的规则是子对象必须和父对象在同一个线程。如果你传了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锁锁很简单从并行到串行访问中间只有一个执行流修改资源接口创建锁资源QMutexmutex;获取锁mutex.lock();释放锁mutex.unlock();2条件变量条件变量不满足条件时形成阻塞式的等待会形成类似一个队列有顺序期间释放锁条件变量属于共享资源必须要在锁的基础上使用为什么要释放锁假设你要洗澡水没烧开你如果不释放锁那么就没有人去烧水也就是没有线程去修改这个资源状态你就永远进不去只有你发现条件不满足释放锁让其他线程去执行生产你才有资源否则你一直拿着锁而锁又是生产和消费共享的会导致单方没有锁形成死锁问题如何看待条件变量带来的性能提升如果没有条件变量那么就会循环执行竞争锁-释放锁导致CPU空转而有了条件变量就会阻塞到那里不会执行空转为什么需要循环判断因为可能存在虚假唤醒比如线程调度器的随机唤醒机制接口QWaitConditioncond创建条件变量cond.wait(mutex锁资源)等待条件cond.wakeOne()唤醒条件3信号量信号量只控制并发数也是一个共享资源需要在锁的基础上使用接口QSemaphoresem(int num)创建信号量资源sem.acquire()信号量-1sem.release()信号量1十、UDP服务端1创建套接字接口类QUdpSocket作用创建一个 UDP 套接字对象例如QUdpSocket *socket new QUdpSocket(this);2绑定端口和IP接口bind作用将 UDP 套接字绑定到本地地址和端口监听这个地址和端口参数QHostAddress::Any绑定到所有本地网络接口即 0.0.0.09090要监听的端口号返回值true表示绑定成功false表示失败例如socket-bind(QHostAddress::Any, 9090);3响应数据事件例如connect(socket, QUdpSocket::readyRead, this, Widget::processRequest);信号QUdpSocket::readyRead当socket收到 UDP有数据可读时Qt 会自动发出这个信号这 个槽函数是自定义的作用类似 Linux的 IO的多路复用4拿到客户端数据包接口socket-receiveDatagram()作用一次性读取完整 UDP 数据报封装成QNetworkDatagram对象包含原始数据.data()客户端 IP.senderAddress()客户端端口.senderPort()例如//获取数据包 const QNetworkDatagram requestDatagram socket-receiveDatagram(); //拿到原始数据 QString request requestDatagram.data();5回发客户端数据包需要单独写一个自定义函数来完成回发给对方的内容假设已经全部放在QStringresponse里面先把要发送的数据进行打包发送的QString转为UDP序列化、发送的IP、发送的端口发送调用 socket-writeDatagram完成发送例如//构造数据包 QNetworkDatagram responseDatagram(response.toUtf8(), requestDatagram.senderAddress(), requestDatagram.senderPort()); //发送 socket-writeDatagram(responseDatagram);十一、UDP客户端如何同时启动多个客户端右键-客户端的 pro 文件在Explore显示-找到对应的build文件在Debug中找到 .exe 文件直接启动即可1目标端口和IP一般是定义两个字符串然后在构建数据包时转化一下即可2创建套接字接口类QUdpSocket作用创建一个 UDP 套接字对象例如QUdpSocket *socket new QUdpSocket(this);3事件驱动作用当服务端有数据发过来时触发这个自定义函数信号QUdpSocket::readyRead例如connect(udpSocket, QUdpSocket::readyRead, this, UdpClient::handleServerResponse);4发送数据包给服务端参数response要发送的内容记得序列化serverIP目标IPserverPORT目标端口例如//构造数据包 QNetworkDatagram responseDatagram(response.toUtf8(), QHostAddress(serverIP), serverPORT); //发送 udpSocket-writeDatagram(responseDatagram);5拿到服务端数据包接口udpSocket-receiveDatagram()作用一次性读取完整 UDP 数据报封装成QNetworkDatagram对象包含原始数据.data()客户端 IP.senderAddress()客户端端口.senderPort()例如//获取数据包 const QNetworkDatagram requestDatagram udpSocket-receiveDatagram(); //拿到原始数据 QString request requestDatagram.data();十二、TCP服务端1创建套接字接口类QTcpServer作用创建一个 TCP 套接字对象例如QTcpServer* tcp_server new QTcpServer(this);2绑定且监听接口类tcp_server-listen作用绑定IP端口监听合二为一参数QHostAddress::Any绑定到所有本地网络接口即 0.0.0.09090要监听的端口号返回值true表示绑定成功false表示失败可以通过 tcp_server-errorString()拿到错误原因例如bool result tcp_server-listen(QHostAddress::Any,9090);3响应新连接例如connect(tcp_server,QTcpServer::newConnection,this,Widget::discover_connect);信号QTcpServer::newConnection有新的客户端连接这个服务端时触发这个信号不是有数据4与客户端通信1拿到客户端此时代表有客户端连接先拿到这个连接相当于 Linux 的 accept接口tcp_server-nextPendingConnection()返回类型QTcpSocket类例如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例如QTcpSocket* tcp_client new QTcpSocket(this);2发起请求接口1tcp_client-connectToHost接口2tcp_client-waitForConnected()QT里连接三次握手是非阻塞的需要手动等待返回值booltrue代表成功false代表失败例如3拿到服务端响应服务端发来数据时会触发信号信号QTcpSocket::readyRead读取内容tcp_client-readAll( )4发送数据给服务端发送内容给服务端tcp_client-write例如十四、HTTP客户端1构造HTTP管理器类QNetworkAccessManager例如QNetworkAccessManager *http_manager new QNetworkAccessManager(this);2构造URL类QUrl需要将字符串转化为 URL例如QUrl url(ui-lineEdit-text()); //等价于 QUrl url(http://127.0.0.1:8080/index);3构建请求对象类QNetworkRequest根据URL构建请求头、请求体——也就是整个请求对象例如QNetworkRequest request(url);4发送请求接口manger-get(request)根据对象进行发送请求返回的结果类QNetworkReply例如QNetworkReply* response manger-get(request);5信号驱动上面你已经给对面发送了请求现在根据信号来看对方是否有数据可读信号QNetworkReply::finished根据请求结果来判断回复结果如果response-error()QNetworkReply::NoError 说明对方正常响应6效果展示