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 核心模块

| 模块 |
作用 |
代表类 |
| 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 设计哲学
- 元对象系统:在 C++ 之上加了一层"反射"能力
- 信号与槽:对象间通信靠"广播-订阅"模式,松耦合
- 事件循环驱动:一切围绕事件循环运转
- 对象树管理内存:parent 删除时自动删除所有 children
- 隐式共享(Copy-on-Write):容器传值时只复制指针,修改时才真正拷贝
2 · 元对象系统(Meta-Object System)
2.1 为什么 C++ 需要元对象系统
C++ 是静态类型语言,编译后类型信息基本丢失。但 GUI 框架需要:运行时知道类名、方法名,能动态调用——信号槽连接、属性系统、动态类型识别都依赖这些信息。
TIP
📌 打个比方:C++ 编译后的代码就像"去掉了目录和索引的书"——内容都在,但你不知道第几章叫什么。Qt 的 MOC(Meta-Object Compiler)就是给这本书重新生成一份目录,让程序在运行时能查到"这个类有哪些信号、哪些槽"。
2.2 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 本质:观察者模式

- 信号(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 什么是事件循环

事件循环是 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 对象树模型

每个 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 多线程

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 编程

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 架构

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 |
常见陷阱
- 忘记 Q_OBJECT:信号槽静默失败,编译不报错
- 构造函数中 emit 信号:还没人 connect,信号丢失。用
QTimer::singleShot(0, ...) 延迟
- 子线程直接操作 UI:可能崩溃。必须通过信号槽或
QMetaObject::invokeMethod
- QThread::wait() 死锁:子线程用 BlockingQueuedConnection 向主线程发信号会死锁
- 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() |