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

相关推荐
Scott9999HH4 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社5 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海5 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖6 小时前
JDK 26 新特性详解
java·开发语言
马优晨6 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区8 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大9 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai9 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜9 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa9 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio