Qt 学习笔记

一份完整的 Qt 学习笔记。重点讲"为什么这么设计"(费曼式 📌 讲解),配 RyBOS 项目实战代码。

图表在 /Qt学习笔记/diagrams/ 目录。图注名即原图文件名,看图注就能直接找到原图用 draw.io 编辑。

1 · Qt 概述与架构

1.1 Qt 是什么

  • Qt 是一个跨平台 C++ 应用程序开发框架,1995 年由 Trolltech 创建
  • 不仅是 GUI 库,而是从底层到 UI 的完整工具链:网络、数据库、多媒体、并发、文件系统
  • 跨平台:同一套代码在 Windows、Linux、macOS、嵌入式设备上编译运行
TIP

📌 打个比方:C++ 标准库给你的是"砖头和水泥"(STL 容器、算法),Qt 给你的是"预制板和装修包"——网络模块、线程池、配置管理、UI 控件都是现成的,你只需组装。C++ 标准库不提供的东西(网络、GUI、线程池、JSON),Qt 全补上了。

1.2 核心模块

图 1 \xb7 Qt 模块架构

模块 作用 代表类
QtCore 非 GUI 核心功能 QObject, QString, QTimer, QThread, QSettings
QtGui GUI 基础 QWidget, QPainter, QImage, QColor
QtWidgets 桌面 UI 控件 QPushButton, QLabel, QVBoxLayout, QMainWindow
QtNetwork 网络通信 QTcpSocket, QUdpSocket, QNetworkAccessManager
QtConcurrent 高级并发 QtConcurrent::run, QFuture, QFutureWatcher
QtWebSockets WebSocket QWebSocket, QWebSocketServer

1.3 设计哲学

  1. 元对象系统:在 C++ 之上加了一层"反射"能力
  2. 信号与槽:对象间通信靠"广播-订阅"模式,松耦合
  3. 事件循环驱动:一切围绕事件循环运转
  4. 对象树管理内存:parent 删除时自动删除所有 children
  5. 隐式共享(Copy-on-Write):容器传值时只复制指针,修改时才真正拷贝

2 · 元对象系统(Meta-Object System)

2.1 为什么 C++ 需要元对象系统

C++ 是静态类型语言,编译后类型信息基本丢失。但 GUI 框架需要:运行时知道类名、方法名,能动态调用——信号槽连接、属性系统、动态类型识别都依赖这些信息。

TIP

📌 打个比方:C++ 编译后的代码就像"去掉了目录和索引的书"——内容都在,但你不知道第几章叫什么。Qt 的 MOC(Meta-Object Compiler)就是给这本书重新生成一份目录,让程序在运行时能查到"这个类有哪些信号、哪些槽"。

2.2 MOC 工作原理

图 2 \xb7 元对象系统 MOC 流水线

头文件 (.h) 含 Q_OBJECT/signals/slots ▼ MOC 预处理器 moc_xxx.cpp (自动生成元信息表) ▼ 正常 C++ 编译 最终可执行文件

MOC 在 C++ 编译器之前运行,扫描 Q_OBJECT 等关键字,生成提供"反射"能力的额外 C++ 代码。

2.3 Q_OBJECT 宏

class MyClass : public QObject
{
    Q_OBJECT  // ← 必须放在类声明最前面
public:
    explicit MyClass(QObject* parent = nullptr);
signals:
    void dataReady(const QByteArray& data);
private slots:
    void onTimeout();
};
TIP

📌 忘记 Q_OBJECT 会怎样?信号槽连接会静默失败——编译不报错,运行时 connect 返回 false。这是 Qt 新手最常见的"幽灵 bug"。规则:继承 QObject 且用了 signals/slots,就必须加 Q_OBJECT。

3 · 信号与槽(Signals and Slots)

3.1 本质:观察者模式

图 3 \xb7 信号与槽机制

  • 信号(Signal):事件发生时"广播",不关心谁在听
  • 槽(Slot):收到信号后执行的回调函数
  • 连接(Connect):建立信号到槽的订阅关系
TIP

📌 打个比方:信号像广播站,槽像收音机。广播站只管发信号,收音机只管收信号后做事。connect 就是"调频"——把收音机调到某个广播站的频率。为什么不直接函数调用?直接调用是"硬耦合"——发送者必须知道接收者是谁。信号槽是"松耦合"——可以随时加新的"收音机"而不改广播站代码。

