ARM-Linux点菜系统:Qt界面业务分离与交叉编译实践
2026/9/13 8:12:28 网站建设 项目流程

简介:基于ARM-LINUX的点菜系统是一份使用Qt Creator开发的完整Qt/C++嵌入式项目资源,面向嵌入式Linux学习者和需要课程设计参考的高校学生。系统实现了注册登录、退出登录、余额充值、下单支付、实时金额显示等核心功能,并能在充值或支付时进行余额上下限判断与提示;点菜界面支持勾选菜品、自选数量、左右上下滑动浏览菜单,能实时汇总所点菜品总额,同时展示当前用户信息以及所有用户中余额最多和最少者及其余额数目,适合作为物联网或嵌入式实训项目的完整范例。压缩包共90个文件,约27.27MB,其中包含cpp源文件、h头文件、ui界面文件、o中间编译文件、jpg图片素材、工程配置pro文件以及开发板运行效果演示视频;通过源码与演示视频,读者既能掌握Qt界面布局、信号槽机制和文本文件存储处理,也能理解ARM6818交叉编译与程序部署的完整思路。目前已有3316人学习下载,整体结构清晰、功能模块完整,可直接用于学习或二次开发。

1. 基于ARM-LINUX点菜系统:先用文本文件也要把界面和业务分离

这套基于ARM-LINUX点菜系统,第一眼看上去像一份课程设计源码,真正值得拆的却是两件事:一是用Qt Widgets/C++组织的界面与业务边界,二是从x86 Qt Creator一路搬到ARM6818开发板的交叉编译链路。压缩包里的sign_in、registers、charge、pay、shop、compare、user_mess都不是摆设,它们分别对应注册登录、余额充值、下单支付、菜单点选和余额排行。最反直觉的是,整个系统没有接数据库,用户数据落在user_mess.txt这样的文本文件里,移植到ARM时省掉了sqlite依赖,但读写并发和异常恢复要自己扛。适合正在做嵌入式Qt项目、需要把界面业务从PC迁到ARM板的人对照着改。

2. 工程结构与用户数据落盘:从sign_in和registers看Qt文件读写边界

2.1 拆开Project.pro里的模块:哪些类在哪一层

拿到压缩包先看Project.pro。SOURCES里列出的文件不是随手扔进去的,基本按用户生命周期分:sign_in.cpp管登录入口,registers.cpp管新用户注册,charge.cpp管充值,pay.cpp管支付,shop.cpp管菜单,compare.cpp管余额排行,user_mess.cpp管当前用户信息展示,hotel.cpp更像一个等待或演示页面。每个类在构造时都尝试打开同一个user_mess.txt,登录成功后把当前账号名传给shop或charge,后续操作再根据账号名回读余额。

这个设计在PC上没问题,因为所有窗口都在同一个进程里,文本文件天然成了共享数据源。坏处是任意一个窗口读到一半时,另一个窗口同时在写,就会有半行数据。所以每个模块都必须按“先读校验,再改写”的流程来做。

下表是设计时可以照着拆的模块职责:

模块职责关键文件
sign_in登录校验、成功后切换窗口sign_in.cpp / ui_sign_in.h
registers注册新账号并写入文本registers.cpp / ui_registers.h
charge充值、余额上限判断charge.cpp / ui_charge.h
pay下单支付、余额扣减pay.cpp / ui_pay.h
shop菜单浏览勾选、数量自选shop.cpp / ui_shop.h
compare显示余额最多和最少用户compare.cpp / ui_compare.h
user_mess显示当前用户信息和实时余额user_mess.cpp / ui_user_mess.h

2.2 用户信息存成“账号|密码|余额”的文本格式

user_mess.txt每行一个用户,字段之间用竖线分隔,例如:

admin|e10adc3949ba59abbe56e057f20f883e|500

这个格式决定所有读取模块都要先trim再split('|')。为什么不用空格或逗号?因为用户名不允许带空格,但密码一旦做hash后是定长十六进制串,用竖线最省事。文件末尾一般保留一个换行,免得最后一行被append时拼在一起。

写盘时不要把密码直接存明文。原工程可能是明文,但真实餐馆里服务员共用一台点菜平板,明文密码谁都能看。建议在写盘前先用QCryptographicHash做SHA-256,登录时把输入hash后比对。这样user_mess.txt被拷走也泄露不了太多信息。

