【QT第五章】系统相关

前言 🚀

Qt 到了系统相关这一章,知识点会明显变杂:数据库事务、鼠标键盘事件、窗口事件、定时器、文件读写、QThread、线程同步、UDP/TCP/HTTP、多媒体......如果按接口名一个个去背,很容易觉得这是一大堆彼此无关的小模块;但如果抓住主线,就会发现它们都在回答同一个更实际的问题:

一个 Qt 程序到底怎样和"系统环境"发生交互。

这种交互有很多方向:和数据库交互,要保证数据一致性;和用户输入交互,要接收鼠标、键盘和窗口事件;和时间交互,要靠定时器安排异步行为;和文件系统交互,要完成文本和二进制读写;和操作系统线程交互,要把耗时任务从 UI 线程中拆出去;和网络交互,要通过 UDPTCPHTTP 做通信;和多媒体设备交互,则要播放音频和视频。

所以这一章真正重要的不是零碎 API,而是形成一个统一认识:Qt 不只是用来画界面,它还提供了大量围绕"事件驱动 + 异步交互 + 资源管理"展开的系统级能力。


一. 数据库事务:为什么多条 SQL 不能只靠"顺序执行" 🧠

数据库操作最容易被低估的一点是:有些业务并不是一条 SQL 就能完成,而是要连续执行多步。此时真正重要的,不只是"这些语句都发出去了",而是:

它们要么整体成功,要么整体失败。

1.1 Qt 中的事务基本用法

cpp 复制代码
QSqlDatabase db = QSqlDatabase::database();

db.transaction();

QSqlQuery query;
query.exec("INSERT INTO account (name, balance) VALUES ('Alice', 1000)");
query.exec("UPDATE account SET balance = balance - 100 WHERE name = 'Alice'");
query.exec("UPDATE account SET balance = balance + 100 WHERE name = 'Bob'");

if (/* 所有操作成功 */) {
    db.commit();
} else {
    db.rollback();
}

1.2 为什么事务如此关键

因为像转账这种场景,本质上包含多个步骤:

  • 一边扣钱
  • 一边加钱

若只执行了一半就出错,而没有回滚,数据就会不一致。

1.3 ACID 应该怎么理解

  • Atomicity 原子性:要么全部成功,要么全部失败
  • Consistency 一致性:事务前后数据应保持合法一致
  • Isolation 隔离性:并发事务之间互不干扰
  • Durability 持久性:一旦提交,结果应被可靠保存

💡 避坑指南:
事务并不是"多条 SQL 打包执行"这么简单,而是"多步操作在语义上必须作为整体成立"。


二. 事件处理:Qt 为什么天生适合做交互程序 🔍

Qt 的核心模型本来就是事件驱动。也就是说,程序不是一条直线顺序往下跑,而是在事件到来时触发相应处理逻辑。

2.1 鼠标追踪为什么默认关闭

若想持续接收鼠标移动事件,需要先显式打开:

cpp 复制代码
this->setMouseTracking(true);

2.2 常见鼠标事件

cpp 复制代码
void MyWidget::mouseMoveEvent(QMouseEvent *event)
{
    QPoint pos = event->pos();
    int x = event->x();
    int y = event->y();
    qDebug() << "鼠标位置:" << x << "," << y;
}

void MyWidget::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        qDebug() << "左键按下";
    } else if (event->button() == Qt::RightButton) {
        qDebug() << "右键按下";
    }
}

2.3 键盘事件怎么理解

cpp 复制代码
void MyWidget::keyPressEvent(QKeyEvent *event)
{
    switch (event->key()) {
    case Qt::Key_W:
    case Qt::Key_Up:
        qDebug() << "向上移动";
        break;
    case Qt::Key_Escape:
        qDebug() << "ESC 退出";
        break;
    }

    if (event->modifiers() & Qt::ControlModifier) {
        qDebug() << "按下了 Ctrl";
    }
}

这里除了按键本身,还可以进一步判断修饰键,例如 CtrlShift 等。

2.4 窗口移动和大小变化为什么也属于事件

