简介这是一份基于Qt6开发的跨平台记事本项目源码面向嵌入式与桌面应用开发初学者及Qt进阶学习者聚焦GUI界面构建、文件编码处理与轻量级文本编辑功能实现。项目完整呈现了现代C在Qt框架下的典型工程实践涵盖主窗口widget、自定义文本编辑器mytextedit、文件读写与编码识别fileencoding等核心模块并包含.ui界面设计、.qrc资源管理、.pro构建配置及编译产物适合作为嵌入式GUI开发入门范例与跨平台应用结构学习样本。压缩包共516个文件含6个关键源码cpp/h、1个UI布局、1个资源文件、1个项目配置及大量编译中间文件idx/o/a与图标资源png整体4.29MB结构清晰、模块职责分明。目前已有776人学习下载读者可直接编译运行深入理解Qt6信号槽机制、资源嵌入流程、编码兼容性处理及嵌入式场景下的内存与性能优化思路。1. 为什么一个“QT记事本”能卡住90%刚学嵌入式GUI的新手它不是练手玩具而是Qt事件循环、文件I/O与跨平台UI渲染的三重压力测试场你打开B站搜“嵌入式 Qt 记事本”会刷出一堆标题带“社长手写”“零基础保姆级”的视频——但点进去看代码十有八九卡在“改完ui界面点击保存没反应”“中文乱码存成问号”“交叉编译后在开发板上打不开窗口”这三座大山。这不是你手残是这个看似最简单的项目实则把Qt核心机制全摊开了考QWidget生命周期怎么和QApplication事件循环咬合QFile在ARM平台读写时编码如何不丢帧QAction触发的槽函数为何在qmake未正确链接libqt5widgets.so时静默失败它适合两类人一类是刚用过STM32裸机点灯、正想摸GUI门道的嵌入式新人另一类是写过Linux终端程序、但第一次面对信号/槽机制和UI线程分离的C开发者。别被“记事本”三个字骗了——它不考算法专考你对Qt底层运行逻辑的肌肉记忆。下面所有步骤我都按某高校嵌入式实验室真实调试记录还原从Ubuntu 22.04主机环境起步目标平台为ARM64RK3399Qt版本锁定5.15.2LTS稳定分支所有命令可直接复制粘贴所有坑都附带strace -e traceopen,write,read抓到的真实系统调用证据。2. 从零搭起可交叉编译的Qt记事本工程qmake工程结构、ARM工具链配置与最小化UI骨架2.1 创建符合嵌入式部署要求的qmake工程结构Qt记事本不是写个main.cpp加个QTextEdit就完事。嵌入式场景下必须提前规划资源路径、字体加载方式和退出清理逻辑。常见错误是直接用Qt Creator默认模板结果在开发板上因找不到/usr/share/fonts崩溃。我采用分层目录结构notepad-embedded/ ├── notepad.pro # 主工程文件声明子目录与依赖 ├── src/ │ ├── main.cpp # QApplication初始化主窗口创建 │ ├── notepad.h # 主窗口类声明继承QMainWindow │ └── notepad.cpp # 实现菜单栏、工具栏、状态栏、文本编辑区 ├── resources/ │ └── fonts/ # 预置NotoSansCJK-Regular.ttc支持中文 └── build-arm64/ # 交叉编译输出目录不提交gitnotepad.pro关键内容如下注意注释里的嵌入式特有配置# notepad.pro QT core widgets gui TARGET notepad TEMPLATE app # 【嵌入式关键】禁用桌面特性减小体积 CONFIG - app_bundle CONFIG c17 # 【嵌入式关键】显式指定字体路径避免运行时搜索失败 DEFINES QT_NO_DEBUG_OUTPUT QMAKE_CXXFLAGS -O2 -marcharmv8-acrccrypto # 【嵌入式关键】资源文件打包进二进制避免运行时缺文件 RESOURCES resources.qrc # 源文件 SOURCES src/main.cpp \ src/notepad.cpp HEADERS src/notepad.h提示resources.qrc需手动创建内容为RCC qresource prefix/fonts fileresources/fonts/NotoSansCJK-Regular.ttc/file /qresource /RCC这样QFontDatabase::addApplicationFont(:/fonts/NotoSansCJK-Regular.ttc)才能在无文件系统权限时加载。2.2 配置ARM64交叉编译工具链并验证Qt构建环境不能直接用x86_64的qmake生成ARM可执行文件。必须先让Qt知道你的交叉编译器在哪。假设你已安装gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu路径为/opt/gcc-linaro# 步骤1创建专用mkspecQt构建规格描述 cp -r $QTDIR/mkspecs/linux-arm-gnueabi-g \ $QTDIR/mkspecs/linux-arm64-gnueabihf-g # 步骤2修改新mkspec中的编译器路径关键 sed -i s/arm-linux-gnueabihf-/aarch64-linux-gnu-/g \ $QTDIR/mkspecs/linux-arm64-gnueabihf-g/qmake.conf # 步骤3指向你的工具链 echo QMAKE_CC /opt/gcc-linaro/bin/aarch64-linux-gnu-gcc \ $QTDIR/mkspecs/linux-arm64-gnueabihf-g/qmake.conf echo QMAKE_CXX /opt/gcc-linaro/bin/aarch64-linux-gnu-g \ $QTDIR/mkspecs/linux-arm64-gnueabihf-g/qmake.conf echo QMAKE_LINK /opt/gcc-linaro/bin/aarch64-linux-gnu-g \ $QTDIR/mkspecs/linux-arm64-gnueabihf-g/qmake.conf验证是否生效# 在notepad-embedded/目录下执行 $QTDIR/bin/qmake -v # 确认Qt版本 $QTDIR/bin/qmake -query QT_INSTALL_LIBS # 看库路径是否指向ARM版Qt $QTDIR/bin/qmake -spec linux-arm64-gnueabihf-g notepad.pro make -j4 # 编译观察是否调用aarch64-linux-gnu-g若make输出中出现aarch64-linux-gnu-g -c ...且无/usr/lib/x86_64-linux-gnu/libQt5Widgets.so链接错误说明环境就绪。2.3 构建最小化UI骨架只保留QTextEditQStatusBar的“裸奔版”新手常犯的错是上来就加QMenuBar、QToolBar结果因Qt插件缺失如libqsvg.so导致QPainter::begin: Widget painting disabled。先做减法src/notepad.h#ifndef NOTEPAD_H #define NOTEPAD_H #include QMainWindow #include QTextEdit #include QStatusBar class Notepad : public QMainWindow { Q_OBJECT public: explicit Notepad(QWidget *parent nullptr); ~Notepad() override; private: QTextEdit *textEdit; QStatusBar *statusBar; private slots: void updateStatus(); // 实时显示光标位置 }; #endif // NOTEPAD_Hsrc/notepad.cpp仅初始化无菜单/保存逻辑#include notepad.h #include QVBoxLayout #include QWidget Notepad::Notepad(QWidget *parent) : QMainWindow(parent), textEdit(new QTextEdit(this)), statusBar(new QStatusBar(this)) { // 【嵌入式关键】强制设置字体避免系统字体不可用 QFont font(Noto Sans CJK SC, 12); font.setStyleStrategy(QFont::PreferAntialias); textEdit-setFont(font); // 设置中心部件 setCentralWidget(textEdit); setStatusBar(statusBar); // 【嵌入式关键】禁用窗口管理器装饰全屏运行 setWindowFlags(Qt::FramelessWindowHint); showFullScreen(); // 开发板通常无窗口管理器 // 实时状态更新 connect(textEdit, QTextEdit::cursorPositionChanged, this, Notepad::updateStatus); } Notepad::~Notepad() default; void Notepad::updateStatus() { QTextCursor cursor textEdit-textCursor(); int line cursor.blockNumber() 1; int col cursor.columnNumber() 1; statusBar-showMessage(QString(Ln %1, Col %2).arg(line).arg(col)); }src/main.cpp#include QApplication #include notepad.h int main(int argc, char *argv[]) { QApplication app(argc, argv); // 【嵌入式关键】设置应用属性避免X11/Wayland冲突 qputenv(QT_QPA_PLATFORM, linuxfb); // 直接Framebuffer输出 qputenv(QT_QPA_FONTDIR, :/fonts); // 从资源加载字体 Notepad window; window.show(); return app.exec(); // 进入事件循环——这是整个项目的心跳 }编译后拷贝到开发板运行scp build-arm64/notepad root192.168.1.10:/usr/bin/ # 开发板上执行确保Framebuffer设备存在 export QT_QPA_PLATFORMlinuxfb export QT_QPA_FONTDIR:/fonts /usr/bin/notepad若看到全屏文本框且状态栏显示行列号说明Qt GUI基础链路已通——这是后续所有功能的地基。3. 文件读写与编码处理QFile QTextCodec 的嵌入式安全组合拳3.1 为什么QFile::write()在ARM板上存中文会变问号根源在QTextCodec的隐式fallback现象PC上正常保存的UTF-8中文在开发板上打开全是????。这不是Qt bug是QTextCodec::codecForLocale()在ARM平台返回了System即GBK或ISO-8859-1而你的文件实际是UTF-8。QFile本身不处理编码它只负责字节流QTextStream才管编码转换。但新手常误用❌ 错误写法绕过QTextStream直接write QByteArrayQFile file(test.txt); if (file.open(QIODevice::WriteOnly)) { file.write(ui-textEdit-toPlainText().toUtf8()); // 问题没指定编码读取时无法反推 file.close(); }✅ 正确写法显式绑定UTF-8编码void Notepad::saveFile(const QString filePath) { QFile file(filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { statusBar-showMessage(Save failed: file.errorString(), 3000); return; } QTextStream out(file); // 【嵌入式关键】强制UTF-8不依赖locale out.setCodec(UTF-8); out ui-textEdit-toPlainText(); file.close(); statusBar-showMessage(Saved: QFileInfo(filePath).fileName(), 2000); }同理读取时也必须显式指定void Notepad::loadFile(const QString filePath) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { statusBar-showMessage(Load failed: file.errorString(), 3000); return; } QTextStream in(file); in.setCodec(UTF-8); // 必须和保存时一致 ui-textEdit-setPlainText(in.readAll()); file.close(); currentFile filePath; statusBar-showMessage(Loaded: QFileInfo(filePath).fileName(), 2000); }注意QTextStream::setCodec(UTF-8)比setEncoding()更底层它直接控制QTextCodec实例避免QTextCodec::setCodecForLocale()的干扰。3.2 嵌入式文件路径安全策略绝对路径、权限检查与SD卡热插拔容错开发板上文件系统不稳定如SD卡突然拔出QFile::open()可能返回false但errorString()为空。必须做双重校验bool Notepad::safeOpenFile(const QString path, QIODevice::OpenMode mode) { QFileInfo info(path); // 【嵌入式关键】检查路径合法性防../越界 if (!info.isAbsolute() || info.path().contains(..)) { statusBar-showMessage(Invalid path: path, 3000); return false; } // 【嵌入式关键】检查父目录是否存在且可写SD卡可能只读 if (mode QIODevice::WriteOnly) { QDir dir(info.path()); if (!dir.exists() || !dir.isWritable()) { statusBar-showMessage(No write permission: info.path(), 3000); return false; } } QFile file(path); if (!file.open(mode)) { // 【嵌入式关键】补充系统调用级错误strace验证过 if (file.error() QFile::PermissionsError) { statusBar-showMessage(Permission denied: path, 3000); } else if (file.error() QFile::ResourceError) { statusBar-showMessage(SD card removed?, 3000); } return false; } return true; }调用时// 保存时 if (safeOpenFile(currentFile, QIODevice::WriteOnly | QIODevice::Text)) { // 执行QTextStream写入... }3.3 中文输入法兼容性QInputMethodEvent与QTextEdit的事件穿透处理在ARM触控屏上软键盘输入中文常出现“输入法候选框不弹出”或“输入后光标错位”。这是因为QTextEdit默认拦截了QInputMethodEvent。解决方案是在notepad.h中声明事件过滤器protected: bool eventFilter(QObject *obj, QEvent *event) override;notepad.cpp中实现bool Notepad::eventFilter(QObject *obj, QEvent *event) { if (obj textEdit event-type() QEvent::InputMethod) { QInputMethodEvent *imeEvent static_castQInputMethodEvent*(event); // 【嵌入式关键】透传给输入法框架 if (imeEvent-commitString().isEmpty()) { return false; // 让Qt输入法框架处理 } } return QMainWindow::eventFilter(obj, event); } // 在构造函数末尾添加 textEdit-installEventFilter(this);血泪经验此补丁必须在showFullScreen()之后安装否则输入法服务无法绑定到窗口。4. 常见问题排查5个让嵌入式Qt记事本“静默死亡”的真实坑位4.1 现象程序启动后黑屏strace显示反复open(/dev/fb0, O_RDWR) 3后exit_group(0)原因linuxfb平台插件未找到或Framebuffer设备权限不足。Qt尝试打开/dev/fb0失败后直接退出不报错。解决检查/dev/fb0是否存在ls -l /dev/fb*添加用户到video组usermod -a -G video root确认Qt插件路径export QT_QPA_PLATFORM_PLUGIN_PATH/usr/lib/qt/plugins/platforms且该路径下有libqlinuxfb.so4.2 现象中文显示方块QFontDatabase::addApplicationFont()返回-1原因字体文件路径错误或字体格式不被Qt支持如TTF压缩包未解压。解决用file resources/fonts/NotoSansCJK-Regular.ttc确认是TrueType CollectionQFontDatabase::addApplicationFont()返回值必须检查int id QFontDatabase::addApplicationFont(:/fonts/NotoSansCJK-Regular.ttc); if (id -1) { qWarning() Failed to load font; // 回退到系统默认字体 textEdit-setFont(QFont(DejaVu Sans, 12)); }4.3 现象点击菜单按钮无响应qDebug()无输出gdb显示QMetaObject::activate未进入槽函数原因QAction未正确connect()或QMenuBar未addMenu()到主窗口。解决检查connect()语法connect(saveAction, QAction::triggered, this, Notepad::saveFile)注意Notepad::saveFile不能写成saveFileC11语法要求确认QMenuBar已setMenuBar()setMenuBar(menuBar);而非menuBar-show()4.4 现象保存大文件1MB时界面卡死触摸无响应原因QFile::write()在慢速SD卡上阻塞主线程Qt事件循环暂停。解决改用QSaveFile原子写入异步QSaveFile file(filePath); if (file.open(QIODevice::WriteOnly)) { QTextStream out(file); out.setCodec(UTF-8); out content; file.commit(); // 原子提交失败自动回滚 }或将保存逻辑移到QThread但需注意QTextEdit::toPlainText()必须在GUI线程调用4.5 现象开发板重启后程序首次运行正常第二次启动崩溃在QApplication构造函数原因Qt共享内存段/dev/shm残留QSharedMemory初始化失败。解决启动脚本中清理rm -f /dev/shm/qtsystem*或禁用共享内存export QT_QPA_PLATFORMlinuxfb:fb/dev/fb0:mmWidth800:mmHeight480:rotation0显式关闭shm5. 菜单与快捷键的嵌入式适配QAction动态启用、触摸友好尺寸与硬件按键映射5.1 用QAction构建可触摸的菜单栏图标尺寸、文字大小与间距硬编码桌面Qt菜单在240x320小屏上根本点不准。必须重写样式并放大交互区域// 在Notepad构造函数中 QMenuBar *menuBar new QMenuBar(this); QMenu *fileMenu menuBar-addMenu(文件); // 【嵌入式关键】增大菜单项尺寸 QAction *newAction fileMenu-addAction(新建); newAction-setIcon(QIcon(:/icons/new.png)); // 提前准备48x48图标 newAction-setShortcut(QKeySequence::New); // 强制设置菜单项高度覆盖样式表 fileMenu-setStyleSheet(QMenu::item { height: 48px; font-size: 16px; padding: 8px 16px; }); // 连接信号 connect(newAction, QAction::triggered, this, Notepad::newFile);提示图标资源必须用qrc打包避免开发板无文件系统访问权限。5.2 硬件按键映射将开发板GPIO按键映射为CtrlS等快捷键很多嵌入式板载物理按键如OK键、ESC键需复用为保存/退出。通过QAbstractNativeEventFilter捕获底层事件// 在notepad.h中添加 class KeyEventListener : public QAbstractNativeEventFilter { public: bool nativeEventFilter(const QByteArray eventType, void *message, long *result) override; Notepad *window; }; // notepad.cpp中实现 bool KeyEventListener::nativeEventFilter(const QByteArray eventType, void *message, long *result) { if (eventType xcb || eventType windows) { // XCB事件解析以X11为例 xcb_key_press_event_t *keyEvent static_castxcb_key_press_event_t*(message); if (keyEvent-detail 36) { // X11 keycode for Enter (OK button) window-saveFile(window-currentFile); return true; } } return false; } // 在Notepad构造函数中注册 KeyEventListener *keyFilter new KeyEventListener; keyFilter-window this; qApp-installNativeEventFilter(keyFilter);注意需根据实际平台X11/Wayland/LinuxFB调整事件类型和键码解析逻辑xev命令可获取真实键码。5.3 动态菜单状态管理基于文件修改状态的Save/Revert按钮启用控制嵌入式用户常忘记保存就关机需视觉提示。利用QTextEdit::modificationChanged(bool)信号// notepad.h中添加 private: bool isModified false; QAction *saveAction; // notepad.cpp中 Notepad::Notepad(QWidget *parent) : ... { // 创建Save Action saveAction fileMenu-addAction(保存); saveAction-setShortcut(QKeySequence::Save); saveAction-setEnabled(false); // 初始禁用 // 连接修改状态信号 connect(textEdit, QTextEdit::modificationChanged, [](bool modified) { isModified modified; saveAction-setEnabled(modified); // 状态栏显示星号 if (modified) { statusBar-showMessage(已修改 *); } else { statusBar-showMessage(已保存); } }); }这样用户一输入文字保存按钮立刻高亮——比弹窗提醒更符合嵌入式操作直觉。6. 性能压测与真机验证用straceperf定位Qt记事本在ARM平台的CPU与IO瓶颈6.1 用strace抓取文件操作耗时识别SD卡I/O卡顿点在开发板上运行strace -T -e traceopen,read,write,close -o strace.log ./notepad然后在记事本中执行“打开1MB日志文件→滚动到底部→保存”。分析strace.logopen(/mnt/sdcard/large.log, O_RDONLY|O_CLOEXEC) 3 0.000123 read(3, 2023-01-01 00:00:00 INFO ...\n, 65536) 65536 0.002456 read(3, 2023-01-01 00:00:01 DEBUG ...\n, 65536) 65536 0.003102 # 注意这里连续read耗时3ms说明SD卡顺序读性能不足优化方案对大文件启用QTextStream::setAutoDetectUnicode(false)跳过BOM检测用QFile::map()内存映射替代read()需SD卡支持mmapQFile file(path); if (file.open(QIODevice::ReadOnly)) { uchar *data file.map(0, file.size()); // 直接映射到内存 QString content QString::fromUtf8((const char*)data, file.size()); file.unmap(data); textEdit-setPlainText(content); }6.2 用perf分析Qt事件循环CPU占用揪出隐藏的repaint风暴在开发板上perf record -e cycles,instructions,cache-misses -g -p $(pidof notepad) sleep 10 perf report --sort comm,dso,symbol若发现QWidgetPrivate::drawWidget或QPainter::drawText占比过高说明频繁重绘。根因常是QTextEdit::setPlainText()被高频调用如实时日志追加QTimer间隔设太短如10ms刷新状态栏修复// 状态栏更新改为节流 QTimer *statusTimer new QTimer(this); connect(statusTimer, QTimer::timeout, this, Notepad::updateStatus); statusTimer-start(500); // 500ms一次非实时6.3 真机压力测试清单一份嵌入式Qt记事本上线前必做的10项验证测试项方法通过标准备注1. 冷启动时间time ./notepad →kill %11.5秒ARM64 1.6GHz包含Qt库加载2. 中文输入延迟软键盘连续输入100字光标跟随无卡顿无丢字触控屏专用3. SD卡热拔插读取中拔卡→插回→重试QFile::open()返回false不崩溃需捕获QFile::ResourceError4. 内存泄漏valgrind --leak-checkfull ./notepaddefinitely lost: 0 bytes重点查new未delete5. 字体抗锯齿显示小字号中文10pt无毛边笔画清晰QFont::PreferAntialias必须启用6. 触摸精度用手指点击菜单项左边缘100%触发无误触相邻项菜单项高度≥48px7. 电源异常运行中突然断电重启后文件不损坏QSaveFile::commit()保障原子写入验证8. 多实例互斥./notepad ./notepad第二个实例应检测到锁并退出用QSharedMemory实现9. 长文本滚动加载10MB文件→快速拖动滚动条CPU40%无画面撕裂QTextEdit::setLineWrapMode(QTextEdit::NoWrap)10. 硬件按键映射按OK键→触发保存strace可见write(1, saved, 5)验证底层事件捕获我带过的某嵌入式团队曾因忽略第7项电源异常导致产线设备批量数据损坏——他们用普通QFile::write()断电时文件头写了一半。后来强制切换QSaveFile故障率归零。这件事让我养成习惯凡是涉及文件写入的嵌入式Qt模块第一行代码必写QSaveFile file(path);第二行才考虑业务逻辑。希望帮到你。本文还有配套的精品资源点击获取