3.2 两种 connect 语法

// 旧式:字符串匹配(不推荐,运行时才报错)
connect(socket, SIGNAL(connected()), this, SLOT(onConnected()));

// 新式:函数指针(推荐,编译期类型检查)
connect(socket, &QTcpSocket::connected, this, &MyClass::onConnected);

// Lambda 作为槽
connect(timer, &QTimer::timeout, [this]() {
    m_retryCount++;
});
TIP

📌 为什么推荐新式 connect?旧式用字符串 "connected()",拼写错误编译不报错。新式用函数指针 &QTcpSocket::connected,编译器直接检查——把运行时错误提前到编译期。

3.3 连接类型

类型 行为 适用场景
Qt::AutoConnection(默认) 同线程→Direct,跨线程→Queued 99% 场景
Qt::DirectConnection 立即在发射线程直接调用槽 极少手动使用
Qt::QueuedConnection 排入接收者线程事件队列,异步执行 跨线程通信
Qt::BlockingQueuedConnection 排队但阻塞等待槽执行完 谨慎使用,可能死锁
TIP

📌 跨线程信号槽怎么工作的?发送者在子线程 emit signal,Qt 发现接收者在主线程,把"调用槽函数"包装成事件投递到主线程事件队列。主线程事件循环取出事件,在主线程执行槽函数。这就是为什么跨线程信号槽要求接收者线程必须运行事件循环。

3.4 自定义信号

class MyWorker : public QObject
{
    Q_OBJECT
signals:
    void progressChanged(int percent);  // 只需声明,MOC 生成实现
    void finished(const QString& result);

public:
    void doWork()
    {
        emit progressChanged(50);  // emit 是空宏,纯标记
        emit finished("Done");
    }
};
TIP

📌 emit 其实什么都不是。#define emit 是空宏,emit finished("Done") 和 finished("Done") 编译后完全一样。但写 emit 是好习惯,让代码更可读。

4 · 事件循环(Event Loop)

4.1 什么是事件循环

图 4 \xb7 事件循环

事件循环是 Qt 的心脏,是一个无限循环,不断取出事件并分发:

while (running) { Event event = eventQueue.dequeue(); dispatch(event); }

处理的事件包括:UI 事件(鼠标、键盘)、定时器事件、网络事件、跨线程信号槽事件、自定义事件。

TIP

📌 打个比方:事件循环像餐厅服务员,不停巡场(while 循环),看到哪桌举手(事件到来)就去处理。如果服务员在某一桌站着不动(阻塞调用),其他桌全没人管——这就是为什么主线程不能做耗时操作。

4.2 为什么不能在主线程做耗时操作

// 阻塞 10 秒,界面完全卡死
void onButtonClicked() {
    QThread::sleep(10);
}

// 放到后台线程
void onButtonClicked() {
    QtConcurrent::run([this]() {
        QThread::sleep(10);  // 线程池中执行,界面正常
    });
}

4.3 QEventLoop:嵌套事件循环

参考 RyBOS @d:\Projects\vcpkg\vcpkg\vcpkg\safeSpace\RyBOS\qt\HttpClient\src\QtHttpClient.h:200-226:

QtHttpResponse request(const QString& method, const QString& url, ...)
{
    QNetworkReply* reply = sendRequest(method, req, body);

    QEventLoop loop;
    QTimer timer;
    timer.setSingleShot(true);

    connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
    connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);

    timer.start(m_timeoutMs);
    loop.exec();  // ← 嵌套事件循环,界面不卡

    if (timer.isActive()) {
        timer.stop();
        return parseReply(reply);
    } else {
        reply->abort();
        return QtHttpResponse{.error = "Request timeout"};
    }
}
TIP

📌 QEventLoop 原理:loop.exec() 开了一个新的 while 循环,继续处理事件队列。当信号触发 loop.quit() 时,内层循环退出,程序继续往下走。这就是"同步等待但不阻塞界面"的魔法。注意风险:用户在等待期间可能重复触发操作。

5 · QObject 与对象树

5.1 对象树模型

图 5 \xb7 QObject 对象树

每个 QObject 可以有一个 parent 和多个 children。parent 被销毁时,所有 children 自动销毁。