cpp 复制代码
void MyWidget::moveEvent(QMoveEvent *event)
{
    QPoint newPos = event->pos();
    qDebug() << "窗口移动到:" << newPos;
}

void MyWidget::resizeEvent(QResizeEvent *event)
{
    QSize newSize = event->size();
    qDebug() << "窗口大小改变为:" << newSize;
}

因为对 Qt 来说,"用户操作""窗口状态改变"本质上都是事件源。

2.5 为什么更推荐信号槽而不是过度依赖事件函数

事件函数虽然直接,但粒度更底层,也更容易在复杂界面里出现逻辑耦合。很多场景下,信号槽表达业务关系会更清晰,也更符合 Qt 的整体设计风格。

💡 避坑指南:
事件处理适合处理底层交互事实,信号槽更适合表达业务层联动。


三. 定时器:为什么时间驱动也是事件系统的一部分 🧱

Qt 的定时器并不是"线程睡眠的替代品",而是事件循环中的一种时间触发机制。

3.1 最基本的 QTimer 用法

cpp 复制代码
QTimer *timer = new QTimer(this);
timer->setInterval(1000);

connect(timer, &QTimer::timeout, this, [=]() {
    qDebug() << "定时器触发!";
});

timer->start();

3.2 单次定时器

cpp 复制代码
QTimer::singleShot(3000, this, [=]() {
    qDebug() << "3秒后执行一次";
});

3.3 它常见的应用场景

  • 进度条更新
  • 简单动画效果
  • 周期性刷新数据
  • 延迟执行某个动作

3.4 为什么它属于事件驱动,而不是阻塞等待

因为定时器不会卡住当前线程去"傻等 3 秒",而是注册一个未来触发事件,等事件循环到点时再回调处理。这样界面线程仍然可以继续响应其他事件。


四. 文件操作:Qt 为什么把文本流和二进制流分得这么清楚 💻

4.1 文本文件读写

cpp 复制代码
QFile file("data.txt");
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
    QTextStream stream(&file);
    stream << "Hello Qt" << Qt::endl;
    stream << "第二行数据" << Qt::endl;
    file.close();
}

读取时:

cpp 复制代码
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
    QTextStream stream(&file);
    while (!stream.atEnd()) {
        QString line = stream.readLine();
        qDebug() << line;
    }
    file.close();
}

4.2 二进制文件读写

cpp 复制代码
QFile binFile("data.bin");
if (binFile.open(QIODevice::WriteOnly)) {
    QDataStream stream(&binFile);
    stream << QString("Hello") << 123 << 3.14;
    binFile.close();
}

读取时:

cpp 复制代码
if (binFile.open(QIODevice::ReadOnly)) {
    QDataStream stream(&binFile);
    QString str;
    int num;
    double pi;
    stream >> str >> num >> pi;
    binFile.close();
}

4.3 QIODevice 打开模式为什么要区分

常见模式有:

  • ReadOnly
  • WriteOnly
  • ReadWrite
  • Append
  • Text
  • Truncate

它们共同决定了:

  • 文件是否可读
  • 是否会清空旧内容
  • 是否是文本模式
  • 是否是追加写入

4.4 文件信息为什么要单独交给 QFileInfo

因为读写本身和"文件元信息"是两类不同问题。QFileInfo 更适合拿这些信息:

  • 文件名
  • 路径
  • 大小
  • 创建时间
  • 修改时间
  • 是否存在

五. Qt 多线程:为什么不能把耗时操作直接写在 UI 线程里 ⚠️

Qt 程序里最经典的一条经验就是:

耗时操作不要放在主线程里。

5.1 为什么 UI 会卡

因为主线程既要负责界面刷新,又要负责处理用户输入和事件循环。若把长时间计算、阻塞 IO、网络等待等任务直接写进去,事件循环就会被拖住,界面自然失去响应。

5.2 QThread 的两种常见使用方式

