简介:面向Qt初学者的基础学习总结工程,通过登录管理、控件布局、窗口切换等典型场景串联核心知识点,适合正在学习C++/Qt桌面开发、希望对照真实工程理解信号与槽、事件循环、模型视图等机制的人群。包内含34个文件,压缩包大小约2.33MB,其中5个cpp源文件与4个h头文件构成主要程序逻辑,4个ui界面文件对应各窗口设计,14个png图片与2张jpg背景图提供界面素材,另有qrc资源清单、pro工程配置及gif动态预览图,便于直接打开工程查看结构。项目已有114人浏览学习,可作为从基础语法过渡到实际开发的参考样例。通过对照源码、界面与资源文件,可快速掌握Qt工程组织方式、跨窗口协作写法以及图片资源管理流程,适合课后巩固或期末综合练习时使用。
1. 从 Qt 基础学习到可运行的总结软件:资源包里到底有什么
Qt 基础学习最怕什么?不是 API 记不住,而是学完一遍回头发现笔记散落各处,代码样例根本对不上知识点。这套「Qt 基础学习总结软件」就是干这个的——把 Qt 控件、信号槽、布局、事件处理的笔记与代码样例整合进一个可运行的桌面程序,左侧目录树按知识点分类,右侧显示对应笔记与可复现代码,还内置了一批笔记范文模板和示例素材,拿来即用。适合刚学完 Qt 基础、正在做第一个综合项目的人,也适合导师拿它当教学案例模板。工程纯 Qt Widgets 实现,不依赖第三方库,Qt 5.15 及以上就能编译。后面按「结构 → 运行 → 避坑 → 改造」的顺序展开。
2. 核心模块拆解:界面框架、信号槽与数据存储怎么选
2.1 主窗口架构:QSplitter 加 QTreeWidget 的组合逻辑
打开 MainWindow.cpp 第一眼就能看到界面组装代码。总结软件的界面没有用 QTabWidget 做多页签,而是用 QSplitter 把窗口分成左右两块:左边是 QTreeWidget 目录树,右边是 QTextEdit 内容区。这种布局在知识管理类工具里很常见,原因有两个。
第一,QTabWidget 的页签栏在条目多时横向溢出,而且每加一个知识点就要新增一个页面对象,代码量成倍增长。QTreeWidget 加 QSplitter 的方案里,左侧树节点只存标题和 ID,右侧 QTextEdit 统一承接内容显示,新增条目只需要往树里插入一行数据。
第二,QSplitter 允许用户拖动分栏比例,窗口窄的时候可以把左侧目录收窄,这个交互 QTabWidget 给不了。常见做法是用 setStretchFactor 控制初始比例:
QSplitter *splitter = new QSplitter(Qt::Horizontal, this); splitter->addWidget(treeWidget); splitter->addWidget(contentEdit); splitter->setStretchFactor(0, 1); // 左侧目录占 1 份 splitter->setStretchFactor(1, 3); // 右侧内容占 3 份 splitter->setCollapsible(0, false); // 禁止完全收起左栏setStretchFactor 的参数是 (index, stretch),index 从 0 开始,stretch 是权重比。设成 1:3,窗口拉大时右侧内容区获得更多空间。setCollapsible(0, false) 防止用户把左侧栏拖没后找不到目录树,这个坑等到了第 4 章细说。
内容区用 QTextEdit 而不是 QPlainTextEdit,因为总结软件里要展示代码片段,QTextEdit 支持富文本渲染,可以给代码块加底色和等宽字体。如果只需要纯文本,换成 QPlainTextEdit 性能更好,大文件打开不卡。树节点存储这块,建议用 Qt::UserRole 保存条目 ID,不要拼在显示文本里,后续做搜索和定位时免得反复解析字符串。QTreeWidget 默认样式比较朴素,建议调用 setHeaderHidden(true) 隐藏列头,纯目录导航用不上;想区分信号槽、布局、事件这些大分类,可以用 setForeground 按分类给文字染色,扫一眼就能定位。
提示:QSplitter 的 setStretchFactor 只在子控件未被用户拖过时生效。用户手动调整过一次比例后,程序里设置的权重会被覆盖,想恢复默认比例需要调用 splitter->setSizes({200, 600})。
2.2 信号槽连接:三种写法与各自边界
Qt 基础学习里信号槽是重头戏。总结软件三处典型连接正好覆盖三种写法。第一处是树节点点击:
// MainWindow 构造函数中 connect(treeWidget, &QTreeWidget::itemClicked, this, &MainWindow::onTreeItemClicked);这是函数指针写法,编译期检查信号和槽签名是否匹配,推荐优先用。注意 itemClicked 的信号参数是 (QTreeWidgetItem*, int),槽函数签名必须一致,参数个数对不上编译器直接报错。旧式 SIGNAL/SLOT 宏写法参数类型是字符串比对,int 和 bool 这种不同基础类型不会被判定匹配,连接在运行期失败、编译期不报错,排查起来很费劲,新代码不建议用。
第二处是搜索框的实时过滤,用 lambda 写法:
connect(searchEdit, &QLineEdit::textChanged, this, [this](const QString &text) { // 顶层节点逐个匹配标题,不匹配的隐藏 for (int i = 0; i < treeWidget->topLevelItemCount(); ++i) { QTreeWidgetItem *item = treeWidget->topLevelItem(i); item->setHidden(!item->text(0).contains(text, Qt::CaseInsensitive)); } });lambda 写法适合槽逻辑短、没必要单独声明成员函数的场景。注意这里捕获了 this,lambda 里访问 treeWidget 等成员变量,必须保证接收者对象存活。QObject 父子关系会在析构时自动断开连接,只要接收者是 this 的子对象或父对象就安全。如果 lambda 捕获的是局部变量且接收者不是 this,生命周期就要格外小心,局部变量销毁后 lambda 还在执行就是野指针问题。
第三处是自定义信号,比如内容编辑后的「已修改」标记。做法是继承 QTextEdit 重写键盘事件:
class ContentEditor : public QTextEdit { Q_OBJECT signals: void modified(); protected: void keyPressEvent(QKeyEvent *event) override { QTextEdit::keyPressEvent(event); if (!event->text().isEmpty()) emit modified(); } };自定义信号触发时机要斟酌:键盘事件里每次按键都发信号,高频输入会把界面拖慢。实际项目里一般配合 QTimer 做防抖,300 毫秒内没有新输入才真正触发保存。emit modified() 只负责通知,实际保存逻辑放在槽函数里,信号和实现解耦,后续加自动保存、手动保存多入口都方便。
2.3 数据存储:JSON 方案还是 QSettings
总结软件的笔记数据有两个存储方向。QSettings 适合存配置项——窗口大小、分栏比例、最近打开的条目,底层是注册表或 ini 文件封装。笔记正文是结构化、可嵌套的内容,用 JSON 文件更合适。两者的适用边界如下:
| 对比维度 | QSettings | JSON 文件 (QJsonDocument) |
|---|---|---|
| 适合数据 | 窗口位置、分栏比例、最近打开项 | 笔记正文、嵌套结构、可分发内容 |
| 存储位置 | 注册表 / ini 文件,位置由系统定 | 自定义路径,随程序分发 |
| 读写方式 | setValue / value 键值对 | 序列化 / 反序列化整个对象树 |
| 可读性 | 键值扁平,一般 | Indented 模式可读性好,便于调试 |
| 跨平台差异 | Windows 注册表、Linux 配置文件,位置不同 | 路径自行控制,行为一致 |
资源包用的是 QJsonDocument 写文件。核心保存函数大概长这样:
void MainWindow::saveData(const QTreeWidget *tree, const QString &filePath) { QJsonArray rootArray; // 最外层是分类数组 for (int i = 0; i < tree->topLevelItemCount(); ++i) { QTreeWidgetItem *topItem = tree->topLevelItem(i); QJsonObject topObj; topObj["category"] = topItem->text(0); QJsonArray childArray; // 分类下的笔记列表 for (int j = 0; j < topItem->childCount(); ++j) { QTreeWidgetItem *child = topItem->child(j); QJsonObject childObj; childObj["title"] = child->text(0); childObj["content"] = child->data(0, Qt::UserRole).toString(); childArray.append(childObj); } topObj["items"] = childArray; rootArray.append(topObj); } QFile file(filePath); if (file.open(QIODevice::WriteOnly)) { file.write(QJsonDocument(rootArray).toJson(QJsonDocument::Indented)); } }关键设计:笔记正文存在 QTreeWidgetItem 的 data(0, Qt::UserRole) 里而不是显示文本里。显示文本是标题,UserRole 存正文,树节点列表不会因为正文很长而渲染卡顿。Qt::UserRole 是自定义数据起始 role 枚举值,从它开始往上都可以自由使用。QJsonDocument::Indented 让文件带缩进和换行,调试时可读性高;笔记文件到了几 MB 以上再改成 Compact 减少体积和写入耗时。
读文件用 QJsonDocument::fromJson 配合 QJsonParseError 做错误接住——路径写错或文件被外部编辑器改坏时,至少能弹出提示而不是静默崩溃。加载完成后建议校验根节点类型是 QJsonArray,防止拿了个空对象就直接遍历导致逻辑异常。
3. 把模板跑起来:环境配置、qmake 编译与首屏验证
3.1 工程文件与构建流程
先说环境。这套工程走 qmake 体系,不是 CMake,工程文件后缀是 .pro。拿到源码包先确认目录结构,心里有数再动手:
QtSummary/ ├── QtSummary.pro ├── main.cpp ├── MainWindow.h ├── MainWindow.cpp ├── resources.qrc ├── assets/ │ ├── add.png │ └── search.png ├── seed.json └── README.mdseed.json 是预置的知识点种子数据,第一次运行程序时复制为 notes.json。assets 目录放界面图标,通过 resources.qrc 打进二进制。工程文件是理解构建逻辑的入口:
QT += core gui widgets TARGET = QtSummary TEMPLATE = app CONFIG += c++11 SOURCES += \ main.cpp \ MainWindow.cpp HEADERS += \ MainWindow.h RESOURCES += \ resources.qrc几个点。QT += widgets 在 Qt 5 及以上必须显式写,漏掉的话 QMainWindow、QTextEdit 全部不可用,报错却常是「找不到头文件」而不是「模块未引入」,很迷惑。CONFIG += c++11 给老工具链兜底,编译器默认 C++17 就不受影响。TARGET 决定生成的可执行文件名字,这里叫 QtSummary。
命令行构建流程(Windows 下用 Qt 自带命令提示符,MinGW 版打开 MinGW 提示符,MSVC 版打开 x64 Native Tools 提示符):
mkdir build && cd build qmake ../QtSummary.pro -spec win32-g++ # MinGW 用 win32-g++,MSVC 用 win32-msvc mingw32-make -j4 # MSVC 环境对应 nmakeqmake 的 -spec 参数指定编译器描述文件,选错最直接的后果是生成的目标文件格式跟当前编译器不匹配。如果提示 qmake 不是内部或外部命令,就是 Qt 的 bin 目录没加进 PATH,用 Qt 自带的命令提示符启动不会有这个问题。Qt Creator 里直接打开 .pro 选套件构建当然更省事,命令行方式适合想理解构建过程、后面要写自动化脚本的人。Qt Creator 默认开启 shadow build,构建目录和源码目录分离,产物在 build-QtSummary-xxx 下;命令行手动构建时建议模仿这个习惯,在 build 目录里跑 qmake,保持源码目录干净。debug 构建保留 qDebug 输出和完整调试信息,release 构建体积小、速度快,但资源加载失败这类提示在 release 下可能看不到,排查问题优先用 debug 构建。
3.2 资源文件系统:qrc 路径的边界与陷阱
resources.qrc 管理图标和示例图片。qrc 本质是 XML 格式:
<RCC> <qresource prefix="/icons"> <file alias="add.png">assets/add.png</file> <file alias="search.png">assets/search.png</file> </qresource> </RCC>加进 qrc 后,代码里引用路径是 :/icons/add.png,冒号开头是 Qt 资源系统固定前缀。prefix 属性把散落的图片目录映射成虚拟路径,alias 可以重命名避免文件名冲突。qrc 文件里的路径是相对 qrc 文件所在目录写的,不是相对工程根目录——很多人第一次写就在这里踩坑。对应代码里的加载方式是:
QIcon addIcon(":/icons/add.png"); ui->addButton->setIcon(addIcon);常见误区是把图片放工程目录就觉得能直接加载,运行时用相对路径 "assets/add.png" 去读,程序部署到别的目录后图片全丢。qrc 资源编译进二进制,不存在部署路径问题,这是它最大优势。但缺点也明显:图片更新必须重新编译整个程序。所以头像、壁纸这类用户自定义资源要放外部目录,程序里优先检查外部路径,找不到再回退到 qrc。另外 qrc 默认启用 zlib 压缩,小图标影响不大,图片很多时可以对比一下压缩开关对资源体积和加载速度的影响。
3.3 编译运行与功能验证清单
编译通过后运行程序,按下面的清单逐项验证,每一项都对应总结软件的核心功能:
| 验证项 | 操作 | 预期结果 |
|---|---|---|
| 启动加载 | 直接运行程序 | 左侧树显示预置分类,每类至少两条笔记 |
| 内容显示 | 点击任意笔记标题 | 右侧显示对应正文,代码片段正常渲染 |
| 搜索过滤 | 输入「lambda」 | 树节点即时过滤,只保留标题匹配项 |
| 新增笔记 | 点击新增按钮,填标题与正文 | 树中多出新节点,内容区同步刷新 |
| 持久化 | 关闭程序重新打开 | 新增笔记仍在,原有数据不丢失 |
第五步验证的是 saveData/loadData 两个函数。如果重启后新笔记消失,优先检查工作目录下有没有生成 notes.json——程序崩溃或任务管理器强制结束进程时 saveData 没机会执行,容易被误判成保存逻辑有 bug。我一般会在 saveData 入口加一行 qDebug() << "saveData called" 确认是否走到写文件逻辑,比反复重启排查快得多。运行目录和源码目录不是同一个位置时,还要确认 notes.json 到底生成在哪,别找错地方。
如果程序启动就闪退,先查 main.cpp 里有没有在 QApplication 创建前调用依赖事件循环的 API;再查 seed.json 是不是被改成了非法 JSON,loadSeedData 解析失败后直接 return,窗口还没显示就退出。这类空白窗口一闪而过的情况,最容易让人误以为是 Qt 环境本身坏了。
4. 避坑指南:Qt 新手最容易翻车的五个现场
下面这五条是新手期血泪经验换来的,每一条按「现象 → 原因 → 解决」说清楚,照着排查能省下大半天时间。
4.1 中文乱码:源文件编码与字符串处理的连锁反应
现象:界面按钮和标题显示成乱码,MSVC 下编译报 C2001 或 C2143 错误,注释没事但字符串常量就出错。
原因:源文件是 UTF-8 编码,编译器按本地代码页(GBK)解析字符串字面量,中文字符被拆成多字节解析,字符串引号配对错乱。Qt 5 里 QStringLiteral 宏声明了源码按 UTF-8 处理,但不声明时默认走本地编码。
解决:统一三件事——源文件保存为 UTF-8(带 BOM 更稳),工程文件加 CONFIG += utf8_source(MSVC 环境生效),显示层用 tr() 或 QStringLiteral 包住中文字符串。MinGW 对 UTF-8 源文件支持比 MSVC 好,问题集中在 Windows + MSVC 组合。项目里已经写了很多裸中文字符串的,最省事是全局替换成 tr("中文"),配上 lupdate/lrelease 生成翻译文件,顺手把多语言支持做掉。
4.2 信号槽连了没反应:重载与类型不匹配
现象:connect 写在构造函数里,编译过了,运行不报错,但点击按钮、拖动滑块就是没反应。
原因:两种情况最常见。一是信号或槽是重载函数,connect 时没有指定具体重载版本,匹配到错误的一个;二是旧式 SIGNAL/SLOT 宏写法下,信号和槽的参数类型必须字符串完全匹配,int 和 bool 这种不同类型不会被判定匹配,连接在运行期静默失败。
解决:优先用新式函数指针语法,让编译器做类型检查。重载场景用 QOverload ::of(&QComboBox::currentIndexChanged) 显式指定。排查连接是否生效,最快的是在槽函数第一行加 qDebug(),运行时看输出面板有没有打印。没打印就把 connect 调用点往前移,检查是不是对象还没创建就 connect,比如 connect 写在成员初始化列表之前。还有一个隐蔽场景:类没加 Q_OBJECT 宏,moc 处理不了元对象信息,新式语法 connect 会直接报编译错,虽然是编译期报错,但容易让人摸不着头脑,看到跟 moc 相关的错误先查 Q_OBJECT。
4.3 布局失效:setGeometry 和 addWidget 混用的后果
现象:窗口拉大后控件不跟着变,或者控件叠在一起,只显示最后一个设过位置的控件。
原因:代码里先用了 setFixedSize 或 setGeometry 给控件设绝对位置,又 addWidget 加进布局。布局管理器接管后,绝对几何设置部分保留、部分被覆盖,表现就是控件「各有各的想法」。
解决:二选一。全走布局:删掉所有 setGeometry 调用,控件 addWidget 时不传父对象,布局设置完调用 setLayout(this)。或者放弃布局:手动在 resizeEvent 里计算坐标,适合自定义绘制控件,不适合普通按钮和文本框。总结软件这类工具界面,守一条规矩——布局优先,用 setMinimumSize/setMaximumSize 约束边界范围,不用 setFixedSize,除非是固定尺寸的对话框。
4.4 资源路径玄学:qrc 路径写错却不报错
现象:程序能编译,运行不崩,但图标空白、图片加载不出来,控制台也没提示。
原因:qrc 里写了 assets/add.png ,但 assets 目录实际在工程根目录之外,或者 qrc 里的相对路径算错了层数;代码里写的是 "://icons/add.png"(双冒号)或漏了开头冒号。资源加载失败的 qDebug 提示只在 debug 构建输出,release 构建下静默失败。
解决:第一步,确认 qrc 内文件路径是相对 qrc 文件所在目录写的,不是相对工程根目录。第二步,代码里统一 QIcon(":/icons/add.png") 完整路径,别省略前缀。第三步,Qt Creator 里双击 qrc 文件,看资源树有没有红色感叹号,有就说明对应文件没找到。第四步,还查不出来就建临时 test.qrc 只放一张图逐步验证,加载成功再往上加内容。
4.5 高 DPI 下界面发糊:缩放策略没设置
现象:2K/4K 显示器上文字发虚,图片边缘有锯齿,Windows 缩放比不是 100% 时尤其明显。
原因:Qt 5 默认不开启高 DPI 缩放,程序按逻辑像素绘制,系统缩放后直接在位图上拉伸。Qt 5.6 起提供高 DPI 支持但默认关闭,Qt 6 默认开启。这套工程跑 Qt 5.15,不设置的话高分屏体验就是糊的。
解决:main.cpp 里、QApplication 构造之前加:
#include <QApplication> int main(int argc, char *argv[]) { #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #endif QApplication app(argc, argv); // ... }AA_EnableHighDpiScaling 让 Qt 按系统缩放比自动调整 DPI,AA_UseHighDpiPixmaps 让图标在高分屏下使用高分辨率版本。setAttribute 必须在 QApplication 创建之前调用,写在构造之后无效。Qt 6 已默认开启,所以用宏隔开版本差异。改完后在 150% 缩放的机器上跑一遍,按钮、图标、文字都应该清晰锐利。
5. 扩展改造:把总结软件变成自己的学习工具
拿到能跑的模板,下一步是改造成贴合自己学习节奏的工具。总结软件的核心价值不是「看别人的笔记」,而是「往里填自己的笔记」。我建议分三个阶段。
第一阶段,数据驱动。启动时检测笔记文件:存在就加载,不存在就把预置种子数据写入。这样反复删改内容都不怕把模板改坏,随时恢复初始状态:
if (!QFile::exists("notes.json")) { loadSeedData(); // 从 qrc 读 seed.json,写入工作目录 } else { loadData("notes.json"); }第二阶段,编辑器增强。把 QTextEdit 换成 QTextBrowser 加编辑模式切换,或者用 QTextDocument::setMarkdown 直接支持 Markdown 渲染(Qt 5.14 起可用)。配合 QTimer 做自动保存:停止输入 500 毫秒后保存当前条目,避免手动保存遗漏,也不用担心高频写文件。
第三阶段,检索增强。把树过滤升级为全文搜索,遍历每个节点的 UserRole 正文,用 QString::contains 做大小写不敏感匹配。几百条笔记以内性能没问题,超过一千条建议上 SQLite,Qt 的 QSQLITE 插件是默认自带的基础驱动。
验证方法我习惯这样做:改完一个功能,先跑手工清单(启动、加载、增删改查、重启持久化),再开 Qt Creator 的 debug 构建把警告清零,最后把构建产物复制到一台没装 Qt 的干净机器跑一遍。干净环境能跑,才算确认依赖没漏。
路径处理是每次必查点。总结软件里文件读写写成相对路径的话,程序工作目录可能和可执行文件目录不一致——资源管理器双击启动时工作目录是 exe 所在目录,命令行带 cd 启动时又是另一个目录。最可靠的做法是用 applicationDirPath 拼绝对路径:
#include <QDir> QString dataDir = QCoreApplication::applicationDirPath() + "/data"; QDir().mkpath(dataDir); // 目录不存在则创建 QString dataFile = dataDir + "/notes.json";从那以后我每次改 Qt 项目,第一件事就是检查所有外部文件引用是否都经过了 applicationDirPath,强制自己走一遍这个流程,至少省下十几次「明明路径没错却找不到文件」的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取