ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

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

简介:面向有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 是 QtCore,gui 是 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 += c++11

QT += 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); // Ctrl+N,跨平台自动适配 connect(newAct, &QAction::triggered, this, &MainWindow::newFile); openAct = new QAction(QStringLiteral("打开(&O)..."), this); openAct->setShortcuts(QKeySequence::Open); // Ctrl+O connect(openAct, &QAction::triggered, this, &MainWindow::openFile); saveAct = new QAction(QStringLiteral("保存(&S)"), this); saveAct->setShortcuts(QKeySequence::Save); // Ctrl+S 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这类标准键而不是硬编码Ctrl+N,好处是跨平台自动适配,macOS 上会映射成 Command+N。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()).value<QFont>(); if (!f.family().isEmpty()) textEdit->setFont(f);

QSettings在 Linux 下落到~/.config/应用名/应用名.conf,Windows 下走注册表,跨平台不用改代码。这是 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数的是「文本块」,对纯文本来说就是行号;如果内容里有硬换行(Shift+Enter 那种软换行不算新块),块数还是按逻辑行算,这点和视觉行数会不一致,做行号栏的时候要注意。

lambda 里捕获this是安全的,因为连接的生命周期和textEdit、this一致,主窗口析构时连接自动断开。如果捕获的是局部变量或者外部对象指针,就得考虑QObject::connect的第五个参数Qt::UniqueConnection或者手动disconnect了,否则对象销毁后再触发信号就是野指针,直接段错误。

5. 编译运行与排查:桌面、交叉编译和必踩的坑

写到这里,代码层面的东西基本清楚了。真正把人卡住的往往不是代码,是编译环境。桌面版跑不起来、交叉编译出来的程序在板子上黑屏、改了.pro没生效,这些问题的排查路径其实很固定,只是没人一次性讲全。

5.1 桌面端编译:qmake、make 与常见报错

先确认环境里有没有 qmake:

qmake --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_PLUGINS=1 ./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_PLATFORM=linuxfb # 告诉动态链接器去哪找 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里加了一个新 cpp,make之后报链接错误说找不到那个类的符号。原因是make只读现成的 Makefile,而 Makefile 是qmake根据.pro生成的,.pro改了不重新qmake,Makefile 里根本没有新文件。解决很简单:每次动完.pro就qmake一次,图省事可以写个别名把qmake && make绑在一起。

第二条:桌面跑得好好的,交叉编译出来在板子上黑屏,终端没任何报错。

现象是程序启动后直接退出,退出码 0,日志一片空白。原因多半是平台插件没找到,Qt 默认会尝试xcb,板子上没有 X11 就直接放弃。排查方法是启动前加export QT_DEBUG_PLUGINS=1,它会打印出插件搜索的所有路径和失败原因,一眼就能看到是「找不到 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 BOM(EF 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 放一个TextEditorWidget,currentChanged信号里切换状态栏的宿主。

6.2 三条可以往下走的改造线

改造方向落点关键点
自动保存QTimer+QSettings记录草稿路径定时器间隔别小于 30 秒,写盘用临时文件再 rename,避免写一半断电
编码自适应loadFile里替换硬编码的 UTF-8先判 BOM,再用字节统计猜 GBK/UTF-8,最后兜底按系统 locale
大文件优化QTextEdit换QPlainTextEdit十万行以上 QTextEdit 会卡,QPlainTextEdit 按块渲染,内存和速度都好一个数量级

换成QPlainTextEdit的时候要注意,它没有setFont之外的富文本能力,toHtml之类的方法不存在,查找接口倒是兼容,find的签名基本一致。这种替换最适合拿来做「确认改动范围」的练习:改完编译一遍,看哪几行报错,就知道原代码对QTextEdit的依赖到底集中在哪些地方。

验证改造是否成功,我一般不看界面,看三件事:新建一个文件输入中文保存再打开,内容一字不差;打开一个 GBK 编码的旧文件不出现乱码;连续开关窗口十次,进程内存不持续增长。前两条靠手动,第三条用top盯一下 RSS 就行,涨了就说明有对象没被父指针接管,属于典型的泄漏。

血泪经验是:这类教学向小项目最容易犯的错,是拿到手就急着加功能,结果把原本清晰的类边界搅乱,改到最后连「保存」按钮该连哪个槽都说不清。我后来的习惯是,动第一行代码之前先把.pro和主窗口头文件通读一遍,画清楚哪个对象归谁管,再动手。希望帮到你。

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

返回列表