// 创建时指定 parent,自动加入对象树
QWidget* central = new QWidget(mainWindow);
QVBoxLayout* layout = new QVBoxLayout(central);
QPushButton* btn = new QPushButton("OK", central);
// mainWindow 析构 → central 析构 → layout 和 btn 自动析构
TIP

📌 为什么用对象树:Qt 诞生于 1995 年,那时 C++ 还没有 std::shared_ptr。对象树是 Qt 自创的"半自动内存管理"——你只需管 parent 的生命周期,children 跟着走。GUI 控件的层级关系天然就是树形的。

5.2 deleteLater:延迟删除

// 危险:信号槽还在调用栈中,直接 delete 可能崩溃
void onReceived() {
    delete socket;
}

// 安全:延迟到事件循环下一轮删除
void onReceived() {
    socket->deleteLater();
}
TIP

📌 deleteLater 为什么安全:它把"删除"包装成事件投递到事件队列。当前事件处理完毕、调用栈展开后,事件循环才执行 delete。这时信号槽调用已结束,不会访问已释放的对象。

6 · QString 与数据类型

6.1 QString

QString s1 = "Hello";
QString s2 = QStringLiteral("世界");  // 编译期生成,推荐
QString s3 = QString("%1 + %2 = %3").arg(1).arg(2).arg(3);

s1.length();           // 长度
s1.contains("ell");    // 是否包含
s1.split(",");         // 分割
s1.toUtf8();           // 转 QByteArray
TIP

📌 QStringLiteral:普通 QString("hello") 运行时构造有开销。QStringLiteral("hello") 编译期就把字符串放在静态存储区,零开销构造。字符串字面量优先用 QStringLiteral。

6.2 QByteArray / QVariant / 容器

// QByteArray — 二进制数据
QByteArray data("hello");
data.toHex();           // 转十六进制
QString::fromUtf8(data); // → QString

// QVariant — 通用值容器
QVariant v = 42;
v.toInt();    // → 42
v.toString(); // → "42"

// QList / QMap / QHash(隐式共享,Copy-on-Write)
QList<QString> list;
list << "a" << "b" << "c";
QMap<QString, int> map;
map["one"] = 1;
TIP

📌 隐式共享(Copy-on-Write):Qt 容器传值时不立即拷贝数据,只共享引用计数指针。只有修改时才真正拷贝。QList<QString> list2 = list1 几乎零开销——直到你 list2.append("x") 才发生真正的拷贝。

7 · QSettings 配置管理

7.1 基础用法

QSettings settings("config/config.ini", QSettings::IniFormat);

settings.beginGroup("database");
settings.setValue("host", "localhost");
settings.setValue("port", 3306);
settings.endGroup();

settings.beginGroup("database");
QString host = settings.value("host", "localhost").toString();
int port = settings.value("port", 3306).toInt();
settings.endGroup();

7.2 RyBOS ConfigManager 封装

参考 @d:\Projects\vcpkg\vcpkg\vcpkg\safeSpace\RyBOS\qt\ConfigManager\src\ConfigManager.h:39-271:

ConfigManager& config = ConfigManager::instance();
config.initialize("config.ini");

// 简洁的 get/set,类型安全
QString host = config.get("database", "host", "localhost");
int port = config.getInt("database", "port", 3306);
config.set("database", "port", 5432);

// 变更通知(信号)
connect(&config, &ConfigManager::valueChanged,
    [](const QString& group, const QString& key, const QVariant& old, const QVariant& newVal) {
        qDebug() << group << "/" << key << "changed:" << old << "->" << newVal;
    });
TIP

📌 为什么要封装 QSettings:1) 类型安全的 get(不用每次 .toInt())2) 线程安全(QMutex)3) 变更通知信号 4) 自动创建配置目录 5) 单例模式

8 · 文件系统

// QFile
QFile file("data.txt");
if (file.open(QIODevice::ReadWrite)) {
    file.write("Hello Qt");
    file.seek(0);
    QByteArray data = file.readAll();
    file.close();
}

// QDir
QDir dir("logs");
if (!dir.exists()) dir.mkpath(".");
QStringList files = dir.entryList(QStringList() << "*.log", QDir::Files);

// QFileInfo
QFileInfo info("data.txt");
info.size();
info.absoluteFilePath();

// 应用目录
QString appDir = QCoreApplication::applicationDirPath();
QString configPath = QDir(appDir).filePath("config/config.ini");

9 · QThread 多线程

