简介:这是一份围绕Qt与Qt Quick实战学习的完整项目源码资料包,面向想要系统掌握Qt C++图形界面与QML声明式界面开发的读者,也可用于课程设计或项目参考。资料系统讲解Qt基础类库、信号与槽通信机制,以及Qt Quick中的QML元素、组件复用、动画与状态机,并配有可直接运行的工程源码,帮助读者从理论到实践逐层深入。
压缩包共包含535个文件,整体约58.53MB。文件类型以79个cpp源文件、55个h头文件、67个qml界面文件、59个qmlproject工程文件为主,辅以29个pro工程描述、26个ui界面文件、21个qrc资源文件和若干png截图、mp3音频、pdf说明文档等素材,目录结构清晰,便于按模块查阅。
目前已有2514人学习浏览。通过对照源码运行,读者可以直观看到QML界面与C++后端逻辑的协作方式,熟悉跨平台项目结构、资源管理与信号槽的实际运用,为独立开发Qt应用打下扎实基础。
1. 一套 QT 实战源码,摸清两条开发路线
第一次接触 Qt 实战源码包,我犯了个认知错误:以为 Qt 就是拖几个控件、连一下信号槽的桌面工具框架。打开这套带完整源代码的 QT 实战资源才发现,它把两条开发路线都覆盖了——传统 QWidget 负责 C++ 工具型界面,Qt Quick 用 QML 声明式语法做产品级 UI。两条线在一个工程里各跑一个多页面应用,代码和运行效果一一对应,对想学 QT 的开发者非常友好,能直接搬进自己的工程改造。适合两类人:一是刚入门 C++、想用 Qt 快速做出能看的界面;二是做后端或嵌入式、需要给程序配一个前端界面但不想引入 Web 技术栈的。接下来按「架构 → 核心代码 → 跑通 → 改造」的顺序拆。
2. 工程架构拆解:两条技术线怎么在同一套源码里共存
先别急着点运行。这套源码拿到手,第一件事是打开目录看结构。我见过太多人把工程塞进 Qt Creator 按一下构建就跑,结果报错一堆,最后发现是没搞清两条技术线的边界。
2.1 为什么同一套源码要维护两条线
QWidget 和 Qt Quick 不是新旧替代关系,而是定位完全不同的两套界面方案。QWidget 走的是 C++ 控件树,界面由QPushButton、QTableWidget这些原生控件拼出来,资源和交互都交给系统原生渲染,适合做工具类软件、后台管理、调试面板这类「功能优先」的场景。Qt Quick 走的是场景图渲染,界面用 QML 声明式语言描述,配合 JavaScript 写交互逻辑,GPU 参与合成,动画和自定义控件成本低,适合做产品感强的界面。
实际项目里这两条线经常混合出现。老模块用 QWidget 顶着,新模块用 QML 写,中间用QQuickWidget把 QML 嵌进 Widgets 窗口,或者反向用createWindowContainer包一个 Widgets 控件到 QML 场景里。这套实战源码的设计思路就是这个:业务逻辑放在共享的 C++ 层,界面上两条线各写一套,让你同时看到两种实现方式的取舍。
| 维度 | QWidget 线 | Qt Quick 线 |
|---|---|---|
| 界面描述方式 | C++ 代码实例化控件 | QML 声明式 + JS |
| 渲染方式 | 系统原生控件 | 场景图 + GPU |
| 数据绑定 | 手动 setText 更新 | 属性绑定自动刷新 |
| 适合场景 | 工具软件、后台管理 | 自定义皮肤、动效要求高的产品 |
| 调试方式 | 打断点看变量 | QML Console + 调试器 |
2.2 源码目录结构逐层拆解
这套资源在目录组织上很典型,一眼能看出是给教学设计的:每条线的工程独立成子目录,共享业务代码抽成公共库。可以对照下面的结构来理解:
QuickPractice/ ├── CommonCore/ # 两条线共享的业务逻辑层 │ ├── datamodel.h │ └── datamodel.cpp ├── WidgetApp/ # QWidget 线工程 │ ├── WidgetApp.pro │ ├── main.cpp │ ├── MainWindow.h │ ├── MainWindow.cpp │ └── forms/ ├── QuickApp/ # Qt Quick 线工程 │ ├── QuickApp.pro │ ├── main.cpp │ └── qml/ │ ├── qml.qrc │ ├── main.qml │ └── pages/ └── shared/ ├── resource.qrc └── images/CommonCore是整个源码包里最值得读的目录,里面是纯 C++ 写的数据结构和业务方法,不依赖任何界面类。WidgetApp和QuickApp是两套可独立编译的工程,它们通过源码引用的方式共享CommonCore。shared放图片和字体这类跨线资源,用resource.qrc统一管理。
这里有个细节要注意:qml.qrc是 Qt Quick 线的资源索引文件,QML 文件只有被它收录才能被打进可执行文件。初学者经常改了 QML 内容不生效,十有八九是 qrc 没更新,或者文件不在 qrc 声明的路径下。
2.3 .pro 构建配置逐行读
这套源码用的是 qmake 构建系统,.pro文件是 Qt 工程的核心配置。虽然新项目越来越多用 CMake,但教学源码里 qmake 仍然是主流,因为.pro文件更短,适合展示工程依赖关系:
QT += quickwidgets widgets gui CONFIG += c++17 TARGET = DemoApp TEMPLATE = app SOURCES += main.cpp \ MainWindow.cpp \ common/datamodel.cpp HEADERS += MainWindow.h \ common/datamodel.h RESOURCES += qml.qrcQT +=这一行声明链接哪些 Qt 模块。quickwidgets负责把 QML 嵌入 Widgets 窗口,widgets是传统控件模块,gui是基础图形模块,写的时候宁多勿少,缺一个编译到一半就报找不到头文件。CONFIG += c++17指定使用 C++17 标准,现代 Qt 项目基本都这么写,为了能用std::optional这类语法。
TARGET是生成的可执行文件名字,TEMPLATE = app说明这是一个应用程序而不是库。SOURCES和HEADERS两个变量把源文件列进去,手工维护时要留意:每当你新建一个.cpp文件,都要记得加进这里,否则网上的 Qt 项目报「undefined reference」大多是这个原因。RESOURCES指向 QML 和图片的.qrc文件,它在编译期会把资源二进制打包进 exe。
3. 核心模块实战:信号槽、QML 页面与 C++/QML 桥接
整包源码里最值得抄走的是三个代码模式:信号槽解耦、QML 页面导航、以及 C++ 和 QML 的互相调用。这三个模式是 Qt 开发的骨架,项目二次开发时八成工作都围绕它们展开。
3.1 Widgets 线:把按钮点击换成业务信号
看一下 WidgetApp 里登录表单的写法。表面上看就是一个按钮点击处理,但代码里藏着分层思路:界面控件只负责收集输入,具体业务交给上层,用信号传递。
// MainWindow.cpp —— 登录按钮点击后只发出请求信号 connect(ui->btnLogin, &QPushButton::clicked, this, [this]() { const QString user = ui->editUser->text().trimmed(); const QString pwd = ui->editPwd->text(); if (user.isEmpty() || pwd.isEmpty()) { ui->labelStatus->setText("用户名和密码不能为空"); return; } emit loginRequested(user, pwd); // 校验通过,把数据交给上层处理 });这个 lambda 里做了两件事:第一步从输入框拿文本并去掉首尾空格,第二步做非空校验,通过后emit loginRequested(user, pwd)把数据抛出去。loginRequested是定义在 MainWindow 里的自定义信号,参数是两个 QString。MainWindow 自身不发起网络请求也不查询数据库,只负责收集界面输入——后续换后端时窗口代码一行都不用动,这是信号槽解耦最大的好处。
trimmed()在这里很关键,用户在输入框里不小心带的空格会被直接去掉,避免把带空格的空字符串当合法输入。这个校验放在界面层虽然简单,但能拦住大部分手误。
3.2 Quick 线:StackLayout 页面导航与列表绑定
Qt Quick 侧的代码风格完全不同。Widgets 线里你要手动new窗口、show(),而 QML 里界面结构直接声明出来,状态切换由属性绑定驱动。
// qml/pages/MainPage.qml —— 三个子页面用 StackLayout 管理 Item { property int currentIndex: 0 StackLayout { id: pageLayout anchors.fill: parent currentIndex: parent.currentIndex LoginPage {} HomeListPage {} DetailPage {} } function switchTo(idx) { currentIndex = idx // 改一个属性,页面自动切换 } }StackLayout来自QtQuick.Layouts模块,它把所有子项堆叠在同一个位置,通过currentIndex决定显示哪一页。这里定义一个currentIndex属性暴露给外部页面,导航栏调switchTo(1)就能跳到第二个页面,不用写一堆setVisible逻辑。这是 QML 和 QWidget 思维差异最大的地方:QWidget 用方法调用来改变状态,QML 用属性赋值来触发状态变化。
列表页的绑定更直接。以下代码把数据源交给model,每个列表项由delegate模板生成,数据变化时视图自动刷新,不需要手动刷新:
ListView { width: parent.width height: parent.height model: dataModel // dataModel 来自 C++ 或 ListModel delegate: Rectangle { width: parent.width height: 56 color: index % 2 ? "#f5f5f5" : "#ffffff" Text { anchors.left: parent.left anchors.leftMargin: 16 anchors.verticalCenter: parent.verticalCenter text: model.title } } }index是 delegate 内置的序号属性,用来做隔行变色。model.title取的是数据项里的 title 字段,字段名和 C++ 侧 QVariantMap 的 key 一致即可。这段代码说明 QML 的数据驱动思路:界面只描述「数据长什么样」,不描述「数据来了后要怎么放进去」。
3.3 桥接层:C++ 对象如何暴露给 QML
Qt Quick 项目里 C++ 和 QML 的通信是最容易踩坑的地方。这套源码用的是最常见的三种方式:setContextProperty注册全局对象、Q_INVOKABLE声明可调用函数、信号回调。
先看 C++ 侧入口:
// QuickApp/main.cpp —— 把 C++ 业务对象暴露给 QML #include <QGuiApplication> #include <QQmlApplicationEngine> #include <QQmlContext> #include "common/dataprovider.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); DataProvider provider; // 业务对象 QQmlApplicationEngine engine; engine.rootContext()->setContextProperty("provider", &provider); engine.loadFromModule("QuickApp", "Main"); return app.exec(); }setContextProperty("provider", &provider)的意思是:QML 里可以直接用名字provider访问这个 C++ 对象。第二个参数传地址,对象本身必须活得比 engine 久。把provider放在栈上、先于 engine 构造,正是为了确保当 QML 加载并调用它时对象仍然存在——这个顺序写反了,程序启动就崩,属于血泪经验。
QML 侧调用 C++ 方法:
Button { text: "刷新" onClicked: { provider.refresh() // 调用 C++ 槽函数 listView.model = provider.getItems() // 返回值直接当 model } }这里能直接调用的前提是refresh()是槽函数或用Q_INVOKABLE标注的方法,getItems()返回QVariantList,QML 才能把它当 ListView 的 model。不是所有 C++ 函数都能被 QML 调用,这是新人最容易翻车的地方。
第三种方式是 C++ 主动通知 QML。数据准备好后发信号:
// DataProvider.cpp —— 数据加载完成后发信号 void DataProvider::fetchData() { // 模拟网络或数据库耗时操作 QVariantList items = loadFromDb(); emit dataReady(items); }QML 里用Connections接收:
Connections { target: provider function onDataReady(list) { console.log("收到数据条数:", list.length) } }信号名dataReady到 QML 端会自动对应onDataReady,这是 Qt 的信号命名规则。理解这条规则后,桥接层剩下的就是套路活。
提示:
setContextProperty注册的对象相当于 QML 全局变量。滥用会让代码耦合变高,正式项目里更推荐用qmlRegisterType注册类型后在 QML 里显式创建实例,但教学源码用 context property 更直观,便于初学者看清数据流向。
4. 从克隆到跑通:环境配置、编译命令与高频问题排查
这部分是实操环节。源码拿到手能跑起来,才算真正开始,不然一切都是纸上谈兵。先说环境,再说命令,最后是高频问题清单。
4.1 环境准备与两种运行方式
安装 Qt 时组件别乱勾,但也不能只装默认项。跑这套源码需要:一个编译器套件(MinGW 或 MSVC,Windows 下二选一)、Qt 5.15 或 Qt 6.x 这类 LTS 版本的 Widgets 与 Quick 模块,再加上 Qt Creator 自带的一体化工具链。安装器里选组件时,Qt 5.15.x节点下展开,勾选Qt Widgets、Qt Quick、Qt Multimedia(源码里的音频示例可能会用到),工具链选MinGW 11.x或MSVC 2019以上均可。
最省事的运行方式:用 Qt Creator 直接打开.pro文件,它会提示选择套件 Kit,选定后点击左下角绿色运行按钮即可编译并启动。中间过程报错先看「问题」面板,多数是套件没配对,重新在「构建套件」里指定编译器就好。
命令行方式适合脚本化和远程调试,也更接近持续集成环境下的做法:
# 进入 Qt 提供的命令行环境后,在工程目录下执行 mkdir build && cd build qmake ..\QuickApp\QuickApp.pro -spec win32-g++ mingw32-make -j8 .\debug\QuickApp.exe-spec win32-g++告诉 qmake 用 MinGW 工具链;换 MSVC 的话把mingw32-make换成nmake,-spec换成win32-msvc。-j8是并行编译参数,能明显加快速度,核数少的机器改成-j4。第一次运行如果提示缺 DLL,先别慌,这是最常见的 Qt 部署问题:
windeployqt .\debug\QuickApp.exe这个命令会扫描 exe 依赖的 Qt 动态库,把Qt6Core.dll、Qt6Quick.dll等文件自动拷贝到 exe 目录。Windows 下发布程序这一步是必须的,否则换一台机器双击就报「找不到 Qt 库」。
4.2 避坑清单:现象、原因、解决
以下是这套源码从下载到跑通过程中最高频的五个问题,基本覆盖了新手期的全部翻车点。
问题一:编译通过,启动后黑屏或没有窗口
现象:进程在任务管理器里存在,但界面没弹出来,或者窗口一片黑。
原因:QML 模块名和engine.loadFromModule第二个参数不一致,或者qml.qrc资源前缀写错,导致 QML 引擎加载不到主界面文件。
解决:在main.cpp里加一行qDebug() << engine.importPathList();查看导入路径,再核对.qrc文件的前缀——默认为/,loadFromModule的第一个参数要和.pro里声明的模块名一致。
问题二:界面中文全部乱码
现象:代码里写的字符串正常,窗口标题和按钮文字全是乱码。
原因:源码文件用 GBK 编码保存,而编译器按 UTF-8 解释(或反过来),说明源文件编码和编译器期望不一致。
解决:Qt 6 项目把所有.cpp和.h文件统一为 UTF-8 with BOM 保存即可;Qt 5 项目在main.cpp顶部加QTextCodec::setCodecForLocale也能救一部分。最彻底的做法还是统一编码到 UTF-8。
问题三:双击 exe 提示缺少 Qt5Core.dll
现象:在 Qt Creator 里运行正常,脱离 Creator 到文件管理器双击就报错。
原因:运行环境的PATH变量里没有 Qt 的bin目录,系统找不到动态库。
解决:发布时用windeployqt补齐依赖(见 4.1),开发期间省事的话把%QTDIR%\bin手动加进系统 PATH,但正式发布还是要走 windeployqt。
问题四:QML 里调用 C++ 方法报 TypeError: Property 'xxx' is not a function
现象:界面上点击按钮后,控制台输出TypeError: Property 'refresh' of object QObject is not a function。
原因:要么setContextProperty注册的名字和 QML 里调用的名字大小写不一致,要么方法本身没声明为槽或Q_INVOKABLE。
解决:先检查名字拼写,逐字母比对;再确认方法声明,在头文件的 public 区加Q_INVOKABLE或把方法放进public slots:区域。
问题五:高分屏上控件模糊、字小
现象:4K 分辨率屏幕上,窗口里的文字和控件边缘发虚。
原因:程序没有开启高 DPI 缩放支持,系统按低分辨率缩放导致模糊。
解决:在main()第一行加QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);和setAttribute(Qt::AA_UseHighDpiPixmaps);,Qt 5 默认关闭这两项,Qt 6 默认开启,这正是很多人升级版本后界面突然变清晰的原因。
5. 二次开发:把示例工程改造成自己的业务起点
源码跑通只是第一步,这章给出具体的改造路径,把教学示例变成能扛业务的基础工程。
改造切入点按优先级排:先改数据层,再改界面层。数据层在CommonCore的DataProvider类里,源码给的是假数据,换成真实接口时把数据来源替换为网络请求或数据库查询即可。关键是保持对外接口不变——refresh()方法调用后照常发dataReady信号,QML 侧一行代码都不用动。下面是替换成网络请求的参考实现:
// DataProvider.cpp —— 假数据换成真实网络请求 void DataProvider::refresh() { QNetworkAccessManager *mgr = new QNetworkAccessManager(this); connect(mgr, &QNetworkAccessManager::finished, this, [this](QNetworkReply *reply) { QByteArray raw = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(raw); QVariantList list; for (const QJsonValue &v : doc.array()) { list << v.toObject().toVariantMap(); } emit dataReady(list); // 界面只认这个信号 }); mgr->get(QNetworkRequest(QUrl("https://api.example.com/items"))); }这段代码把网络返回的 JSON 数组转换成QVariantList后直接走原来的信号通道。QML 侧没有任何改动负担,因为界面订阅的是dataReady,不关心数据从内存来还是从网络来。这就是第 3 章拆桥接层时强调的:解耦做得好,换数据源只是替换一个类的事。
界面层改造建议从复制页面开始,而不是重写。导航结构在MainPage.qml的StackLayout里,新增页面三步走:在pages目录复制一个现有页面改成新布局,在MainPage.qml里加上新页面声明,然后在switchTo对应的地方加上跳转逻辑。改造中要验证两个点:一是qml.qrc是否收录了新文件,二是在 QML 编辑器开着实时预览时修改后界面有没有即时刷新——没刷新先检查 qrc,这是最隐蔽的坑。
这套源码我前后跑过两次。第一次直接点运行翻车在缺 DLL 上,第二次静下心按目录结构重读,才意识到两条线的边界比想象中重要:QWidget 管工具,QML 管体验,桥接层管通信。从那以后,我在 Qt Creator 里新建工程都强制走一遍「先配 .pro、再跑 windeployqt、最后验证 QML 控制台」的流程,绕开了不少玄学问题。希望帮到你。
本文还有配套的精品资源,点击获取