简介:这是一份基于Qt框架实现的轻量级记事本源码项目,面向Qt初学者、C++入门者以及需要简洁文本编辑工具的开发者。项目定位为无状态栏、无查找替换的基础记事本,界面干净,适合用来学习Qt窗口创建、用户输入处理与文件读写等核心操作。压缩包共15个文件,约143KB,包含3个cpp源文件、3个h头文件、2个ui界面文件,以及pro工程文件、qrc资源文件、ico图标和jpg图片等,覆盖主窗口、对话框、程序入口与界面布局等关键模块,结构完整便于直接编译运行。目前已有302人学习下载,可作为Qt编程入门练手与二次开发的起点。读者可从中掌握Qt项目组织方式、信号槽机制与基础文本编辑器实现思路,并在此基础上扩展查找替换、状态栏或更多文本格式支持,具备较强的可扩展性与参考价值。
1. 从零手写一个 Notepad_QT_简单记事本:为什么它是桌面开发最好的练手项目
很多人第一次接触 Qt 时,都会卡在一个尴尬的位置:文档看了一堆,信号槽、布局、事件循环这些词都认识,但真让自己从空目录开始搭一个能跑、能存盘、能复制粘贴的窗口程序,手就停住了。Notepad_QT_简单记事本这个标题看着朴素,恰恰是补上这一环的最佳载体——它足够小,一天能写完;又足够完整,菜单栏、工具栏、状态栏、文件对话框、文本编辑控件、快捷键、编码处理全都能碰到。对刚学 C++ 或刚转桌面端的人来说,与其去啃一个几万行的开源工程,不如先把记事本从零敲一遍,把 Qt 的骨架摸清楚。这篇笔记就按我实际写这类小工具的顺序,把选型、代码、参数和踩过的坑一次讲透,新手能照着复现,熟手也能对照检查自己的边界处理。
2. 选型与骨架:QMainWindow 还是 QWidget,控件怎么摆
2.1 为什么主窗口用 QMainWindow 而不是裸 QWidget
记事本这类程序,核心诉求是「菜单 + 工具栏 + 中央编辑区 + 底部状态栏」四件套。QMainWindow 天生就为这种结构准备了 menuBar()、addToolBar()、setCentralWidget()、statusBar() 四个挂载点,你不需要自己写布局去拼这些区域。如果一开始用裸 QWidget 加 QVBoxLayout,菜单栏得手动塞,状态栏得自己造一个 QLabel 贴底部,窗口缩放时还要处理各区域的比例,纯属给自己找麻烦。
常见做法是:主窗口继承 QMainWindow,中央控件放一个 QPlainTextEdit 或 QTextEdit。这里有个选型分叉——QPlainTextEdit 为纯文本和大文档优化,底层用更省内存的布局,几万行也不卡;QTextEdit 支持富文本、HTML、图片,功能多但开销大。记事本只处理纯文本,选 QPlainTextEdit 更合适,这也是我一般会推荐的默认项。
2.2 用代码而不是 .ui 文件搭界面的理由
Qt Creator 提供了拖拽式的 .ui 设计器,但对练手项目,我更建议纯代码构建。原因有三:一是 .ui 最终也会被 uic 转成 C++ 代码,直接写代码少一层黑匣子;二是代码里控件的创建顺序、父子关系、信号连接一目了然,出问题好排查;三是版本管理时纯代码 diff 清晰,.ui 的 XML 冲突处理起来很烦。下面是最小骨架:
// mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QPlainTextEdit> class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); private slots: void newFile(); // 新建 void openFile(); // 打开 void saveFile(); // 保存 void saveFileAs(); // 另存为 private: void createActions(); // 创建动作 void createMenus(); // 创建菜单 void createToolBars(); // 创建工具栏 void createStatusBar(); // 创建状态栏 QPlainTextEdit *textEdit; // 中央编辑区 QString currentFile; // 当前文件路径,空表示未保存过 }; #endif// mainwindow.cpp 片段:构造函数与中央控件 #include "mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), currentFile("") { textEdit = new QPlainTextEdit(this); setCentralWidget(textEdit); // 关键:把编辑区设为中央控件 setWindowTitle("未命名 - 记事本"); resize(800, 600); createActions(); createMenus(); createToolBars(); createStatusBar(); }逻辑说明:构造函数里先建 textEdit,再 setCentralWidget,顺序不能反,否则中央区域是空的。currentFile 用空字符串表示「还没关联磁盘文件」,这个约定后面保存逻辑会反复用到。resize 给个初始尺寸,避免窗口小到看不见内容。
参数说明:QPlainTextEdit 的父对象传 this,Qt 的对象树会自动管理内存,不用手动 delete。setWindowTitle 里的「未命名」是给用户的提示,打开或保存后要动态改。
2.3 动作、菜单、工具栏的复用关系
菜单项和工具栏按钮本质是同一个 QAction,创建一次,两处挂载即可,改快捷键或禁用状态时不用同步两遍。这是 Qt 设计里很省心的一点。
void MainWindow::createActions() { QAction *newAct = new QAction("新建", this); newAct->setShortcut(QKeySequence::New); // 自动适配平台快捷键 connect(newAct, &QAction::triggered, this, &MainWindow::newFile); QAction *openAct = new QAction("打开", this); openAct->setShortcut(QKeySequence::Open); connect(openAct, &QAction::triggered, this, &MainWindow::openFile); QAction *saveAct = new QAction("保存", this); saveAct->setShortcut(QKeySequence::Save); connect(saveAct, &QAction::triggered, this, &MainWindow::saveFile); QAction *saveAsAct = new QAction("另存为", this); saveAsAct->setShortcut(QKeySequence::SaveAs); connect(saveAsAct, &QAction::triggered, this, &MainWindow::saveFileAs); // 把动作存成成员或通过 findChild 复用,这里为简洁直接建菜单 QMenu *fileMenu = menuBar()->addMenu("文件"); fileMenu->addAction(newAct); fileMenu->addAction(openAct); fileMenu->addSeparator(); fileMenu->addAction(saveAct); fileMenu->addAction(saveAsAct); QToolBar *fileToolBar = addToolBar("文件"); fileToolBar->addAction(newAct); fileToolBar->addAction(openAct); fileToolBar->addAction(saveAct); }逻辑说明:QKeySequence::New 这类标准键会自动映射成 Ctrl+N(Windows/Linux)或 Cmd+N(macOS),比硬编码字符串更稳。同一个 QAction 同时 addAction 到菜单和工具栏,点击任意一处都触发同一个槽。
参数说明:setShortcut 接受 QKeySequence,也可以直接写 QKeySequence("Ctrl+S"),但跨平台时标准枚举更省事。addSeparator 在菜单里画一条分隔线,工具栏里一般不用。
3. 文件读写与编码:记事本最容易翻车的地方
3.1 QFile + QTextStream 的读写套路
打开和保存是记事本的核心,也是最容易出编码问题的地方。Qt 里读写文本文件,标准组合是 QFile 负责字节流,QTextStream 负责编码转换。
void MainWindow::openFile() { QString fileName = QFileDialog::getOpenFileName( this, "打开文件", QString(), "文本文件 (*.txt);;所有文件 (*)"); if (fileName.isEmpty()) return; // 用户取消 QFile file(fileName); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { statusBar()->showMessage("无法打开文件", 3000); return; } QTextStream in(&file); in.setEncoding(QStringConverter::Utf8); // Qt6 写法 textEdit->setPlainText(in.readAll()); file.close(); currentFile = fileName; setWindowTitle(QFileInfo(fileName).fileName() + " - 记事本"); }void MainWindow::saveFile() { if (currentFile.isEmpty()) { saveFileAs(); // 没关联文件就转另存为 return; } QFile file(currentFile); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { statusBar()->showMessage("保存失败", 3000); return; } QTextStream out(&file); out.setEncoding(QStringConverter::Utf8); out << textEdit->toPlainText(); file.close(); statusBar()->showMessage("已保存", 2000); }逻辑说明:open 时加 QIODevice::Text 标志,Windows 下会把 \r\n 归一化处理,避免读进来多出空行。QTextStream 的编码设置是关键,不设的话 Qt6 默认走 UTF-8,但显式写出来更保险,也方便以后改成 GBK 兼容老文件。
参数说明:QFileDialog::getOpenFileName 的第三个参数是起始目录,传 QString() 表示用系统默认。过滤器字符串里 ;; 分隔不同类别,(*.txt) 是通配符。setEncoding 在 Qt5 里是 setCodec("UTF-8"),Qt6 改成了 QStringConverter,迁移时注意。
3.2 编码不一致导致中文乱码的排查思路
乱码是记事本类程序最高频的问题。现象是:用系统自带记事本存的中文 txt,用你的程序打开变成问号或方块。原因通常是文件本身是 GBK/GB18030 编码,而你的 QTextStream 按 UTF-8 解码。解决办法有两个方向:一是读取时先探测编码,二是给用户一个编码选择入口。简单项目里,我一般先按 UTF-8 读,读出来如果全是替换字符 U+FFFD,再回退用 GB18030 重读一次。
QString readWithFallback(const QString &path) { QFile file(path); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return QString(); QByteArray raw = file.readAll(); file.close(); // 先按 UTF-8 解码 QString text = QString::fromUtf8(raw); if (text.contains(QChar::ReplacementCharacter)) { // 出现替换字符,回退 GB18030 QTextCodec *codec = QTextCodec::codecForName("GB18030"); if (codec) text = codec->toUnicode(raw); } return text; }逻辑说明:QString::fromUtf8 遇到非法字节会插入 U+FFFD 替换字符,用它当探测信号简单有效。GB18030 是 GBK 的超集,兼容性更好。注意 QTextCodec 在 Qt6 里被移到了 Qt5Compat 模块,新项目建议直接用 QStringConverter 或第三方编码探测库。
参数说明:codecForName 传 "GB18030" 而不是 "GBK",覆盖字符更全。如果项目要处理日文、韩文,这套回退逻辑就不够了,得引入 uchardet 之类的探测库。
3.3 未保存修改的拦截:closeEvent 的正确写法
用户点了关闭但没保存,直接退出会丢数据,这是体验硬伤。要在 closeEvent 里判断文档是否被修改过。
void MainWindow::closeEvent(QCloseEvent *event) { if (!textEdit->document()->isModified()) { event->accept(); return; } QMessageBox::StandardButton ret = QMessageBox::warning( this, "记事本", "文档已修改,是否保存?", QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); if (ret == QMessageBox::Save) { saveFile(); event->accept(); } else if (ret == QMessageBox::Discard) { event->accept(); } else { event->ignore(); // 取消关闭 } }逻辑说明:QPlainTextEdit 的 document() 返回 QTextDocument,isModified() 在内容变动后为 true,保存后 Qt 会自动重置。三个按钮分别对应保存、丢弃、取消,取消时 event->ignore() 阻止窗口关闭。
参数说明:QMessageBox::warning 的按钮组合用按位或。注意 saveFile 里如果用户又取消了文件对话框,这里应该重新 ignore,严谨写法是把 saveFile 改成返回 bool。
4. 避坑与常见问题:那些让我返工三次的细节
4.1 现象:工具栏按钮图标不显示,只有文字
原因:QAction 只设了 text 没设 icon,或者图标路径用了相对路径但工作目录不对。解决:用 QIcon(":/icons/new.png") 走 Qt 资源系统(.qrc),把图片编译进可执行文件,避免发布时丢图。资源路径以冒号开头,这是 Qt 的约定。
4.2 现象:打开大文件(几十 MB)界面卡死几秒
原因:在主线程里同步 readAll 加 setPlainText,UI 事件循环被阻塞。解决:小工具可以接受,但要优化的话,用 QProgressDialog 配合分块读取,或者把读取放到 QtConcurrent::run 里,读完再回主线程 setPlainText。注意 QPlainTextEdit 本身对大文档比 QTextEdit 友好得多,换控件往往比改架构更立竿见影。
4.3 现象:另存为后标题栏没更新,再点保存还是存到旧文件
原因:saveFileAs 里忘了更新 currentFile 和 setWindowTitle。解决:另存为成功后必须把新路径赋给 currentFile,并刷新标题。这个 bug 很隐蔽,因为第一次保存看起来是对的,第二次才暴露。
4.4 现象:中文路径下打开文件失败
原因:早期 Qt 版本或某些平台 API 对非 ASCII 路径处理不一致。解决:全程用 QString 而非 char*,QFile 内部会处理平台差异。如果调用了第三方 C 库,注意在边界处做 toLocal8Bit 或 fromLocal8Bit 转换,别直接把 QString 当 char* 用。
4.5 现象:程序发布到别的机器提示缺 DLL
原因:动态链接的 Qt 库没打包。解决:Windows 下用 windeployqt 工具自动拷贝依赖,Linux 下用 linuxdeployqt 或直接静态编译。这一步不做,程序在自己机器上跑得好好的,换台机器就翻车,属于典型的「最后一公里」问题。
5. 进阶技巧:把记事本做得更顺手
5.1 用 QSettings 记住窗口大小和最近打开的文件
每次启动都回到默认 800x600、最近文件列表为空,体验很割裂。QSettings 能把这些状态持久化,Windows 下写注册表,Linux 下写 ini,跨平台不用改代码。
// 构造时恢复 QSettings settings("MyCompany", "SimpleNotepad"); resize(settings.value("window/size", QSize(800, 600)).toSize()); move(settings.value("window/pos", QPoint(100, 100)).toPoint()); // closeEvent 里保存 settings.setValue("window/size", size()); settings.setValue("window/pos", pos());逻辑说明:QSettings 的构造参数是组织名和应用名,决定了存储位置。value 的第二个参数是默认值,首次运行时用它。注意 move 恢复位置时要防止窗口跑到屏幕外,多显示器拔掉后这个坑很常见,可以加个屏幕范围校验。
参数说明:toSize()、toPoint() 是 QVariant 的转换方法,存进去时 Qt 自动包装,取出来要显式转回。
5.2 查找替换的最小实现
记事本没有查找替换说不过去。QPlainTextEdit 自带 find() 方法,配合一个输入框就能做。
void MainWindow::findNext(const QString &keyword) { if (keyword.isEmpty()) return; bool found = textEdit->find(keyword); // 默认从光标处向后找 if (!found) { // 到末尾了,回到开头再找一次 QTextCursor cursor = textEdit->textCursor(); cursor.movePosition(QTextCursor::Start); textEdit->setTextCursor(cursor); textEdit->find(keyword); } }逻辑说明:find 默认区分大小写、向后搜索,返回 bool 表示是否找到。找不到时把光标移回开头再试一次,形成循环查找。要支持大小写切换,用 QTextDocument::FindCaseSensitively 标志组合。
参数说明:find 还有 FindBackward、FindWholeWords 等标志,按位或组合。替换的话,找到后用 textCursor().insertText() 覆盖选中内容即可。
5.3 一个我踩过的坑:自动换行与状态栏行号
QPlainTextEdit 默认开启自动换行,长行会折行显示,但状态栏如果显示「第 X 行」用的是块号而非视觉行号,用户会觉得对不上。我一般会关掉自动换行(setLineWrapMode(QPlainTextEdit::NoWrap)),或者状态栏改显示「块号 + 列号」,用 textCursor().blockNumber() 和 positionInBlock() 取值。这个细节不影响功能,但影响用户对程序「专不专业」的判断。
写这类小工具,我的习惯是先把最小可运行版本跑通,再逐个补边界处理,最后才碰界面美化。每次加功能前先问自己:用户取消操作时程序会不会崩?文件不存在时提示清不清楚?中文路径和编码有没有兜住?把这几条守住,一个 Notepad_QT_简单记事本就已经比很多练手项目扎实了。希望帮到你。
本文还有配套的精品资源,点击获取