图 6 \xb7 QThread 多线程模式

9.1 继承 QThread

参考 RyBOS @d:\Projects\vcpkg\vcpkg\vcpkg\safeSpace\RyBOS\qt\VideoPlayer\src\QtVideoThread.h:49-67:

class VideoThread : public QThread
{
    Q_OBJECT
public:
    explicit VideoThread(QObject* parent = nullptr)
        : QThread(parent)
    {
        qRegisterMetaType<QImage>("QImage");  // 跨线程信号槽需注册自定义类型
    }
signals:
    void frameReady(const QImage& frame, int channelId);
protected:
    void run() override {
        // run() 在新线程中执行
        while (!m_stop) {
            QImage image = matToQImage(frame);
            emit frameReady(image, m_channelId);  // 跨线程发射信号
        }
    }
};

9.2 Worker-QObject 模式(推荐)

class Worker : public QObject {
    Q_OBJECT
public slots:
    void doWork() {
        // 在 worker 线程中执行
        emit resultReady(result);
    }
signals:
    void resultReady(const QString& result);
};

QThread* thread = new QThread;
Worker* worker = new Worker;
worker->moveToThread(thread);

connect(thread, &QThread::started, worker, &Worker::doWork);
connect(worker, &Worker::resultReady, [](const QString& result) {
    qDebug() << "Result:" << result;
});
connect(thread, &QThread::finished, worker, &QObject::deleteLater);
thread->start();
TIP

📌 两种方式的区别:继承 QThread 简单直接,但 run() 里没有事件循环,不能用定时器/信号槽。Worker 模式被 moveToThread 后,该线程自动有事件循环,可以用信号槽、QTimer。简单循环任务用继承,长期驻留响应信号用 Worker。

9.3 跨线程通信

// 跨线程信号槽(AutoConnection 自动变 QueuedConnection)
connect(worker, &Worker::resultReady, this, &MainWindow::onResult);

// QMetaObject::invokeMethod 投递任务到指定线程
QMetaObject::invokeMethod(this, [this]() {
    label->setText("Updated from thread");  // 在主线程执行
}, Qt::QueuedConnection);
TIP

📌 为什么不能直接从子线程更新 UI:Qt 的 GUI 操作不是线程安全的。规则:所有 GUI 操作必须在主线程执行。跨线程更新 UI 必须通过信号槽或 QMetaObject::invokeMethod。

📌 qRegisterMetaType:跨线程信号槽时参数会被拷贝到接收线程。Qt 内置类型已注册,自定义类型需要 qRegisterMetaType<MyType>("MyType") 注册。

9.4 线程安全

QMutex mutex;
void threadSafeFunction() {
    QMutexLocker locker(&mutex);  // RAII,构造加锁析构解锁
    // 临界区
}

10 · QThreadPool 与 QtConcurrent

10.1 线程池

参考 RyBOS @d:\Projects\vcpkg\vcpkg\vcpkg\safeSpace\RyBOS\qt\TaskScheduler\src\QtTaskScheduler.h:52-139:

class QtTask : public QRunnable {
public:
    using Callback = std::function<void()>;
    explicit QtTask(Callback cb) : m_callback(std::move(cb)) {
        setAutoDelete(true);  // 执行完自动 delete
    }
    void run() override { if (m_callback) m_callback(); }
};

// 线程池
QThreadPool* pool = new QThreadPool(this);
pool->setMaxThreadCount(QThread::idealThreadCount());  // CPU 核心数
pool->start(new QtTask([]() { /* 后台任务 */ }));
pool->waitForDone();
TIP

📌 为什么用线程池:创建线程有系统开销,频繁创建销毁浪费资源。线程池预先创建一组线程,任务来了直接复用。适合大量短任务。

10.2 QtConcurrent

// 在线程池中运行
QFuture<QString> future = QtConcurrent::run([]() -> QString {
    QThread::sleep(2);
    return "Done";
});

// 非阻塞监控
QFutureWatcher<QString>* watcher = new QFutureWatcher<QString>;
connect(watcher, &QFutureWatcher<QString>::finished, [=]() {
    qDebug() << "Result:" << watcher->result();
    watcher->deleteLater();
});
watcher->setFuture(future);

11 · AsyncTask 模式:阻塞但不卡界面

