ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用Qt写出不乱码的记事本:编码探测与换行符处理全解析

用Qt写出不乱码的记事本:编码探测与换行符处理全解析 简介基于Qt框架的轻量级记事本源码项目面向Qt、C初学者完整展示从窗口创建、菜单布局到文件打开保存的基础实现思路。界面刻意保持极简未添加状态栏与查找替换等附加功能适合快速了解跨平台桌面应用的工程组织方式和界面事件处理流程。压缩包共十五个文件整体大小约一百四十三KB其中包含C源文件、头文件、用户界面文件、工程配置文件、资源文件以及图标和说明图片目录结构清晰便于按模块对照学习。目前已有三百零二人学习下载。源码将主窗口类与对话框类分离可直观体会控件信号与槽机制通过编译运行可验证基础编辑功能后续也容易自行扩展状态栏或查找替换能力适合作为课程设计或自学练手项目。1. 用 Qt 写一个简单记事本先别急着写代码“Notepad_QT_简单记事本_”这个工程名字看起来平平无奇但它背后指向的是每个 Qt 初学者都会做一遍的经典练习把打开、保存、查找替换、字数统计这些最熟悉的桌面操作用 Qt Widgets 重新实现一遍。做这个小项目的价值不在“记事本”本身而在于把 QMainWindow、QPlainTextEdit、QFileDialog、QTextCursor 这四样核心组件串起来等于把 Qt 桌面开发的主干走了一遍。这篇笔记直接按可编译的 Qt 5.15 LTS 工程来写适合刚学完信号与槽的初学者也适合想快速产出内部文本小工具、又不想引入复杂框架的工程师。你会发现记事本虽小但编码、换行符、部署这些真正折磨人的问题一个都少不了。2. 编辑器核心选型与主窗口骨架为什么是 QPlainTextEdit 而不是 QTextEdit2.1 QPlainTextEdit、QTextEdit、QTextBrowser记事本为什么锁死 QPlainTextEdit很多第一次动手的人会顺手放一个 QTextEdit因为教程里出现得最多。QTextEdit 是富文本编辑器默认支持一部分 HTML可以插入图片、修改字体颜色、设置表格QPlainTextEdit 则是为纯文本场景设计的内部按块block组织文本绘制和滚动都针对大段文字做过优化。同样是打开一个几 MB 的日志文件QTextEdit 会明显卡顿QPlainTextEdit 在多数机器上依然能流畅翻页。记事本只需要处理纯文本所以核心组件锁死 QPlainTextEdit这是性能和实现成本双重考虑下来的结论。QTextBrowser 则是 QTextEdit 的只读子类适合做帮助文档、富文本查看器不适合做编辑器。如果你的目标是“能打字的记事本”不需要考虑它。还有一个容易忽略的选项如果只是做日志监控可以在 QPlainTextEdit 上调用setMaximumBlockCount(5000)让文本区只保留最近 5000 行这样长时间运行也不会把内存吃满——这个参数对记事本同样有意义打开的日志文件再大界面也不会拖死。提示QPlainTextEdit 的setMaximumBlockCount是行数上限超出后自动丢弃旧块对“打开一个大日志文件”的场景很实用但对普通 txt 建议设为 00 表示不限制。2.2 最小可运行的 QMainWindow 工程菜单、工具栏、状态栏一起搭我一般用 Qt Creator 新建 Qt Widgets Application但不用它自动生成的 MainWindow.ui而是全部手写界面。这样做的原因是手写代码能把“菜单、工具栏、状态栏、中心组件”的关系交代清楚用 Qt Designer 拖界面虽然快但新手很容易被.ui编译机制带偏。以下是整个工程的入口文件。#include QApplication #include MainWindow.h int main(int argc, char *argv[]) { QApplication app(argc, argv); app.setApplicationName(SimpleNotepad); // 这个名字影响第 6 章 QSettings 的存储路径 app.setOrganizationName(DevWorkshop); MainWindow w; w.show(); return app.exec(); // 进入事件循环直到窗口关闭 }逻辑说明QApplication 是每个 Qt GUI 程序必须创建的对象它负责事件循环和全局设置setApplicationName和setOrganizationName看起来是元数据实际上决定第 6 章 QSettings 默认写入的注册表或配置文件位置不设置的话 QSettings 可能无法正常工作。MainWindow 的构造里把菜单、工具栏、状态栏和中心编辑区一次性搭好。MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { setWindowTitle(tr(记事本 - 未命名)); resize(1000, 680); editor new QPlainTextEdit(this); setCentralWidget(editor); // 编辑区占满主窗口剩余空间 connect(editor, QPlainTextEdit::textChanged, this, MainWindow::onTextChanged); // 文本变化后刷新状态栏和标题标记 statusBar()-showMessage(tr(就绪), 3000); createActions(); createMenus(); createToolBar(); }参数说明resize(1000, 680)是初始窗口尺寸没有设置最小尺寸用户仍可自由拖拽statusBar()-showMessage(msg, 3000)的第二个参数是显示毫秒数3000 表示 3 秒后自动清空。createActions、createMenus、createToolBar三个方法按名字拆分避免构造函数写得太长。动作和菜单的完整代码如下void MainWindow::createActions() { openAction new QAction(tr(打开...), this); openAction-setShortcut(QKeySequence::Open); // 自动映射 CtrlO connect(openAction, QAction::triggered, this, MainWindow::openFile); saveAction new QAction(tr(保存), this); saveAction-setShortcut(QKeySequence::Save); // 自动映射 CtrlS connect(saveAction, QAction::triggered, this, MainWindow::saveFile); quitAction new QAction(tr(退出), this); quitAction-setShortcut(QKeySequence::Quit); connect(quitAction, QAction::triggered, this, QWidget::close); } void MainWindow::createMenus() { QMenu *fileMenu menuBar()-addMenu(tr(文件(F))); fileMenu-addAction(openAction); fileMenu-addAction(saveAction); fileMenu-addSeparator(); fileMenu-addAction(quitAction); } void MainWindow::createToolBar() { QToolBar *toolBar addToolBar(tr(常用)); toolBar-setMovable(false); toolBar-addAction(openAction); toolBar-addAction(saveAction); }QKeySequence::Open 和 QKeySequence::Save 是 Qt 内置的标准快捷键枚举在 Windows 上映射为 CtrlO、CtrlS在 macOS 上自动变成 Command 键比硬编码QKeySequence(CtrlO)更稳。QToolBar 的setMovable(false)是给工具栏上锁防止用户不小心把它拖出来变成悬浮窗口如果是高级用户使用的工具可以把这行去掉允许自由排版。2.3 字体与缩进的 3 个常用参数Monospace 字体和 Tab 距离记事本的核心体验是“代码和配置文件的缩进要整齐”因此字体必须选等宽字体Tab 键距离要固定。以下是三个必调参数QFont mono(Courier New); mono.setStyleHint(QFont::Monospace); // 系统没有 Courier New 时自动替换为其他等宽字体 mono.setPointSize(10); editor-setFont(mono); editor-setLineWrapMode(QPlainTextEdit::WidgetWidth); editor-setTabStopDistance(4 * QFontMetricsF(editor-font()).horizontalAdvance(QLatin1Char( )));参数说明setStyleHint(QFont::Monospace)很关键在 Linux 或 macOS 上没有 Courier New 时Qt 会根据这个提示自动选择 DejaVu Sans Mono、Menlo 等系统等宽字体避免回退到中文字体导致对齐失败。setLineWrapMode(QPlainTextEdit::WidgetWidth)是自动换行文本超过窗口宽度时自动折行如果做日志工具很多人喜欢NoWrap加水平滚动条记事本默认用自动换行更贴近 Windows 记事本的行为。setTabStopDistance在 Qt 5.10 以后是重载函数参数是像素写成 4 个空格宽度按下 Tab 后光标正好跳 4 个空格老代码里的setTabStopWidth在 Qt 5.10 起被标记废弃不要混用。到这里一个能打字、能显示菜单栏和状态栏的记事本骨架已经跑起来了。剩下的问题全部集中在“文件怎么打开、怎么保存、怎么防止乱码”上这是记事本项目里最容易翻车的一段。3. 打开与保存的完整实现编码探测、换行符统一与 BOM 处理3.1 打开文件时怎么判断 UTF-8 还是 GBKBOM 优先非法 UTF-8 兜底记事本最常见的翻车现场就是乱码。Qt 5 之后内部字符串默认是 UTF-8但 Windows 旧版记事本保存中文时默认用 ANSI也就是 GBK/GB18030网上下载的配置文件可能是 UTF-8 无 BOMLinux 过来的文件换行符还是 LF。所以打开文件不能只做QFile::readAll()加toUtf8()要先做编码探测和换行符统一。下面是完整的 openFile 实现。bool MainWindow::openFile(const QString filePath) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { QMessageBox::warning(this, tr(打开失败), file.errorString()); return false; } QByteArray data file.readAll(); // 一次性读入文件不大时最简单 file.close(); // 先把三类换行符统一成 \n后面所有逻辑只处理 \n data.replace(\r\n, \n); data.replace(\r, \n); QByteArray utf8Bom QByteArray::fromHex(efbbbf); QTextCodec *codec nullptr; if (data.startsWith(utf8Bom)) { data.remove(0, 3); // 去掉 BOM 后再解码否则首行会出现不可见字符 codec QTextCodec::codecForName(UTF-8); } else { QString utf8Guess QString::fromUtf8(data); // 按 UTF-8 尝试解码 if (!utf8Guess.contains(QChar::ReplacementCharacter)) { codec QTextCodec::codecForName(UTF-8); } else { codec QTextCodec::codecForName(GB18030); // 兼容 GBK/GB2312 } } editor-setPlainText(codec-toUnicode(data)); editor-document()-setModified(false); setWindowModified(false); setWindowTitle(tr(%1 - 记事本).arg(QFileInfo(filePath).fileName())); return true; }逻辑说明data.replace(\r\n, \n)必须写在replace(\r, \n)之前因为如果先把\r替换成\n原来的\r\n会变成\n\n这就翻车了。这段代码先处理 CRLF再处理单独的 CR能把 Windows、Unix、老 Mac 三种换行符统一成 LF。编码探测的核心是QString::fromUtf8(data)如果字节流不是合法的 UTF-8解码结果里会出现 UFFFD 替换符通过判断替换符是否存在来决定是否切到 GB18030。这个方案比“带 BOM 就 UTF-8不带就当 GBK”更稳因为很多 Linux 生成的文本是没有 BOM 的 UTF-8。需要注意的差异QTextCodec 在 Qt 6 中被移到了 Qt5Compat 模块使用 Qt 6 时需要在 .pro 文件里加QT core5compat否则#include QTextCodec会报找不到头文件。GB18030 是 GBK 的超集向下兼容所以用它做兜底不会损失旧文件。这个方案还有个理论盲区如果原文件本身就是 UTF-8 编码的替换符字符 UFFFD会被误判成 GB18030实际工程中概率极低不必为此增加复杂度。3.2 保存成 Windows 记事本能认的文件写回时统一换行符和可选 BOM保存同样不能只写toPlainText().toUtf8()。原因是 QPlainTextEdit 内部的换行符统一是\n但 Windows 记事本默认用\r\n如果直接写文件Windows 老版本记事本会把整个文本显示成一行。另外要不要写 UTF-8 BOM决定了文件在 Windows 记事本里能否被正确识别为 UTF-8。bool MainWindow::saveFile(const QString filePath) { QFile file(filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { QMessageBox::warning(this, tr(保存失败), file.errorString()); return false; } QByteArray data editor-toPlainText().toUtf8(); #ifdef Q_OS_WIN data.replace(\n, \r\n); // 仅 Windows 下把 LF 转回 CRLF #endif data.prepend(QByteArray::fromHex(efbbbf)); // 写入 UTF-8 BOM兼容老版 Windows 记事本 file.write(data); file.close(); editor-document()-setModified(false); setWindowModified(false); setWindowTitle(tr(%1 - 记事本).arg(QFileInfo(filePath).fileName())); return true; }参数说明QIODevice::WriteOnly | QIODevice::Truncate表示只写并清空原文件缺了 Truncate 时如果新内容比原文件短文件末尾会残留旧数据。#ifdef Q_OS_WIN是编译期判断在 Windows 上才做\n到\r\n的转换交叉编译到 Linux 或 macOS 时不会误转。data.prepend(...)是往字节数组头部插入 BOMEF BB BF。新版 Windows 10/11 记事本已经能自动识别无 BOM 的 UTF-8但老版本和部分第三方编辑器不行所以保存时默认带 BOM 最稳。这套默认策略对绝大多数场景够用。如果你的工具要给程序员用可以在保存对话框里加一个“编码”下拉框提供 UTF-8 with BOM、UTF-8 without BOM、GB18030 三个选项然后在 saveFile 里用 QTextCodec 按选择转码。简单记事本不必做这么细但存储这个设计点后续扩展有方向。3.3 文件打开失败的 4 个边界只读、大文件、占用、空文件打开文件失败不是只有“路径不存在”一种情况实际使用中更常遇到下面四种每一种的提示方式都不同。场景现象处理方式文件被其他程序占用QFile::open 返回 falseerrorString 是 Permission denied弹窗提示文件正被占用不要直接覆盖保存文件只读能打开但保存会失败打开前用 QFileInfo::isWritable() 检测弹窗告知以只读方式打开超大型文件几十 MB 的 txt 打开时界面卡顿数秒超过 10MB 时提示用户并提供分块读取或“继续打开”选项空文件readAll 返回空字节编辑器残留上一次内容setPlainText 空字符串即可打开前先确认当前未保存修改只读检测可以放在 openFile 前面代码就一行if (!QFileInfo(filePath).isWritable()) { QMessageBox::information(this, tr(提示), tr(文件为只读打开后保存可能失败。)); }这里容易忽略的问题是第一次打开文件成功后用户没关窗口又打开另一个文件前一个文件的未保存内容会直接丢掉。正确做法是在 openFile 开始时先判断editor-document()-isModified()为真就先走保存确认流程再加载新文件。这个逻辑虽然听起来啰嗦但它是“记事本会不会丢用户数据”的分水岭建议在写打开功能时第一时间带上。4. 查找替换与字数统计直接把 QTextCursor 用成一套可复用方法4.1 查找下一个这次与下次的高亮与光标处理查找功能是记事本里复杂度最高的交互核心是 QTextDocument::find 和 QTextCursor 的配合。容易踩的坑是“连续点击查找下一个”时光标停留在匹配文本中间下次查找会原地返回同一个结果看起来像卡住了。下面是能正确循环查找的实现。bool MainWindow::findNext(const QString keyword, bool caseSensitive, bool backward) { QTextDocument *doc editor-document(); if (keyword.isEmpty()) return false; QTextCursor cursor editor-textCursor(); QTextDocument::FindFlags flags; if (caseSensitive) flags | QTextDocument::FindCaseSensitively; if (backward) flags | QTextDocument::FindBackward; // 关键处理从选区末尾开始找避免连续点击时卡在同一个匹配上 QTextCursor searchStart cursor; int fromPos backward ? cursor.selectionStart() : cursor.selectionEnd(); searchStart.setPosition(fromPos); QTextCursor found doc-find(keyword, searchStart, flags); if (found.isNull()) { // 找不到时从文档另一头再找一次实现循环查找 QTextCursor restart backward ? QTextCursor(doc-end()) : QTextCursor(doc-begin()); found doc-find(keyword, restart, flags); if (found.isNull()) { statusBar()-showMessage(tr(找不到%1).arg(keyword), 3000); return false; } statusBar()-showMessage(backward ? tr(已循环到文档末尾) : tr(已循环到文档开头), 3000); } editor-setTextCursor(found); editor-ensureCursorVisible(); return true; }逻辑说明QTextCursor::setPosition是移动到指定字符位置这里用selectionEnd()作为向前查找起点、selectionStart()作为向后查找起点本质是“跳过当前选区”。如果用户没有选区selectionEnd 和 selectionStart 都是光标当前位置行为正常。doc-find(keyword, searchStart, flags)的重载从 searchStart 位置开始搜索找到后返回一个新的光标指向匹配文本并选中它。循环查找的 restart 分支用QTextCursor(doc-end())作为向后查找的起点注意这个光标的构造方式直接在位置构造比doc-end()返回的迭代器再转光标要简洁。这里还有一个小坑如果 keyword 是空字符串QTextDocument::find 会返回无效光标所以函数开头做了 empty 判断。statusBar 提示不要用tr(找不到%1)的字符串拼接代替argQt 的翻译机制依赖 arg 方式硬拼接会导致中文环境下翻译失效。4.2 全部替换与高亮所有匹配ExtraSelection 的正确用法替换功能通常分两步先查找目标再点击替换。批量替换直接遍历 QTextDocument 最高效。下面是一组“替换一个”和“高亮所有匹配”的代码。bool MainWindow::replaceOne(const QString findText, const QString replaceText) { QTextCursor cursor editor-textCursor(); if (!cursor.hasSelection() || cursor.selectedText() ! findText) return false; cursor.insertText(replaceText); findNext(findText, caseSensitiveChecked, backwardChecked); return true; } void MainWindow::highlightAll(const QString keyword) { QListQTextEdit::ExtraSelection selections; editor-setExtraSelections(selections); if (keyword.isEmpty()) return; QTextDocument *doc editor-document(); QTextCursor it; int count 0; while ((it doc-find(keyword, it, QTextDocument::FindCaseSensitively)).isNull() false) { if (count 1000) break; // 防止匹配过多导致界面卡顿 QTextEdit::ExtraSelection sel; sel.cursor it; sel.format.setBackground(QColor(#FFF2A8)); sel.format.setForeground(Qt::black); selections.append(sel); } editor-setExtraSelections(selections); }参数说明doc-find(keyword, it)的第二个参数是查找起点。第一次传入默认构造的 QTextCursor等价于从文档开头找每次匹配后返回的光标已经移动到匹配文本后面所以循环不会无限重复。QTextEdit::ExtraSelection包含一个 cursor 和一个 format代表“文档中一块要被特殊渲染的选区”。setExtraSelections是一次性替换全部所以每次重新搜索前要先清空上一次的列表否则旧高亮会叠加。这里要特别注意cursor.selectedText() ! findText的比较QTextCursor::selectedText 会把段落分隔符转成 U2029直接和普通字符串比较遇到跨行匹配会不相等。不过记事本的查找串一般不含换行这个判断够用。替换后调用insertText会自动重写选区光标保持在插入位置下一步要继续查找的话需要像 4.1 那样从选区末尾接着搜。4.3 状态栏字数、行数、行列号统计的显示策略文本统计是记事本最直观的反馈。很多实现用editor-toPlainText().length()数字符这会把段落符也算进去导致行尾多一个字符。正确做法是基于 QTextDocument 的 characterCount。void MainWindow::refreshStatusBar() { QTextDocument *doc editor-document(); int chars doc-characterCount() - 1; // characterCount 包含最后一个隐式段落符 int lines doc-lineCount(); int words countWords(editor-toPlainText()); statusBar()-showMessage(tr(字符%1 字数%2 行数%3) .arg(chars).arg(words).arg(lines)); } int MainWindow::countWords(const QString text) { QRegularExpression re([A-Za-z0-9_]|[\\u4e00-\\u9fa5]); QRegularExpressionMatchIterator it re.globalMatch(text); int count 0; while (it.hasNext()) { it.next(); count; } return count; }说明characterCount() - 1是去掉文档末尾隐藏的段落标记得到真正的字符数。lineCount 直接读取文档结构比text.split(\n).size()更高效尤其在大文件上优势明显。countWords 用正则把“连续英文数字下划线”算一个词单个汉字算一个词这符合中文软件的常见习惯但不适合统计中文词组。这个函数里正则表达式每次调用都会编译性能敏感时可以把它定义为静态成员变量。refreshStatusBar 挂在编辑器的 textChanged 信号上每次按键都会触发对于 10MB 级大文件会有轻微开销真碰到超大文件可以改成 QTimer 300ms 去抖只在用户停止输入 300ms 后才刷新。5. Qt 记事本最容易踩的 5 个坑版本混用、平台插件与闪退排查5.1 fatal: cannot mix incompatible qt library (version ex50601) with this librar这是 Qt 开发里出现频率极高的编译期报错完整提示是cannot mix incompatible qt library (version ex50601) with this librar。现象是编译链接时报错甚至可能在运行qmake后就立刻出现。原因基本可以锁定为“编译工具链看到了两套 Qt”最常见的情况是系统 PATH 里先排到了旧版本的 Qt bin 目录而 Qt Creator 的 Kit 用的是新版本或者你手动安装了 Qt 5.6 和 Qt 5.15qmake.exe 被环境变量指到了旧的那一份。还有一种发生在库文件层面链接器先找到了 Qt5Core.dll又找到另一个目录的 Qt5Widgets.dll版本号一个 5.6 一个 5.15于是直接罢工。排查办法是在命令行里先确认实际生效的 qmakewhere qmake qmake -vWindows 用whereLinux 用which。如果 qmake 的路径和 Qt Creator Kit 里选的版本对不上那就把环境变量 PATH 里旧 Qt 的 bin 目录删掉。还有一类是“同一个 Qt 安装目录下既有 MSVC 构建的库又有 MinGW 构建的库”这种情况最容易发生在反复重装之后。遇到它没什么优雅解法我一般直接卸载 Qt删除安装目录清掉残留的环境变量然后重装一套 5.15 LTS并固定用同一套编译器。这套操作看起来像玄学但确实比手动改一处漏一处省时间。5.2 qt.qpa.plugin: Could not find the qt platform plugin linuxfb把 Qt 程序部署到树莓派或 ARM 嵌入式板上运行时终端经常会打出qt.qpa.plugin: Could not find the qt platform plugin linuxfb然后程序直接退出。这个报错的本质是 Qt 的 QPA 插件没加载到。Qt 的 GUI 在 Linux 上依赖平台插件来对接不同的显示系统xcb 对应 X11linuxfb 对应对接帧缓冲。运行时 Qt 会去 plugins/platforms 目录查找插件找不到就报这个错。解决方法是给程序指定插件路径和平台类型export QT_QPA_PLATFORM_PLUGIN_PATH/opt/Qt/5.15.2/plugins/platforms export QT_QPA_PLATFORMlinuxfb ./your_app如果只想在代码里兜底可以在 main 函数最前面加qputenv(QT_QPA_PLATFORM_PLUGIN_PATH, /opt/Qt/5.15.2/plugins/platforms);注意 qputenv 必须在 QApplication 构造之前执行否则插件加载已经走完了再设置就来不及。交叉编译场景下还要检查一件事libqlinuxfb.so 依赖的 libQt5Gui、libQt5Core 是否也一起拷到板子上可以用ldd libqlinuxfb.so查看。很多树莓派部署翻车不是因为路径没写对而是依赖库不全导致加载插件时静默失败。5.3 Windows 11 打开旧文件中文乱码编码探测比事后转码省事很多人在 Windows 10/11 上用自写的 Qt 记事本打开以前 Windows 记事本保存的 txt发现中文全是乱码。原因很明确Windows 老版本记事本“另存为”时的默认编码是 ANSI中文环境下就是 GBK而 Qt 5 内部默认按 UTF-8 解码字符串。高版本 Windows 记事本本身已经默认 UTF-8但历史遗留的老文件编码不会因此自动升级。解决思路不在事后转码而是在打开文件时做编码探测也就是第 3.1 节那段逻辑BOM 优先其次按 UTF-8 严格解码出现替换符再退回 GB18030。很多教程直接教QTextCodec::codecForLocale()这在 Qt 5 的多语言环境里不可靠codecForLocale 返回的编码随系统区域设置变化并不是一个稳定的“中文编码”。自己写探测逻辑反而更可控。保存时默认带 BOM 也是配合这个策略带 BOM 的文件再次打开时不需要猜测编码直接走 UTF-8 分支能在很大程度上降低乱码概率。5.4 双击 exe 崩溃 0xc0000005dll 没打包全的踩坑记录Qt 程序在开发机 Qt Creator 里跑得好好的把 exe 拷到别的机器双击后直接闪退Windows 事件日志里记录异常代码0xc0000005这是 Access Violation。这个错误在 Qt 开发里的排名极高根源九成是依赖的 Qt 动态库没有部署完整。Qt 的 Release 模式 exe 依赖 Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll还依赖 platforms/qwindows.dll缺任何一个都可能闪退。更隐蔽的是把 Debug 版本的 Qt5Cored.dll 当成 Release 库复制了过去文件名带 d 结尾和 Release exe 一起用就会出问题。解决办法是使用 Qt 官方提供的部署工具 windeployqt在 Qt 命令行里执行windeployqt --release --no-translations your_app.exe它会自动分析 exe 的依赖把需要的 Qt dll 和 platforms 插件复制到 exe 旁边。执行完后检查 exe 同目录下是否有platforms/qwindows.dll和 Qt5Core.dll、Qt5Widgets.dll。最重要的经验是不要从别的机器或别的编译器环境手工拷贝 dllMinGW 构建的 dll 和 MSVC 构建的 dll 混用后续大概率会引出 5.1 那种版本不兼容报错。如果程序还依赖第三方库比如 OpenSSL用 windeployqt 部署后还要再单独处理。5.5 关闭前提示保存closeEvent 与 windowModified 的组合记事本最容易流失的数据不是文件损坏而是用户改了一堆内容后直接点关闭按钮走人。默认情况下 QMainWindow 关闭不会询问保存所以必须重写 closeEvent。void MainWindow::closeEvent(QCloseEvent *event) { if (!editor-document()-isModified()) { event-accept(); return; } QMessageBox::StandardButton ret QMessageBox::question( this, tr(未保存), tr(文件内容已修改是否保存), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); if (ret QMessageBox::Save) { if (saveFile()) { event-accept(); } else { event-ignore(); } } else if (ret QMessageBox::Discard) { event-accept(); } else { event-ignore(); } }逻辑说明editor-document()-isModified()是 Qt 内置的文档修改标记编辑器内容一旦变化就自动置真调用setModified(false)会复位不需要自己维护 bool 变量。QMessageBox 的三个按钮对应保存、不保存、取消保存失败时不能关闭窗口所以调用event-ignore()用户选取消同样必须忽略关闭事件。这段代码配合标题栏的setWindowModified使用标题会显示一个星号让用户在视觉上也知道当前处于未保存状态。6. 把换行符替换与编码选择做成可切换功能跨系统不乱码的收尾技巧6.1 换行符替换菜单只用一次遍历完成 CRLF、LF、CR 互相转换不同来源的文本换行符不一致是很多人搜“notepad 换行符替换”的真正需求。Windows 记事本老版本认 CRLFLinux 工具链和 Git 默认 LF老 Mac 用 CR。在记事本里做成一个“换行符转换”菜单项只需一个函数void MainWindow::convertLineEnding(const QString target) { QString text editor-toPlainText(); text.replace(\r\n, \n); // 先把 CRLF 统一成 LF text.replace(\r, \n); // 再处理单独 CR if (target QLatin1String(CRLF)) text.replace(\n, \r\n); else if (target QLatin1String(CR)) text.replace(\n, \r); editor-setPlainText(text); }参数说明这个函数先统一成 LF再按目标格式转换避免\r\n被二次替换。比如直接replace(\r\n, \r)会把一个 CRLF 变成一个单独的 CR逻辑没问题如果反过来先替换单独的\r就会翻车。函数不做 undo因为它直接操作纯文本QPlainTextEdit 的撤销栈会被清空如果一定要支持撤销可以改用编辑器提供的 replace 接口或者 QTextCursor 的 beginEditBlock/endEditBlock 包一层。6.2 记录上次目录与编码QSettings 让记事本记住用户习惯每次打开文件都从头翻目录体验并不好。QSettings 能记住上次打开的目录这个细节能让工具显得成熟很多QSettings settings; QString lastDir settings.value(lastOpenDir, QDir::homePath()).toString(); QString path QFileDialog::getOpenFileName( this, tr(打开), lastDir, tr(文本文件 (*.txt);;所有文件 (*))); if (!path.isEmpty()) { settings.setValue(lastOpenDir, QFileInfo(path).absolutePath()); openFile(path); }注意QSettings 默认构造要求 QApplication 已经设置了 organizationName 和 applicationName否则会找到无效的存储位置。第 2.2 节里 main 函数那两行setApplicationName和setOrganizationName在这里才真正发挥作用。保存路径也一样记录用户在“打开→修改→保存”之后第二次打开自动停在同一个目录。这个交互微不足道但实际使用频率极高属于典型的低成本高收益功能。6.3 发布前自测清单用 4 个样本文件验证编码和换行符记事本最怕的问题是小样本看着正常换一批文件就乱码。我每次做完这类工具都会准备 4 个测试样本UTF-8 无 BOM 中文文本、GBK 编码中文文本可以用 Windows 记事本“另存为 ANSI”生成、CRLF 换行的 Windows 文本、LF 换行的 Linux 文本。验证顺序是用自写记事本打开第一个文件中文不乱码且字数正确打开第二个文件不乱码打开第三个文件状态栏行数与源文件一致保存后到“记事本”里重新打开显示正常。如果四个样本全过就再测一次 10MB 级大文件打开是否卡死以及只读文件打开时是否给出提示。这一套走完基本可以放心把程序发给同事用了。我自己的习惯是把“打开前检测未保存修改”和“关闭前询问保存”这两段逻辑最先写好因为它们决定工具会不会害人丢数据至于界面多漂亮、动画多流畅反而是最后才考虑的事。文本工具这种日用品稳定性永远排在功能前面。希望这份笔记能帮你在做记事本类 Qt 工具时少走几个弯路。本文还有配套的精品资源点击获取
返回列表