☰
Qt学习之路2020整理版PDF:从环境搭建到项目实战的避坑指南
2026/10/14 3:49:41 网站建设 项目流程

简介:这份《QT学习之路2-2020整理版》面向希望系统掌握Qt框架的开发者,从零基础入门到进阶实战均可参考。内容整理自豆子博客,在原有79章基础上补充了2016年后更新的15章,共94章,涵盖Qt简介、信号槽机制、自定义信号槽、模块划分、MainWindow、资源文件、对象模型、布局管理器、菜单栏工具栏状态栏、对话框数据传递,以及多线程、国际化、数据库与网络编程等高级主题,并涉及Qt Creator使用与调试技巧。资源为1个PDF文件,压缩包约5.79MB,目录结构清晰,便于按章节顺序学习或按主题检索。目前已有869人学习下载,适合需要一份完整、连贯的Qt学习路线图,并希望理解信号槽设计模式与跨平台GUI开发最佳实践的读者。

1. Qt 学习之路的 PDF 整理版到底值不值得啃

很多人第一次接触 Qt 是在一个很具体的场景里:手头有个桌面工具要做,界面要跨平台,C++ 底子还行但没碰过信号槽,于是搜到一份流传很广的连载笔记,标题里带着「Qt 学习之路」和「2020 整理版」。这份东西本质是一套按章节推进的 Qt 入门连载,从环境搭建、控件使用一路讲到网络、多线程、数据库和图形视图,更新到 94 章,被整理成 PDF 方便离线看。它解决的不是「Qt 是什么」这种科普问题,而是「我照着一条有顺序的路径,能不能把 Qt Widgets 这套东西真正用起来」。适合谁?适合有 C++ 基础、要做桌面端、想系统过一遍 Qt 核心模块的人。不适合谁?想直接上 QML 做移动端、或者只想抄一个能跑的 Demo 就走的人。这份材料的价值在于顺序和覆盖面,坑在于它偏旧、偏 Widgets、部分章节的工程配置和现在主流的 CMake 流程对不上,直接照抄会翻车。

2. 把 PDF 连载变成能跑的环境:Qt 版本与构建系统怎么选

拿到一份 94 章的连载,第一件让人卡住的事不是语法,是环境。2020 年前后整理的 Qt 内容,默认语境多半是 Qt 5.12 到 5.15 这一段 LTS,构建工具在 qmake 和 CMake 之间摇摆。你如果直接按老章节里的.pro文件走,在 Qt 6 上会遇到一批编译错误,因为 Qt 6 把不少模块拆了、把QString的一些隐式转换收紧了。所以第一步不是急着看第 3 章,而是先把版本和构建系统定下来。

2.1 Qt 5.15 与 Qt 6.x 的取舍:先看你的目标平台

选版本这件事,我的经验是先看交付目标,而不是看教程写的是哪个版本。如果目标是 Windows 桌面工具、要长期维护、依赖一些第三方库,Qt 5.15 的生态最稳,绝大多数老教程能直接跑通,遇到问题搜到的答案也最多。如果目标是新项目、要用 Qt 6 的新特性、或者未来可能上 Qt Quick,那就直接上 Qt 6 的某个 LTS,但要做好心理准备:连载里凡是涉及QTextCodec、QRegExp、QDesktopWidget这类在 Qt 6 里被移除或替换的类,你都得自己换成QStringConverter、QRegularExpression、QScreen。

对比项Qt 5.15 LTSQt 6.x LTS
与老教程兼容度高,基本照抄中,需替换若干类
qmake 支持完整保留但官方主推 CMake
CMake 支持可用首选
图形栈传统默认走 RHI
适合场景维护老项目、跟教程新项目、长期演进

提示:不要在同一台机器上把 Qt 5 和 Qt 6 的环境变量混着配,PATH里谁在前谁生效,混用是「明明装了却找不到头文件」这类玄学问题的头号来源。

2.2 用 CMake 建一个最小可编译工程

连载里大量用 qmake,但你现在新建工程,我一般直接上 CMake,因为 Qt 6 官方推荐、和外部库集成更顺。下面是一个最小 Widgets 工程的CMakeLists.txt,可以直接抄。

cmake_minimum_required(VERSION 3.16) project(DemoApp LANGUAGES CXX) # 让 CMake 自动处理 Qt 的 moc/uic/rcc set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 找到 Qt 的 Widgets 模块,Qt6 用 Qt6,Qt5 改成 Qt5 find_package(Qt6 REQUIRED COMPONENTS Widgets) add_executable(DemoApp main.cpp mainwindow.cpp mainwindow.h ) target_link_libraries(DemoApp PRIVATE Qt6::Widgets)