参考 RyBOS @d:\Projects\vcpkg\vcpkg\vcpkg\safeSpace\RyBOS\qt\AsyncTask\src\AsyncTaskExecutor.h:70-122:

template<typename ResultType>
static ResultType execute(std::function<ResultType()> task, ...)
{
    QFuture<ResultType> future = QtConcurrent::run(task);
    QEventLoop loop;
    QFutureWatcher<ResultType> watcher;
    connect(&watcher, &QFutureWatcher<ResultType>::finished, &loop, &QEventLoop::quit);
    watcher.setFuture(future);
    loop.exec();  // ← 界面不卡,事件循环继续运转
    return future.result();
}
TIP

📌 精妙之处:QtConcurrent::run 把任务丢到线程池,QEventLoop::exec 在主线程开嵌套事件循环。表面是"同步等待"(函数返回时结果就绪),实际上主线程事件循环在运转,界面不卡。这是把异步包装成同步的经典 Qt 模式。

12 · TCP 编程

图 7 \xb7 Qt 网络通信栈

12.1 TCP 客户端

参考 @d:\Projects\vcpkg\vcpkg\vcpkg\safeSpace\RyBOS\qt\TcpSocket\src\QtTcpSocket.h:51-157:

class QtTcpClient : public QObject
{
    Q_OBJECT
public:
    explicit QtTcpClient(QObject* parent = nullptr)
        : QObject(parent), m_socket(new QTcpSocket(this))
    {
        connect(m_socket, &QTcpSocket::connected, this, &QtTcpClient::connected);
        connect(m_socket, &QTcpSocket::readyRead, this, &QtTcpClient::onReadyRead);
    }
    void connectToHost(const QString& host, quint16 port) {
        m_socket->connectToHost(host, port);
    }
    qint64 send(const QByteArray& data) { return m_socket->write(data); }
signals:
    void connected();
    void dataReceived(const QByteArray& data);
private slots:
    void onReadyRead() {
        QByteArray data = m_socket->readAll();
        if (!data.isEmpty()) emit dataReceived(data);
    }
};

12.2 TCP 服务器

参考 @d:\Projects\vcpkg\vcpkg\vcpkg\safeSpace\RyBOS\qt\TcpSocket\src\QtTcpSocket.h:235-344:

class QtTcpServer : public QObject
{
    Q_OBJECT
public:
    explicit QtTcpServer(QObject* parent = nullptr)
        : QObject(parent), m_server(new QTcpServer(this))
    {
        connect(m_server, &QTcpServer::newConnection, this, &QtTcpServer::onNewConnection);
    }
    bool listen(quint16 port) { return m_server->listen(QHostAddress::Any, port); }
    void broadcast(const QByteArray& data) {
        for (auto conn : m_connections) conn->send(data);
    }
private slots:
    void onNewConnection() {
        while (m_server->hasPendingConnections()) {
            QTcpSocket* socket = m_server->nextPendingConnection();
            auto conn = new QtTcpConnection(socket, this);
            m_connections.append(conn);
            connect(conn, &QtTcpConnection::disconnected, [this, conn]() {
                m_connections.removeOne(conn);
                conn->deleteLater();
            });
            emit newConnection(conn);
        }
    }
};
TIP

📌 Qt 网络编程核心模式:1) 异步非阻塞——connectToHost 立即返回,连接成功发 connected 信号 2) 信号驱动——数据到达发 readyRead,在槽里 readAll() 3) 不阻塞主线程——靠事件循环驱动异步 I/O,不需要手动开线程

13 · UDP 编程

参考 @d:\Projects\vcpkg\vcpkg\vcpkg\safeSpace\RyBOS\qt\UdpSocket\src\QtUdpSocket.h:68-184:

class QtUdpServer : public QObject
{
    Q_OBJECT
public:
    explicit QtUdpServer(QObject* parent = nullptr)
        : QObject(parent), m_socket(new QUdpSocket(this))
    {
        connect(m_socket, &QUdpSocket::readyRead, this, &QtUdpServer::onReadyRead);
    }
    bool start(quint16 port) { return m_socket->bind(QHostAddress::Any, port); }
    qint64 sendTo(const QHostAddress& addr, quint16 port, const QByteArray& data) {
        return m_socket->writeDatagram(data, addr, port);
    }
signals:
    void dataReceived(const QHostAddress& sender, quint16 port, const QByteArray& data);
private slots:
    void onReadyRead() {
        while (m_socket->hasPendingDatagrams()) {
            QByteArray datagram;
            datagram.resize(m_socket->pendingDatagramSize());
            QHostAddress addr; quint16 port;
            m_socket->readDatagram(datagram.data(), datagram.size(), &addr, &port);
            emit dataReceived(addr, port, datagram);
        }
    }
};

