
简介基于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.hregisters注册新账号并写入文本registers.cpp / ui_registers.hcharge充值、余额上限判断charge.cpp / ui_charge.hpay下单支付、余额扣减pay.cpp / ui_pay.hshop菜单浏览勾选、数量自选shop.cpp / ui_shop.hcompare显示余额最多和最少用户compare.cpp / ui_compare.huser_mess显示当前用户信息和实时余额user_mess.cpp / ui_user_mess.h2.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的读写指针不好控制而且中途断电会留下半行。我一般用临时文件renamebool 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里放一个QScrollAreascrollArea内部放一个装了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 findChildrenQCheckBox *(); for (QCheckBox *ck : cks) { if (!ck-isChecked()) continue; QSpinBox *spin qvariant_castQSpinBox *(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, QOverloadint::of(QSpinBox::valueChanged), this, Shop::refreshTotal); connect(ck, QCheckBox::stateChanged, this, Shop::refreshTotal);这里直接用findChildrenQCheckBox*()会递归到滚动区域里所有菜品行不会漏。price属性是intspin属性是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/binQt在/opt/arm/qt5export 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 CONFIGrelease 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 root192.168.1.68:/opt/point/ scp -r /opt/arm/qt5/lib root192.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_PLATFORMlinuxfb:fb/dev/fb0:width1024:height600 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.shchmod x后执行。调整linuxfb后面的width和height时先用fbset确认当前分辨率再改start.sh否则触摸坐标会整体偏移。本文还有配套的精品资源点击获取