简介:这份资源面向希望系统掌握 Qt 与 Qt Quick 的开发者,从 C++ GUI 基础到 QML 声明式界面均有覆盖,适合初学者入门,也适合有经验者查漏补缺。内容围绕信号与槽编程模型、控件与布局、QML 元素与自定义组件、动画与状态机、多平台适配、网络与数据库模块以及 Qt Creator 调试测试等实战主题展开,并配有可运行的项目源码,便于边读边练、对照分析。压缩包共 535 个文件,约 58.53MB,以 cpp、h 源码与 qml、qmlproject 界面文件为主体,辅以 pro、pri 工程配置、ui 界面文件、qrc 资源及 png、mp3 等素材,结构完整,可直接导入 Qt Creator 编译运行。目前已有 2515 人学习下载,适合作为 Qt 跨平台开发的实践参考。
1. Qt Widgets 与 Qt Quick 双线实战:一份能跑起来的源码包到底怎么用
很多人学 Qt 卡在同一个地方:文档看得懂,信号槽也背得出,真给一个带界面的需求就不知道从哪下手。这份《Qt 和 Qt Quick 实战》源码包解决的正是这个断层——它不是零散示例,而是把 Qt Widgets 传统桌面开发和 Qt Quick(QML)声明式界面两条线并排铺开,每个知识点都配可编译的工程。适合已经会一点 C++、想系统过一遍 Qt 实战的人,也适合做嵌入式 HMI、工控上位机、跨平台工具软件的从业者拿来当参考模板。下面按「工程怎么组织 → 两条线各自怎么落地 → 编译和运行会翻哪些车」的顺序拆,源码包里的工程结构、构建配置、关键类用法都会落到具体文件和参数上,照着改就能跑。
2. 工程结构与构建系统:qmake 与 CMake 两套骨架怎么选
拿到源码包第一件事不是急着编译,而是先看清目录和构建文件。Qt 项目翻车十有八九出在构建系统上:头文件找不到、moc 没跑、资源文件没编进去,全是 .pro 或 CMakeLists.txt 写漏了。这一章先把骨架讲透,后面两条线的代码才有地方落。
2.1 目录布局与模块划分
典型的 Qt 实战工程会按「可执行程序 + 若干静态库/动态库 + 资源 + 第三方依赖」来分层。源码包里常见的一种布局是这样的:
project/ ├── app/ # 主程序入口,main.cpp 在这里 │ ├── main.cpp │ └── app.pro ├── core/ # 业务逻辑,纯 C++,不依赖界面 │ ├── datamodel.h/.cpp │ └── core.pro ├── widgets/ # Qt Widgets 界面层 │ ├── mainwindow.h/.cpp │ └── widgets.pro ├── quick/ # Qt Quick 界面层 │ ├── main.qml │ ├── qml.qrc │ └── quick.pro ├── resources/ # 图标、qss、翻译文件 └── common.pri # 公共编译配置这样分的好处是业务逻辑(core)和界面层解耦,Widgets 版和 Quick 版可以共用同一套数据模型。实际动手时,common.pri里放编译器选项、C++ 标准、输出目录这些公共配置,各子工程的 .pro 用include(../common.pri)引入,避免每个文件重复写一遍。
# common.pri 里我一般会固定这几项 CONFIG += c++17 QMAKE_CXXFLAGS += -Wall -Wextra DESTDIR = $$PWD/../bin # 所有产物统一输出到 bin OBJECTS_DIR = $$PWD/../build/obj MOC_DIR = $$PWD/../build/mocDESTDIR统一输出目录很关键——多子工程时如果每个 .pro 各自输出,运行时动态库找不到是常态。OBJECTS_DIR和MOC_DIR分开也是血泪经验,不然增量编译时 moc 生成的中间文件和 .o 混在一起,改一个头文件能触发全量重编。
2.2 qmake 与 CMake 的取舍
源码包里如果同时给了 .pro 和 CMakeLists.txt,别觉得是冗余。qmake 胜在简单,Qt 自家工具链对它的支持最顺,qmake && make两步就能出包;CMake 胜在生态,接第三方库(比如 OpenCV、FFmpeg)时find_package比 qmake 的LIBS +=干净得多。我的建议是:纯 Qt 项目、团队小,qmake 够用;要接一堆外部依赖或者要跟其他 CMake 工程合并,直接上 CMake。
CMake 版的最小骨架长这样:
cmake_minimum_required(VERSION 3.16) project(DemoApp LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) # 自动跑 moc,处理 Q_OBJECT set(CMAKE_AUTORCC ON) # 自动编译 .qrc 资源 set(CMAKE_AUTOUIC ON) # 自动处理 .ui 文件 find_package(Qt6 REQUIRED COMPONENTS Widgets Quick Qml) add_executable(DemoApp app/main.cpp widgets/mainwindow.cpp widgets/mainwindow.h quick/qml.qrc ) target_link_libraries(DemoApp PRIVATE Qt6::Widgets Qt6::Quick Qt6::Qml)AUTOMOC、AUTORCC、AUTOUIC这三个开关是 CMake 玩 Qt 的命门。漏了AUTOMOC,带Q_OBJECT宏的类会报undefined reference to vtable,这个报错新手基本看不懂,其实是 moc 没生成元对象代码。漏了AUTORCC,QML 文件或图片在运行时加载失败,报qrc:/xxx.qml: No such file,但文件明明在目录里——因为它压根没被编进二进制。
提示:Qt5 和 Qt6 的 CMake 包名不同,Qt5 是
Qt5::Widgets,Qt6 是Qt6::Widgets。源码包如果按 Qt5 写的,迁到 Qt6 时find_package和target_link_libraries两处都要改,别只改一处然后对着链接错误发呆。
2.3 资源文件与部署路径
.qrc资源文件是 Qt 项目里最容易被低估的部分。它把图片、QML、qss 编进二进制,好处是部署时不用带一堆散文件,坏处是改一个图标就得重编。实战里我一般把「几乎不变」的图标、字体放 qrc,「可能热更」的配置、皮肤放外部目录,用QCoreApplication::applicationDirPath()拼路径读取。
// 读取外部配置的稳妥写法,避免工作目录不同导致找不到文件 QString cfgPath = QDir(QCoreApplication::applicationDirPath()) .filePath("config/settings.ini"); QSettings settings(cfgPath, QSettings::IniFormat);用相对路径QSettings("config/settings.ini", ...)在 IDE 里跑没事,双击 exe 就找不到——因为工作目录变了。这是部署阶段最常见的坑之一,applicationDirPath()以可执行文件所在目录为基准,行为稳定。
3. Qt Widgets 线:从 MainWindow 到自定义控件的落地
Widgets 是 Qt 的老本行,工控上位机、桌面工具软件大量在用。源码包里 Widgets 部分通常覆盖主窗口框架、菜单工具栏、模型/视图、自定义绘制这几块。这一章挑最能体现「实战」的部分讲:主窗口怎么搭、Model/View 怎么替代裸控件、自定义控件怎么画。
3.1 主窗口框架与信号槽连接
QMainWindow自带菜单栏、工具栏、状态栏、中心部件、停靠窗口五件套,实战里先把这套骨架立起来,再往里填业务。源码包里的 MainWindow 一般会在构造函数里做三件事:建 UI、连信号槽、初始化数据。
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); // 菜单动作连接到槽,用新式语法,编译期就能检查签名 connect(ui->actionOpen, &QAction::triggered, this, &MainWindow::onOpenFile); connect(ui->actionExit, &QAction::triggered, this, &QWidget::close); // 状态栏常驻信息 statusBar()->showMessage(tr("就绪")); } void MainWindow::onOpenFile() { QString path = QFileDialog::getOpenFileName( this, tr("打开文件"), QString(), tr("数据文件 (*.csv *.txt);;所有文件 (*)")); if (path.isEmpty()) return; // 用户取消,直接返回,别往下走 loadData(path); }新式connect语法(函数指针版)比老的SIGNAL/SLOT宏好在编译期检查参数匹配,写错了直接编译报错,而不是运行时静默失败。QFileDialog::getOpenFileName返回空串表示用户点了取消,这个判断必须加,否则后面loadData("")会触发一堆莫名其妙的错误。
信号槽还有个实战细节:跨线程连接默认是Qt::AutoConnection,同线程直连、跨线程排队。如果槽函数里操作了 UI 而信号来自工作线程,必须确保连接类型是Qt::QueuedConnection(Auto 在跨线程时自动变成 Queued,但显式写出来更清楚),否则界面更新可能崩在非主线程。
3.2 Model/View 替代裸控件
数据量一上来,往QTableWidget里一行行setItem就是灾难——几千行卡到没法用。Qt 的 Model/View 架构就是为这个场景设计的:数据放 Model,显示交给 View,中间用代理(Delegate)控制渲染。源码包里如果有表格/列表展示,大概率用的是QAbstractTableModel子类。
class CsvTableModel : public QAbstractTableModel { Q_OBJECT public: explicit CsvTableModel(QObject *parent = nullptr) : QAbstractTableModel(parent) {} int rowCount(const QModelIndex & = QModelIndex()) const override { return m_rows.size(); } int columnCount(const QModelIndex & = QModelIndex()) const override { return m_header.size(); } QVariant data(const QModelIndex &idx, int role) const override { if (!idx.isValid() || role != Qt::DisplayRole) return {}; return m_rows.at(idx.row()).at(idx.column()); } QVariant headerData(int section, Qt::Orientation o, int role) const override { if (role != Qt::DisplayRole || o != Qt::Horizontal) return {}; return m_header.value(section); } void setData(const QVector<QStringList> &rows, const QStringList &header) { beginResetModel(); // 批量更新必须包起来 m_rows = rows; m_header = header; endResetModel(); } private: QVector<QStringList> m_rows; QStringList m_header; };beginResetModel()/endResetModel()这对调用是批量替换数据时的标准动作,漏了它 View 不会刷新,或者刷新出越界崩溃。如果是单行增删,用beginInsertRows/endInsertRows更精细,能保留选中状态和滚动位置。data()里只处理Qt::DisplayRole是最小实现,实战里还要处理Qt::TextAlignmentRole(对齐)、Qt::ForegroundRole(按值染色)等。
View 侧用QTableView配setModel(),再setSortingEnabled(true)就能点表头排序——前提是 Model 实现了sort()。这些细节源码包里一般都有,重点看它怎么处理空数据、怎么处理列宽自适应(horizontalHeader()->setSectionResizeMode)。
3.3 自定义控件与 QPainter 绘制
标准控件不够用时就得自己画。QWidget子类重写paintEvent,用QPainter画仪表盘、进度环、波形图这类东西,是工控和嵌入式 HMI 的日常。核心是三件事:开抗锯齿、按控件尺寸算坐标、处理高 DPI。
void GaugeWidget::paintEvent(QPaintEvent *) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing, true); // 以控件短边为基准算半径,保证缩放时比例不变 const int side = qMin(width(), height()); const QRectF rect((width() - side) / 2.0, (height() - side) / 2.0, side, side); const qreal radius = side / 2.0 - 8; // 背景圆环 p.setPen(QPen(QColor("#e0e0e0"), 10)); p.drawArc(rect.adjusted(8, 8, -8, -8), 0, 360 * 16); // 数值弧,m_value 范围 0~100 p.setPen(QPen(QColor("#2d7ff9"), 10, Qt::SolidLine, Qt::RoundCap)); const int span = static_cast<int>(360 * 16 * (m_value / 100.0)); p.drawArc(rect.adjusted(8, 8, -8, -8), 90 * 16, -span); }drawArc的角度单位是 1/16 度,这个反直觉的设定坑过不少人——传 360 画出来只有 22.5 度。rect.adjusted用来给画笔宽度留边距,不然圆环会被控件边界裁掉一半。高 DPI 屏上如果线条发虚,检查是否开了Qt::AA_EnableHighDpiScaling(Qt5)或用了 Qt6 默认的高 DPI 支持,绘制坐标尽量用qreal而非int。
4. Qt Quick 线:QML 界面与 C++ 后端怎么对接
Qt Quick 用 QML 写界面,声明式、带动画、适合触摸屏和现代 UI。但它不是「纯前端」——业务逻辑还是 C++,QML 通过上下文属性、注册类型、信号槽跟 C++ 通信。这一章讲清 QML 工程怎么组织、C++ 对象怎么暴露给 QML、模型怎么绑定。
4.1 QML 工程结构与 QQmlApplicationEngine 启动
Quick 工程的入口是QQmlApplicationEngine,加载一个根 QML 文件,窗口由 QML 里的ApplicationWindow或Window创建。跟 Widgets 的QApplication + MainWindow是两套思路。
#include <QGuiApplication> #include <QQmlApplicationEngine> #include <QQmlContext> #include "datamodel.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); // C++ 对象暴露给 QML,QML 里直接用 backend 这个名字 DataModel model; QQmlApplicationEngine engine; engine.rootContext()->setContextProperty("backend", &model); const QUrl url(QStringLiteral("qrc:/quick/main.qml")); QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, &app, [url](QObject *obj, const QUrl &objUrl) { if (!obj && url == objUrl) QCoreApplication::exit(-1); // 加载失败直接退出 }, Qt::QueuedConnection); engine.load(url); return app.exec(); }setContextProperty是最简单的 C++ 到 QML 通道,适合少量全局对象。对象多了以后更推荐qmlRegisterType注册类型,在 QML 里像普通组件一样实例化,作用域清晰。objectCreated那个连接是官方模板里的标准写法,作用是 QML 加载失败时别让程序带着空窗口跑下去——没有它,QML 语法错误只会打印一行警告,窗口一片空白,新手容易以为是环境问题。
QML 文件通过.qrc编进二进制,路径qrc:/quick/main.qml里的quick是 qrc 文件里的前缀,跟磁盘目录名不必一致,但建议保持一致,省得自己绕晕。
4.2 C++ 模型暴露给 QML:Q_PROPERTY 与 QAbstractListModel
QML 绑定 C++ 数据有两条路:简单值用Q_PROPERTY加NOTIFY信号,列表用QAbstractListModel。前者适合状态量(温度、连接状态),后者适合列表展示(设备列表、日志)。
class DataModel : public QObject { Q_OBJECT Q_PROPERTY(double temperature READ temperature NOTIFY temperatureChanged) public: double temperature() const { return m_temperature; } public slots: void refresh() // QML 里可以直接调 backend.refresh() { // 模拟采集,实际接串口/网络 setTemperature(m_temperature + 0.5); } signals: void temperatureChanged(); private: void setTemperature(double v) { if (qFuzzyCompare(m_temperature, v)) return; m_temperature = v; emit temperatureChanged(); } double m_temperature = 25.0; };Q_PROPERTY三件套:READ取值的 getter、NOTIFY变更信号、可选WRITEsetter。QML 里写Text { text: backend.temperature },temperatureChanged一发射,界面自动更新——这就是声明式绑定的威力,不用手动setText。qFuzzyCompare判等是浮点数的常规操作,直接==比较在精度误差下会失效,导致信号狂发。
列表场景用QAbstractListModel,QML 侧配ListView和delegate:
class DeviceListModel : public QAbstractListModel { Q_OBJECT public: enum Roles { NameRole = Qt::UserRole + 1, StatusRole }; int rowCount(const QModelIndex & = QModelIndex()) const override { return m_items.size(); } QVariant data(const QModelIndex &idx, int role) const override { if (!idx.isValid() || idx.row() >= m_items.size()) return {}; const auto &item = m_items.at(idx.row()); switch (role) { case NameRole: return item.name; case StatusRole: return item.online; default: return {}; } } QHash<int, QByteArray> roleNames() const override { return { {NameRole, "name"}, {StatusRole, "online"} }; } private: struct Item { QString name; bool online; }; QVector<Item> m_items; };roleNames()是 QML 侧能按名字取字段的关键,漏了它 delegate 里model.name取不到值,只能拿到model.display。Qt::UserRole + 1起步是约定,避免跟内置角色冲突。
4.3 QML 与 C++ 的信号槽双向通信
C++ 发信号、QML 用Connections接收;QML 发信号、C++ 用connect接。双向都通,才算真正打通。
// QML 侧接收 C++ 信号 Connections { target: backend function onTemperatureChanged() { console.log("新温度:", backend.temperature) } } // QML 侧调用 C++ 的 slot Button { text: "刷新" onClicked: backend.refresh() }QML 里onXxxChanged这种命名是自动生成的信号处理器,Xxx首字母大写。如果 C++ 信号叫dataUpdated,QML 里就是onDataUpdated。这个命名规则不熟的话,信号发了 QML 没反应,查半天以为是连接没建立。
反过来,QML 里定义signal qmlEvent(string msg),C++ 侧拿到根对象后connect(root, SIGNAL(qmlEvent(QString)), ...)或者用QMetaObject::invokeMethod。实战里更推荐把 QML 的信号接到一个 C++ 的QObject上,而不是直接操作根对象,耦合更低。
5. 编译、运行与部署:那些让你怀疑环境的坑
代码写对了不代表能跑起来。Qt 的编译和部署有一批固定坑位,这一章按「现象 → 原因 → 解决」列几条高频的,都是我在实际工程里反复遇到的。
5.1 常见问题排查清单
现象一:编译报undefined reference to vtable for XxxClass。原因:类里写了Q_OBJECT宏,但 moc 没跑,元对象代码没生成。qmake 下通常是头文件没加进HEADERS,或者 .pro 里漏了QT += core;CMake 下是漏了set(CMAKE_AUTOMOC ON)。 解决:qmake 检查HEADERS += xxx.h并重新执行qmake(改 .pro 后必须重跑 qmake,光 make 不生效);CMake 补上 AUTOMOC 后删掉 build 目录重新 configure。
现象二:程序启动报This application failed to start because no Qt platform plugin could be initialized。原因:动态链接部署时platforms/qwindows.dll(Windows)没跟 exe 放一起,或者QT_PLUGIN_PATH指错了。 解决:用windeployqt(Windows)或macdeployqt(macOS)自动拷依赖,别手动一个个找 dll。Linux 下检查libQt6Core.so等是否在LD_LIBRARY_PATH里。
# Windows 下发布前跑一遍,把 Qt 依赖拷到 exe 同目录 windeployqt --release --qmldir ../quick bin/DemoApp.exe--qmldir参数指向 QML 源码目录,工具会扫描import语句把用到的 QML 模块一并拷过来。漏了这个参数,QML 版程序在别人机器上白屏,报module "QtQuick.Controls" is not installed。
现象三:QML 界面能显示但交互无响应,控制台刷TypeError: Cannot read property 'xxx' of undefined。原因:setContextProperty在engine.load()之后才调用,QML 加载时对象还不存在。 解决:所有setContextProperty、qmlRegisterType必须在engine.load()之前完成。顺序错了不报编译错,只在运行时出问题,属于典型的玄学 bug。
现象四:中文显示成方块或乱码。原因:源码文件编码不是 UTF-8,或者字符串没走tr()/QStringLiteral。 解决:源文件统一存 UTF-8(无 BOM),Qt5 在main里加QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8")),Qt6 默认 UTF-8 一般不用管。QML 文件本身也必须是 UTF-8。
现象五:改了 QML 文件,重新编译运行还是旧界面。原因:QML 编在 qrc 里,增量编译没检测到 qrc 内容变化,或者跑的是旧的可执行文件。 解决:确认改的是 qrc 里登记的那份文件(不是磁盘上另一份同名副本),必要时touch一下 .qrc 或清理重建。开发阶段可以临时用文件系统路径加载 QML(engine.load(QUrl::fromLocalFile(...)))绕过 qrc,改完直接重启看效果,省编译时间。
注意:Qt5 迁 Qt6 时,
QTextCodec从 core 移到了 core5compat 模块,qmlRegisterType的版本参数写法也有变化。源码包如果是 Qt5 的,迁移前先看官方 porting guide 的模块变更表,别一个错一个错地试。
6. 从源码包到自己的项目:一套可复用的改造流程
源码包最大的价值不是「跑通」,而是「拆开改成自己的」。我拿到任何一份 Qt 实战源码,都会走一遍固定流程:先跑通原始工程确认环境没问题,再挑一个最接近需求的示例做最小改动验证,最后把公共部分抽成模板。这套流程能避免一上来就大改、改崩了分不清是环境问题还是代码问题。
第一步,跑通原始工程。用源码包自带的构建方式(qmake 或 CMake)原样编译,不改任何代码。跑通了说明 Qt 版本、编译器、依赖都对得上;跑不通先解决环境,别急着看业务代码。这一步我一般会记录下 Qt 版本号和编译器版本,后面新建工程时对齐。
第二步,做最小改动验证。挑一个功能最接近的示例,只改一个变量——比如把表格的列数从 3 改成 5,或者把 QML 里的颜色换掉。改完能正常编译运行,说明你理解了它的数据流;改完崩了,正好定位到具体是哪个环节耦合太紧。
第三步,抽公共模板。把common.pri或 CMake 的公共配置、日志封装、配置读取、主窗口骨架抽出来,做成自己的项目模板。下次新工程直接复制模板,省掉重复搭骨架的时间。
# 我常用的模板初始化脚本,复制骨架并替换工程名 #!/bin/bash set -e TPL=~/qt-template DST=$1 [ -z "$DST" ] && { echo "用法: $0 <目标目录>"; exit 1; } cp -r "$TPL" "$DST" # 把模板里的占位工程名替换掉 grep -rl "__PROJECT_NAME__" "$DST" | xargs sed -i "s/__PROJECT_NAME__/$(basename "$DST")/g" echo "模板已生成到 $DST"set -e让脚本遇错即停,避免半途失败还继续往下跑。grep -rl找出所有含占位符的文件再批量替换,比手动改靠谱。这套脚本跑几次之后,新建 Qt 工程从半小时缩到两分钟。
第四步,验证部署。开发机能跑不等于交付机能用。用windeployqt打包后,找一台没装 Qt 的机器(或者干净虚拟机)双击运行,重点看三件事:界面能不能出来、图片和 QML 资源加载是否正常、有没有缺 dll 的弹窗。这一步翻车最多,但也是最值得做的——总比交付后现场出问题强。
从那以后我每次拿到新的 Qt 源码包,都强制先跑通原始工程、再做最小改动、最后抽模板,三步不走完不碰业务代码。这套习惯帮我省掉了大量「到底是环境问题还是我改错了」的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取