UDP 心跳

参考 @d:\Projects\vcpkg\vcpkg\vcpkg\safeSpace\RyBOS\qt\UdpSocket\src\QtUdpSocket.h:336-428:QTimer 定时触发 writeDatagram 发包,简单高效。

TIP

📌 UDP vs TCP:TCP 面向连接,connectToHost → write/readAll;UDP 无连接,bind → readDatagram/writeDatagram。UDP 每个包独立,需自己处理分包、丢包。

14 · HTTP 客户端

参考 @d:\Projects\vcpkg\vcpkg\vcpkg\safeSpace\RyBOS\qt\HttpClient\src\QtHttpClient.h:103-389:

class QtHttpClient : public QObject
{
    Q_OBJECT
public:
    explicit QtHttpClient(QObject* parent = nullptr)
        : QObject(parent), m_manager(new QNetworkAccessManager(this)), m_timeoutMs(30000) {}

    // 同步 GET(QEventLoop 等待)
    QtHttpResponse get(const QString& url, const QMap<QString, QString>& headers = {}) {
        return request("GET", url, QByteArray(), headers);
    }
    // 异步 GET(回调)
    void getAsync(const QString& url, HttpCallback callback, ...) {
        requestAsync("GET", url, QByteArray(), callback, headers);
    }
};
TIP

📌 QNetworkAccessManager 是异步的——get()/post() 立即返回 QNetworkReply*,数据在后台接收,完成后发 finished 信号。同步模式用 QEventLoop 包装。生产环境推荐异步模式,避免嵌套事件循环的重入风险。

15 · WebSocket

参考 @d:\Projects\vcpkg\vcpkg\vcpkg\safeSpace\RyBOS\qt\WebSocket\src\QtWebSocket.h:55-151:

class QtWebSocketClient : public QObject
{
    Q_OBJECT
public:
    explicit QtWebSocketClient(QObject* parent = nullptr)
        : QObject(parent), m_socket(new QWebSocket(..., this))
    {
        connect(m_socket, &QWebSocket::connected, this, &QtWebSocketClient::connected);
        connect(m_socket, &QWebSocket::textMessageReceived, this, &QtWebSocketClient::messageReceived);
    }
    void connectToServer(const QString& url) { m_socket->open(QUrl(url)); }
    qint64 send(const QString& msg) { return m_socket->sendTextMessage(msg); }
signals:
    void connected();
    void messageReceived(const QString& message);
};
TIP

📌 WebSocket vs TCP:WebSocket 建立在 HTTP 握手之上,提供消息边界(每条消息完整到达),TCP 是字节流(需自己分包)。WebSocket 更适合实时通信(聊天、推送)。

16 · QWidget 与布局

16.1 布局管理器

// 垂直布局
QVBoxLayout* vLayout = new QVBoxLayout(parent);
vLayout->addWidget(button1);
vLayout->addStretch();  // 弹性空间

// 水平布局
QHBoxLayout* hLayout = new QHBoxLayout;
hLayout->addWidget(label);
hLayout->addWidget(inputField);

// 网格布局
QGridLayout* grid = new QGridLayout;
grid->addWidget(label1, 0, 0);  // row=0, col=0
grid->addWidget(input1, 0, 1);

// 嵌套
QVBoxLayout* main = new QVBoxLayout(window);
main->addLayout(hLayout);
main->addLayout(grid);
TIP

📌 为什么用布局而不是绝对坐标:窗口可缩放、字体可变、不同平台 DPI 不同。布局管理器自动计算控件位置和大小,窗口缩放时自动重排。

16.2 RyBOS VideoPlayer UI

参考 @d:\Projects\vcpkg\vcpkg\vcpkg\safeSpace\RyBOS\qt\VideoPlayer\src\QtVideoPlayerWidget.h:282-355:用 QVBoxLayout 放 GLVideoWidget 和占位符,叠加标签用 move() + raise() 手动定位,在 resizeEvent 中重新定位。

TIP

