☰
嵌入式Qt记事本开发全链路:从交叉编译到中文文件IO
2026/10/14 16:41:48 网站建设 项目流程

简介:这是一份基于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主机环境起步,目标平台为ARM64(RK3399),Qt版本锁定5.15.2(LTS稳定分支),所有命令可直接复制粘贴,所有坑都附带strace -e trace=open,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/ # 交叉编译输出目录(不提交git)

notepad.pro关键内容如下(注意注释里的嵌入式特有配置):

# notepad.pro QT += core widgets gui TARGET = notepad TEMPLATE = app # 【嵌入式关键】禁用桌面特性,减小体积 CONFIG -= app_bundle CONFIG += c++17 # 【嵌入式关键】显式指定字体路径,避免运行时搜索失败 DEFINES += QT_NO_DEBUG_OUTPUT QMAKE_CXXFLAGS += -O2 -march=armv8-a+crc+crypto # 【嵌入式关键】资源文件打包进二进制(避免运行时缺文件) RESOURCES += resources.qrc # 源文件 SOURCES += src/main.cpp \ src/notepad.cpp HEADERS += src/notepad.h

提示:resources.qrc需手动创建,内容为:

<RCC> <qresource prefix="/fonts"> <file>resources/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:创建专用mkspec(Qt构建规格描述) 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骨架:只保留QTextEdit+QStatusBar的“裸奔版”

新手常犯的错是上来就加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_H

src/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 root@192.168.1.10:/usr/bin/ # 开发板上执行(确保Framebuffer设备存在) export QT_QPA_PLATFORM=linuxfb 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 QByteArray):

QFile 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_cast<QInputMethodEvent*>(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.so

4.2 现象:中文显示方块,QFontDatabase::addApplicationFont()返回-1

原因:字体文件路径错误或字体格式不被Qt支持(如TTF压缩包未解压)。
解决:

  • 用file resources/fonts/NotoSansCJK-Regular.ttc确认是TrueType Collection
  • QFontDatabase::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不能写成saveFile(C++11语法要求)
  • 确认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_PLATFORM=linuxfb:fb=/dev/fb0:mmWidth=800:mmHeight=480:rotation=0(显式关闭shm)

5. 菜单与快捷键的嵌入式适配: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按键映射为Ctrl+S等快捷键

很多嵌入式板载物理按键(如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_cast<xcb_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. 性能压测与真机验证:用strace+perf定位Qt记事本在ARM平台的CPU与IO瓶颈

6.1 用strace抓取文件操作耗时,识别SD卡I/O卡顿点

在开发板上运行:

strace -T -e trace=open,read,write,close -o strace.log ./notepad

然后在记事本中执行“打开1MB日志文件→滚动到底部→保存”。分析strace.log:

open("/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卡支持mmap):
    QFile 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 %1<1.5秒(ARM64 1.6GHz)包含Qt库加载
2. 中文输入延迟软键盘连续输入100字光标跟随无卡顿,无丢字触控屏专用
3. SD卡热拔插读取中拔卡→插回→重试QFile::open()返回false,不崩溃需捕获QFile::ResourceError
4. 内存泄漏valgrind --leak-check=full ./notepaddefinitely lost: 0 bytes重点查new未delete
5. 字体抗锯齿显示小字号中文(10pt)无毛边,笔画清晰QFont::PreferAntialias必须启用
6. 触摸精度用手指点击菜单项左边缘100%触发,无误触相邻项菜单项高度≥48px
7. 电源异常运行中突然断电重启后文件不损坏(QSaveFile::commit()保障)原子写入验证
8. 多实例互斥./notepad & ./notepad第二个实例应检测到锁并退出用QSharedMemory实现
9. 长文本滚动加载10MB文件→快速拖动滚动条CPU<40%,无画面撕裂QTextEdit::setLineWrapMode(QTextEdit::NoWrap)
10. 硬件按键映射按OK键→触发保存strace可见write(1, "saved", 5)验证底层事件捕获

我带过的某嵌入式团队,曾因忽略第7项(电源异常)导致产线设备批量数据损坏——他们用普通QFile::write(),断电时文件头写了一半。后来强制切换QSaveFile,故障率归零。这件事让我养成习惯:凡是涉及文件写入的嵌入式Qt模块,第一行代码必写QSaveFile file(path);,第二行才考虑业务逻辑。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询