方式一:继承 QThread
cpp 复制代码
class WorkerThread : public QThread {
    Q_OBJECT
public:
    void run() override {
        for (int i = 0; i < 100; i++) {
            qDebug() << "线程工作中:" << i;
            msleep(100);
        }
    }
};
方式二:moveToThread(推荐)
cpp 复制代码
class Worker : public QObject {
    Q_OBJECT
public slots:
    void doWork() {
        for (int i = 0; i < 100; i++) {
            emit progress(i);
            QThread::msleep(100);
        }
        emit finished();
    }
signals:
    void progress(int value);
    void finished();
};

配套使用:

cpp 复制代码
QThread *thread = new QThread();
Worker *worker = new Worker();

worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Worker::doWork);
connect(worker, &Worker::finished, thread, &QThread::quit);
connect(worker, &Worker::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QObject::deleteLater);

thread->start();

5.3 为什么 moveToThread 更推荐

因为它更符合 Qt 的对象模型和信号槽机制:

  • 线程负责执行环境
  • 工作对象负责业务逻辑
  • 对象职责更清晰
  • 生命周期管理更自然

六. 线程同步:Qt 里为什么也要强调锁和条件等待 🔗

6.1 为什么会有竞争条件

假设两个线程同时执行:

cpp 复制代码
counter++;

这并不是原子操作,因此最终结果可能小于预期值。

6.2 QMutex:最直接的互斥锁

cpp 复制代码
class Counter : public QObject {
    Q_OBJECT
public:
    void increment() {
        QMutexLocker locker(&mutex);
        ++value;
    }

    int getValue() {
        QMutexLocker locker(&mutex);
        return value;
    }
private:
    int value = 0;
    QMutex mutex;
};

6.3 为什么 QMutexLocker 很重要

它本质上和 lock_guard 类似,属于 RAII 风格锁管理器:

  • 构造时加锁
  • 离开作用域时自动解锁

6.4 QReadWriteLock 适合什么场景

当共享数据是:

  • 读取特别多
  • 修改相对少

那么用读写锁更合适:

  • 读锁可以共享
  • 写锁必须独占

6.5 线程同步类如何理解

特点 适合场景
QMutex 同一时刻只允许一个线程进入 一般临界区保护
QMutexLocker 自动管理加锁解锁 防止忘解锁、异常安全
QReadWriteLock 读共享,写独占 读多写少
QSemaphore 控制同时可用资源数量 资源池、限流
QWaitCondition 等待/唤醒 生产者-消费者、线程协调

七. 网络编程:Qt 为什么特别强调异步信号槽而不是阻塞式处理 🗺️

Qt 网络模块非常典型地体现了它的整体哲学:

不要阻塞当前线程,而是让事件到来时通过信号异步处理。

7.1 UDP 通信:轻量、无连接

服务端典型流程:

cpp 复制代码
QUdpSocket *udpSocket = new QUdpSocket(this);
udpSocket->bind(QHostAddress::Any, 1234);

connect(udpSocket, &QUdpSocket::readyRead, this, [=]() {
    while (udpSocket->hasPendingDatagrams()) {
        QByteArray data;
        data.resize(udpSocket->pendingDatagramSize());
        QHostAddress sender;
        quint16 port;

        udpSocket->readDatagram(data.data(), data.size(), &sender, &port);
        qDebug() << "收到来自" << sender << ":" << port << "的数据:" << data;
    }
});

7.2 TCP 通信:面向连接

服务端核心流程:

  • listen() 监听端口
  • newConnection 到来时取出连接
  • readyRead 信号中读取数据
  • disconnected 时做资源释放

7.3 为什么资源释放强调 deleteLater()

因为 Qt 很多对象都深度参与事件循环。若在信号处理过程或还有未处理事件时直接 delete,就可能引发悬空访问或状态不一致问题。

更稳妥的模式通常是:

cpp 复制代码
connect(socket, &QTcpSocket::disconnected, socket, &QObject::deleteLater);
socket->disconnectFromHost();

7.4 多线程 TCP 服务器为什么本质上是在拆双重循环

主线程负责接收连接,工作线程负责处理数据,这样就把:

  • "不断收新连接"
  • "不断处理已有连接读写"