📌 叠加控件技巧:FPS 标签等"浮层"不加入布局,用 move() 定位 + raise() 提到最上层,在 resizeEvent 中重新定位。

17 · 事件系统

class MyWidget : public QWidget {
protected:
    void mousePressEvent(QMouseEvent* event) override {
        if (event->button() == Qt::LeftButton)
            qDebug() << "Left click at" << event->pos();
        QWidget::mousePressEvent(event);  // 调用基类保持默认行为
    }
    void keyPressEvent(QKeyEvent* event) override {
        if (event->key() == Qt::Key_Escape) close();
        else QWidget::keyPressEvent(event);
    }
    void resizeEvent(QResizeEvent* event) override {
        QWidget::resizeEvent(event);
        repositionOverlays();
    }
};

事件过滤器

textEdit->installEventFilter(this);  // 监听 textEdit 的事件

bool MyWindow::eventFilter(QObject* watched, QEvent* event) {
    if (watched == textEdit && event->type() == QEvent::KeyPress) {
        // 拦截特定按键
        return true;  // true = 事件被过滤
    }
    return QWidget::eventFilter(watched, event);
}
TIP

📌 事件过滤器 vs 重写事件函数:重写只能在自己子类中拦截;事件过滤器可以拦截任何对象的事件,不需要继承。更灵活——全局快捷键、输入验证等。

18 · QPainter 绘图

void MyWidget::paintEvent(QPaintEvent*) {
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

    painter.setPen(QPen(Qt::blue, 2));
    painter.setBrush(QBrush(Qt::red));
    painter.drawRect(10, 10, 100, 50);

    painter.setPen(Qt::white);
    painter.setFont(QFont("Arial", 14));
    painter.drawText(rect(), Qt::AlignCenter, "Hello Qt");

    QImage image("photo.jpg");
    painter.drawImage(50, 50, image.scaled(200, 200, Qt::KeepAspectRatio));
}
TIP

📌 双缓冲绘图:先画到离屏 QPixmap,再一次性绘制到屏幕,避免逐步绘制时的闪烁。Qt QWidget 默认已使用双缓冲,但复杂自定义绘制时手动控制更可靠。

19 · QSS 样式

// 全局样式
qApp->setStyleSheet(
    "QPushButton { background-color: #4CAF50; color: white; border: none; padding: 8px 16px; border-radius: 4px; }"
    "QPushButton:hover { background-color: #45a049; }"
    "QPushButton:pressed { background-color: #3d8b40; }"
);

// 单个控件
label->setStyleSheet("background: rgba(0,0,0,150); color: #fff; font-size: 12px;");

RyBOS 中用 RyB--VideoPlayerWidget 作为 QSS 选择器(命名空间::类名 → 命名空间--类名),适合整类统一样式。

20 · 定时器

// 周期定时器
QTimer* timer = new QTimer(this);
timer->setInterval(1000);
connect(timer, &QTimer::timeout, [this]() { qDebug() << "Tick"; });
timer->start();

// 单次定时器
QTimer::singleShot(3000, [this]() { qDebug() << "3s later"; });

timer->stop();

RyBOS VideoPlayer 中用 QTimer 实现重连检查(每5秒)和省略号动画(每500ms)。

TIP

📌 QTimer 需要事件循环:QTimer 不是"开线程定时执行",而是在事件循环中注册时间戳,每次迭代检查是否到期。所以 QTimer 必须在有事件循环的线程中使用。QThread::run() 默认不启动事件循环,除非调 exec()。

21 · Model/View 架构

图 8 \xb7 Model/View 与 AsyncTask 模式

Model(数据) View(展示) Delegate(绘制/编辑) QAbstractItemModel ←→ QListView ←→ QStyledItemDelegate QAbstractTableModel ←→ QTableView
// 简单用法
QStandardItemModel* model = new QStandardItemModel(this);
model->setHorizontalHeaderLabels({"Name", "Age"});
model->appendRow({new QStandardItem("Alice"), new QStandardItem("25")});

QTableView* view = new QTableView;
view->setModel(model);
TIP

📌 Model/View 的好处:同一份 Model 可以用 QTableView、QListView、QTreeView 展示,数据改了视图自动刷新。数据简单时用 QListWidget/QTableWidget(便捷类)更省事。

22 · 跨平台与 CMake

条件编译

