☰
Qt学习之路2整理版94章实战指南:信号槽、模型视图与多线程避坑
2026/10/15 6:07:53 网站建设 项目流程

简介:这份《QT学习之路2-2020整理版》面向希望系统掌握Qt框架的开发者,从零基础入门到进阶实战均可参考。内容整理自豆子博客,在原有79章基础上补充了2016年后更新的15章,共94章,涵盖Qt简介、信号槽机制、自定义信号槽、模块划分、MainWindow、动作与资源文件、对象模型、布局管理器、菜单栏工具栏状态栏、对话框及数据传递等核心主题,并延伸至多线程、国际化、数据库集成与网络编程等高级方向,同时涉及Qt Creator使用与调试技巧。资源为1个PDF文件,压缩包约5.79MB,单文件结构便于通读与检索。目前已有869人学习,适合结合Qt5与Qt Creator边学边练,逐步建立完整的Qt知识体系与项目实践思路。

1. Qt 学习之路 2 整理版:一份 PDF 为什么值得你花两周啃完

如果你正在用 Qt 写桌面端,却总在信号槽的线程归属、QWidget 与 QML 的选型边界、模型视图的刷新时机上反复翻车,那这份《Qt 学习之路 2-2020 整理版(更新到 94 章)》大概率能省下你大量试错时间。它不是 API 速查手册,而是一条从 QWidget 手写界面、事件循环、绘图、网络、多线程,一路推到 QML 与 Qt Quick 的完整路径。94 章的体量意味着它把「能跑起来」和「能维护」之间的灰色地带讲得比较细。适合两类人:一是刚接桌面项目、需要快速建立全局观的新手;二是写了半年 Qt 但知识碎片化、想系统补齐事件、模型、线程这几块的老手。下面我按自己带人复现这份资料的经验,把它拆成可执行的路线。

2. 把 94 章拆成四条主线:先跑通哪条,后补哪条

2.1 为什么不能从第 1 章顺着读到第 94 章

这份整理版章节多,但内部其实有清晰的依赖关系。顺着读最大的问题是:前 20 章还在讲 QWidget 布局和信号槽基础,你还没建立起「一个完整窗口程序长什么样」的直觉,就被拖进绘图和自定义控件,很容易中途放弃。我的做法是先按目标分四条主线:界面与事件主线、模型视图主线、多线程与网络主线、QML 与 Qt Quick 主线。每条主线内部再按「最小可运行 → 加交互 → 加数据 → 加并发」四步推进。这样即使只啃完前两条,也已经能独立交付一个带表格和表单的桌面工具。

2.2 四条主线的章节映射与验收标准

下面这张表是我带人时用的映射,章节号按整理版常见编排给出区间,具体以你手上 PDF 的目录为准,重点是验收标准。

主线覆盖内容建议投入验收标准
界面与事件QWidget、布局、信号槽、事件分发、绘图3 天能独立写出带自定义绘制和鼠标交互的窗口
模型视图QStringListModel、QTableView、委托、排序过滤2 天能用 QAbstractTableModel 接一份真实数据并支持编辑
多线程与网络QThread、线程间信号、QTcpSocket、QNetworkAccessManager3 天能写出不卡 UI 的下载或采集程序
QML 与 QuickQML 语法、属性绑定、C++ 与 QML 交互4 天能用 C++ 注册类型并在 QML 里调用

验收标准比读完多少章重要。很多人读完多线程章节,仍然写不出一个不卡界面的耗时任务,原因是没有把「信号槽跨线程默认队列连接」这件事真正跑一遍。

2.3 环境准备:一次把编译套件配到位

Qt 的版本选择直接决定你后面踩不踩坑。整理版是 2020 年内容,对应 Qt 5.12 到 5.15 区间最稳。我的建议是装 Qt 5.15 LTS 加 Qt Creator,编译器在 Windows 上用 MinGW 或 MSVC 都行,但一旦选定就别中途换,否则 moc 生成的中间文件容易出玄学问题。

