资讯动态

Qt 记事本源码解析:qmake、信号槽与嵌入式交叉编译

发布时间:2026/10/30 3:10:19 来源:尧图企业网站定制
简介面向有C基础的学习者与嵌入式GUI入门者的Qt6记事本实战工程围绕跨平台文本编辑器展开涵盖主窗口交互、文本编辑控件封装、文件读写与多编码识别借助uchardet库、界面布局与资源打包等模块可用于理解信号与槽机制、QWidget窗口组织方式以及智能指针、范围for等现代C写法在Qt中的落地。压缩包共516个文件、约4.29MB以编码识别库的idx索引文件与png图标资源为主另含cpp与h源码、o与a编译产物、ui界面文件、qrc资源文件及pro工程配置源码与构建产物齐备导入Qt Creator即可对照调试运行。已有778人学习下载。项目在内存占用与模块裁剪上的取舍也可作为嵌入式场景的参考适合按模块拆读逐步还原一个可运行的桌面小工具。1. 记事本虽小这个 Qt 项目能干什么、适合谁很多人第一次真正把 Qt 跑起来都是从写一个 Qt 记事本开始的。这份源码就是典型的小体量、五脏俱全一个主窗口、一块文本编辑区菜单栏、工具栏、状态栏齐全新建、打开、保存、另存为、查找替换、字体设置、光标行列显示一个不缺。交互逻辑对标我们熟悉的系统记事本底层是 Qt Widgets 的信号槽加 QFile/QTextStream 读写代码量不大但骨架完整该有的类边界都有。它适合两类人一类是刚把 C 语法过完想找个能立刻看见图形界面的东西练手另一类是做嵌入式 Linux 的板子上需要一个本地文本查看和编辑工具交叉编译过去就能用。关键词里的「qt 嵌入式」正好点出第二个落点——桌面跑通只是第一步真正见功力的是能不能在开发板上把窗口拉起来。2. 工程结构与 .pro 配置qmake 骨架和目录怎么搭拿到一份 Qt 源码我习惯先不看 cpp先看.pro。它是 qmake 的工程描述文件决定了这个项目链接哪些模块、编译哪些源文件、产物叫什么名字。记事本这种规模.pro通常就十几行但这十几行里每一行都有讲究改错一行就是一堆undefined reference。2.1 .pro 文件qmake 的工程描述就这几行一个能跑的最小版本大概长这样# 模块声明core 是 QtCoregui 是 QtGui QT core gui # Qt5 之后 QWidget 被拆到单独的 widgets 模块 # 这行是为了同时兼容 Qt4 和 Qt5/6 greaterThan(QT_MAJOR_VERSION, 4): QT widgets # 产物名称Linux 下没有后缀Windows 下会自动补 .exe TARGET Notepad # 工程模板app 表示生成可执行程序lib 表示生成库 TEMPLATE app # 参与编译的源文件和头文件反斜杠是续行 SOURCES \ main.cpp \ mainwindow.cpp \ finddialog.cpp HEADERS \ mainwindow.h \ finddialog.h # 如果用了 .ui 拖控件还需要这一行 # FORMS mainwindow.ui # C 标准Qt5.12 之后建议显式指定 CONFIG c11QT core gui这两行是新手最容易忽略的地方。core提供QString、QFile、QTextStream这些非界面类gui提供窗口系统相关的基础设施而widgets才是QMainWindow、QTextEdit这些控件的归属。Qt5 把 widgets 从 gui 里拆出去之后很多老教程里的.pro直接拷过来编译会报找不到QMainWindow就是少了greaterThan(...)那一行。TEMPLATE app和TEMPLATE lib的区别也常被搞混前者生成可执行文件并自动加main入口后者生成动态库、不加入口做成组件复用时才改成 lib。SOURCES和HEADERS里的文件如果漏写症状很典型编译阶段正常链接阶段报undefined reference to MainWindow::MainWindow(QWidget*)。原因不是代码错了是 moc 没有对那个头文件生成元对象代码因为 qmake 根本没把mainwindow.h纳入管理。改.pro之后一定要重新执行一次qmake只make是不会重新生成 Makefile 的这一步我踩过不止一次。2.2 目录结构与类的职责划分这个项目的文件组织是标准的 Qt Widgets 单窗口结构直接平铺在工程根目录没有多余的子目录文件职责notepad.proqmake 工程描述模块、目标名、源文件清单main.cpp程序入口创建QApplication和主窗口mainwindow.h主窗口类声明槽函数、成员控件、私有方法mainwindow.cpp界面搭建、菜单动作、文件读写实现finddialog.h/.cpp查找替换对话框独立于主窗口把查找对话框单独拆成一个类是这个项目在结构上比较讲究的地方。很多入门版本会把查找输入框直接塞进主窗口的工具栏用一个QLineEdit顶着代码是短了但查找的历史记录、大小写选项、向上向下方向这些状态全堆在主窗口里主窗口很快就变成几千行的黑匣子。拆成FindDialog之后查找相关的状态封闭在对话框内部主窗口只负责接收「要找什么、什么方向」这一个信号职责一下就清楚了。主窗口类的声明部分值得逐行看一遍class MainWindow : public QMainWindow { Q_OBJECT // 必须有moc 靠它生成元对象代码 public: explicit MainWindow(QWidget *parent nullptr); protected: void closeEvent(QCloseEvent *event) override; // 关闭前拦一道问要不要保存 private slots: void newFile(); void openFile(); bool saveFile(); bool saveAsFile(); void findText(); void setEditorFont(); void about(); private: void createActions(); // 先建动作 void createMenus(); // 再用动作拼菜单 void createToolBars(); void createStatusBar(); bool maybeSave(); // 有未保存改动时弹窗确认 void loadFile(const QString path); bool writeFile(const QString path); QTextEdit *textEdit; QString currentFile; // 当前文件绝对路径空表示未保存过 QAction *newAct, *openAct, *saveAct, *saveAsAct, *exitAct; QAction *cutAct, *copyAct, *pasteAct, *findAct, *fontAct; QLabel *posLabel; // 状态栏右下角显示「行 x, 列 y」 };Q_OBJECT宏是信号槽能工作的前提它让 moc 在编译前生成一份moc_mainwindow.cpp。少了它connect会在运行时报QObject::connect: No such slot而且编译期完全不报错属于典型的运行期玄学问题。currentFile这个成员的设计也很关键它同时承担「当前文件路径」和「是否已存在磁盘上」两个语义saveFile()里判断它是不是空串就能决定该直接写还是转去saveAsFile()弹文件对话框。2.3 为什么用 Qt Widgets 而不是 QML这个项目选 Widgets 而不是 QML是有现实理由的。记事本的交互模型是「密集的桌面控件 键盘快捷键 菜单结构」Widgets 天生就是为这套东西设计的QMenuBar、QToolBar、QStatusBar直接挂在QMainWindow上QAction一处定义、菜单和工具栏两处复用这些能力开箱即用。QML 更适合触摸屏、动画多、界面自绘的场景做记事本这种传统桌面程序反而要写一堆 Qt Quick Controls 的适配代码。放到嵌入式语境下这个选择更明显。Widgets 的运行时依赖是libQt5Widgets.so、libQt5Gui.so、libQt5Core.so这几个库交叉编译后拷到板子上就能跑QML 还要额外的 QML 引擎和场景图渲染后端对没有 GPU 的板子来说linuxfb平台插件配 Widgets 是最省事的组合。不过 Widgets 也不是没代价——它默认走 CPU 渲染界面动画基本没有视觉上是「能用」而不是「好看」。做工具类程序这恰恰是优点做产品级界面就得掂量了。3. 主窗口拆解菜单栏、工具栏与文件读写完整链路界面搭建这部分代码看起来零碎其实有一条很固定的推演顺序先建 QAction再用 QAction 拼菜单和工具栏最后把 QAction 的 triggered 信号接到槽函数上。这条链路走通了整个主窗口就活了。文件读写则是另一条链路用户点「打开」→ 弹文件对话框 → 拿到路径 → QFile 打开 → QTextStream 读出文本 → 塞进 QTextEdit → 更新窗口标题和 modified 状态。3.1 main.cpp 与 QApplication 的入口约定入口文件短得可以背下来但每一行都有必要#include mainwindow.h #include QApplication int main(int argc, char *argv[]) { // 每个 Qt GUI 程序有且只有一个 QApplication 实例 QApplication app(argc, argv); // 设置全局应用名QSettings 默认会拿它当配置目录名 app.setApplicationName(QStringLiteral(Notepad)); MainWindow w; w.show(); // 进入事件循环返回值作为进程退出码 return app.exec(); }QApplication必须在任何 QWidget 之前构造因为它初始化了窗口系统连接、字体数据库、样式表解析器这一整套基础设施。把QApplication换成QCoreApplication程序会直接崩在QWidget构造那一行因为后者不带 GUI 支持。app.exec()是阻塞调用进入 Qt 的事件循环鼠标点击、键盘输入、定时器全部靠这个循环分发直到最后一个窗口关闭才返回。这也是为什么主窗口的closeEvent里能拦下关闭动作——事件循环把关闭事件先交给窗口处理窗口选择接受还是忽略。3.2 MainWindow 构造函数五个 create 函数搭骨架构造函数本身很短重活都甩给了几个create*私有方法MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , textEdit(new QTextEdit(this)) // 传 this 让父对象负责析构 , posLabel(new QLabel(this)) { setCentralWidget(textEdit); // 编辑区占满中央区域 createActions(); createMenus(); createToolBars(); createStatusBar(); resize(1000, 700); setWindowTitle(QStringLiteral(无标题[*] - 记事本)); // 文档修改状态变化时自动刷新标题栏的 * 标记 connect(textEdit-document(), QTextDocument::modificationChanged, this, QWidget::setWindowModified); }setCentralWidget是QMainWindow的固定套路中央区域只能有一个控件菜单栏在顶部、工具栏在其下、状态栏在底部四块区域互不重叠。new QTextEdit(this)里那个this是父对象指针Qt 的对象树会负责在窗口析构时把它一起删掉所以代码里看不到delete textEdit这是 Qt 内存管理的基本约定。标题栏里的[*]是个特殊占位符配合setWindowModified(bool)使用当文档 modified 为 true 时[*]的位置会显示一个星号为 false 时消失。这个机制比手动拼字符串可靠得多因为星号位置会随平台变化有的系统放前面有的放后面交给 Qt 处理就不用操心。createActions()里定义动作、设快捷键、连信号void MainWindow::createActions() { newAct new QAction(QStringLiteral(新建(N)), this); newAct-setShortcuts(QKeySequence::New); // CtrlN跨平台自动适配 connect(newAct, QAction::triggered, this, MainWindow::newFile); openAct new QAction(QStringLiteral(打开(O)...), this); openAct-setShortcuts(QKeySequence::Open); // CtrlO connect(openAct, QAction::triggered, this, MainWindow::openFile); saveAct new QAction(QStringLiteral(保存(S)), this); saveAct-setShortcuts(QKeySequence::Save); // CtrlS connect(saveAct, QAction::triggered, this, MainWindow::saveFile); // 剪切/复制/粘贴的状态跟着编辑区光标走交给 Qt 内置的槽自动管理 cutAct new QAction(QStringLiteral(剪切(X)), this); cutAct-setShortcuts(QKeySequence::Cut); connect(cutAct, QAction::triggered, textEdit, QTextEdit::cut); connect(textEdit, QTextEdit::copyAvailable, cutAct, QAction::setEnabled); // 没选中就置灰 // 其余动作略 }用QKeySequence::New这类标准键而不是硬编码CtrlN好处是跨平台自动适配macOS 上会映射成 CommandN。copyAvailable(bool)这个信号在选中文本变化时发出接到QAction::setEnabled上就实现了「没选中时剪切按钮置灰」的效果不需要自己写判断逻辑。这类内置信号槽省下来的代码量在稍大一点的项目里非常可观。3.3 打开与保存QFile QTextStream 的读写链路文件读写是整个项目最值得仔细看的一段因为这里藏着编码、路径、未保存拦截三个坑点void MainWindow::openFile() { if (!maybeSave()) return; // 先问当前文件改了没要不要存 const QString path QFileDialog::getOpenFileName( this, QStringLiteral(打开文件), QDir::homePath(), // 默认落到用户主目录 QStringLiteral(文本文件 (*.txt);;所有文件 (*))); if (path.isEmpty()) return; // 用户点了取消 loadFile(path); } void MainWindow::loadFile(const QString path) { QFile file(path); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { QMessageBox::warning(this, QStringLiteral(打开失败), QStringLiteral(无法读取文件%1).arg(path)); return; } QTextStream in(file); in.setCodec(UTF-8); // Qt5 写法Qt6 换成 setEncoding(QStringConverter::Utf8) textEdit-setPlainText(in.readAll()); file.close(); currentFile path; setWindowTitle(QStringLiteral(%1[*] - 记事本) .arg(QFileInfo(path).fileName())); textEdit-document()-setModified(false); // 刚读完不算修改 }QIODevice::Text这个打开标志在 Linux 上没区别但在 Windows 上会把\r\n自动转成\n写出去的时候再转回来。少了它从 Windows 拷过来的文本会在 Linux 下每行末尾多一个不可见字符。setCodec(UTF-8)是 Qt5 的写法Qt6 已经把QTextStream::setCodec移除换成了setEncoding(QStringConverter::Utf8)。如果你拿到的源码是 Qt5 版本在 Qt6 环境下编译这一行会直接报「no member named setCodec」这也是跨大版本迁移最常见的断裂点之一。保存走的是另一条路saveFile判断当前有没有路径没有就转saveAsFile拿路径bool MainWindow::saveFile() { if (currentFile.isEmpty()) return saveAsFile(); // 新文件先问路径 return writeFile(currentFile); } bool MainWindow::writeFile(const QString path) { QFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::warning(this, QStringLiteral(保存失败), QStringLiteral(无法写入文件%1).arg(path)); return false; } QTextStream out(file); out.setCodec(UTF-8); out textEdit-toPlainText(); // 这里用 toPlainText 丢掉富文本格式 file.close(); currentFile path; textEdit-document()-setModified(false); setWindowTitle(QStringLiteral(%1[*] - 记事本) .arg(QFileInfo(path).fileName())); statusBar()-showMessage(QStringLiteral(已保存), 2000); // 2 秒后自动清除 return true; }toPlainText()而不是toHtml()决定了这个记事本存的是纯文本。如果改成toHtml()文件里会带一大坨 Qt 生成的 HTML 标签用其他编辑器打开就是乱码一样的结构。这一点初学者容易忽略以为QTextEdit既然是富文本控件就该存富文本实际上存什么格式完全由你输出时调用的方法决定。maybeSave()是关闭拦截的核心逻辑不难但所有入口都得调bool MainWindow::maybeSave() { if (!textEdit-document()-isModified()) return true; const auto ret QMessageBox::warning( this, QStringLiteral(记事本), QStringLiteral(文档有未保存的修改是否保存), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); if (ret QMessageBox::Save) return saveFile(); if (ret QMessageBox::Cancel) return false; return true; // Discard直接放行 }这个方法必须在「新建、打开、关闭窗口、退出程序」四个入口都调用一次。漏掉任何一个用户就能在未保存的情况下丢掉一整篇文档这是记事本类程序最不能忍的缺陷。4. 编辑增强实战查找替换、字体缩放与状态栏联动基础的文件读写跑通之后这个记事本的「编辑器」属性才真正开始体现。查找替换、字体设置、光标位置显示这三块分别对应了 Qt 里三种典型的交互模式模态对话框收集参数、系统对话框返回结果、信号驱动界面更新。4.1 查找对话框QDialog QTextDocument::find查找功能拆成了FindDialog主窗口只负责响应它的信号。对话框本身用一个QLineEdit输入关键词、两个QCheckBox管大小写和反向查找加一个「查找下一个」按钮void FindDialog::findNext() { const QString text keywordEdit-text(); if (text.isEmpty()) return; // 空关键词直接忽略不然会死循环 QTextDocument::FindFlags flags; if (caseBox-isChecked()) flags | QTextDocument::FindCaseSensitively; if (backwardBox-isChecked()) flags | QTextDocument::FindBackward; emit requestFind(text, flags); // 把参数抛给主窗口去执行 }主窗口侧的槽函数负责真正的定位还要处理「找到底了从头再来一遍」这个体验细节void MainWindow::findText() { if (!findDialog) { findDialog new FindDialog(this); connect(findDialog, FindDialog::requestFind, this, MainWindow::doFind); } findDialog-show(); findDialog-raise(); // 已经开着就提到最前不重复创建 findDialog-activateWindow(); } void MainWindow::doFind(const QString text, QTextDocument::FindFlags flags) { bool found textEdit-find(text, flags); if (!found) { // 当前光标已在文档尾/首回到另一端再找一轮 QTextCursor cur textEdit-textCursor(); cur.movePosition((flags QTextDocument::FindBackward) ? QTextCursor::End : QTextCursor::Start); textEdit-setTextCursor(cur); found textEdit-find(text, flags); } if (!found) statusBar()-showMessage(QStringLiteral(未找到%1).arg(text), 3000); }QTextEdit::find是从当前光标位置往后找的不会自动绕回文档开头。不加回绕逻辑用户搜到文末再点「查找下一个」就会一直停在最后一处看起来像功能坏了。回绕前要先把光标移到开头正向或末尾反向否则find会从原位置继续往边界方向找结果可能是原地不动。FindDialog用show()而不是exec()是为了做成非模态——用户能在查找框开着的同时继续点编辑区改文字。用exec()会锁住整个程序直到对话框关闭边查边改就没法做了。不过非模态对话框要注意生命周期不能建成栈对象必须是new出来的堆对象父指针设成主窗口主窗口析构时一起销毁。4.2 字体与格式QFontDialog 和 setFont字体设置这块用现成的系统对话框最省事void MainWindow::setEditorFont() { bool ok false; const QFont current textEdit-font(); const QFont chosen QFontDialog::getFont(ok, current, this, QStringLiteral(选择字体)); if (ok) { textEdit-setFont(chosen); // ok 是输出参数用户点取消时为 false } }QFontDialog::getFont的第一个参数是bool*输出参数只有用户点了确定才置 true。这个模式在 Qt 里很常见QFileDialog::getOpenFileName用返回空串表示取消QColorDialog::getColor用ok输出参数记住每种对话框的约定能少写不少判断。setFont作用在QTextEdit上时会作用于整个控件的默认字体已经带格式的富文本片段不受影响。如果做的是纯文本记事本这个行为没问题如果希望统一覆盖全部内容得遍历文档里的所有QTextFragment逐个设字符格式代码量会翻好几倍。做纯文本编辑的场景这里不用纠结。字体选择还牵扯一个「持久化」问题用户这次选了 16 号宋体下次打开程序又变回默认字体。常见做法是用QSettings存一下构造时读出来应用// 保存 QSettings settings; // 默认写到用户配置目录 settings.setValue(editor/font, textEdit-font()); // 读取 QSettings settings; const QFont f settings.value(editor/font, QFont()).valueQFont(); if (!f.family().isEmpty()) textEdit-setFont(f);QSettings在 Linux 下落到~/.config/应用名/应用名.confWindows 下走注册表跨平台不用改代码。这是 Qt 里做配置持久化最轻的方案避免自己写 ini 解析。4.3 状态栏联动cursorPositionChanged 信号状态栏右下角显示「行 x, 列 y」实现只需要一个信号连接void MainWindow::createStatusBar() { statusBar()-addPermanentWidget(posLabel); // 靠右固定不会被临时消息盖掉 connect(textEdit, QTextEdit::cursorPositionChanged, this, [this] { const QTextCursor cur textEdit-textCursor(); const int line cur.blockNumber() 1; // blockNumber 从 0 起 const int col cur.columnNumber() 1; // columnNumber 也是 0 起 posLabel-setText(QStringLiteral(行 %1列 %2).arg(line).arg(col)); }); }addPermanentWidget和addWidget的区别值得记一下前者加进去的控件在最右侧且不会被子类调用showMessage弹出的临时消息覆盖后者加在左侧临时消息一出现就被顶掉。行列显示属于常驻信息必须用addPermanentWidget。blockNumber()和columnNumber()都是从 0 开始的加 1 是为了符合编辑器的通用显示习惯。blockNumber数的是「文本块」对纯文本来说就是行号如果内容里有硬换行ShiftEnter 那种软换行不算新块块数还是按逻辑行算这点和视觉行数会不一致做行号栏的时候要注意。lambda 里捕获this是安全的因为连接的生命周期和textEdit、this一致主窗口析构时连接自动断开。如果捕获的是局部变量或者外部对象指针就得考虑QObject::connect的第五个参数Qt::UniqueConnection或者手动disconnect了否则对象销毁后再触发信号就是野指针直接段错误。5. 编译运行与排查桌面、交叉编译和必踩的坑写到这里代码层面的东西基本清楚了。真正把人卡住的往往不是代码是编译环境。桌面版跑不起来、交叉编译出来的程序在板子上黑屏、改了.pro没生效这些问题的排查路径其实很固定只是没人一次性讲全。5.1 桌面端编译qmake、make 与常见报错先确认环境里有没有 qmakeqmake --version # 正常输出类似 # QMake version 3.1 # Using Qt version 5.15.2 in /usr/lib/x86_64-linux-gnu如果提示 command not found说明只装了编译器没装 Qt 开发包。Debian/Ubuntu 系一般是qtbase5-dev加qt5-qmake装完之后qmake和moc、uic这些工具都会进 PATH。编译三步走cd notepad qmake notepad.pro # 生成 Makefile make -j4 # 并行编译-j 后跟核数 ./Notepad # 运行产物名字由 TARGET 决定make -j4里的 4 按自己机器核数调nproc能查。小项目体会不到差别文件一多并行编译能省一半时间。常见的几个报错和对应原因Project ERROR: Unknown module(s) in QT: widgets—— 装的是 Qt4 或者只装了 QtCore缺 widgets 开发包。cannot find -lQt5Widgets—— 链接器找不到库通常是 Qt 装在了非标准路径需要在.pro里加LIBS -L/path/to/qt/lib或者用qmake -query QT_INSTALL_LIBS确认实际位置。undefined reference to vtable for MainWindow—— moc 没跑。检查头文件里有没有Q_OBJECT以及.pro的HEADERS里有没有列上这个文件。改完记得重新qmake。程序能编译运行时报could not find or load the Qt platform plugin xcb—— 缺 xcb 平台插件装libqt5gui5对应的插件包或者用QT_DEBUG_PLUGINS1 ./Notepad看它到底在哪些路径找插件。5.2 交叉编译到嵌入式板子工具链与平台插件交叉编译的核心思路是用目标板对应的 qmake而不是桌面版的 qmake。桌面版 qmake 生成的 Makefile 里写死了g编出来的程序架构是 x86拷到 ARM 板子上根本执行不了。前提是你已经有一套为目标板编好的 Qt 库路径假设是/opt/qt-arm/。编译流程# 先确认这个 qmake 是给 ARM 编的 /opt/qt-arm/bin/qmake -query QT_INSTALL_LIBS # 期望输出类似 /opt/qt-arm/lib而不是 /usr/lib/x86_64-linux-gnu /opt/qt-arm/bin/qmake notepad.pro make -j4 file Notepad # 输出里应该出现 ARM 或 aarch64 字样出现 x86-64 说明用错 qmake 了如果手上没有现成的交叉版 qmake就得自己编一份 Qt 源码配置阶段指定工具链和平台插件./configure \ -prefix /opt/qt-arm \ -xplatform linux-arm-gnueabi-g \ -no-opengl \ -nomake examples -nomake tests \ -qt-libjpeg -qt-libpng-xplatform指定的是qtbase/mkspecs/下的目录名里面那个qmake.conf定义了QMAKE_CC、QMAKE_CXX具体用什么交叉编译器。-no-opengl对没有 GPU 的板子很关键带上 OpenGL 会让 Qt 依赖一堆图形驱动库板子上根本跑不起来。产物拷到板子上运行时还有两个环境变量必须设# 告诉 Qt 用哪个平台插件没有 X11 的板子走 framebuffer export QT_QPA_PLATFORMlinuxfb # 告诉动态链接器去哪找 Qt 库 export LD_LIBRARY_PATH/opt/qt-arm/lib:$LD_LIBRARY_PATH ./Notepad有触摸屏的板子linuxfb插件还得配合 tslib 才能拿到触摸事件编译 Qt 时要加-tslib运行时设TSLIB_TSDEVICE/dev/input/event1之类的设备节点。这一步查设备节点最靠谱的方式是cat /proc/bus/input/devices看哪个设备标着 touchscreen。5.3 排查清单五条血泪经验下面这几条是我在这类 Qt 嵌入式项目上真翻过的车按现象、原因、解决三段式记下来下次遇到能直接对号入座。第一条改了.pro但编译行为没变。现象是往SOURCES里加了一个新 cppmake之后报链接错误说找不到那个类的符号。原因是make只读现成的 Makefile而 Makefile 是qmake根据.pro生成的.pro改了不重新qmakeMakefile 里根本没有新文件。解决很简单每次动完.pro就qmake一次图省事可以写个别名把qmake make绑在一起。第二条桌面跑得好好的交叉编译出来在板子上黑屏终端没任何报错。现象是程序启动后直接退出退出码 0日志一片空白。原因多半是平台插件没找到Qt 默认会尝试xcb板子上没有 X11 就直接放弃。排查方法是启动前加export QT_DEBUG_PLUGINS1它会打印出插件搜索的所有路径和失败原因一眼就能看到是「找不到 platforms 目录」还是「加载 libqlinuxfb.so 失败」。解决是把plugins/platforms/整个目录拷到板子上 Qt 库同级的位置或者用-platform linuxfb显式指定。第三条中文显示成方块或者问号。现象是界面上的中文按钮全部变成豆腐块。原因是板子上的 Qt 没带中文字体或者 fontconfig 找不到字体目录。解决是把一个中文字体文件比如常见的开源黑体拷到板子的/usr/share/fonts/下然后fc-cache -fv刷一下缓存如果板子上没装 fontconfig就在程序启动时用QFontDatabase::addApplicationFont(/path/to/font.ttf)手动注册返回的 family 名再setFont应用上去。第四条打开别人给的 txt内容乱码。现象是文件里明明是中文读进来全是问号或奇怪符号。原因是setCodec(UTF-8)硬编码而源文件可能是 GBK 编码。解决思路是先探测编码再解码简单做法是读原始字节判断有没有 UTF-8 BOMEF BB BF没有就按本地编码试一次更稳的做法是引入编码探测逻辑读前几千字节做统计判断。记事本类程序如果不做这一步用户拿到什么文件都可能乱码。第五条关窗口时程序卡住或者直接崩。现象是点右上角关闭弹了「是否保存」的框点保存之后程序没退出或者直接段错误。原因通常是closeEvent里调了maybeSave()但没根据返回值决定event-accept()还是event-ignore()或者maybeSave里的saveFile又触发了别的窗口事件形成递归。正确写法是void MainWindow::closeEvent(QCloseEvent *event) { if (maybeSave()) event-accept(); // 放行窗口关闭事件循环退出 else event-ignore(); // 拦下窗口留着 }event-ignore()是后悔药没有它用户点了取消之后窗口照样关掉未保存的内容就真丢了。6. 二次开发改造从记事本到可复用编辑器组件这份源码最容易被低估的地方是它其实是一块可以反复利用的积木。真要做二次开发我建议第一步先把MainWindow里的编辑能力抽成一个独立控件而不是在主窗口上继续堆功能。6.1 抽成可复用编辑组件把QTextEdit加上文件读写、查找、状态更新这一套封成一个TextEditorWidget对外只暴露loadFromFile、saveToFile、isModified三个方法主窗口退化成纯粹的容器。这样同一份编辑逻辑既能塞进记事本也能塞进日志查看器、配置编辑器甚至做成多标签页——多标签页版本就是QTabWidget里每个 tab 放一个TextEditorWidgetcurrentChanged信号里切换状态栏的宿主。6.2 三条可以往下走的改造线改造方向落点关键点自动保存QTimerQSettings记录草稿路径定时器间隔别小于 30 秒写盘用临时文件再 rename避免写一半断电编码自适应loadFile里替换硬编码的 UTF-8先判 BOM再用字节统计猜 GBK/UTF-8最后兜底按系统 locale大文件优化QTextEdit换QPlainTextEdit十万行以上 QTextEdit 会卡QPlainTextEdit 按块渲染内存和速度都好一个数量级换成QPlainTextEdit的时候要注意它没有setFont之外的富文本能力toHtml之类的方法不存在查找接口倒是兼容find的签名基本一致。这种替换最适合拿来做「确认改动范围」的练习改完编译一遍看哪几行报错就知道原代码对QTextEdit的依赖到底集中在哪些地方。验证改造是否成功我一般不看界面看三件事新建一个文件输入中文保存再打开内容一字不差打开一个 GBK 编码的旧文件不出现乱码连续开关窗口十次进程内存不持续增长。前两条靠手动第三条用top盯一下 RSS 就行涨了就说明有对象没被父指针接管属于典型的泄漏。血泪经验是这类教学向小项目最容易犯的错是拿到手就急着加功能结果把原本清晰的类边界搅乱改到最后连「保存」按钮该连哪个槽都说不清。我后来的习惯是动第一行代码之前先把.pro和主窗口头文件通读一遍画清楚哪个对象归谁管再动手。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