逻辑说明:AUTOMOC负责扫描带Q_OBJECT的头文件生成 moc 代码,AUTOUIC处理.ui文件,AUTORCC处理.qrc资源。这三个开关不开,你会遇到「信号槽连不上」或者「找不到 ui_mainwindow.h」的报错。参数上,CMAKE_CXX_STANDARD建议 17,Qt 6 本身要求至少 C++17。find_package的组件按需加,用到网络就加Network,用到 SQL 就加Sql,别一次性全加,链接时间会变长。

2.3 从 PDF 章节到本地工程目录的映射

连载是按知识点分章的,但真实工程是按模块分目录的。我的做法是建一个learning仓库,每个章节一个子目录,每个子目录一个独立可编译工程,公共的头文件放common/。这样你看到第 40 章想试多线程,直接进ch40_thread/编译,不会污染前面的代码。目录结构大致是:

learning/ common/ logger.h ch03_widget/ CMakeLists.txt main.cpp ch40_thread/ CMakeLists.txt worker.cpp worker.h

这样做的好处是每章都能独立验证,坏处是重复代码多。等你过了 30 章,可以把反复出现的工具类抽到common/,但别一开始就抽,过早抽象会让你分不清哪些是 Qt 的坑、哪些是你自己封装的坑。

3. 信号槽与事件循环:连载里最容易「看懂了但写不对」的部分

信号槽是 Qt 的门面,也是新手翻车最集中的地方。连载会告诉你connect怎么用,但很少把「为什么连了没反应」讲透。这一章把信号槽、事件循环、线程亲和性串起来讲,因为这三件事在真实项目里是绑在一起的。

3.1 connect 的第五个参数:默认值害过很多人

QObject::connect有第五个参数Qt::ConnectionType,默认是AutoConnection。它的行为是:如果发送者和接收者在同一个线程,走直连(同步调用);如果不在同一个线程,走队列连接(异步,投递到接收者线程的事件循环)。问题就出在这:跨线程时如果你手动传了Qt::DirectConnection,槽函数会在发送者线程里执行,一旦槽里碰了接收者线程的 UI 对象,就是未定义行为,轻则界面卡死,重则崩溃。

// 错误示范:跨线程用 DirectConnection 操作 UI connect(worker, &Worker::resultReady, this, &MainWindow::onResult, Qt::DirectConnection); // 危险 // 正确做法:跨线程保持默认 AutoConnection,或显式 QueuedConnection connect(worker, &Worker::resultReady, this, &MainWindow::onResult, Qt::QueuedConnection);

逻辑说明:QueuedConnection会把调用打包成事件投递到接收者线程的事件循环,参数需要能被 Qt 的元类型系统拷贝,所以自定义类型必须用qRegisterMetaType注册,否则运行时报「Cannot queue arguments of type」。参数上,如果你不确定在哪个线程,就别写第五个参数,让AutoConnection自己判断,这是最稳的默认。

3.2 事件循环不是可有可无的背景板

很多「槽函数不执行」的问题,根因是事件循环没跑起来。QApplication::exec()启动主事件循环,所有队列连接、定时器、网络回调都靠它驱动。如果你在main里干了一件耗时的事再调exec(),那之前投递的事件全堆着。更隐蔽的是在槽里写死循环,事件循环被堵住,界面直接假死。

// 反例:在槽里同步等待,堵死事件循环 void MainWindow::onButtonClicked() { while (!taskDone) { // 忙等,事件循环停摆,界面无响应 } } // 正例:用 QTimer 或异步信号驱动 void MainWindow::onButtonClicked() { worker->start(); // 耗时逻辑丢到工作线程 }

逻辑说明:Qt 是事件驱动的,任何「等一个结果」的写法都要换成信号回调或者QEventLoop局部循环(后者要小心重入)。参数上,QTimer::singleShot(0, ...)是个常用技巧,把一段逻辑推迟到当前事件处理完之后执行,能避免在构造或信号处理中直接操作还没准备好的对象。

3.3 线程亲和性与 moveToThread 的正确姿势

连载讲多线程时,常见两种写法:继承QThread重写run,或者用moveToThread。前者简单但容易把「线程对象」和「线程里跑的逻辑」混为一谈。我一般推荐moveToThread,因为它把工作对象和线程解耦,信号槽的队列连接也更好理解。

// worker.h class Worker : public QObject { Q_OBJECT public slots: void doWork(); signals: void resultReady(const QString &r); }; // 使用处 QThread *thread = new QThread(this); Worker *worker = new Worker; worker->moveToThread(thread); connect(thread, &QThread::started, worker, &Worker::doWork); connect(worker, &Worker::resultReady, this, &MainWindow::onResult); connect(worker, &Worker::resultReady, thread, &QThread::quit); connect(thread, &QThread::finished, worker, &QThread::deleteLater); thread->start();

