简介:一套源自霍亚飞老师实战整理的Qt学习源码资料,面向Qt初学者与进阶开发者,围绕Qt与Qt Quick两条主线,系统覆盖C++图形界面基础、QML声明式界面、动画状态机、跨平台适配以及网络数据库等知识点,适合希望通过真实项目快速提升Qt开发能力,并希望摆脱纯理论阅读的学习者。压缩包共535个文件,约58.53MB,其中包含79个C++源文件、67个QML界面文件、55个头文件和29个工程文件,也配有179张PNG截图、少量GIF演示与MP3/LRC素材,以及PDF说明文档,基本涵盖从工程搭建到界面调试的完整环节。目前已有2513人学习使用,说明其作为Qt实践参考的价值得到了不少开发者的认可。结合示例工程与源码,可重点研究信号与槽通信、QML组件复用、动画与状态控制、网络与数据库接入等具体实现,还可在Qt Creator中直接打开多工程项目,梳理pro/qmlproject的工程组织方式,便于边学边改、边改边验证。
1. 学 Qt 和 Qt Quick 实战源码包,先把“跑起来”这个坎迈过去
很多学 QT 的朋友,尤其刚碰 Qt Quick,卡住的地方往往不是语法到底怎么背,而是手里代码不闭环。书里讲信号槽给一段,讲 ListView 又给一段,拼到真实项目里就翻车。这份 QT 实战源码包解决的核心问题,就是让你拿到一套从 main.cpp 入口、C++ 模型、SQLite 存取,到 QML 界面和统计图联动的完整工程,并且能直接编译运行。它适合刚看完 Qt 基础、准备进入 Qt Quick 阶段的人,也适合想快速搭桌面工具原型、再把自己的 C++ 逻辑拼进去的开发者。先跑通,再照着改,比从空工程一路踩坑来得踏实。
2. 技术架构拆解:Qt Quick 界面层与 C++ 逻辑层如何协作
我拿到源码包做的第一件事,不是急着跑 build,而是先把目录结构看了一遍。目录能直接说明作者想让 QML 和 C++ 各管哪块。这套源码包主工程的界面基本都在 qml/ 目录下,业务逻辑放在 src/ 下,入口除了 main.cpp 之外,日常二次开发几乎不用动 QML 之外的代码层。这个分层思路,比界面文件和工作线程全搅在一起的老工程要好维护得多。
2.1 为什么主工程选 Qt Quick,而不是 QWidgets
很多人入门 Qt 用的第一套控件是 QWidgets,所以看到主工程用 Qt Quick 会有疑问:是不是为了炫技?实际从交互形态看,这个任务面板有顶部搜索框、列表项左滑删除、底部统计图 hover 反馈,这些用 QWidgets 做也能做,但代码量会明显变大。Qt Quick 的优势在于声明式组合,布局、动画、状态切换交给 QML 引擎,C++ 只暴露数据和方法。
比较实在的选型原则是:界面以自定义交互、动效为主,选 Qt Quick;界面以标准表格、表单、树为主,业务逻辑又很重,继续用 QWidgets 反而省事。不要一个项目里既用 Quick 又用 Widgets 混着拼页面,后期控件层级和焦点管理会比较痛苦。QML 慢的说法也得分场景,绘制部分通过 Scene Graph 走 GPU,真正拖慢界面的往往是你在 JavaScript 里做了大量字符串处理或排序。源码包的做法是把重活放到 C++ 模型里,QML 只负责拿数据刷视图,这是一个很值得抄的分工。
2.2 源码包目录结构:工程怎么组织才不跑乱
主工程目录划分大致如下:
qt-quick-practice/ ├── CMakeLists.txt ├── src/ │ ├── main.cpp │ ├── AppController.h / AppController.cpp │ ├── TaskModel.h / TaskModel.cpp │ ├── DatabaseManager.h / DatabaseManager.cpp │ └── qml/ │ ├── Main.qml │ ├── pages/ │ │ └── TaskPage.qml │ └── components/ │ ├── TaskItem.qml │ └── StatChart.qml ├── resources/ │ ├── icons/ic_add.svg │ └── qml.qrc └── tests/ └── tst_chart.qmlAppController 是界面与数据之间的门面,TaskModel 负责暴露列表数据,DatabaseManager 管 SQLite,QML 文件里只出现控件和动画。这种结构最大的好处是,你想换数据库实现,不必动界面;想改交互,也不必碰 SQL。main.cpp 里把这几层通过 ContextProperty 串起来,职责很清楚。
resources/qml.qrc 把 QML 和图片一起编进资源文件,发布时不用带着一整套 qml 源码。需要提醒的是,QML 打进 qrc 后修改文件要重新构建,调试阶段如果嫌繁琐,可以先把 QML 放在外置目录用绝对路径加载,界面调完再切回资源打包,这是我自己常用的调试节奏。
2.3 通信链路:ContextProperty、信号槽与模型绑定怎么配
main.cpp 的装配代码是理解整个通信链路的钥匙:
// main.cpp 关键片段 int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); DatabaseManager db; db.init(); // 先建库建表,避免后续调用落空 TaskModel model; model.setDatabase(&db); // 模型和数据层绑定 AppController controller; controller.setModel(&model); QQmlApplicationEngine engine; engine.rootContext()->setContextProperty("app", &controller); engine.rootContext()->setContextProperty("taskModel", &model); engine.loadFromModule("TaskApp", "Main"); // Qt 6 推荐按模块加载 return app.exec(); }这段代码的顺序很关键:先初始化数据库,再加载 QML。因为 QML 里可能一行 import 加载完就立刻调用了 app 的方法,此时数据库还没就绪,轻则查询失败,重则闪退。controller 和 model 必须声明在 main 函数栈上,或者在堆上创建并保证生命周期长于 engine,否则 QML 侧持有了悬空指针,运行期会出现“玄学崩溃”。
三个通信通道的适用场景分别是:ContextProperty 适合注入控制器、全局配置这类单例;信号槽适合 C++ 侧数据变化后主动通知 QML 刷新;QAbstractItemModel 适合列表类数据源。不要一股脑把所有对象都 setContextProperty,像当前时间、临时状态这类高频变化的东西,尽量放到模型或信号里,避免 QML 到处绑。
3. 环境准备与运行:从源码包到界面弹出的完整步骤
源码包代码再完整,环境不对也白搭。这一章按我实际复现的顺序写,从版本选择到第一次看到窗口弹出,每一步都可能遇到问题,所以我尽量把出错现象也带上。
3.1 版本选择:Qt 6.5 LTS 还是 5.15,先统一再构建
源码包以 Qt 6 的 CMake 方式组织,但兼容 Qt 5.15 的写法也有保留。如果你完全新装环境,建议直接用 Qt 6.5 LTS,组件选全一些,尤其是 Qt Quick Controls 和 Qt SQL 这两个模块,后面 90% 的报错都和它们有关。
如果你手里只有 Qt 5.15,问题也不大,但要注意 QML 里的 import 版本。Qt 5 常见写法是:
import QtQuick 2.15 import QtQuick.Controls 2.15Qt 6 里可以去掉小版本号,直接用默认版本。源码包里的 QML 如果写死了 2.15,在 Qt 6 下通常也能跑,因为兼容导入路径还在;反过来拿 Qt 6 的 import 写法去 5.15 跑,就会直接报 module not found。另一个容易踩的是套件位数,Windows 上 MinGW 64 位和 MSVC 64 位的库不通用,选择套件时一定要和安装的 Qt 版本对应。
3.2 Qt Creator 打开工程的快速流程
用 Qt Creator 打开根目录的 CMakeLists.txt,第一次打开会弹 Kit Selection,选择你刚装的 Qt 对应套件。如果界面下方没有出现构建按钮,先检查 CMake 是否配置成功,点“Configure Project”让它重新扫描。这里有个常见坑:你电脑上装了多个 Qt 版本,Creator 自动选的套件不是你预期的版本,构建时就会冒出“找不到 Qt6Config.cmake”之类的问题。
我的建议是,打开工程后先看编译输出里的 Qt 路径,确认用的是哪个版本,再点 Run。不要一上来就改代码,先把原样跑通一次。如果构建特别慢,检查是否开了 Debug 模式或是否在重新生成所有 moc 文件;首次构建慢是正常的,之后增量构建会快很多。
3.3 CMake 命令行构建与 qml 独立调试
不依赖 IDE 的构建命令也要会,排查问题时经常要用:
mkdir -p build && cd build cmake .. -DCMAKE_PREFIX_PATH=/opt/Qt/6.5.2/gcc_64 cmake --build . -j4 ./qt-quick-practiceCMAKE_PREFIX_PATH 必须指向 Qt 安装目录,否则 CMake 找不到 Qt6 的库。Windows 上如果不用 -DCMAKE_PREFIX_PATH,也可以把 Qt 的 bin 目录加进 PATH,效果一样。构建完成后直接运行生成的二进制,能复现和 IDE 里一样的运行环境。
如果想单独调试 QML 界面,不用每次编译整个 C++ 工程,可以使用 Qt 自带的 qml 工具:
/path/to/qt/bin/qml src/qml/Main.qml这个命令会直接加载 QML 文件,适合快速调布局、颜色、动画。但要注意,qml 工具不会执行 main.cpp 里的 C++ 逻辑,所以依赖 app 和 taskModel 的数据部分会不可用,这是正常的。用它调试纯 UI 展示,回到正式工程再验证数据联动,效率更高。
3.4 第一次运行:你该观察哪几个日志点
main.cpp 里建议保留一个入口检测,这段代码看着简单,但能省不少排查时间:
QQmlApplicationEngine engine; engine.loadFromModule("TaskApp", "Main"); if (engine.rootObjects().isEmpty()) { qCritical() << "QML load failed, check module name and qrc"; return -1; }loadFromModule 的第一个参数是模块名,要和 CMake 里 qml_module 或资源定义的名称完全一致。如果不写这段检查,QML 加载失败时往往只在控制台输出一行 warning,界面不出现,程序也不退出,你很难判断是路径错了还是模块缺失。有了这段日志,至少能快速定位到“资源没加载出来”这一层。
第一次正常运行后,建议再看看数据库文件落在哪个路径。可以在 DatabaseManager 初始化时加一句 qDebug() << path,确认数据库写进了用户数据目录而不是安装目录,这决定了后面删除数据、版本升级时会不会丢数据。
4. 核心代码逐段讲解:任务面板从数据到界面的完整链路
这套源码包主工程是一个任务面板,功能包括新增任务、标记完成、左滑删除和按分类统计。这一章按照“数据层 -> 模型层 -> QML 视图 -> 统计联动”的顺序拆,每一层都有可抄的代码。
4.1 数据层:SQLite 打开与建表
DatabaseManager 的初始化是第一个关键点:
bool DatabaseManager::init() { const QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/tasks.db"; QDir().mkpath(QFileInfo(path).absolutePath()); m_db = QSqlDatabase::addDatabase("QSQLITE", "task_conn"); m_db.setDatabaseName(path); if (!m_db.open()) { qWarning() << "open db failed" << m_db.lastError().text(); return false; } QSqlQuery query(m_db); return query.exec("CREATE TABLE IF NOT EXISTS tasks (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "title TEXT NOT NULL," "done INTEGER DEFAULT 0)"); }这里要留意的不是 SQL 语句本身,而是连接名。addDatabase 的第二个参数指定连接名,如果不写,会使用默认连接。默认连接在工程里只有一个数据库时没问题,但一旦出现多个 DatabaseManager 实例、或者你后续要开两个数据库,就会因为“connection still in use”报错。显式连接名加上每次查询都传入 m_db,是避开这类问题的标准写法。
路径也没有硬编码到项目目录,而是用 QStandardPaths 获取当前用户的 AppDataLocation。这个细节,Windows 上差别极大:如果写到程序安装目录,很可能因为权限不足创建失败;路径切到用户目录后,写库和读库的权限问题基本消失。
4.2 模型层:自定义 QAbstractListModel
TaskModel 继承 QAbstractListModel,提供给 QML 的数据结构如下:
QVariant TaskModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return {}; const Task &t = m_tasks.at(index.row()); switch (role) { case TitleRole: return t.title; case DoneRole: return t.done; case IdRole: return t.id; } return {}; } void TaskModel::addTask(const QString &title) { beginInsertRows(QModelIndex(), rowCount(), rowCount()); Task task{title, false, nextId()}; m_tasks.append(task); m_db->insertTask(task); endInsertRows(); }data 函数决定 QML 里 model.title、model.done 能拿到什么。roles 的枚举值必须大于 Qt::UserRole,否则会和系统内部角色冲突。addTask 里 beginInsertRows 和 endInsertRows 是必须成对出现的,ListView 靠这对调用才知道“新增了一行”,才能触发行插入动画和视图刷新。只往 m_tasks 里 append 但忘了通知视图,界面就是不动,这个问题新手很容易踩。
如果要做“标记完成”操作,还需要实现 setData 并发出 dataChanged 信号,这样只刷新那一行的显示,而不是把整个列表 reset。最偷懒但也是最伤性能的写法是改变数据后直接 beginResetModel 再 endResetModel,表格一长,拖动和动画都会明显卡顿。继承模型的基本原则就是:能局部通知就不要全局重置。
4.3 QML 层:ListView 与滑动删除
任务列表的 QML 大致是这样的结构:
ListView { id: taskListView model: taskModel spacing: 8 clip: true delegate: TaskItem { required property var model width: ListView.view.width - 16 onDeleteRequested: taskModel.removeRow(index) onToggleDone: taskModel.setDone(index, !model.done) } }delegate 里的 required property var model 在 Qt 5.15 和 Qt 6 里都能用,它把模型的当前行数据整体传入 TaskItem。如果遇到旧版本不支持 required,可以改成直接在 delegate 里用property string taskTitle: model.title的方式显式取字段。相比前者,显式声明字段更利于编辑器补全,也方便控制哪些数据暴露给 item。
index 在 delegate 里可以直接用,但我更建议在信号里传递model.index或者显式传入 row,因为某些场景下 delegate 被缓存或重排,直接拿闭包里的 index 可能拿到旧值。左滑删除的实现通常会写到 TaskItem 内部,用 SwipeDelegate 或者自己用 ListView 的interactive属性配合手势做,不管哪种方式,删除动作的最终落点都要回到taskModel.removeRow,这样数据层和界面层的边界才清晰。
4.4 统计图联动:点击柱状图过滤列表
统计联动是这套源码包里比较亮眼的功能。底部的 StatChart 用 Repeater 画出几个分类柱体:
Repeater { model: chartData delegate: Rectangle { width: countBar.width * 0.8 height: model.value / statModel.maxValue * chartArea.height color: mouseArea.containsMouse ? "#2a9d8f" : "#264653" MouseArea { anchors.fill: parent hoverEnabled: true onClicked: chart.onStatClicked(model.category, model.value) } } }柱状图高度的正确做法是除以当前最大值再乘以可用高度,而不是直接用 model.value 当像素高度。很多初学版本会写死 maxValue,数据一涨图表就溢出,改成动态最大值后,图表能自适应。color 根据 hoverEnabled 状态切换,是为了让鼠标移入时有反馈,但 hoverEnabled 会额外产生鼠标事件,列表项数量很大时要注意性能。
点击柱子后触发chart.onStatClicked,再由 Controller 或模型层设置分类过滤条件,并调用 beginResetModel 刷新列表。这里有个设计取舍:为什么不在 QML 里直接 filter?因为过滤逻辑涉及 SQL 查询和状态保存,放 C++ 层能保证列表滚动位置、统计数字和数据库状态一致。图表只负责告诉 C++“用户点了哪个分类”,剩下的交给模型层,职责分配就很干净。
5. 避坑指南:从编译到运行最常见的五道坎
这一章是复现源码包时最值得看的内容。下面五条我基本都实际遇到过,按“现象、原因、解决”三步写,方便直接照着排查。
5.1 改了头文件,构建后还在跑旧逻辑
现象:在 TaskModel.h 里加了新信号,重新构建运行,QML 调用时仍然提示不是函数。原因:CMake 的 AUTOMOC 没把新头文件纳进去,或者构建目录里遗留了旧 moc 产物。解决:确认 CMakeLists.txt 里有set(CMAKE_AUTOMOC ON),然后把 build 目录清掉重新构建一次。如果还不行,把头文件单独 touch 一下强制 moc 重新扫描。从那之后我每次改头文件,都会顺手看一眼 moc 文件时间戳。
5.2 编译通过,运行时提示 module "QtQuick.Controls" is not installed
现象:C++ 编译完全正常,启动后 QML 加载阶段报这个错。原因:安装 Qt 时没勾选 Qt Quick Controls 模块,或者用了import QtQuick.Controls 2.15,但当前 Qt 6 环境下的模块路径对不上。解决:回到 Qt 安装工具里补装 Quick Controls,并检查 Kit 的 qml 目录有没有对应模块。Qt 6 里建议直接import QtQuick.Controls,不要硬写版本号,减少匹配失败的概率。
5.3 数据库文件写不进项目目录
现象:程序正常运行,添加任务也没报错,但重启后数据全部消失。原因:数据库文件写到了安装目录或 qrc 虚拟路径,实际没有落盘,或者权限不足导致创建失败。解决:用 QStandardPaths 拿用户目录:
const QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/data.db"; QDir().mkpath(QFileInfo(path).absolutePath());另外要注意,如果多个地方创建了 DatabaseManager,并且都用同一个连接名初始化 SQLite,第二个实例会报“connection still in use”。解决方法是每个数据库使用独立连接名,或者把 DatabaseManager 做成单例。
5.4 中文字符串显示成乱码
现象:MSVC 编译环境下,源码里 QStringLiteral("新增任务") 运行出来是乱码,QML 里的中文也变方块。原因:MSVC 默认按本地代码页读取源文件,和 UTF-8 源文件编码不一致,导致中文字符串在编译期被截断。解决:在 CMakeLists 里给 MSVC 加编译选项:/utf-8,把源文件统一保存为 UTF-8。QML 的中文如果显示为方块,多半是运行环境没有匹配字体,给 Text 设置font.family或者直接使用系统默认字体。
5.5 窗口一闪而过
现象:点击运行,界面刚闪一下就退出,控制台没有明确报错。原因:QML 加载失败,而 main.cpp 没有检查加载结果就直接进入事件循环,窗口没被创建,程序自然退出。解决:在 load 之后立刻判断engine.rootObjects().isEmpty(),为空就输出错误日志。常见加载失败点包括 CMake 里的模块名和 loadFromModule 写的不一致、qml.qrc 里少加了新 QML 文件。排查时优先看 qrc 文件列表,再确认模块名。
6. 二次开发思路:把任务面板改成自己的记账小工具
这套源码包最适合的玩法,不是拿它当练习题背,而是改成自己真正会用的工具。我的习惯是先换数据源,再动界面,最后补一个自动化验证。
第一处可以改的是数据层。任务面板存的是任务,你可以把表结构改成记账流水,增加 amount、category、created_at 字段。TaskModel 里的角色做相应调整,QML 里改一下显示字段名,核心交互框架不用动。如果不想用 SQLite,也可以把 DatabaseManager 替换成 JSON 文件读写,只要保持 TaskModel 的接口不变,AppController 和界面层完全不会受影响。
第二处改界面。把 TaskItem 里的删除按钮改成自定义图标,把 StatChart 的柱状图换成饼图,这些都在 QML 层完成。建议改造时守住一个原则:界面只通过 app 和 taskModel 与 C++ 通信,不直接访问数据库对象。这样才能保证后续继续改界面,不会动到数据逻辑。
最后是验证。源码包里 tests 目录预置了 QML 测试框架,可以加入针对 TaskModel 的简单用例:
TestCase { name: "TaskModelTests" function test_addTask() { taskModel.addTask("new item") compare(taskModel.rowCount(), beforeCount + 1) } }在 Qt Creator 里直接运行测试用例,能避免每次手点界面验证,至少保证数据层逻辑在频繁改造中没有被弄坏。我自己后来拿到任何新资源,都会先跑通最小构建,再加上一个最小测试,才敢放开手改界面。希望这个流程也能帮到你,少走点弯路。
本文还有配套的精品资源,点击获取