简介:面向 Qt 初学者的基础学习总结资源包,内容覆盖开发环境配置、信号与槽机制、QWidgets 与 QML、模型视图编程、网络与数据库访问等核心主题,适合正在系统梳理 Qt 知识框架、准备课程设计或入门实践的开发者参考。压缩包共 34 个文件,包含 5 个 C++ 源码文件、4 个界面布局文件、4 个头文件、14 张界面或流程截图,另有工程与资源配置文件及示例动图,压缩后大小约 2.33MB,结构清晰,便于按模块阅读源码和对照运行效果。已有 114 人学习浏览,说明其对同类学习者具有一定参考价值。资源内含可直接打开的 Qt 工程示例,源码、界面布局和资源文件相互配套,方便边读边跑,兼有模板与素材参考价值。读者可结合图文总结理解窗口控件、登录界面、功能模块拆分等实际写法,借助图片与动图掌握界面设计、交互动画等细节,为独立开发 Qt 应用打下基础。
1. 用“总结软件”把 Qt 基础串成一条线
“Qt基础学习总结1设计的总结软件”这个标题乍看有点绕,拆开读就是:用 Qt 做一个小型总结/笔记工具,用来管理学习过程中积累的知识点。这类工具的核心价值不是“写日记”,而是让零散的 C++ 和 Qt 知识点有一个能录入、能分类、能搜索的本地落脚点。我见过不少学 Qt 的人把控件 Demo 刷了几十个,打开就有,关掉就忘,最后连一个能天天打开用的程序都没留下。做一个总结软件,恰好能把最常用的控件、信号槽、布局和本地存储全部串在一起。
这个项目适合两类人:一类是 C++ 基础过完、想找第一个完整界面项目的新手;另一类是写过一些 Qt 小工具但对架构没感觉的开发者。它会逼你把“信号槽到底怎么连”“列表和编辑框怎么联动”“数据存哪里”这些问题一次想清楚。下面从 Qt 的几个核心机制讲起,再逐步落到工程结构、功能实现和踩坑记录,最后给出几个让工具更好用的进阶技巧。
2. 写总结软件前需要先弄懂的几个 Qt 机制
2.1 为什么选 QWidgets 而不是 QML
总结软件属于典型的桌面工具型应用:界面元素密集、逻辑集中、需要大量控件交互。QWidgets 体系下,QListWidget、QTextEdit、QLineEdit 这类控件是现成的,布局管理器负责窗口缩放时的自动调整,代码写起来直白,调试也方便。QML 更适合界面效果要求高、需要大量动画和触摸交互的场景,但入门阶段同时学 QML 和 C++ 两头对接,容易分散精力。
我一般建议第一个综合项目用 QWidgets 走到底。原因很实在:招聘和实际工作中,Qt Widgets 在工控、桌面工具、上位机领域的使用面仍然很广;你学会的那套信号槽和模型视图思想,之后迁移到 QML 也不浪费。这个选择本身没有对错,但把有限的学习时间放在一个体系上,能更快看到完整结果。
2.2 事件循环与信号槽:让列表点击驱动编辑区刷新
Qt 程序从QApplication::exec()开始就进入事件循环,鼠标点击、键盘输入、定时器到期都会封装成事件,分发给对应控件。信号槽是 Qt 最核心的对象间通信机制,它让发送对象和接收对象之间不需要互相持有对方指针。在总结软件里,最典型的连接就是:
connect(listWidget, &QListWidget::currentItemChanged, this, &MainWindow::onCurrentItemChanged);这行代码的意思是:当列表当前选中项变化时,自动调用MainWindow里的onCurrentItemChanged槽函数。参数currentItemChanged是 Qt5 推荐的函数指针写法,编译时就能检查信号和槽是否存在,比旧的SIGNAL/SLOT宏更安全。槽函数里再去读取列表项对应的数据,刷新右侧编辑框内容。
理解信号槽的关键在于:信号和槽只是“通知”和“响应”的关系,不关心谁发出的、在哪个线程发的。刚开始写的时候容易犯一个错误——在构造函数里直接调用槽函数,以为能“手动触发一次加载”。正确的做法是设置好初始选中项,让信号自然触发,或者显式调用对应槽函数但不冗余连接信号。
2.3 QTextDocument:编辑框里存的不只是字符串
QTextEdit是总结软件的核心编辑器,但它内部真正管理内容的是QTextDocument。文档由 Block(段落)、Fragment(片段)、Cursor(光标)构成,支持字体、颜色、对齐等富文本格式。QTextEdit::setPlainText()是入门常用的方法,但它会用纯文本重建整个文档,原有的格式和选择历史都会丢掉。
这一点直接关系到保存功能的设计。如果每次加载笔记都调用setPlainText(),用户之前设置过的标题字号、加粗标记会全部消失。更合理的做法是:切换列表项时,用setPlainText()加载纯文本;真正需要支持格式时,再用QTextCursor和QTextCharFormat做局部格式修改。后面进阶部分会专门讲格式工具栏的实现路径。
2.4 界面分区的基本思路
总结软件的界面可以拆成三个区域:左侧是笔记列表,右侧是编辑区,底部是搜索框和操作按钮。第一个版本用布局管理器就能完成,不需要引入过于复杂的QDockWidget或QSplitter。
用外层QVBoxLayout包住搜索框和主体区域,主体区域用QHBoxLayout放列表和编辑框,底部再用一个QHBoxLayout放新增、删除、保存按钮。布局管理器的好处是窗口拉伸时控件自动跟随,不用手写尺寸计算。等这个骨架跑通,再考虑是否升级成可拖拽的分隔条布局。
3. 搭建工程与主窗口骨架:从创建项目到跑出第一个空白窗口
3.1 用 qmake 组织一个最小工程
工程文件是 Qt 项目的入口,qmake 会根据.pro文件生成 Makefile。新建一个文件夹SummaryNote,在里面创建SummaryNote.pro:
QT += core gui widgets TARGET = SummaryNote TEMPLATE = app CONFIG += c++17 SOURCES += \ main.cpp \ mainwindow.cpp HEADERS += \ mainwindow.h DEFINES += QT_DEPRECATED_WARNINGSQT += widgets必须显式声明,因为 Qt5 之后 Widgets 模块不再随 core 默认引入。CONFIG += c++17启用 C++17 标准,方便使用auto和智能指针等特性。DEFINES += QT_DEPRECATED_WARNINGS会在使用旧接口时给出编译警告,对学习新写法有帮助。如果用的是 Qt 6,这个文件同样适用。
3.2 主窗口类的基本结构与界面代码
先定义主窗口的头文件mainwindow.h,声明界面控件和核心槽函数:
#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> class QListWidget; class QTextEdit; class QLineEdit; class QPushButton; class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); private slots: void addNote(); void deleteNote(); void saveCurrentNote(); void onCurrentItemChanged(); private: QListWidget *listWidget; QTextEdit *editor; QLineEdit *searchEdit; QPushButton *addButton; QPushButton *deleteButton; QPushButton *saveButton; }; #endif // MAINWINDOW_H构造函数里完成界面组装。setCentralWidget把整个容器设为主窗口中央控件,后续增删控件只改容器内部,不影响窗口框架:
#include "mainwindow.h" #include <QListWidget> #include <QTextEdit> #include <QLineEdit> #include <QPushButton> #include <QVBoxLayout> #include <QHBoxLayout> #include <QLabel> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { setWindowTitle("总结软件"); resize(900, 600); listWidget = new QListWidget(this); editor = new QTextEdit(this); searchEdit = new QLineEdit(this); searchEdit->setPlaceholderText("搜索标题…"); addButton = new QPushButton("新增", this); deleteButton = new QPushButton("删除", this); saveButton = new QPushButton("保存", this); QHBoxLayout *searchLayout = new QHBoxLayout(); searchLayout->addWidget(searchEdit); QHBoxLayout *buttonLayout = new QHBoxLayout(); buttonLayout->addWidget(addButton); buttonLayout->addWidget(deleteButton); buttonLayout->addWidget(saveButton); buttonLayout->addStretch(); QHBoxLayout *bodyLayout = new QHBoxLayout(); bodyLayout->addWidget(listWidget, 1); bodyLayout->addWidget(editor, 3); QVBoxLayout *mainLayout = new QVBoxLayout(); mainLayout->addLayout(searchLayout); mainLayout->addLayout(bodyLayout, 1); mainLayout->addLayout(buttonLayout); QWidget *central = new QWidget(this); central->setLayout(mainLayout); setCentralWidget(central); connect(addButton, &QPushButton::clicked, this, &MainWindow::addNote); connect(deleteButton, &QPushButton::clicked, this, &MainWindow::deleteNote); connect(saveButton, &QPushButton::clicked, this, &MainWindow::saveCurrentNote); }布局参数里bodyLayout->addWidget(listWidget, 1)的第二个参数是拉伸系数,1:3 表示列表占一份宽度、编辑区占三份宽度。addStretch()会在按钮组后面插入弹性空白,让按钮默认靠左排列。此时点击按钮还没有实际功能,需要先让窗口跑起来,确认布局正常。
3.3 构建运行:命令行与 IDE 两种方式
命令行构建是这个项目最可靠的验证方式。进入工程目录:
qmake SummaryNote.pro make -j4 ./SummaryNoteqmake生成 Makefile,make -j4用 4 个并行任务编译。如果系统里同时装了 Qt5 和 Qt6,可能存在多个qmake,需要确认当前qmake版本与代码使用的模块匹配。在 Qt Creator 里则简单得多:打开.pro文件,选择构建套件后直接点运行,输出窗口会显示编译信息和程序启动状态。
这个阶段的目标只有一个:窗口正常显示,列表和编辑区能拉伸,按钮存在但点击无响应是正常的。确认骨架没问题,再进入功能实现。
4. 核心功能落地:列表联动、增删改查、本地存储与搜索导出
4.1 用 QListWidget 管理总结条目
QListWidget适合中低数量级条目的管理。每条记录对应一个QListWidgetItem,用setData(Qt::UserRole, id)给每个条目绑定一个唯一编号,避免靠列表位置识别数据:
void MainWindow::addNote() { saveCurrentNote(); // 先保存当前编辑内容 static int idCounter = 1; QString title = QString("知识总结 %1").arg(idCounter++); QListWidgetItem *item = new QListWidgetItem(title); item->setData(Qt::UserRole, idCounter); listWidget->addItem(item); listWidget->setCurrentItem(item); editor->clear(); editor->setFocus(); }setData的Qt::UserRole是自定义数据起始位,后续存取编号都走这个角色。setCurrentItem会让新条目成为选中项,并触发currentItemChanged信号。这里要注意一个陷阱:saveCurrentNote()放在创建新条目之前,否则当前正在编辑但还没保存的内容会丢。
4.2 列表与编辑器的联动
联动是整个软件最核心的交互。切换列表项时,编辑器要显示对应标题的内容;切换之前,要先把当前编辑内容保存到对应的条目上。用标志位防止信号递归:
void MainWindow::onCurrentItemChanged() { if (m_loading) { return; } saveCurrentNote(); QListWidgetItem *item = listWidget->currentItem(); if (!item) { editor->clear(); return; } m_loading = true; editor->setPlainText(item->data(Qt::UserRole + 1).toString()); m_loading = false; }m_loading是布尔成员变量,初始化为false。当程序内部用代码设置编辑器内容时,如果setPlainText触发了内容变更信号,而该信号关联到存储逻辑,就会形成“保存→加载→保存”的死循环。加上这个标志位后,代码块里的操作流程就不会二次触发联动。
保存当前内容的实现:
void MainWindow::saveCurrentNote() { QListWidgetItem *item = listWidget->currentItem(); if (!item) { return; } QString content = editor->toPlainText(); item->setData(Qt::UserRole + 1, content); // 用内容前两行作为标题 QStringList lines = content.split('\n'); QString title = lines.first().trimmed(); if (title.isEmpty()) { title = "未命名总结"; } item->setText(title); }标题自动取第一行内容,省去单独维护标题栏的复杂度。注意item->setText会修改列表项显示文字,但不会触发currentItemChanged信号,所以这里不会产生联动副作用。这是 QListWidget 列表项文本与用户数据分离的典型用法。
4.3 删除条目与空状态处理
删除时最怕的就是误删。简单的做法是删除前弹窗确认,然后手动管理选中项:
void MainWindow::deleteNote() { QListWidgetItem *item = listWidget->currentItem(); if (!item) { return; } int row = listWidget->row(item); QString title = item->text(); QMessageBox::question(this, "确认删除", QString("确定删除「%1」吗?").arg(title)); // 实际项目中建议根据返回值判断 delete listWidget->takeItem(row); if (listWidget->count() > 0) { int nextRow = qMin(row, listWidget->count() - 1); listWidget->setCurrentRow(nextRow); } else { editor->clear(); } }takeItem(row)只是把条目从列表剥离,不会立刻销毁对象,但这里delete释放它的内存。删除后必须手动选择下一项,否则当前项变为空,编辑器内容就会停留在已删除条目的内容上。实际项目里QMessageBox::question的返回值要接住,根据Yes/No决定是否执行删除,避免用户误点确认。
4.4 JSON 本地存储:让数据重启不消失
没有存储的总结软件只是玩具。这里选 JSON 格式,因为 Qt 的QJsonDocument可以直接解析和生成,可读性比二进制格式好。存储结构做成两个数组并联:一个存标题,一个存内容:
bool MainWindow::saveData() { QJsonArray contentArray; for (int i = 0; i < listWidget->count(); ++i) { QListWidgetItem *item = listWidget->item(i); QString title = item->text(); QString content = item->data(Qt::UserRole + 1).toString(); contentArray.append(QJsonValue(content)); } QJsonArray titleArray; for (int i = 0; i < listWidget->count(); ++i) { titleArray.append(listWidget->item(i)->text()); } QJsonObject root; root["titles"] = titleArray; root["contents"] = contentArray; QFile file(m_dataPath); if (!file.open(QIODevice::WriteOnly)) { return false; } file.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); return true; }m_dataPath用QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)拼接文件名,这是 Qt 推荐的跨平台数据目录,避免把数据文件放在程序安装目录导致权限问题。QJsonDocument::Indented参数让 JSON 带缩进,调试时可以直接打开文件检查。
加载数据时,先清理列表,再逐个恢复条目:
bool MainWindow::loadData() { QFile file(m_dataPath); if (!file.open(QIODevice::ReadOnly)) { return false; } QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); QJsonObject root = doc.object(); QJsonArray titleArray = root["titles"].toArray(); QJsonArray contentArray = root["contents"].toArray(); listWidget->clear(); for (int i = 0; i < titleArray.size(); ++i) { QListWidgetItem *item = new QListWidgetItem(titleArray[i].toString()); item->setData(Qt::UserRole + 1, contentArray[i].toString()); listWidget->addItem(item); } if (listWidget->count() > 0) { listWidget->setCurrentRow(0); } return true; }加载完成后调用setCurrentRow(0),会自动触发currentItemChanged,编辑器随之加载第一条内容。这里注意:加载时不要手动调用editor->setPlainText,一切交给信号槽链路。
4.5 搜索过滤与 HTML 导出
搜索功能用QLineEdit::textChanged信号,遍历列表项并按标题匹配设置隐藏状态:
void MainWindow::onSearchTextChanged(const QString &text) { QString keyword = text.trimmed(); for (int i = 0; i < listWidget->count(); ++i) { QListWidgetItem *item = listWidget->item(i); bool match = item->text().contains(keyword, Qt::CaseInsensitive); item->setHidden(!match); } }Qt::CaseInsensitive忽略大小写,适合英文标题场景。setHidden是 QListWidgetItem 的方法,隐藏后不参与显示和选中。
导出功能用QTextEdit::toHtml()保存为 HTML 文件,方便备份和分享:
void MainWindow::exportHtml() { QString path = QFileDialog::getSaveFileName( this, "导出为 HTML", QString(), "HTML 文件 (*.html)"); if (path.isEmpty()) { return; } QFile file(path); if (!file.open(QIODevice::WriteOnly)) { return; } QTextStream out(&file); out << editor->toHtml(); }toHtml()返回的是完整 HTML 文档,包含样式声明,直接用浏览器打开即可预览。QTextStream构造时指定QIODevice::WriteOnly,默认编码在 Qt6 里是 UTF-8,中文不会乱码。这个导出路径绕开了手动拼字符串的麻烦,也顺带实现了富文本格式的保留。
5. Qt 做总结软件最容易翻车的 5 个问题:现象、原因与解决
5.1 切换列表项时内容被清空或张冠李戴
现象:在 A 条目里写了一段内容,点击 B 条目再点回 A,A 的内容变成空白,或者显示的是 B 的内容。
原因:切换时onCurrentItemChanged先调用saveCurrentNote(),但此时currentItem()已经指向了新选中的 B。保存函数把编辑框内容写给了 B,A 的内容仍停留在之前的编辑器里,随后加载 B 的内容时 A 的旧内容就被覆盖没了。
解决:把“保存旧项”和“加载新项”分开处理。在currentItemChanged触发时,先用listWidget->itemChanged信号确保旧项内容已经存好,或者在手写切换逻辑时通过QListWidgetItem指针判断是否同一个条目。简单稳妥的做法是:让点击槽函数先记录当前行号,保存后再切换选中项。核心思路是不要依赖currentItem()获取“上一个条目”。
5.2 中文乱码:源文件、读写文件两个环节都可能出错
现象:保存后重启,显示的内容变成乱码;或者代码里的中文字符串在界面上显示为“?”。
原因:两种常见来源。一是源文件编码和编译器默认编码不一致,比如 Windows 下 MSVC 把源文件按本地代码页读取;二是读写文件时没有指定 UTF-8,QFile::readAll()返回的字节流按错误编码解读。
解决:源文件统一保存为 UTF-8 编码,MSVC 编译时在.pro文件里加QMAKE_CXXFLAGS += /utf-8。文件读写统一走QTextStream并显式设置编码:Qt6 里用stream.setEncoding(QStringConverter::Utf8),Qt5 里用stream.setCodec("UTF-8")。不要依赖系统默认编码,跨平台部署时这一项必查。
5.3 窗口缩放时布局变形或控件挤成一团
现象:窗口最大化时列表区域无限变宽,编辑区反而被压扁;或者缩到最小后按钮消失。
原因:布局里的拉伸系数没设置,或者控件的最小尺寸策略不对。addWidget(widget)默认拉伸系数是 0,意思是控件按自身sizeHint显示,不随窗口变化。
解决:列表和编辑区都设置合理的拉伸系数,例如bodyLayout->addWidget(listWidget, 1)、bodyLayout->addWidget(editor, 3),体会 1:3 的比例关系。按钮行用addStretch()补位。再给listWidget设置setMinimumWidth(150),给editor设置setMinimumHeight(200),这样窗口再小也不会把编辑区压没。sizePolicy默认是Preferred,在多数布局场景下够用,不需要过度配置。
5.4 删除条目后按钮触发位置错乱
现象:删除第 2 条后,再点击新增按钮,新条目插到了列表中间而不是末尾;或者删除后编辑器显示的仍是已删除内容。
原因:删除后没有手动重置选中项,currentRow停留在删除位置,后续操作的row()基于旧位置计算。编辑器内容清除逻辑失效,是因为当前项已经变成空指针或指向了被覆盖的项。
解决:删除操作统一走takeItem→ 手动选择下一项 → 确认编辑器加载新选中内容的流程。具体来说,删除后先判断count()是否为 0,为 0 则editor->clear();否则用qMin(row, count() - 1)计算下一选中行,并setCurrentRow触发一次正常的联动加载。
5.5 窗口关闭时数据未保存,一夜回到解放前
现象:写了一小时内容,点了右上角关闭,再打开程序发现全没了。
原因:没有重写closeEvent。QTextEdit里的文字只存在于内存,列表虽然有内容,但从未触发保存。依赖“手动点保存”的交互模式,用户总有忘记的时候。
解决:重写主窗口的closeEvent,在销毁前统一保存:
void MainWindow::closeEvent(QCloseEvent *event) { saveCurrentNote(); saveData(); event->accept(); }这是最基础的兜底。更稳妥的方案是加一个“未保存修改”标记,在编辑器textChanged信号里置位,标题栏显示*,关闭时只有标记为真才保存,避免每次关闭都做无意义的文件写入。这套机制在进阶部分会用到。
6. 让总结软件更好用的三个进阶技巧
6.1 用 QTextCharFormat 加一个极简格式工具栏
编辑器里选中文段,点击“加粗”按钮,核心逻辑是用当前光标选区构造格式对象:
QTextCharFormat fmt; fmt.setFontWeight(QFont::Bold); QTextCursor cursor = editor->textCursor(); cursor.mergeCharFormat(fmt); editor->mergeCurrentCharFormat(fmt);mergeCharFormat只修改选中部分的字体属性,不会破坏其他文本内容,也不会重建整个文档。比setPlainText再拼字符串的土办法好得多。照这个思路,字号、颜色、标题级别都能做,撤销栈也不会丢。
6.2 布局升级:用一个 QSplitter 替换固定比例
把bodyLayout里的两个控件塞进QSplitter,用户就能手动拖动分界线:
QSplitter *splitter = new QSplitter(Qt::Horizontal); splitter->addWidget(listWidget); splitter->addWidget(editor); splitter->setStretchFactor(0, 1); splitter->setStretchFactor(1, 3); bodyLayout->addWidget(splitter, 1);然后splitter->setSizes(QList<int>{200, 700})指定初始宽度。这个改动只有几行,但体验提升非常明显,属于投入产出比最高的升级。
6.3 给自己定一份验收清单
项目做到什么程度算“真会了”,用下面的路径自查:新建条目成功吗?切换后旧内容不丢吗?搜索能过滤列表吗?重启后数据还在吗?导出 HTML 用浏览器打开格式对吗?窗口拉伸控件不变形吗?关闭窗口时内容自动保存吗?每一条都能点头,再往界面上加花哨功能不迟。
最后说个我自己的教训:早期做完类似工具后交付给朋友用,对方回复“界面不错,但我写了五百字关掉再开全没了”。问题就出在没做closeEvent兜底保存。那以后凡是有编辑功能的桌面程序,我第一件事就是先把掉电不丢数据这条做掉。进步不是体现在代码写得多花哨,而是能不能把用户最容易碰到的那条坑先填平。希望帮到你。
本文还有配套的精品资源,点击获取