逻辑说明:moveToThread之后,worker的槽函数会在新线程的事件循环里执行,前提是新线程的exec()跑起来了(QThread::run默认会调exec)。参数上,deleteLater要连到finished上,否则线程结束但 worker 泄漏。注意worker不能在栈上创建,必须是堆对象,因为它的生命周期由线程事件循环管理。

4. 避坑与排查:94 章连载里那些没人明说的坑

这一章集中讲我在跟着这类连载做练习时踩过的坑,每条按现象、原因、解决来写,都是能复现的。

4.1 现象:编译过了,运行时报「could not find or load the Qt platform plugin windows」

原因:可执行文件运行时找不到 Qt 的平台插件,通常是PATH里没有 Qt 的bin目录,或者发布时没带platforms/qwindows.dll。开发机上装了 Qt Creator 一般不会遇到,一旦你把 exe 拷到别的目录就炸。

解决:开发阶段把 Qt 的bin加进PATH;发布时用windeployqt(Windows)或对应平台的部署工具,把依赖的 dll 和platforms目录一起拷过去。别手动一个个拷 dll,漏一个就是这种报错。

4.2 现象:中文在界面上显示成乱码

原因:源码文件编码、QString字面量编码、运行环境编码三者不一致。Qt 5 里QString从const char*构造时默认按 Latin-1 处理,源码是 UTF-8 就会乱。

解决:源码统一存 UTF-8,Qt 5 里在main开头加QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8")),或者直接用QStringLiteral/u8"..."。Qt 6 默认按 UTF-8 处理,基本不用管,但如果你从 Qt 5 迁过来,把老的QTextCodec调用删掉,否则编译不过。

4.3 现象:.ui文件改了但界面没变

原因:AUTOUIC没开,或者构建目录里缓存了旧的ui_*.h。CMake 的自动处理依赖文件时间戳,偶尔会抽风。

解决:确认CMAKE_AUTOUIC ON,然后删掉构建目录重新生成。如果还不行,检查.ui文件是否被加进了add_executable的源列表,AUTOUIC需要能看到它。

4.4 现象:信号槽连接返回 true,但槽就是不执行

原因:接收者对象在连接之后被销毁了,或者连接建立时接收者还没构造完。还有一种情况是重载信号,connect时没指定具体重载,编译器选了你不想要的那个。

解决:用 Qt 5 的函数指针语法时,重载信号要用QOverload或static_cast明确指定。接收者生命周期用QPointer或deleteLater管理,别在栈上创建跨作用域的对象。

4.5 现象:程序退出时崩溃,栈里是QObject::~QObject

原因:对象析构顺序问题。子对象先于父对象析构,或者线程还在跑但对象已经被删。Qt 的对象树靠父子关系管理内存,跨线程的对象如果没处理好销毁时机,退出时必崩。

解决:确保QThread先quit再wait,worker 用deleteLater。主窗口的子控件交给对象树,别手动delete。退出前把还在跑的定时器、网络请求停掉。

5. 从连载练习到真实项目:把 94 章的知识点串成一条线

跟着连载一章章敲,最大的问题是知识点是散的,到了真实项目里不知道先写哪个类。我的做法是找一个足够小但完整的项目当靶子,比如一个「本地日志查看器」:能打开文件、按关键字过滤、分页显示、支持大文件不卡界面。这个项目能把文件 IO、模型视图、多线程、信号槽、资源文件全串起来,正好覆盖连载中后段的核心章节。

5.1 用 QAbstractTableModel 替代 QTableWidget

连载早期会教QTableWidget,因为它直观。但真实项目里数据量一大,QTableWidget每个单元格都是一个QTableWidgetItem对象,几万行就卡。正确做法是自定义QAbstractTableModel,只存数据,视图按需取。