参考 @d:\Projects\vcpkg\vcpkg\vcpkg\safeSpace\RyBOS\qt\NetworkTools\src\QtNetworkTools.h:120-132:

#ifdef Q_OS_WIN
    args << "-n" << "1" << "-w" << QString::number(timeoutMs) << host;
#else
    args << "-c" << "1" << "-W" << QString::number(timeoutMs / 1000) << host;
#endif

CMake

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_AUTOMOC ON)   # 自动 MOC
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
find_package(Qt6 COMPONENTS Core Gui Widgets Network REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Core Qt6::Widgets Qt6::Network)
TIP

📌 CMAKE_AUTOMOC:自动扫描 Q_OBJECT 宏,运行 MOC 生成 moc_xxx.cpp 并编译链接。没有这个设置需手动 qt5_wrap_cpp() 每个头文件——非常繁琐。Qt 项目标配。

23 · 编码约定与最佳实践

RyBOS Qt/C++ 编码规范

来自 @d:\Projects\vcpkg\vcpkg\vcpkg\safeSpace\RyBOS\docs\RyBOS-Qt-Cpp编码约定.txt:

约定 规则 示例
命名空间 统一 namespace RyB namespace RyB { ... }
类命名 Qt 模块加 Qt 前缀 QtHttpClient, QtTcpServer
大括号 Allman 风格(左括号换行) if (ok)\n{\n ...\n}
成员变量 m_ 前缀 m_socket, m_port
静态成员 s_ 前缀 s_instance
C++ 标准 C++17 CMAKE_CXX_STANDARD 17
Header-only 优先头文件实现 #ifndef XXX_H ... #endif
connect 新式函数指针 connect(obj, &Class::signal, this, &MyClass::slot)
跨平台 Qt 用 Q_OS_WIN #ifdef Q_OS_WIN

常见陷阱

  1. 忘记 Q_OBJECT:信号槽静默失败,编译不报错
  2. 构造函数中 emit 信号:还没人 connect,信号丢失。用 QTimer::singleShot(0, ...) 延迟
  3. 子线程直接操作 UI:可能崩溃。必须通过信号槽或 QMetaObject::invokeMethod
  4. QThread::wait() 死锁:子线程用 BlockingQueuedConnection 向主线程发信号会死锁
  5. deleteLater vs delete:信号槽调用栈中直接 delete 可能崩溃,用 deleteLater

RyBOS 模块速查表

模块 路径 核心类 用途
HttpClient qt/HttpClient/ QtHttpClient HTTP 同步/异步请求
TcpSocket qt/TcpSocket/ QtTcpClient, QtTcpServer TCP 通信
UdpSocket qt/UdpSocket/ QtUdpServer, QtUdpHeartbeat UDP + 心跳
WebSocket qt/WebSocket/ QtWebSocketClient, QtWebSocketServer WebSocket
ConfigManager qt/ConfigManager/ ConfigManager INI 配置(线程安全)
TaskScheduler qt/TaskScheduler/ QtThreadPool, QtTaskScheduler 线程池+定时调度
AsyncTask qt/AsyncTask/ AsyncTaskExecutor 阻塞但不卡界面
VideoPlayer qt/VideoPlayer/ VideoThread, VideoPlayerWidget 视频流(GPU加速)
LogManager qt/LogManager/ LogManager, TagLog 日志管理
NetworkTools qt/NetworkTools/ QtPing, QtPortScanner Ping/端口扫描/DNS

附录 · Qt 类速查

类 用途 常用方法
QObject 所有 Qt 对象基类 parent(), children(), deleteLater()
QString 字符串 arg(), split(), contains(), toUtf8()
QByteArray 二进制数据 append(), toHex(), mid()
QVariant 通用值容器 toInt(), toString(), value<T>()
QTimer 定时器 start(), stop(), singleShot()
QSettings 配置持久化 setValue(), value(), beginGroup()
QThread 线程 start(), wait(), moveToThread()
QMutex 互斥锁 lock(), unlock()(用 QMutexLocker)
QPainter 绘图 drawRect(), drawText(), drawImage()
QFile 文件读写 open(), readAll(), write()
QTcpSocket TCP 客户端 connectToHost(), write(), readAll()
QUdpSocket UDP bind(), writeDatagram(), readDatagram()
QNetworkAccessManager HTTP get(), post(), put()
QWebSocket WebSocket open(), sendTextMessage()