# 以命令行方式验证 Qt 环境是否可用(Linux/macOS 示例) qmake -v # 确认 qmake 指向 Qt 5.15 which qmake # 查看实际路径,避免多版本串台 qtcreator -version # 确认 Creator 版本与 Qt 匹配

逻辑说明:qmake -v输出里会带 Qt 版本和编译套件路径,如果显示的不是你装的 5.15,说明 PATH 里有旧版本抢先。参数上,which qmake用来定位真实二进制,多版本共存时这一步能省掉大量「为什么编译报错但代码没问题」的排查。注意:Windows 下如果同时装了 MSVC 和 MinGW 两套 Kit,Qt Creator 的构建套件里要确认默认 Kit 与 qmake 一致,否则会出现链接期符号找不到。

2.4 第一个可运行程序:别急着写业务

环境好了之后,先跑一个最小窗口,确认信号槽、moc、资源系统三条链路都通。

// main.cpp —— 最小可运行窗口,验证信号槽与 moc 是否正常 #include <QApplication> #include <QPushButton> int main(int argc, char *argv[]) { QApplication app(argc, argv); QPushButton btn("点我"); // 连接信号与槽,验证 moc 是否参与编译 QObject::connect(&btn, &QPushButton::clicked, [&btn]() { btn.setText("已点击"); }); btn.resize(200, 80); btn.show(); return app.exec(); }

逻辑说明:这段代码同时验证了三件事——QApplication 事件循环能起来、connect的新式语法能被 moc 正确处理、lambda 作为槽能被调用。参数上,resize只是给个初始尺寸,真正布局要靠布局管理器。如果编译报「undefined reference to vtable」,八成是类定义在头文件里但没走 moc,或者 qmake 的 HEADERS 没包含该文件。这一步跑通,后面所有章节的示例才有复现基础。

3. 信号槽与事件循环:Qt 最容易被误解的两块

3.1 信号槽的连接类型到底怎么选

信号槽是 Qt 的门面,但连接类型选错是线程问题的头号来源。默认的AutoConnection在跨线程时会变成队列连接,槽函数在接收者所属线程执行;而同线程时是直连,槽函数立即执行。很多人写多线程时以为槽一定在子线程跑,结果 UI 操作崩了,就是没搞清这条规则。

// 跨线程连接:显式指定队列连接,避免依赖 AutoConnection 的隐式判断 QThread workerThread; Worker *worker = new Worker(); worker->moveToThread(&workerThread); // 队列连接:槽在 worker 所属线程执行,适合耗时任务 QObject::connect(&workerThread, &QThread::started, worker, &Worker::doHeavyWork, Qt::QueuedConnection); // 结果回传主线程:接收者是 this(主线程对象),自动队列连接 QObject::connect(worker, &Worker::resultReady, this, &MainWindow::onResult); workerThread.start();

逻辑说明:moveToThread把 worker 的线程归属改到子线程,之后发给它的队列连接槽都会在子线程执行。参数上,Qt::QueuedConnection强制走事件队列,参数会被拷贝,所以自定义类型必须用qRegisterMetaType注册,否则运行时报「Cannot queue arguments of type」。Qt::DirectConnection只在确定同线程且需要同步返回时用,跨线程用它会直接在调用者线程执行,UI 操作就会出问题。

3.2 事件循环不是「主循环」那么简单

事件循环是 Qt 的心脏,但很多人只把它当成app.exec()。实际上每个 QThread 默认也有自己的事件循环(QThread::exec()),定时器、队列连接、deleteLater都依赖它。子线程里如果只重写run()而不调用exec(),那么发给这个线程对象的队列槽永远不会执行。