class LogModel : public QAbstractTableModel { Q_OBJECT public: int rowCount(const QModelIndex &parent = QModelIndex()) const override { return m_rows.size(); } int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 3; // 时间、级别、内容 } QVariant data(const QModelIndex &index, int role) const override { if (role != Qt::DisplayRole) return {}; const auto &row = m_rows.at(index.row()); switch (index.column()) { case 0: return row.time; case 1: return row.level; case 2: return row.content; } return {}; } private: struct Row { QString time, level, content; }; QVector<Row> m_rows; };

逻辑说明:rowCount和columnCount告诉视图有多少行列,data按角色返回数据。参数上,role除了DisplayRole还有TextAlignmentRole、ForegroundRole等,按需重写。数据量大时,data会被频繁调用,别在里面做耗时计算,提前把数据准备好。

5.2 大文件读取放到工作线程,用信号回传批次

直接在主线程读几百 MB 的日志文件,界面必卡。把读取逻辑放到moveToThread的 worker 里,每读一批就发一个信号,主线程的模型追加数据。

void LogWorker::loadFile(const QString &path) { QFile file(path); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { emit errorOccurred(file.errorString()); return; } QTextStream in(&file); QVector<LogModel::Row> batch; while (!in.atEnd()) { batch.append(parseLine(in.readLine())); if (batch.size() >= 500) { emit batchReady(batch); batch.clear(); } } if (!batch.isEmpty()) emit batchReady(batch); emit finished(); }

逻辑说明:分批大小 500 是个经验值,太小信号频繁、太大界面更新有延迟。batchReady用队列连接投递到主线程,模型在槽里beginInsertRows/endInsertRows包住追加操作,否则视图不会刷新。参数上,QTextStream的编码要显式设成 UTF-8,不然中文日志会乱。

5.3 用 QSortFilterProxyModel 做过滤,别自己重排

过滤功能很多人第一反应是在模型里手动筛,然后重建数据。正确做法是套一层QSortFilterProxyModel,把过滤逻辑交给它,原始模型不动。

auto *proxy = new QSortFilterProxyModel(this); proxy->setSourceModel(logModel); proxy->setFilterKeyColumn(2); // 按内容列过滤 proxy->setFilterCaseSensitivity(Qt::CaseInsensitive); connect(searchBox, &QLineEdit::textChanged, proxy, &QSortFilterProxyModel::setFilterFixedString); tableView->setModel(proxy);

逻辑说明:setFilterKeyColumn指定按哪一列过滤,setFilterFixedString是子串匹配,要正则就用setFilterRegularExpression。参数上,setFilterCaseSensitivity设成不区分大小写更符合用户预期。注意代理模型和源模型的行号映射,mapToSource和mapFromSource在需要定位原始数据时用。

5.4 资源文件与部署:把图标和样式打进 exe

连载里讲.qrc的章节容易被跳过,但真实项目里图标、qss 样式、默认配置都得靠它。建一个resources.qrc,把文件列进去,CMake 里AUTORCC会自动处理。

<RCC> <qresource prefix="/"> <file>icons/app.png</file> <file>styles/main.qss</file> </qresource> </RCC>

逻辑说明:prefix是资源在:/下的路径前缀,代码里用:/icons/app.png访问。参数上,大文件别塞进 qrc,会撑大 exe 且编译变慢,图片多的话考虑外部目录加运行时加载。qss 用QFile(":/styles/main.qss")读出来setStyleSheet应用。

6. 进阶技巧:让这份老材料继续产生价值

一份 2020 年整理的连载,放到现在最大的价值不是它的代码,而是它的知识顺序。我的习惯是把它当索引,而不是当答案。每看完一章,去 Qt 官方文档找对应类的当前版本说明,对比一下哪些 API 变了、哪些被标记废弃。这样你既有了系统学习的路径,又不会被旧代码带偏。

具体做法上,我会给每个章节建一个NOTES.md,记三样东西:这一章的核心类、我实际跑通的代码片段、以及和当前 Qt 版本的差异点。比如第 20 章讲QRegExp,我就在笔记里写「Qt 6 已移除,改用QRegularExpression,语法从QRegExp的 Perl 风格换成 PCRE」,下次再遇到直接查笔记,不用重新踩一遍。

验证自己有没有真学会,有个简单办法:把某一章的示例功能用另一种方式重写。比如连载用QTableWidget实现的表格,你用QTableView加自定义模型重写一遍;连载用继承QThread实现的多线程,你用moveToThread重写。重写过程中卡住的地方,就是你真正没理解的地方。

还有一个容易被忽略的点:调试手段。Qt 里qDebug()是最常用的,但跨线程时输出会交错,建议用QLoggingCategory分类,或者干脆在关键路径上打时间戳。遇到界面卡死,先看是不是主线程被堵,用QThread::currentThreadId()在槽里打一下,确认执行线程,比盲目加日志快得多。

我自己的教训是,早年跟着这类连载学的时候,总想着一口气看完 94 章,结果看到第 30 章就忘了第 10 章的控件用法。后来改成每 5 章停下来做一个小工具,把这几章的知识点用上,反而记得牢。这份材料不怕你看得慢,就怕你只看不写。希望帮到你。

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

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

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

立即咨询