
簡介基于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否則觸摸坐標會整體偏移。本文還有配套的精品資源點擊獲取