// 子线程中启动事件循环,让队列连接和定时器生效 class Worker : public QObject { Q_OBJECT public slots: void doHeavyWork() { // 耗时任务,注意不要在这里操作 UI QThread::sleep(2); emit resultReady(QStringLiteral("done")); } signals: void resultReady(const QString &msg); }; // 若继承 QThread 并重写 run,需要显式 exec() class MyThread : public QThread { protected: void run() override { // 初始化后进入事件循环,否则队列槽不执行 exec(); } };

逻辑说明:exec()让线程进入事件循环,之后deleteLater、队列连接、QTimer才会正常工作。参数上,QThread::sleep只是模拟耗时,真实项目里应换成可中断的任务。注意:在run()里直接new的对象如果没有父对象,线程退出后不会自动释放,容易内存泄漏,常见做法是用deleteLater配合事件循环收尾。

3.3 用最小例子验证线程归属

判断槽在哪个线程执行,最直接的办法是打印QThread::currentThread()。

// 打印当前线程,验证槽函数的实际执行线程 qDebug() << "slot thread:" << QThread::currentThread(); qDebug() << "main thread:" << qApp->thread();

逻辑说明:如果两行输出一致,说明槽在主线程;不一致说明在子线程。参数上没什么可调的,关键是养成在跨线程代码里加这行的习惯。我一般会在moveToThread前后各打一次,确认归属变化符合预期。这一步能挡掉后面八成的「界面卡死」和「随机崩溃」。

4. 模型视图与自定义委托:数据一多就翻车的重灾区

4.1 为什么 QTableWidget 撑不住真实数据

新手常从 QTableWidget 入手,几百行没问题,一旦上万行加实时刷新就开始卡。原因是 QTableWidget 为每个单元格创建了 QTableWidgetItem 对象,内存和刷新开销都随行数线性增长。正确路径是 QTableView 加自定义 QAbstractTableModel,视图只渲染可见区域,模型只提供数据接口。

// 自定义表格模型:只实现必要接口,视图按需取数 class TableModel : public QAbstractTableModel { Q_OBJECT public: explicit TableModel(QObject *parent = nullptr) : QAbstractTableModel(parent) {} int rowCount(const QModelIndex &parent = QModelIndex()) const override { return parent.isValid() ? 0 : m_data.size(); } int columnCount(const QModelIndex &parent = QModelIndex()) const override { return parent.isValid() ? 0 : 3; } QVariant data(const QModelIndex &index, int role) const override { if (!index.isValid() || role != Qt::DisplayRole) return {}; const auto &row = m_data.at(index.row()); switch (index.column()) { case 0: return row.name; case 1: return row.value; case 2: return row.time; } return {}; } // 批量更新时用 beginResetModel/endResetModel 或精确的 insertRows void resetData(const QVector<Row> &rows) { beginResetModel(); m_data = rows; endResetModel(); } private: struct Row { QString name; double value; QString time; }; QVector<Row> m_data; };

逻辑说明:rowCount和columnCount里对parent.isValid()的判断是标准写法,避免树形模型误用。data只处理DisplayRole,其他角色返回空。参数上,批量替换数据用beginResetModel/endResetModel,局部增删用beginInsertRows/endInsertRows,后者能保留选中状态和滚动位置。如果刷新时整表闪烁,多半是用了 reset 而不是精确插入。

4.2 委托:让编辑体验不别扭

默认编辑器对数字、日期、枚举都不友好,自定义委托是绕不开的。

// 自定义委托:为数值列提供 QSpinBox 编辑器 class SpinDelegate : public QStyledItemDelegate { Q_OBJECT public: QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &) const override { auto *spin = new QSpinBox(parent); spin->setRange(0, 100000); return spin; } void setEditorData(QWidget *editor, const QModelIndex &index) const override { auto *spin = qobject_cast<QSpinBox *>(editor); spin->setValue(index.data(Qt::EditRole).toInt()); } void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override { auto *spin = qobject_cast<QSpinBox *>(editor); spin->interpretText(); model->setData(index, spin->value(), Qt::EditRole); } };