拆成了两个层次的独立事件循环。


八. HTTP 编程:为什么 get() 发请求不等于"立刻拿到结果" 🔍

8.1 Qt 的 HTTP 入口

cpp 复制代码
QNetworkAccessManager *manager = new QNetworkAccessManager(this);
QNetworkRequest request(QUrl("https://api.example.com/data"));
QNetworkReply *reply = manager->get(request);

8.2 最容易误解的点

get() 只是发出请求,不是同步等待响应。

真正的结果要通过异步信号回来,例如:

cpp 复制代码
connect(reply, &QNetworkReply::finished, this, [=]() {
    if (reply->error() == QNetworkReply::NoError) {
        QByteArray data = reply->readAll();
        qDebug() << "响应数据:" << data;
    }
    reply->deleteLater();
});

8.3 为什么返回的数据不一定是 HTML

在实际开发里,HTTP 常常拿到的是:

  • JSON
  • XML
  • 二进制文件
  • 某种约定好的接口数据格式

九. 多媒体:Qt 为什么能把播放能力也统一进对象模型里 💻

9.1 音频播放

cpp 复制代码
QMediaPlayer *player = new QMediaPlayer(this);
QAudioOutput *audioOutput = new QAudioOutput(this);

player->setAudioOutput(audioOutput);
player->setSource(QUrl::fromLocalFile("/path/to/music.mp3"));
player->play();

常见控制包括:

  • play()
  • pause()
  • stop()
  • setPosition()

9.2 视频播放

cpp 复制代码
QVideoWidget *videoWidget = new QVideoWidget(this);
QMediaPlayer *videoPlayer = new QMediaPlayer(this);

videoPlayer->setVideoOutput(videoWidget);
videoPlayer->setSource(QUrl::fromLocalFile("/path/to/video.mp4"));
videoPlayer->play();

9.3 这说明 Qt 多媒体模块的设计特点

它并不是单纯给几个"播放函数",而是继续沿用对象化和信号机制,把播放器、音频输出、视频显示组件和播放进度变化统一纳入同一套对象协作体系中。


总结 📝

系统相关这一章真正重要的,不是分散去记几十个类名,而是建立这样一个统一认识:

Qt 不只是界面框架,它还是一套围绕事件驱动和异步交互展开的系统能力封装。

围绕这条主线再回头看整章内容,很多原本看似杂乱的知识点就会自然连起来:

  • 事务是在保证数据库操作的一致性
  • 事件系统是在接收用户与窗口系统的反馈
  • 定时器是在让"时间"也成为事件源
  • 文件读写是在和文件系统交互
  • 线程与同步是在解决耗时任务和共享资源访问问题
  • 网络模块是在用异步方式与外部服务通信
  • 多媒体模块则是在统一管理音视频播放资源

所以,这一章最终最值得记住的一句话可以压缩成:

Qt 的系统相关能力,本质上是在同一套对象模型和信号槽机制下,把数据库、输入、时间、文件、线程、网络和多媒体这些外部交互入口统一了起来。

相关推荐
李白你好4 小时前
Java GUI-未授权漏洞检测工具
java·开发语言
leo__5204 小时前
拉丁超立方抽样(Latin Hypercube Sampling, LHS)MATLAB实现
开发语言·matlab
sycmancia4 小时前
Qt——Qt中的标准对话框
开发语言·qt
橙露4 小时前
Python 对接 API:自动化拉取、清洗、入库一站式教程
开发语言·python·自动化
Omigeq4 小时前
1.4 - 曲线生成轨迹优化算法(以BSpline和ReedsShepp为例) - Python运动规划库教程(Python Motion Planning)
开发语言·人工智能·python·算法·机器人
2301_808414384 小时前
自动化测试的实施
开发语言·python
波波0075 小时前
写出稳定C#系统的关键:不可变性思想解析
开发语言·c#·wpf
dr_yingli5 小时前
fMRI(3-1)报告(个体化报告)生成器说明
开发语言·matlab
hrhcode5 小时前
【java工程师快速上手go】一.Go语言基础
java·开发语言·golang