2.3 注册时做重复账号检查与写盘

注册逻辑的顺序是“先读全文件检查账号,再追加写”。不能打开WriteOnly直接写,那样会把原有用户清空。更不能打开ReadWrite直接写,因为文件指针停在文件开头,会覆盖第一行。下面这段是我在registers.cpp里常用的写法:

bool Registers::registerUser(const QString &account, const QString &pwd, int initBalance) { QFile file("user_mess.txt"); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return false; // 先读文件确认账号唯一 while (!file.atEnd()) { QString line = QString::fromUtf8(file.readLine()).trimmed(); if (line.section('|', 0, 0) == account) { file.close(); QMessageBox::warning(this, "提示", "账号已存在"); return false; } } file.close(); // 使用SHA-256哈希后写盘,避免明文密码 QByteArray sha = QCryptographicHash::hash(pwd.toUtf8(), QCryptographicHash::Sha256).toHex(); if (file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) { QTextStream out(&file); out << account << '|' << sha.constData() << '|' << initBalance << '\n'; file.close(); return true; } return false; }

代码里第一段open用ReadOnly,读完关闭;第二段用Append再写,避免文件指针覆盖问题。section('|',0,0)只取每行第一个字段做账号比对,比split更省内存。initBalance最好从配置或ini文件读,不要写死在代码里,因为不同门店初始赠送金额不一样。登录时在sign_in里把输入框的QString做同样hash后再和文件第二段比对,比对成功再emit一个loginSuccess信号,主窗口收到信号后跳转到shop。

3. 余额充值、下单支付与上下限判断:数据库不存在的状态下怎样保证数据一致

3.1 充值与支付都要先回读最新余额

多窗口共享一个文本文件时,最忌讳的是每个窗口在构造时读一次余额,之后一直用内存值。比如登录时余额500,充值窗口打开后如果另一个窗口已经改成300,内存里的500就过期了。所以charge和pay每次弹窗或点击按钮时,都要重新loadBalance。

int Pay::loadBalance(const QString &account) { QFile file("user_mess.txt"); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return -1; while (!file.atEnd()) { const QStringList fields = QString::fromUtf8(file.readLine()).trimmed().split('|'); // 防止读到空行或末尾空串导致越界 if (fields.size() >= 3 && fields.at(0) == account) return fields.at(2).toInt(); } return -1; }

这里split后必须判断fields.size()>=3,因为文本文件最后一行可能是空行,空行split出来size是0,直接用at(2)会越界。返回-1表示账号不存在,UI层要弹提示而不是把-1显示到余额标签上。

3.2 充值、支付的上下限规则表

原文提到的“上下限判断”不是指登录次数,而是余额边界。充值要设置单次上限和总额上限,支付要判断点菜总额不能小于等于0,也不能超过当前余额。整理成规则表:

操作条件处理
充值充值金额 <= 0拒绝并提示“请输入有效金额”
充值当前余额 + 充值金额 > 上限拒绝并提示超出余额上限
支付没有勾选任何菜品提示“请先点菜”
支付菜品总额 > 当前余额提示余额不足并跳转到充值
支付菜品总额 == 0提示未选菜

上限建议用一个全局常量管理,例如constexpr int kMaxBalance = 10000;,或者写进config.ini,不要散落在charge.cpp和pay.cpp里。这样后续调整成50000时只用改一处。

3.3 写回时的原子性处理

余额写回不能简单地“打开文件,找到该行,替换文本”,因为QFile的读写指针不好控制,而且中途断电会留下半行。我一般用临时文件+rename:

bool updateBalance(const QString &account, int newBalance) { QFile in("user_mess.txt"); if (!in.open(QIODevice::ReadOnly | QIODevice::Text)) return false; QFile tmp("user_mess.txt.tmp"); if (!tmp.open(QIODevice::WriteOnly | QIODevice::Text)) return false; bool hit = false; QTextStream out(&tmp); while (!in.atEnd()) { QString line = QString::fromUtf8(in.readLine()).trimmed(); QStringList fields = line.split('|'); // 找到账号后替换余额字段,其余字段原样保留 if (fields.size() == 3 && fields.at(0) == account) { fields[2] = QString::number(newBalance); hit = true; } out << fields.join('|') << '\n'; } in.close(); tmp.close(); // 写入临时文件后一次性rename,避免掉电留下半行数据 if (!hit || !QFile::rename("user_mess.txt.tmp", "user_mess.txt")) { QFile::remove("user_mess.txt.tmp"); return false; } return true; }

提示:Linux上rename直接覆盖目标文件是原子操作;Windows上QFile::rename遇到目标已存在会失败,所以不要在PC联调阶段用同一个函数直接改文件,至少先QFile::remove再rename。最终要跑的是ARM开发板,以Linux行为为准。

上面代码先读原文件,按账号找到要改的用户,把余额字段替换成新值,再写临时文件。退出时如果账号不存在,hit为false,就不会用一条没找到的记录覆盖整个文件。支付成功后还要调用UI刷新,比如ui->label_balance->setText(QString("¥%1").arg(newBalance)),实时刷新不要等下一次重新读。

4. 菜单浏览、数量选择与实时总额:从ui_shop到compare的数据流

4.1 QScrollArea + QCheckBox + QSpinBox 的组合方式

点菜窗口要求“在窗口内左右上下滑动浏览菜单”,这基本就是QScrollArea的活。ui_shop.ui里放一个QScrollArea,scrollArea内部放一个装了QVBoxLayout的QWidget。每个菜品一行,最左边是QCheckBox勾选,中间是菜名和价格,右边是QSpinBox调整数量。设置widgetResizable为true之后,菜品超过可视区域就会出现滚动条。

生成菜品行时,我把每个菜品行的价格和数量控件绑定到QCheckBox的dynamic property上,这样最后统计时不用费劲维护一个专门的菜品对象数组:

for (const Dish &d : dishes) { auto *row = new QWidget(menuWidget); auto *lay = new QHBoxLayout(row); auto *ck = new QCheckBox(d.name, row); auto *spin = new QSpinBox(row); spin->setRange(0, 99); spin->setValue(0); lay->addWidget(ck); lay->addWidget(new QLabel(QString("¥%1").arg(d.price), row)); lay->addWidget(spin); menuLayout->addWidget(lay); // 把价格和数量控件绑定到复选框上,统计时统一遍历 ck->setProperty("price", d.price); ck->setProperty("spin", QVariant::fromValue(spin)); }

这里价格和指针都挂在QCheckBox上,QVariant::fromValue对指针类型默认可用,不需要额外的Q_DECLARE_METATYPE。数量范围限制在0到99,防止点菜数量输入负数,支付时总额算出负数。横向滑动靠QScrollArea自身的horizontalScrollBar,如果菜品图片比较宽,记得设置QScrollArea的widgetResizable为false并手动给内层widget设置最小宽度,否则横向滚动条不出来。

4.2 金额汇总的两种写法:逐个遍历与信号驱动

实时显示点菜总额有两种实现方式。第一种是点“结算”时再遍历所有行,代码简单,用户体验差。第二种是把每个QCheckBox的stateChanged和QSpinBox的valueChanged接到同一个刷新槽,一勾选就更新总和。原工程要求“点菜时实时显示所点菜品总额”,所以必须用第二种。

void Shop::refreshTotal() { int total = 0; const auto cks = findChildren<QCheckBox *>(); for (QCheckBox *ck : cks) { if (!ck->isChecked()) continue; QSpinBox *spin = qvariant_cast<QSpinBox *>(ck->property("spin")); if (spin) // 勾选后必须能取到数量控件 total += ck->property("price").toInt() * spin->value(); } ui->label_total->setText(QString("¥%1").arg(total)); m_currentTotal = total; } connect(spin, QOverload<int>::of(&QSpinBox::valueChanged), this, &Shop::refreshTotal); connect(ck, &QCheckBox::stateChanged, this, &Shop::refreshTotal);

这里直接用findChildren<QCheckBox*>(),会递归到滚动区域里所有菜品行,不会漏。price属性是int,spin属性是QSpinBox*,取用时要判断空指针,避免某个菜品行初始化时信号提前触发导致崩溃。如果交叉编译的Qt版本较老,QOverload用不了,就把connect改成传统SIGNAL/SLOT宏,逻辑等价。

4.3 compare模块:一次遍历找出余额最多和最少用户

compare窗口要显示所有用户中余额最多和最少的人,不用先读进QList再排序。文件数据量不大,一次遍历同时维护最大值和最小值更直接:

void Compare::showRank() { QFile f("user_mess.txt"); if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return; QString maxUser, minUser; int maxMoney = -1, minMoney = INT_MAX; while (!f.atEnd()) { const QStringList p = QString::fromUtf8(f.readLine()).trimmed().split('|'); if (p.size() < 3) continue; int money = p.at(2).toInt(); if (money > maxMoney) { maxMoney = money; maxUser = p.at(0); } // minMoney初始化为INT_MAX,保证第一个用户进入候选 if (money < minMoney) { minMoney = money; minUser = p.at(0); } } ui->label_max->setText(maxUser + " " + QString::number(maxMoney)); ui->label_min->setText(minUser + " " + QString::number(minMoney)); }

注意minMoney不能初始化成0,否则当所有用户余额都大于0时,最小值永远为0且minUser为空。初始化为INT_MAX后,第一个用户就会被当成最小值候选。这里用split一次性取三个字段,比section调用三次更直观。

5. ARM6818交叉编译与部署:x86能跑不算完,换交叉链后要查这四件事

5.1 用ARM交叉qmake重新生成Makefile

在x86的Qt Creator里能编译通过,只说明业务逻辑没大错。移植到ARM6818时,必须用arm交叉编译链里的qmake重新生成Makefile。假设交叉编译器在/opt/arm/bin,Qt在/opt/arm/qt5:

export PATH=/opt/arm/bin:/opt/arm/qt5/bin:$PATH # 使用ARM交叉编译链的qmake,防止沿用x86的Makefile /opt/arm/qt5/bin/qmake Project.pro -spec linux-arm-gnueabihf-g++ CONFIG+=release make -j4 && file PointSystem

如果输出ELF 32-bit LSB ARM, EABI5,说明已经是ARM架构;如果还是x86-64,说明PATH里的qmake来自PC上的Qt,没有生效。有些板子自带的是qt5.3.1 arm镜像,qmake会放在/usr/local/qt5/bin/qmake,原理一样,别拿PC的qmake混用。

5.2 板子上的运行库、环境变量与tslib校准

编出来的可执行文件还要带上Qt运行库。常用做法是把交叉编译后的Qt整目录scp到板子:

scp PointSystem root@192.168.1.68:/opt/point/ scp -r /opt/arm/qt5/lib root@192.168.1.68:/usr/local/qt5/

然后在开发板开机脚本里export这些环境变量:

export LD_LIBRARY_PATH=/usr/local/qt5/lib:/usr/local/tslib/lib:$LD_LIBRARY_PATH export QT_QPA_PLATFORM=linuxfb:fb=/dev/fb0:width=1024:height=600 export QT_QPA_FONTDIR=/usr/local/qt5/fonts export TSLIB_TSDEVICE=/dev/input/event1

触摸不准就先跑ts_calibrate,生成的pointercal文件要放在tslib能读到的位置。

5.3 用readelf验证动态库:不要把x86的.so放到ARM板上

PC上的libQt5Core.so是x86平台的,不能放到ARM板。上板前用readelf确认可执行文件依赖:

arm-linux-gnueabihf-readelf -d PointSystem | grep NEEDED

重点看libQt5Widgets.so.5、libQt5Core.so.5这些依赖是否在开发板的LD_LIBRARY_PATH能找到。file、readelf、fbset这几个linux常用命令在排查时的作用比Qt调试器更直接。

5.4 启动脚本和显示分辨率的匹配

现象先查什么
画面只有左上角一小块fb分辨率比启动参数大,改width/height
触摸点击偏移重新ts_calibrate并确认TSLIB_TSDEVICE
提示缺libQt5Widgets.so.5LD_LIBRARY_PATH没指到板子上的Qt lib
中文全部方块QT_QPA_FONTDIR里没有中文字体,换成文泉驿字体

最后把上面所有export写进start.sh,chmod +x后执行。调整linuxfb后面的width和height时,先用fbset确认当前分辨率,再改start.sh,否则触摸坐标会整体偏移。

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

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

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

立即咨询