逻辑说明:createEditor返回编辑器控件,setEditorData把模型数据灌进编辑器,setModelData把编辑结果写回模型。参数上,interpretText()确保用户手动输入未回车时也能取到值,少了这行会出现「改了没生效」。注意:委托里不要持有模型指针,视图会管理生命周期。

4.3 排序过滤用 QSortFilterProxyModel 而不是自己写

自己写排序过滤容易和模型刷新打架,标准做法是套一层代理。

// 代理模型:排序 + 关键字过滤,视图直接接代理 auto *proxy = new QSortFilterProxyModel(this); proxy->setSourceModel(tableModel); proxy->setFilterKeyColumn(0); // 按第 0 列过滤 proxy->setFilterCaseSensitivity(Qt::CaseInsensitive); proxy->setSortRole(Qt::DisplayRole); tableView->setModel(proxy); tableView->setSortingEnabled(true); // 过滤时只改关键字,代理自动重算 proxy->setFilterFixedString("keyword");

逻辑说明:setSourceModel绑定源模型,setFilterKeyColumn指定过滤列,setSortRole决定排序依据。参数上,setFilterFixedString是精确子串匹配,模糊匹配用setFilterRegularExpression。如果过滤后行号错乱,检查是不是在源模型上做了 reset 而代理没收到信号——正常情况下代理会自动转发。

5. 多线程、网络与 QML:从能跑到能交付的最后一公里

5.1 多线程采集:别让 UI 等数据

真实项目里最常见的场景是后台采集或下载,UI 只负责展示。核心原则是:耗时操作放子线程,结果通过信号回主线程,UI 只碰主线程对象。

// 采集任务:子线程执行,结果信号回主线程 class Collector : public QObject { Q_OBJECT public slots: void startCollect() { while (!m_stop) { // 模拟采集,真实场景替换为网络或串口读取 QThread::msleep(100); emit progress(++m_count); } } void stop() { m_stop = true; } signals: void progress(int count); private: std::atomic_bool m_stop{false}; int m_count = 0; };

逻辑说明:std::atomic_bool保证停止标志跨线程可见,避免用普通 bool 导致优化后读不到变化。参数上,msleep只是节流,真实采集要处理超时和重连。注意:stop()如果通过队列连接调用,得等当前循环走完才生效,想立即停可以用Qt::DirectConnection但要保证线程安全。

5.2 网络请求:QNetworkAccessManager 的复用与超时

QNetworkAccessManager 要复用,不要每次请求都 new,否则连接池和 cookie 管理会乱。

// 复用 manager,设置超时,处理完成与错误 m_manager = new QNetworkAccessManager(this); QNetworkRequest req(QUrl("https://example.invalid/api")); req.setTransferTimeout(5000); // 5 秒超时 QNetworkReply *reply = m_manager->get(req); connect(reply, &QNetworkReply::finished, this, [reply]() { if (reply->error() == QNetworkReply::NoError) { QByteArray data = reply->readAll(); // 处理数据 } else { qWarning() << "network error:" << reply->errorString(); } reply->deleteLater(); // 必须释放 });

逻辑说明:setTransferTimeout避免请求悬挂,finished里统一处理成功与失败,deleteLater释放 reply。参数上,超时时间按业务定,内网可以短,公网适当放宽。注意:readAll之后 reply 的数据就没了,需要多次读取的场景要边收边处理。

5.3 C++ 与 QML 交互:注册类型而不是塞上下文

QML 侧要调用 C++,推荐用qmlRegisterType注册类型,而不是setContextProperty到处塞。

// 注册 C++ 类型供 QML 使用 qmlRegisterType<TableModel>("App.Models", 1, 0, "TableModel"); // QML 侧 // import App.Models 1.0 // TableModel { id: model }

