QT和QT Quick实战:从源码到可运行应用的完整链路解析
简介针对QT与Qt Quick开发者的实战资料包依托霍亚飞相关教学思路围绕Qt基础类、信号槽通信、QML声明式界面、动画状态机、跨平台适配及网络数据库等核心主题组织内容既适合刚接触Qt的初学者跟随案例理解GUI框架也适合有一定经验的开发者查漏补缺、提升工程实践能力。压缩包共535个文件约58.53MB其中以79个cpp源码、55个头文件、29个pro工程、67个qml界面描述和59个qmlproject项目文件为主另有26个ui表单、21个qrc资源文件及179张png图片以及少量mp3音频和歌词文件完整呈现了一个Qt项目的典型目录与构建结构便于按模块检索和复现。已有2514人学习下载是社区认可度较高的QT实战资源。通过阅读并运行这些源码可以直观掌握窗口组件布局、对象间信号槽异步通信、QML组件复用与动画过渡的实现方式还能参考工程配置、资源组织与调试测试技巧快速迁移到自己的跨平台应用开发中。1. QT和QT Quick实战拿到源代码只是第一步跑通才是真学QT凡是带“源代码”三个字的QT实战资源最容易翻车的就是第一步能在Qt Creator里打开工程但不一定能在你自己的环境里编译出来。我最早学QT走的弯路全栽在“只读不跑”——看懂了ListView的写法却不知道启动画面为什么黑屏也不清楚Qt Quick模块版本要跟着Qt版本走。这份附带完整源码的QT和QT Quick实战资料价值不在文件数量而在把QML界面、C逻辑、资源管理和打包发布这条完整链路串给你看。适合已经会基础C语法、想通过完整项目提升实战能力的人也适合被碎片教程绕晕、需要一份能复现的工程当参照的人。2. 工程骨架与环境选型跑通demo之前先确认三件事2.1 版本选型为什么我建议用Qt 5.15的LTS来学网上能下载到的QT和QT Quick实战源码相当一部分是在Qt 5.12到5.15之间写的。常见的坑是你装了Qt 6.4打开别人的.pro文件编译时不报错跑到QML加载阶段却黑屏控制台里刷出一堆“module is not installed”。原因通常是qml模块的导入版本变了比如import QtQuick 2.12在Qt 6里要被改写成import QtQuick但源码里没改。这里我给一个保守选型建议对比项Qt 5.15.2Qt 6.5QML模块写法import QtQuick 2.15import QtQuickqmake支持成熟能用但部分模块改用CMake更顺新特性没有有Qt Quick 3D、更快的编译老源码兼容性好需要偶尔改导入语句长期学习价值适中更适合新项目我一般会建议新手直接装5.15.2因为这份QT实战资源里的源码多数按5.x写的没必要把时间花在改兼容性上。等跑通并改过几个demo之后再迁移到6.x那时候你看到的报错就不会玄学了。2.2 工程文件剖析qml.qrc才是被忽略的主角拿到源码先别急着点运行先把.pro文件和资源文件的结构看明白。大多数入门项目长这样QT quick qml widgets CONFIG c17 TARGET MyDemoApp TEMPLATE app SOURCES \ main.cpp \ appmodel.cpp HEADERS \ appmodel.h RESOURCES qml.qrc这份文件里有三个值得注意的点QT quick qml widgets决定了你要链接哪些Qt模块缺少widgets时部分控件会编译不过CONFIG c17是因为新版QML和信号槽代码经常用到C17特性RESOURCES qml.qrc则决定了QML文件以什么路径被找到。再看qml.qrc很多人放错文件就在这里RCC qresource prefix/ fileqml/main.qml/file fileqml/PageHome.qml/file fileassets/icons/home.png/file /qresource /RCCprefix/表示路径从根开始所以在main.cpp里加载QML时用QUrl(qrc:/qml/main.qml)才能访问到。如果你把main.qml放在qrc根目录却写qrc:/qml/main.qml程序能编译但启动后白屏。这个错误我在帮别人排查时见过太多次九成是路径没对上。2.3 qmake与CMake跟源码走不要顺手“翻译”现在较新的Qt默认生成CMake工程但很多QT Quick实战源码仍是qmake写的。我的建议是初学阶段不要试图把qmake工程改成CMake除非你非常清楚自己在做什么。qmake的解构更直白变量赋值、依赖Qt模块几行就结束CMake则需要写find_package(Qt6 COMPONENTS Quick Qml)这类内容还要处理qt_standard_project_setup()。对初学者来说多一层封装就多一个黑匣子。源码本身用qmake你就用qmake打开等你把项目的信号槽、界面结构吃透再花一个晚上把项目迁移到CMake那时你的困惑会是“CMake的语法”而不是“源码为什么跑不起来”。3. 核心模块拆解把QML界面和C后端之间那层窗户纸捅破3.1 注册C类型QML里到底怎么调用C对象很多学QT的人卡在同一个点上QML是声明式语言里面写Rectangle、Text很顺手但想调用C逻辑就不懂了。这份源码里最常见的一种做法是把一个继承自QObject的类实例注册进QML上下文或者在main.cpp里直接setContextProperty。我先说更常见的面向对象注册方式看这段#include QGuiApplication #include QQmlApplicationEngine #include QQmlContext #include appmodel.h int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQmlApplicationEngine engine; // 注册一个全局单例给QML使用 AppModel model; engine.rootContext()-setContextProperty(appModel, model); engine.load(QUrl(QStringLiteral(qrc:/qml/main.qml))); return app.exec(); }这里是把AppModel实例注册成名为appModel的全局对象。QML里可以直接写appModel.loadData()不需要走信号槽也不需要新建对象。这种做法的优点是简单缺点是全局可见项目变大后变量名容易冲突。参数说明setContextProperty的第一个名字建议用驼峰避免和QML内建关键字重名否则会有“覆盖了内建属性”的隐患。另一种是注册类型让QML里自己实例化qmlRegisterTypeAppModel(com.demo.models, 1, 0, AppModel);注册之后QML里写AppModel { id: model }就能创建一个实例。对比起来如果你的类要保存业务状态、要跨页面共享用setContextProperty更省事如果每个页面都想要独立实例注册类型更合适。3.2 数据绑定与信号槽property和signal是怎么配合的QML和C之间的数据交互核心是Q_PROPERTY和信号。看这段头文件class AppModel : public QObject { Q_OBJECT Q_PROPERTY(QString currentName READ currentName WRITE setCurrentName NOTIFY currentNameChanged) Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) public: explicit AppModel(QObject *parent nullptr); QString currentName() const; void setCurrentName(const QString name); int currentIndex() const; void setCurrentIndex(int index); signals: void currentNameChanged(); void currentIndexChanged(); };这段代码里最关键的是NOTIFY关键字。QML里如果你写了Text { text: appModel.currentName }当C端setCurrentName被调用时因为currentNameChanged信号发出QML界面会自动刷新。如果没有NOTIFY界面只会在第一次创建时读取值后面C改了也不会动。我见过很多新手在C里改了数据QML界面纹丝不动就在那怀疑是不是信号槽写错了。其实多半是Q_PROPERTY里少了NOTIFY或者set函数里忘了emit currentNameChanged()。这个细节决定了数据绑定能不能双向通信算得上QT实战里最核心的套路之一。3.3 列表模型为什么ListView不该塞一堆Repeater源码里但凡有列表页基本都会出现QAbstractListModel。直接QML写ListView加上model: []数组适合静态展示一旦列表要增删改纯QML数组会越来越乱。C模型的好处是职责清晰数据变更时通过信号通知QML精准刷新。看典型模型头文件class TaskModel : public QAbstractListModel { Q_OBJECT public: enum TaskRoles { TitleRole Qt::UserRole 1, DoneRole, DueDateRole }; int rowCount(const QModelIndex parent QModelIndex()) const override; QVariant data(const QModelIndex index, int role) const override; QHashint, QByteArray roleNames() const override; };rowCount告诉ListView有多少行data根据role返回每行的字段roleNames把角色映射成QML里的属性名。QML端就能这么写ListView { model: taskModel delegate: RowLayout { Text { text: model.title } CheckBox { checked: model.done } } }注意delegate里拿到的是model.title、model.done这两个名字正好对应roleNames里返回的title和done。如果显示空优先检查roleNames有没有写对这是最容易踩的坑。4. 源码实战做一个跨平台待办清单应用并让数据持久化4.1 明确目标从源码里借骨架而不是背代码很多人学源代码喜欢逐行背这样学完自己写还是不会。我的做法是从源码里剥出骨架再套到自己的需求上。这次我们就照着常见的QT Quick实战项目套路做一个小型待办清单支持添加、勾选、删除退出后数据不丢。技术栈选择界面用QML业务逻辑放在C的QAbstractListModel里数据落盘用QJsonDocument写入本地文件。不引入数据库因为待办列表的数据量远没到需要SQLite的程度JSON文件更直观。先定义数据结构写在模型头文件里struct TaskItem { QString title; bool done false; QDateTime dueDate; };这个结构体就是一行数据的原型之后data()函数返回它的字段。用结构体比用QVariantMap好维护字段一变编译器会提醒你。4.2 模型实现增删改查要触发哪些信号核心代码在数据变更部分void TaskModel::addTask(const QString title) { beginInsertRows(QModelIndex(), m_tasks.size(), m_tasks.size()); m_tasks.append({title, false, QDateTime::currentDateTime()}); endInsertRows(); } void TaskModel::removeTask(int row) { if (row 0 || row m_tasks.size()) return; beginRemoveRows(QModelIndex(), row, row); m_tasks.removeAt(row); endRemoveRows(); } void TaskModel::setDone(int row, bool done) { if (row 0 || row m_tasks.size()) return; m_tasks[row].done done; // 通知ListView这一行刷新但布局不变 emit dataChanged(index(row), index(row), {DoneRole}); }这里每段代码都对应一个行为习惯插入前后要调用beginInsertRows和endInsertRows删除同理只改单行数据的用dataChanged。如果少了beginInsertRowsListView显示的行数会错乱甚至崩溃这属于Qt模型视图里的经典翻车点。写文件部分要放到模型外或者至少单独成函数。常见做法是用QJsonArrayvoid TaskModel::saveToFile(const QString path) { QJsonArray arr; for (const auto task : m_tasks) { QJsonObject obj; obj[title] task.title; obj[done] task.done; obj[due] task.dueDate.toString(Qt::ISODate); arr.append(obj); } QFile file(path); if (file.open(QIODevice::WriteOnly)) { file.write(QJsonDocument(arr).toJson()); } }参数说明path是本地绝对路径Windows下建议存放在QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)不要直接放程序目录否则程序安装在C:\Program Files下会因为权限不足写不进去。加载就是反向操作读文件、解析QJsonArray、循环beginInsertRows插入。这里可以直接调用addTask但要注意addTask每次调用都重置了dueDate所以更稳妥的做法是单独写一个loadFromFile直接构造TaskItem结构体再插入。4.3 QML界面与多窗口状态管理界面部分以ApplicationWindow为根加一个ListView和底部输入区ApplicationWindow { visible: true width: 480 height: 720 title: qsTr(待办清单) ColumnLayout { anchors.fill: parent ListView { id: listView Layout.fillWidth: true Layout.fillHeight: true model: taskModel clip: true delegate: SwipeDelegate { // 左右滑动删除该项 } } RowLayout { TextField { id: input; placeholderText: qsTr(输入待办内容) } Button { text: qsTr(添加) onClicked: { if (input.text.length 0) { taskModel.addTask(input.text) input.clear() } } } } } }注意clip: true这个属性如果漏了ListView里的delegate在滚动时会露出边界。另外SwipeDelegate依赖QtQuick.Controls 2.15如果你用的是Qt 6需要确认导入语句。多窗口在Qt Quick里有两类做法一是在同一个QML文件里放多个Window或Popup二是C侧创建多个QQmlApplicationEngine。源码里多半会用第一种因为共用同一个context数据同步方便。Popup { id: editPopup anchors.centerIn: parent modal: true ColumnLayout { TextField { id: editInput } Button { text: qsTr(保存) onClicked: { taskModel.editTitle(listView.currentIndex, editInput.text) editPopup.close() } } } }用Popup而不是新的Window可以避免任务栏出现多个窗口、焦点丢失的问题。modal: true会让它挡住主窗口操作这是移动端风格表单的常用写法。5. 避坑记录环境、数据、打包最容易翻车的五个点5.1 QML控件全是英文/中文乱码现象程序运行后界面文字变成方块或者中文显示成问号。原因Qt Quick默认字体在缺少中文字体环境时回退失败。Windows上常见的是Qt没找到Microsoft YaHeiLinux上常见缺字体比如没装fonts-wqy-microhei。解决在main.cpp里设置应用字体QFont font(Microsoft YaHei); font.setPointSize(10); QGuiApplication::setFont(font);Linux下改成font.setFamily(WenQuanYi Micro Hei)。如果还乱码检查源文件编码是不是UTF-8QML里字符串乱码大多是保存成了GBK。5.2 加载QML报“module not found”现象编译通过启动时QML页面黑屏控制台显示module QtQuick.Controls is not installed。原因你的Qt版本里没有对应的模块或者导入版本号与安装版本不匹配。解决先执行qmlimportscanner或者看Qt安装目录下的qml文件夹确认存在QtQuick/Controls.2。如果存在把QML文件里的import QtQuick.Controls 2.5改成import QtQuick.Controls。如果连文件夹都没有重装Qt时勾选Qt Quick Controls 2组件。5.3 修改模型后ListView不刷新现象C里sleep(2)之后数据变了界面上还是旧列表。原因没有调用beginInsertRows、dataChanged这些通知函数。解决排查路径是把模型的所有增删操作都写成“先通知、再改数据、再通知结束”。操作列表数据时只改QList不通知界面永远不知道数据变了。这不算玄学是模型视图框架的约定。5.4 打包后换台电脑运行闪退现象开发机上正常复制整个文件夹到另一台Windows电脑双击exe闪退。原因缺少Qt运行库或者少了平台插件qwindows.dll。解决用windeployqt部署windeployqt MyDemoApp.exe --release --no-translations部署完必须检查输出目录里有没有platforms/qwindows.dll和qml目录。QML项目比纯Widgets项目更麻烦必须在命令行加--qmldir参数指定QML文件位置windeployqt MyDemoApp.exe --qmldir %QTDIR%/qml少了这一步QML模块不会被拷贝换电脑必翻车。Linux下对应的部署步骤是扫描依赖并拷贝so文件处理方式类似但还要注意链接器路径。5.5 高DPI缩放下界面模糊现象在4K屏上打开程序文字和控件边缘发虚。原因程序没有启用高DPI支持Qt按低分辨率渲染后又被系统拉伸。解决在main.cpp里最前面加QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);注意项目是Qt 6的话这行可能被废弃Qt 6默认开启了高DPI这主要针对Qt 5的坑。我在一份源码里看到作者写了这行但没生效排查后发现是写在了窗口创建之后顺序错了。6. 把源码变成自己的三个复写技巧和验证方法看源码不能只停留在“能跑”。我会拿一份实战项目做三件事改名字、加功能、压启动时间。第一全局搜索旧项目名把.pro里的TARGET、代码里的窗口标题、QML里所有title、资源前缀里的路径统一改成新名字。这一步能逼你找出所有隐藏的硬编码路径。第二加一个小功能——比如给待办项目增加“截止日期”从TaskItem结构体加字段到roleNames增加角色到QML delegate增加显示完整走一遍数据链路。第三用Qt Creator自带的Profiler打开QML调试器看启动阶段哪些组件耗时最长把不需要的动画和透明效果先关掉再逐个开关对比。验证方法上我会先跑三组场景空数据启动、大量数据滚动、断网启动。空数据启动看是否白屏大量数据滚动看帧率是否掉到30以下断网启动是为了确认程序不会因为网络请求卡在加载页。这三关过了这份源码才算真正消化了。从那以后我每次拿到新的QT实战资源都强制自己先跑通、再改一个字段、再删一段代码三步走完才敢说学会了。希望帮到你。本文还有配套的精品资源点击获取