逻辑说明:注册类型让 QML 能直接实例化并参与属性绑定,比上下文属性更清晰。参数上,版本号要和 QML 的 import 一致。注意:注册要在QQmlApplicationEngine加载 QML 之前完成,否则 QML 找不到类型。

6. 避坑与排查:那些让我加班到凌晨的 Qt 问题

6.1 界面卡死但 CPU 不高

现象:点击按钮后界面无响应,任务管理器里 CPU 占用很低。原因:耗时操作写在了主线程,事件循环被阻塞,界面无法重绘。解决:把耗时逻辑移到子线程,通过信号回传进度;如果必须同步,至少用QCoreApplication::processEvents()临时让出,但这不是长久之计,容易引发重入问题。

6.2 跨线程信号参数报「Cannot queue arguments」

现象:跨线程发信号时运行时报无法队列化参数类型。原因:自定义类型没有注册元类型,队列连接需要拷贝参数。解决:在main里或类型定义后调用qRegisterMetaType<MyType>("MyType"),并确保类型可拷贝。

6.3 模型刷新后选中行丢失

现象:数据更新后表格选中状态和滚动位置回到顶部。原因:用了beginResetModel/endResetModel整表重置。解决:局部增删改用beginInsertRows/endInsertRows和beginRemoveRows/endRemoveRows,只改单元格用dataChanged信号。

6.4 程序退出时崩溃在析构

现象:关闭窗口时随机崩溃,堆栈指向子线程对象。原因:子线程还在跑,主线程已经销毁了相关对象。解决:在closeEvent里先请求停止,quit()加wait()等线程退出,再让父对象析构。注意wait要有超时兜底,避免死等。

6.5 QML 属性绑定不更新

现象:C++ 侧数据变了,QML 界面没反应。原因:属性没有 NOTIFY 信号,绑定无法感知变化。解决:给 Q_PROPERTY 加 NOTIFY,并在 setter 里发信号;模型类则确保正确发出dataChanged。

7. 把 94 章变成自己的知识库:一个可复用的验证习惯

啃完这份整理版,真正的分水岭不是读了多少章,而是有没有形成「每学一块就写一个最小验证」的习惯。我带人时要求每章至少留一个可运行的小工程,命名成ch03_signal_slot、ch12_model_view这样,半年后回头看,这些工程就是自己的排错字典。具体做法是:每学完一个主题,先写一个能跑的最小例子,再故意制造一个错误,观察报错信息,最后把正确写法和错误现象一起记在工程 README 里。比如信号槽那块,我会同时留一个直连和一个队列连接的例子,对比打印线程 ID;模型视图那块,留一个 reset 和一个精确插入的例子,对比选中状态是否保留。

进阶一点,可以用 Qt Test 给核心逻辑写单元测试,尤其是模型和工具类,这些不依赖界面的部分最适合自动化验证。下面是一个最小测试骨架。

// 用 Qt Test 验证模型行数,避免手工点界面 #include <QtTest> #include "tablemodel.h" class TestModel : public QObject { Q_OBJECT private slots: void rowCountMatchesData() { TableModel model; QVector<TableModel::Row> rows(3); model.resetData(rows); QCOMPARE(model.rowCount(), 3); } }; QTEST_MAIN(TestModel) #include "testmodel.moc"

逻辑说明:QTEST_MAIN生成入口,QCOMPARE做断言,#include "testmodel.moc"让 moc 处理测试类。参数上,测试工程用QT += testlib,跑make check或直接运行可执行文件。这样每次改模型逻辑,先跑测试再开界面,能挡掉大量低级回归。

我自己的血泪经验是:Qt 的问题八成出在「线程归属」和「对象生命周期」这两件事上,剩下两成是「模型信号没发对」。所以每写一段跨线程或模型代码,先问自己三个问题——这个对象属于哪个线程、它什么时候被销毁、数据变了谁负责通知。把这三个问题养成条件反射,再回头看那 94 章,会发现它们其实都在反复讲同一件事。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询