diff --git a/.vscode/settings.json b/.vscode/settings.json index 8d3bb30..c4dfd96 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,5 @@ { - "clangd.arguments": [ - "--compile-commands-dir=build" - ], + "editor.multiCursorModifier": "alt", "python-envs.defaultEnvManager": "ms-python.python:conda", "python-envs.defaultPackageManager": "ms-python.python:conda", "python-envs.pythonProjects": [] diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f08c6b..77513d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,20 +26,28 @@ endif() # 主可执行程序 DTU-HMI 及其源码列表 add_executable(DTU-HMI src/main.c + src/common/utf8.c src/thread_utils.c src/remoteDisplay.c - src/Drv/menu.c + src/Drv/menu/app/menu.c + src/Drv/menu/model/display.c + src/Drv/menu/model/menu_model.c + src/Drv/menu/view/menu_view.c + src/Drv/menu/presenter/menu_presenter.c + src/Drv/menu/model/menu_tree_builder.c + src/Drv/menu/view/menu_layout.c + src/Drv/menu/presenter/menu_navigator.c + src/Drv/menu/view/menu_renderer_lcd.c src/Drv/lcd/lcd.c src/Drv/lcd/lcd_draw.c src/Drv/lcd/lcd_text.c src/Drv/key.c src/Drv/lcd/ascii.c - src/Drv/display.c src/TCP/tcp.c ) # 可执行程序头文件搜索路径 -target_include_directories(DTU-HMI PRIVATE include src src/TCP) +target_include_directories(DTU-HMI PRIVATE include src src/TCP src/Drv/lcd src/common) # 按平台链接系统库: # - Windows 需要 Winsock2(ws2_32) diff --git a/Testing/Temporary/CTestCostData.txt b/Testing/Temporary/CTestCostData.txt new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/Testing/Temporary/CTestCostData.txt @@ -0,0 +1 @@ +--- diff --git a/docs/系统架构设计文档.md b/docs/系统架构设计文档.md index 1563f61..88ff0fb 100644 --- a/docs/系统架构设计文档.md +++ b/docs/系统架构设计文档.md @@ -24,26 +24,40 @@ ## 4. 系统分层架构 -```text - +----------------------+ - | AppLayer | - | main.c | - +----------+-----------+ - | - +--------------+--------------+ - | | - v v - +---------+----------+ +---------+------------------+ - | MenuLayer | | RemoteDisplayLayer | - | menu.c | | remoteDisplay.c | - +----+-----------+---+ +-----+-----------+----------+ - | | | | - v v v v - +------+-----+ +--+---------+ +---+----+ +---+------------------+ - |DisplayLayer| |InputLayer | |Input | |PlatformLayer | - |lcd/Ascii/ | |key.c | |key.c | |tcp.c + thread_utils.c | - |display.c | +------------+ +-------+ +------------------------+ - +------------+ +```mermaid +block-beta +columns 1 + a["菜单"] + block:textdraw + columns 2 + a1["文字显示"] a2["图像显示"] + end + block:draw + columns 2 + block:text_group + columns 2 + block:left_text + columns 1 + b["Lcd_ShowStr"] + block:showstr + columns 2 + s1["Lcd_Pub_Ascii"] + s2["Lcd_Pub_UTF8"] + end + end + b1["utf8_next"] + end + block:draw_group + columns 4 + c1["Lcd_LineH"] + c2["Lcd_LineV"] + c3["Lcd_Invert"] + c4["Lcd_FillRect"] + end + end + block:lcd + lcda["Lcd_Init"] lcdb["Lcd_SetPixel"] lcdc["Lcd_GetPixel"] + end ``` ### 4.1 应用层 @@ -56,15 +70,16 @@ ### 4.2 菜单业务层 -- 文件:`src/Drv/menu.c`、`src/Drv/menu.h` +- 文件:`src/Drv/menu/`、`src/Drv/menu/app/menu.h` - 职责: + - 采用 MVP 分层组织菜单模块(`Model / Presenter / View`) - 基于静态菜单模型构建运行时菜单树 - - 处理按键驱动的菜单状态迁移 + - 处理按键驱动的菜单状态迁移与路径重建 - 执行菜单显示坐标计算与多级菜单渲染调度 ### 4.3 显示层 -- 文件:`src/Drv/lcd/lcd.c`、`src/Drv/lcd/lcd_draw.c`、`src/Drv/lcd/lcd_text.c`、`src/Drv/lcd/text_codec.c`、`src/Drv/lcd/ascii.c`、`src/Drv/display.c` +- 文件:`src/Drv/lcd/lcd.c`、`src/Drv/lcd/lcd_draw.c`、`src/Drv/lcd/lcd_text.c`、`src/Drv/lcd/text_codec.c`、`src/Drv/lcd/ascii.c`、`src/Drv/menu/model/display.c` - 职责: - 管理 LCD 显存 `g_tCVsr` 与像素绘制 - 提供 ASCII/UTF-8 字符显示能力 @@ -96,9 +111,9 @@ ### 5.1 菜单模型与菜单树 -- 静态菜单模型:`tagMenuModel`(定义于 `display.h`,数据在 `display.c`) -- 运行时菜单项:`tagMenuItem`(定义于 `menu.c` 内) -- 全局控制:`g_tMenuCtrl`、`g_tDspCtrl` +- 静态菜单模型:`tagMenuModel`(定义于 `src/Drv/menu/model/display.h`,数据在 `src/Drv/menu/model/display.c`) +- 运行时菜单项:`tagMenuItem`(定义于 `src/Drv/menu/common/menu_item_types.h`,实例由 `src/Drv/menu/app/menu.c` 管理) +- 运行时控制:`tagMenuCtrl`、`tagDspCtrl`(定义于 `src/Drv/menu/common/menu_state_types.h`,实例为 `app/menu.c` 内部私有) 关键关系: @@ -128,7 +143,7 @@ [系统初始化] | v -[Menu_Route] +[MenuApp_PollInput] | v [Sleep 20ms] @@ -137,9 +152,9 @@ [计数器累加] | v -[是否到刷新周期?] --否--> [Menu_Route] +[是否到刷新周期?] --否--> [MenuApp_PollInput] | - +--是--> [Menu_Show_Proc] --> [Menu_Route] + +--是--> [MenuApp_Render] --> [MenuApp_PollInput] ``` ### 6.2 菜单交互流程 @@ -150,7 +165,85 @@ - 左/ESC:回退上级或退回主层 - 右/确认:进入子级或执行叶子回调 - 渲染: - - `Menu_Show_Proc` 根据路径增量刷新或全量刷新 + - `MenuApp_Render` 调用 Presenter/View,根据路径进行增量刷新或全量刷新 + +### 6.4 当前 Menu 详细设计(MVP) + +#### 6.4.1 模块拆分 + +- `src/Drv/menu/app/menu.c` + - 菜单应用 Facade(`MenuApp_*` 对外接口) + - 持有运行时私有状态(`s_menuCtrl`、`s_dspCtrl`、`s_menuItems`) +- `src/Drv/menu/presenter/menu_presenter.c` + - 控制调度中枢:处理输入事件、调用导航器、触发重建路径与刷新 +- `src/Drv/menu/model/menu_model.c` + - 模型初始化:树构建、菜单名修饰、初始状态建立 +- `src/Drv/menu/view/menu_view.c` + - 纯渲染:顶部栏、多级菜单框、高亮反显、全量/增量刷新策略 +- `src/Drv/menu/presenter/menu_navigator.c` + - 纯导航状态机:按键到 `MenuNavResult`(是否刷新、是否跳过渲染) +- `src/Drv/menu/view/menu_layout.c` + - 菜单布局计算:宽度统计、层级矩形定位、越界回退策略 +- `src/Drv/menu/model/display.c` + - 静态菜单模型表 `g_tMenuModelTab`(业务菜单定义) + +#### 6.4.2 运行时调用时序 + +```text +MenuApp_Init + -> MenuPresenter_Setup + -> MenuPresenter_InitModel + -> MenuModel_Bootstrap + -> MenuTree_0LevelNumCal + -> MenuTree_MainCreate + -> MenuView_Layout + -> MenuLayout_PositionCal + +主循环: + MenuApp_PollInput + -> Key_Read + -> MenuApp_HandleInput(key) + -> MenuPresenter_HandleInput + -> MenuNavigator_ProcessKey + -> (needRefresh) MenuNavigator_RebuildRoute + -> MenuPresenter_Refresh + -> MenuView_RenderByState +``` + +#### 6.4.3 关键状态数据 + +- `tagMenuItem` + - 菜单节点实体,包含树关系(`ptHigher/ptLower/ptBefore/ptBehind`)与显示矩形 +- `tagMenuCtrl` + - 导航核心状态(`ptCurrent`、`ptRoute[4]`、`ptCurBak`、`pt0Level` 等) +- `tagDspCtrl` + - 显示控制状态(当前主要使用首帧标记 `bFirst`) +- `MenuNavState / MenuNavResult` + - Presenter 与 Navigator 之间的状态快照与处理结果 + +#### 6.4.4 对外数据接口(当前基线) + +- `void MenuApp_Init(void)` + - 初始化菜单应用(Presenter/Model/View) +- `void MenuApp_HandleInput(uint8_t keyVal)` + - 注入输入事件并驱动导航状态变更 +- `void MenuApp_PollInput(void)` + - 从 `Key_Read()` 读取按键并转发到 `MenuApp_HandleInput` +- `void MenuApp_Render(void)` + - 主动触发一次渲染(用于周期刷新或外部强制重绘) +- `const tagMenuItem *MenuApp_GetMenuItems(uint16_t *count)` + - 只读导出当前菜单项数组(调试/测试使用) + +#### 6.4.5 内部接口边界(模块间) + +- Model -> View + - 仅通过控制状态与布局结果共享,不直接调用绘制原语 +- Presenter -> Model + - 仅在初始化阶段触发 `MenuModel_Bootstrap` +- Presenter -> View + - 通过 `MenuPresenter_Refresh` 驱动渲染,不暴露底层 LCD 细节 +- Presenter -> Navigator + - 通过 `MenuNavState` 快照交互,避免导航器直接操作外部全局变量 ### 6.3 远程显示协议流程 @@ -220,10 +313,12 @@ ctest --test-dir build -C Debug --output-on-failure ## 10. 模块依赖关系(代码级) - `main.c` 依赖:`menu`、`key`、`remoteDisplay`、`tcp`、`thread_utils` -- `menu.c` 依赖:`lcd`、`display`、`key` +- `src/Drv/menu/app/menu.c` 依赖:`presenter/menu_presenter`、`model/display`、`key`、`lcd` +- `src/Drv/menu/presenter/menu_presenter.c` 依赖:`model/menu_model`、`view/menu_view`、`presenter/menu_navigator` +- `src/Drv/menu/view/menu_view.c` 依赖:`model/menu_layout`、`view/menu_render_port`、`lcd` - `remoteDisplay.c` 依赖:`lcd`、`key`、`tcp`、`thread_utils` - `lcd.c` 依赖:`ascii` -- `display.c` 提供:静态菜单表(被 `menu.c` 使用) +- `src/Drv/menu/model/display.c` 提供:静态菜单表(被 menu 模块使用) ## 11. 已知风险与改进建议 @@ -242,3 +337,32 @@ ctest --test-dir build -C Debug --output-on-failure - 每次新增模块或调整主流程时同步更新本文档 - 测试策略更新需同步维护“第 9 章 测试架构” +## 13. 菜单重构故障复盘(2026-03) + +### 13.1 现象 + +- 菜单分层重构后,程序在启动阶段(当前入口为 `MenuApp_Init`)出现访问冲突,表现为“运行即崩溃”。 + +### 13.2 根因 + +- 根因位于菜单树构建模块 `MenuTree_MainCreate`。 +- 在“层级回退(`byCurClass > byNextClass`)”分支中,缺少对中间层级链表的及时收口(首尾成环)处理。 +- 后续 `Menu_PositionCal` 在遍历同级链表时访问到异常节点,导致崩溃。 + +### 13.3 修复措施 + +- 恢复并对齐原稳定逻辑: + - 当层级回退时,立即对回退区间层级执行首尾成环收口。 + - 每轮处理后更新 `ptCurrent = ptNextNode`,保证状态推进一致。 + - 循环结束后按当前实际层级执行最终收口。 + +### 13.4 回归防线 + +- 新增启动路径集成回归用例:`tests/test_p2_menu_runtime_startup.c`。 +- 覆盖最易回归的启动路径: + - `MenuApp_Init()` + - `Key_Init()` + - 首次 `MenuApp_PollInput()` + - `MenuApp_Render()` +- 该用例用于防止“菜单树可编译但启动崩溃”的问题再次进入主干。 + diff --git a/src/Drv/lcd/lcd_draw.c b/src/Drv/lcd/lcd_draw.c index 9de6b8a..36ecc96 100644 --- a/src/Drv/lcd/lcd_draw.c +++ b/src/Drv/lcd/lcd_draw.c @@ -13,6 +13,7 @@ * - 坐标区间语义在不同函数中不同,使用时需特别注意(见各函数注释)。 * ------------------------------------------------------------------------- */ #include "lcd_draw.h" +#include #ifdef DEBUG #include @@ -170,6 +171,81 @@ int8_t Lcd_LineV(uint16_t wYStart, uint16_t wYEnd, uint16_t wXStart, uint16_t wW return LCD_OK; } +/* ------------------------------------------------------------------------- + * 函数名: Lcd_Line + * 功能: + * 绘制任意方向线段(可设置线宽)。 + * 说明: + * - 水平/垂直线走 Lcd_LineH/Lcd_LineV 快速路径。 + * - 斜线采用 Bresenham 风格误差迭代,并做对称双端绘制。 + * ------------------------------------------------------------------------- */ +int8_t Lcd_Line(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend, uint16_t wWidth, uint8_t color) +{ + uint16_t wDX, wDY, wSignY, wSignX, wDecision; + uint16_t wCurx, wCury, wNextx, wNexty, wPY, wPX; + + if (Lcd_ColorCheck(color) != LCD_OK) return LCD_ERR_INVALID_COLOR; + if (wYstart == wYend) return Lcd_LineH(wXstart, wXend, wYstart, wWidth, color); + if (wXstart == wXend) return Lcd_LineV(wYstart, wYend, wXstart, wWidth, color); + + wDX = (uint16_t)abs((int)wXstart - (int)wXend); + wDY = (uint16_t)abs((int)wYstart - (int)wYend); + if (((wDX >= wDY && (wXstart > wXend)) || ((wDY > wDX) && (wYstart > wYend)))) { + M_GuiSwap(wXend, wXstart); + M_GuiSwap(wYend, wYstart); + } + wSignY = (wYend - wYstart) / wDY; + wSignX = (wXend - wXstart) / wDX; + + if (wDX >= wDY) { + wCurx = wXstart; + wCury = wYstart; + wNextx = wXend; + wNexty = wYend; + wDecision = (wDX >> 1); + for (; wCurx <= wNextx;) { + if (wDecision >= wDX) { + wDecision -= wDX; + wCury += wSignY; + wNexty -= wSignY; + } + for (wPY = wCury - wWidth / 2; wPY <= wCury + wWidth / 2; wPY++) { + Lcd_SetPixel(wCurx, wPY, color); + } + for (wPY = wNexty - wWidth / 2; wPY <= wNexty + wWidth / 2; wPY++) { + Lcd_SetPixel(wNextx, wPY, color); + } + wCurx++; + wNextx--; + wDecision += wDY; + } + } else { + wCurx = wXstart; + wCury = wYstart; + wNextx = wXend; + wNexty = wYend; + wDecision = (wDY >> 1); + for (; wCury <= wNexty;) { + if (wDecision >= wDY) { + wDecision -= wDY; + wCurx += wSignX; + wNextx -= wSignX; + } + for (wPX = wCurx - wWidth / 2; wPX <= wCurx + wWidth / 2; wPX++) { + Lcd_SetPixel(wPX, wCury, color); + } + for (wPX = wNextx - wWidth / 2; wPX <= wNextx + wWidth / 2; wPX++) { + Lcd_SetPixel(wPX, wNexty, color); + } + wCury++; + wNexty--; + wDecision += wDX; + } + } + + return LCD_OK; +} + /* 宏: M_Max / M_Min * 作用: * 计算两个值的较大/较小值,用于将任意方向输入规范化为最小/最大边界。 diff --git a/src/Drv/lcd/lcd_draw.h b/src/Drv/lcd/lcd_draw.h index 210de97..d64c437 100644 --- a/src/Drv/lcd/lcd_draw.h +++ b/src/Drv/lcd/lcd_draw.h @@ -9,5 +9,6 @@ int8_t Lcd_FillRect(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t int8_t Lcd_Invert(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend); int8_t Lcd_LineH(uint16_t wXStart, uint16_t wXEnd, uint16_t wYStart, uint16_t wWidth, uint8_t color); int8_t Lcd_LineV(uint16_t wYStart, uint16_t wYEnd, uint16_t wXStart, uint16_t wWidth, uint8_t color); +int8_t Lcd_Line(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend, uint16_t wWidth, uint8_t color); #endif diff --git a/src/Drv/lcd/lcd_text.c b/src/Drv/lcd/lcd_text.c index 031c0b8..da5323c 100644 --- a/src/Drv/lcd/lcd_text.c +++ b/src/Drv/lcd/lcd_text.c @@ -1,11 +1,11 @@ /* ------------------------------------------------------------------------- * 文件名: lcd_text.c * 模块职责: - * 提供文本渲染与 UTF-8 解析能力,包含: - * 1) UTF-8 单字符解码(utf8_next) - * 2) ASCII 字符绘制(内部函数 Lcd_Pub_Ascii) - * 3) 中文位图绘制(内部函数 Lcd_Pub_UTF8) - * 4) 字符串渲染入口(Lcd_ShowStr) + * 提供文本渲染能力,包含: + * 1) ASCII 字符绘制(内部函数 Lcd_Pub_Ascii) + * 2) 中文位图绘制(内部函数 Lcd_Pub_UTF8) + * 3) 字符串渲染入口(Lcd_ShowStr) + * 4) 依赖 common/utf8 的 utf8_next 进行 UTF-8 解析 * * 设计说明: * - 显示像素最终通过 Lcd_SetPixel 写入显存。 @@ -30,50 +30,6 @@ static textConfig text_cfg = { .rowSpace = 2 }; -/* ------------------------------------------------------------------------- - * 函数名: utf8_next - * 功能: - * 从 UTF-8 字节流当前位置解析“一个字符”,输出 Unicode 码点与字节长度。 - * - * 参数: - * utf8 - 指向当前待解析字节 - * out_unicode - 输出解析得到的 Unicode 码点 - * - * 返回值: - * 0 : 解析失败/字符串结束 - * 1 : ASCII 单字节 - * 2 : 两字节 UTF-8 - * 3 : 三字节 UTF-8 - * - * 注意: - * - 当前实现不支持 4 字节 UTF-8(如 emoji)。 - * - 对截断序列(缺少后续字节)返回 0。 - * ------------------------------------------------------------------------- */ -uint8_t utf8_next(const unsigned char *utf8, uint32_t *out_unicode) -{ - unsigned char c = utf8[0]; - if (c == 0) { - *out_unicode = 0; - return 0; - } - if (c < 0x80) { - *out_unicode = c; - return 1; - } - if ((c & 0xE0) == 0xC0) { - if (utf8[1] == 0) return 0; - *out_unicode = (uint32_t)((c & 0x1F) << 6 | (utf8[1] & 0x3F)); - return 2; - } - if ((c & 0xF0) == 0xE0) { - if (utf8[1] == 0 || utf8[2] == 0) return 0; - *out_unicode = (uint32_t)((c & 0x0F) << 12 | (utf8[1] & 0x3F) << 6 | (utf8[2] & 0x3F)); - return 3; - } - *out_unicode = 0; - return 0; -} - /* ------------------------------------------------------------------------- * 函数名: Lcd_Pub_Ascii(内部) * 功能: diff --git a/src/Drv/lcd/lcd_text.h b/src/Drv/lcd/lcd_text.h index 06b736c..60f9dbb 100644 --- a/src/Drv/lcd/lcd_text.h +++ b/src/Drv/lcd/lcd_text.h @@ -2,6 +2,7 @@ #define __LCD_TEXT_H__ #include "lcd.h" +#include "utf8.h" typedef struct { uint8_t wGBFontWidth; @@ -12,7 +13,6 @@ typedef struct { uint16_t rowSpace; } textConfig; -uint8_t utf8_next(const unsigned char *utf8, uint32_t *out_unicode); int8_t Lcd_ShowStr(uint16_t x, uint16_t y, uint8_t *pcString); int8_t Lcd_ShowTest(uint16_t x, uint16_t y, uint8_t *pcString); diff --git a/src/Drv/menu.c b/src/Drv/menu.c deleted file mode 100644 index bbd7e49..0000000 --- a/src/Drv/menu.c +++ /dev/null @@ -1,1239 +0,0 @@ -#include "menu.h" -#include -#include -#include -#include -#include "lcd/lcd.h" -#include "lcd/lcd_draw.h" -#include "lcd/lcd_text.h" -#include "display.h" -#include "key.h" - -/* 简单的静态菜单树: - * - * 层级 0: [运行界面] [定值设置] [查看数据] - * │ │ - * 层级 1: (子菜单...) (子菜单...) - */ - typedef struct _MENU_ITEM_ - { - struct _MENU_ITEM_ *ptHigher; // 上级菜单指针 - struct _MENU_ITEM_ *ptLower; // 下级菜单指针 - struct _MENU_ITEM_ *ptBefore; // 同级上方指针 - struct _MENU_ITEM_ *ptBehind; // 同级下方指针 - - uint8_t byClass; // 菜单分级标志; - uint8_t byName[50]; // 菜单字符串; - uint8_t byTip[50]; // 菜单提示文本; - uint8_t byAttrib; // 菜单属性,设置菜单特殊显示效果; - uint16_t wPassword; // 访问密码,0x0000表示没有密码; - uint16_t wPara; // 菜单执行函数参数; - FUNCPTR pfnWinProc; // 界面执行函数指针; - - uint16_t wPos; /* 当前菜单在本级中的位置 */ - uint16_t wNum; /* 下级菜单的总项数 */ - uint16_t wSPosX; /*下级菜单框 x 轴的起始坐标*/ - uint16_t wSPosY; /*下级菜单框 y 轴的起始坐标*/ - uint16_t wEPosX; /*下级菜单框 x 轴的结束坐标*/ - uint16_t wEPosY; /*下级菜单框 y 轴的结束坐标*/ - }tagMenuItem,*tagPMenuItem; - -// 显示控制结构 -typedef struct -{ - tagPMenuItem ptMenuCur; // 菜单当前指针 - - uint8_t byLayer; // 显示层 - uint8_t bySide; // 用于显示定值侧别 - - uint16_t wGroup; // 最大页面数 - uint16_t wPage; // 当前页面数 - uint16_t wRes; // RES - uint16_t wPassword; // 输入密码 - - uint16_t wPara; // 菜单参数 - uint16_t wItemN; // 当前菜单对应显示项数 - uint16_t wPos; // 数据定位 - uint16_t wBaseNo; // 相位基准(debug) - - uint8_t byNumOfPage; // 每页显示项数 - uint8_t byCount; // 通用计数 - uint8_t byAdjustX; - uint8_t bGetSet; // 上电召定值标志 - uint8_t bLightByNewRec; // 由新记录点亮背光灯.modified by zhanggl.111109 - - unsigned bPwdOK:1; // 密码状态 - unsigned bFirst:1; // 第一次进入界面<是/否> - unsigned bChanged:1; // 数据是否被修改 - unsigned bModFlag:1; // 是否处于修改状态 - unsigned bEdit:1; // 重入标志 - unsigned bFlash:1; // 闪烁标志 - unsigned bRunLayer:1; // 运行层标志 - unsigned bitOutput:1; - unsigned bShowMode:1; // F1功能键切换同轴显示模式 - unsigned bShowCursor:1; // F2功能键切换游标显示模式 - unsigned bRes:1; -}tagDspCtrl,*tagPDspCtrl; - -typedef struct -{ // 菜单控制结构 - uint8_t by0LevelNum; // 0级菜单项数 - uint8_t byLeftMove; // 菜单左移长度 - uint8_t bySecPage; // 第二页菜单标志 - FUNCPTR fnExecute; // 执行函数指针 - - tagPMenuItem ptHead; // 菜单链表头指针 - tagPMenuItem ptCurrent; /* 菜单链表当前指针 */ - tagPMenuItem ptRoute[4]; // 0-3级菜单的指针路径 - tagPMenuItem ptCurBak; /* ptCurrent 的备份,通过比较ptCurBak和ptCurrent是否相等来判断是否发生移动 */ - tagPMenuItem pt0Level; // 0级菜单链表指针备份 - - tagPMenuItem ptSeeSetSoft; // 查看定值菜单结点 - tagPMenuItem ptSetValSoft; // 整定定值菜单结点 - tagPMenuItem ptSeeSetFigure; // 查看定值菜单结点 - tagPMenuItem ptSetValFigure; // 整定定值菜单结点 - tagPMenuItem ptSeeSetPara; // 查看定值菜单结点 - tagPMenuItem ptSetValPara; // 整定定值菜单结点 -}tagMenuCtrl,*tagPMenuCtrl; - -tagDspCtrl g_tDspCtrl; /* 显示控制全局结构 */ -tagMenuCtrl g_tMenuCtrl; /* 菜单全局控制结构 */ - - -tagMenuItem g_tMenuItem[300]; // 菜单链表空间定义 - -#define MENU_GUI_SWAP(a, b) \ - do { \ - (a) ^= (b); \ - (b) ^= (a); \ - (a) ^= (b); \ - } while (0) - -static void Menu_Line(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend, uint16_t wWidth, uint8_t color) -{ - uint16_t wDX, wDY, wSignY, wSignX, wDecision; - uint16_t wCurx, wCury, wNextx, wNexty, wPY, wPX; - - if (wYstart == wYend) { - Lcd_LineH(wXstart, wXend, wYstart, wWidth, color); - return; - } - if (wXstart == wXend) { - Lcd_LineV(wYstart, wYend, wXstart, wWidth, color); - return; - } - - wDX = (uint16_t)abs((int)wXstart - (int)wXend); - wDY = (uint16_t)abs((int)wYstart - (int)wYend); - if (((wDX >= wDY && (wXstart > wXend)) || ((wDY > wDX) && (wYstart > wYend)))) { - MENU_GUI_SWAP(wXend, wXstart); - MENU_GUI_SWAP(wYend, wYstart); - } - wSignY = (wYend - wYstart) / wDY; - wSignX = (wXend - wXstart) / wDX; - - if (wDX >= wDY) { - wCurx = wXstart; - wCury = wYstart; - wNextx = wXend; - wNexty = wYend; - wDecision = (wDX >> 1); - for (; wCurx <= wNextx;) { - if (wDecision >= wDX) { - wDecision -= wDX; - wCury += wSignY; - wNexty -= wSignY; - } - for (wPY = wCury - wWidth / 2; wPY <= wCury + wWidth / 2; wPY++) { - Lcd_SetPixel(wCurx, wPY, color); - } - for (wPY = wNexty - wWidth / 2; wPY <= wNexty + wWidth / 2; wPY++) { - Lcd_SetPixel(wNextx, wPY, color); - } - wCurx++; - wNextx--; - wDecision += wDY; - } - } else { - wCurx = wXstart; - wCury = wYstart; - wNextx = wXend; - wNexty = wYend; - wDecision = (wDY >> 1); - for (; wCury <= wNexty;) { - if (wDecision >= wDY) { - wDecision -= wDY; - wCurx += wSignX; - wNextx -= wSignX; - } - for (wPX = wCurx - wWidth / 2; wPX <= wCurx + wWidth / 2; wPX++) { - Lcd_SetPixel(wPX, wCury, color); - } - for (wPX = wNextx - wWidth / 2; wPX <= wNextx + wWidth / 2; wPX++) { - Lcd_SetPixel(wPX, wNexty, color); - } - wCury++; - wNexty--; - wDecision += wDX; - } - } -} - -static void Menu_MeiTouPic(uint16_t wYStart, uint16_t wWidth) -{ - Lcd_LineH(16, 144, wYStart, wWidth, LCD_FONT); - Menu_Line(8, wYStart - 8, 16, wYStart, wWidth, LCD_FONT); - Menu_Line(144, wYStart, 152, wYStart - 8, wWidth, LCD_FONT); -} - -static void Menu_ButtonPush(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y) -{ - Lcd_LineH(left_x, right_x, top_y, 1, LCD_FONT); - Lcd_LineV(top_y, bottom_y, left_x, 1, LCD_FONT); - Lcd_LineH(left_x, right_x, bottom_y, 1, LCD_FONT); - Lcd_LineV(top_y, bottom_y, right_x, 1, LCD_FONT); -} - -void Menu_0LevelNumCal(void) -{ - tagPMenuModel ptModelIndex; /* 当前遍历到的菜单表项指针 */ - uint16_t wLoop; /* 表项下标,0 .. MENU_MAX_ITEM-1 */ - uint8_t by0LevelNum; /* 0 级菜单项计数 */ - - by0LevelNum = 0; - for (wLoop = 0; wLoop < MENU_MAX_ITEM; wLoop++) { - ptModelIndex = (tagPMenuModel)&g_tMenuModelTab[wLoop]; - if (0 == ptModelIndex->byClass) { - by0LevelNum++; - } - } - g_tMenuCtrl.by0LevelNum = by0LevelNum; -} -/****************************************************************************** - * 函数名: Menu_Main_Creat - * 功能: 根据静态菜单表 g_tMenuModelTab 在 g_tMenuItem 中建立可遍历的菜单树 - * (双向链表 + 父子关系,同级首尾成环) - * 参数: 无 - * 返回: 无 - * 说明: 1. 表内按深度优先顺序排列:先 0 级,再其下 1 级,再 2 级… 同级按表顺序 - * 2. 通过 byClass(0/1/2/3)区分层级 - * 3. 用 ptFirst[级]、ptLast[级] 记录每级首尾,便于链接与最后首尾成环 - *****************************************************************************/ -void Menu_Main_Creat(void) -{ - tagPMenuItem ptFirst[4]; /* 各级(0..3)当前“首”结点,用于成环与回退时链接 */ - tagPMenuItem ptLast[4]; /* 各级当前“尾”结点 */ - tagPMenuItem ptCurrent; /* 表顺序上的“当前”结点(刚处理完的) */ - tagPMenuItem ptNextNode; /* 本轮要接入的“下一”结点 */ - - tagPMenuModel ptModelIndex; - uint16_t wLoop1, wLoop2; - uint8_t byCurClass; /* 当前结点层级 */ - uint8_t byNextClass; /* 下一结点层级 */ - - /* ---------- 第 0 项:作为 0 级首项,初始化并记为 0 级首/尾 ---------- */ - ptFirst[0] = &g_tMenuItem[0]; - ptLast[0] = ptFirst[0]; - - ptCurrent = &g_tMenuItem[0]; - ptModelIndex = (tagPMenuModel)&g_tMenuModelTab[0]; - ptCurrent->wPos = 0; - ptCurrent->wNum = 0; - ptCurrent->wSPosX = 0; - ptCurrent->wSPosY = 0; - ptCurrent->wEPosX = 0; - ptCurrent->wEPosY = 0; - - ptCurrent->ptHigher = NULL; - ptCurrent->ptLower = NULL; - ptCurrent->ptBefore = NULL; - ptCurrent->ptBehind = NULL; - - ptCurrent->byClass = ptModelIndex->byClass; - ptCurrent->byAttrib = ptModelIndex->byAttrib; - ptCurrent->wPassword = ptModelIndex->wPassword; - ptCurrent->wPara = ptModelIndex->wPara; - ptCurrent->pfnWinProc = ptModelIndex->pfnWinProc; - memcpy(ptCurrent->byName, ptModelIndex->byName, 50); - memcpy(ptCurrent->byTip, ptModelIndex->byTip, 50); -#ifdef DEBUG - printf("Menu_Main_Creat:%d级菜单:%s\n", ptCurrent->byClass, ptCurrent->byName); -#endif - - /* ---------- 从第 1 项起,按表顺序逐个接入树 ---------- */ - for (wLoop1 = 1; wLoop1 < MENU_MAX_ITEM; wLoop1++) - { - /* 获取下一项菜单项 */ - ptNextNode = &g_tMenuItem[wLoop1]; - /* 获取下一项菜单项的模型索引 */ - ptModelIndex = (tagPMenuModel)&g_tMenuModelTab[wLoop1]; - - /* 初始化下一项菜单项的各项属性 */ - ptNextNode->wPos = 0; - ptNextNode->wNum = 0; - ptNextNode->wSPosX = 0; - ptNextNode->wSPosY = 0; - ptNextNode->wEPosX = 0; - ptNextNode->wEPosY = 0; - ptNextNode->ptHigher = NULL; - ptNextNode->ptLower = NULL; - ptNextNode->ptBefore = NULL; - ptNextNode->ptBehind = NULL; - - ptNextNode->byClass = ptModelIndex->byClass; - ptNextNode->byAttrib = ptModelIndex->byAttrib; - ptNextNode->wPassword = ptModelIndex->wPassword; - ptNextNode->wPara = ptModelIndex->wPara; - ptNextNode->pfnWinProc = ptModelIndex->pfnWinProc; - memcpy(ptNextNode->byName, ptModelIndex->byName, 50); - memcpy(ptNextNode->byTip, ptModelIndex->byTip, 50); -#ifdef DEBUG - printf("Menu_Main_Creat:%d级菜单:%s\n", ptNextNode->byClass, ptNextNode->byName); -#endif - - byCurClass = ptCurrent->byClass; - byNextClass = ptNextNode->byClass; - - /* 根据当前菜单项和下一菜单项的层级关系,更新菜单链表 */ - if (byCurClass < byNextClass) - { - ptFirst[byNextClass] = ptNextNode; - ptLast[byNextClass] = ptFirst[byNextClass]; - - ptCurrent->ptLower = ptNextNode; - ptNextNode->ptHigher = ptCurrent; - } - else if (byCurClass == byNextClass) - { - ptLast[byNextClass] = ptNextNode; - ptCurrent->ptBehind = ptNextNode; - - ptNextNode->ptBefore = ptCurrent; - ptNextNode->ptHigher = ptCurrent->ptHigher; - } - else if (byCurClass > byNextClass) /* 下一项更浅 */ - { - /* 下一项更浅:下一项接到 byNextClass 级当前尾的后面*/ - ptLast[byNextClass]->ptBehind = ptNextNode; - ptNextNode->ptBefore = ptLast[byNextClass]; - ptNextNode->ptHigher = ptLast[byNextClass]->ptHigher; - /* 更新 byNextClass 级当前尾 */ - ptLast[byNextClass] = ptNextNode; - - /* 当层级回退时,更新各级首尾成环 */ - for (wLoop2 = byCurClass; wLoop2 > byNextClass; wLoop2--) - { - ptLast[wLoop2]->ptBehind = ptFirst[wLoop2]; /* 尾的下一个是首 */ - ptFirst[wLoop2]->ptBefore = ptLast[wLoop2]; /* 首的前一个是尾 */ - } - } - ptCurrent = ptNextNode; - } - - /* ---------- 从当前所在级到 0 级,各级首尾成环(循环链表) ---------- */ - byCurClass = ptCurrent->byClass; - for (wLoop1 = 0; wLoop1 <= byCurClass; wLoop1++) - { - ptLast[wLoop1]->ptBehind = ptFirst[wLoop1]; - ptFirst[wLoop1]->ptBefore = ptLast[wLoop1]; - } -} -uint8_t utf8_len_cal(uint8_t *str) -{ - uint8_t strLen = 0; - uint32_t unicode; - uint8_t index = 0; - uint8_t n = 0; - - while (str[index] != '\0') - { - n = utf8_next(str + index, &unicode); - if (n > 1) - { - strLen += 2; - } - else - { - strLen++; - } - index += n; - } - return strLen; -} -/****************************************************************************** - * 函数名: Menu_charLenCal - * 功能: 1.计算指定级菜单下所有子项的最大显示长度 - * 2.为有下级的菜单名追加右箭头符号 '\x10' - * 3.更新 wPos 字段,表示当前菜单在本级中的位置 - * 4. wNum 指定级菜单的下级级菜数量 - * 参数: bylevel - 当前层级(调用方一般为 0,表示从 0 级菜单的子级开始) - * byMenuNum - 各级菜单项数量数组 [0..3],本函数会更新 bylevel+1 级计数 - * ptFirst - 各级首项指针数组,本函数会写入 ptFirst[bylevel+1] - * ptIndex - 各级当前项指针数组,本函数会更新 ptIndex[bylevel] 及子级 - * 返回: 该级所有菜单项中的最大字符长度(含可能追加的右箭头 '\x10') - * 说明: 1. 从 ptIndex[bylevel] 的子级(ptLower)开始,沿 ptBehind 遍历同级所有项 - * 2. 若有下级菜单且名称末尾无右箭头,则在 byName 末尾追加 '\x10' 并补 '\0' - * 3. 同级菜单首尾成环,遍历回 ptFirst 时结束 - * 4. 供 Menu_PositionCal 用于计算下拉菜单矩形框宽度(byMaxLen * 字宽) - *****************************************************************************/ -uint8_t Menu_charLenCal(uint8_t bylevel, uint8_t *byMenuNum, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex) -{ - uint16_t wLoop; - uint8_t byStrLen; /* 当前菜单项名称长度(含可能追加的右箭头) */ - uint8_t utf8Len; - uint8_t byMaxLen; /* 同级菜单中的最大显示长度 */ - - /* 记录子级首项,供后续遍历与布局使用 */ - ptFirst[bylevel + 1] = ptIndex[bylevel]->ptLower; - - bylevel = bylevel + 1; - ptIndex[bylevel] = ptFirst[bylevel]; - - byMenuNum[bylevel] = 1; - ptIndex[bylevel]->wPos = 1; - - byMaxLen = 0; - for (wLoop = 0; wLoop < MENU_MAX_ITEM; wLoop++) - { - /* 计算当前菜单项名称长度 */ - utf8Len = utf8_len_cal(ptIndex[bylevel]->byName); - byStrLen = (uint8_t)strlen((const char *)ptIndex[bylevel]->byName); - /* 若有下级且名称末尾尚无右箭头,则追加 '\x10' 并更新长度 */ - if ((ptIndex[bylevel]->ptLower != NULL) && ('\x10' != ptIndex[bylevel]->byName[byStrLen - 1])) - { - ptIndex[bylevel]->byName[byStrLen] = '\x10'; - ptIndex[bylevel]->byName[byStrLen + 1] = '\0'; - utf8Len += 1; - } - - if (byMaxLen < utf8Len) - { - byMaxLen = utf8Len; - } -#ifdef DEBUG - printf("计算%d级菜单位置:%s,长度%d\n", bylevel, ptIndex[bylevel]->byName, byMaxLen); -#endif - - /* 移至同级下一项(ptBehind),同级首尾成环 */ - ptIndex[bylevel] = ptIndex[bylevel]->ptBehind; - if (ptIndex[bylevel] == ptFirst[bylevel]) /* 同级首尾成环,遍历回 ptFirst 时说明遍历完了当前级的所有菜单,结束 */ - { - break; - } - byMenuNum[bylevel]++; /* 同级菜单项数量+1 */ - ptIndex[bylevel]->wPos = byMenuNum[bylevel]; /* 同级菜单项序号 + 1 */ - } - - /* 将子级菜单总数回填到父项 wNum */ - ptIndex[bylevel - 1]->wNum = byMenuNum[bylevel]; - return byMaxLen; -} -void Menu_Sub2PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex) -{ - uint16_t wLoop; - uint16_t wSPosX; - uint16_t wSPosY; - uint16_t wEPosX; - uint16_t wEPosY; - uint16_t wTemp; - uint8_t byMaxLen; - uint8_t byMaxNum; - uint16_t byMenuPos; - uint8_t byItemNum; - uint8_t byMenuNum[4]; - - byMaxNum = (MENU_YMAX - MENU_YMIN - 6) / LINE_HEIGHT; // 页内纵向可显示菜单数 - - ptIndex[bylevel] = ptFirst[bylevel]; - for (wLoop = 0; wLoop < MENU_MAX_ITEM; wLoop++) - { - if ((ptIndex[bylevel]->ptLower) != NULL) // 当前2级菜单有下级菜单 - { - byMaxLen = Menu_charLenCal(bylevel, byMenuNum, ptFirst, ptIndex); // 计算3级菜单项数及字符数 - - wSPosX = ptIndex[bylevel - 1]->wEPosX; // 3级菜单矩形框横坐标起始 - ptIndex[bylevel]->wSPosX = wSPosX; - - wEPosX = - wSPosX + byMaxLen * MENU_WITDTH + MENU_XADD; // 3级菜单矩形框横坐标终止 - ptIndex[bylevel]->wEPosX = wEPosX; - - byMenuPos = ptIndex[bylevel]->wPos; // 2级菜单在菜单列中的位置 - byItemNum = byMenuNum[bylevel]; // 2级菜单项数 - - if ((byItemNum > byMaxNum) && (byMenuPos >(byMaxNum - 1))) // 2级菜单项数大于一页且2级菜单当前位置不在第一页 - { - byMenuPos = byMaxNum - (byItemNum - byMenuPos); // 第一页尾显示↓ 第二页头显示↑ - } - - wSPosY = ptIndex[bylevel - 1]->wSPosY; - wSPosY = - wSPosY + (byMenuPos - 1) * LINE_HEIGHT; // 计算3级菜单框起始坐标 - - byItemNum = byMenuNum[bylevel + 1]; // 3级菜单项数 - wEPosY = wSPosY + byItemNum * LINE_HEIGHT + MENU_YADD; // 计算3级菜单框终止坐标 - - if (wEPosY < MENU_YMAX) // 3级菜单框与2级对应位置不越限 - { - ptIndex[bylevel]->wSPosY = wSPosY; // 3级菜单矩形框纵起始坐标定位 - ptIndex[bylevel]->wEPosY = wEPosY; // 3级菜单矩形框纵终止坐标定位 - } - else - { - wEPosY = wSPosY - (byItemNum - 1) * LINE_HEIGHT - MENU_YADD; - if ((wEPosY > MENU_YMIN) && (wEPosY < MENU_YMAX)) // 菜单向上不越限 - { - wTemp = wSPosY; - wSPosY = wEPosY; - wEPosY = wTemp + LINE_HEIGHT; - } - else // 菜单显示超过一页 - { - wSPosY = ptIndex[0]->wSPosY; // 3级菜单框起始坐标同1级菜单 - wEPosY = wSPosY + byItemNum * LINE_HEIGHT + MENU_YADD; // 计算3级菜单框终止坐标 - - if (wEPosY > MENU_YMAX) - { - wEPosY = MENU_YMAX; // 菜单纵向一屏可显示 - } - } - ptIndex[bylevel]->wSPosY = wSPosY; // 3级菜单矩形框纵起始坐标定位 - ptIndex[bylevel]->wEPosY = wEPosY; // 3级菜单矩形框纵终止坐标定位 - } - } - /* 移至同级下一项 */ - ptIndex[bylevel] = ptIndex[bylevel]->ptBehind; - /*如果移至同级下一项后,发现回到了同级菜单的第一个菜单,则说明遍历完了同级菜单的所有菜单,结束*/ - if (ptIndex[bylevel] == ptFirst[bylevel]) - { - break; - } - } -} - -/* - * 功能:根据当前菜单层级 bylevel,计算该层级所有子菜单(如二级菜单)的矩形框位置。 - * 主要负责: - * - 统计下一级菜单的最大字符长度,用于确定菜单框的宽度(X 方向) - * - 根据上一级菜单条目的位置以及当前条目在本列中的位置,计算菜单框的起始/结束 Y 坐标 - * - 处理菜单项超过一屏时的翻页显示逻辑,保证菜单框不超出屏幕边界 - * - * 参数说明: - * bylevel :当前要计算的菜单层级(例如 1 表示一级菜单,2 表示二级菜单) - * ptFirst :保存各层级“同级菜单链表首节点”的指针数组 - * ptIndex :在遍历过程中使用的“当前菜单指针数组”,ptIndex[bylevel] 表示当前层级正在处理的菜单项 - */ -void Menu_Sub1PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex) -{ - uint16_t wSPosY; // 当前层级菜单框的纵向起始坐标(Start Y) - uint16_t wEPosY; // 当前层级菜单框的纵向结束坐标(End Y) - uint8_t byMenuNum[4]; // 各层级菜单项数量的临时缓存数组,供长度/行数计算使用 - uint8_t byMaxLen; // 下一级菜单条目中,最长标题的字符数,用来确定菜单框宽度 - uint8_t byMaxNum; // 在一屏竖直方向上最多能显示多少条菜单项 - uint16_t byMenuPos; // 当前菜单在本层菜单链表中的序号位置(用于计算其在屏幕中的纵向位置) - uint8_t byItemNum; // 当前参与计算的“菜单项数量”(根据语境,既可能是当前层,也可能是子菜单层) - uint16_t wTemp; // 用于中间计算时的临时变量(例如交换起止坐标等) - - byMaxNum = (MENU_YMAX) / LINE_HEIGHT; // 在当前屏幕高度内,竖直方向上一页最多能显示的菜单行数 - - ptIndex[bylevel] = ptFirst[bylevel]; // 从本层级菜单链表的首节点开始遍历 - for (uint16_t wLoop = 0; wLoop < MENU_MAX_ITEM; wLoop++) // 遍历当前层级的所有菜单项(以最大项数为上限防止死循环) - { - if ((ptIndex[bylevel]->ptLower) != NULL) // 如果当前菜单项存在下一级菜单(即有子菜单需要显示) - { - // 统计下一级菜单的条目数和每个条目的字符长度,返回该下一级中最长标题的字符数 - // 同时,把各层级菜单数量写入 byMenuNum[] 中,用于后面计算 Y 坐标和分页 - byMaxLen = Menu_charLenCal(bylevel, byMenuNum, ptFirst, ptIndex); // 计算2级菜单项数及字符数 - - /* X 坐标计算:二级(或更深层级)菜单的矩形框紧挨着上一级菜单的右侧展开 */ - ptIndex[bylevel]->wSPosX = ptIndex[bylevel - 1]->wEPosX; // 2级菜单矩形框横坐标起始 = 上一级菜单矩形框的结束 X - ptIndex[bylevel]->wEPosX = ptIndex[bylevel]->wSPosX + byMaxLen * MENU_WITDTH + MENU_XADD; // 2级菜单矩形框横坐标终止 = 起始 X + 最大字符宽度 + 额外边框间距 - - - byMenuPos = ptIndex[bylevel]->wPos; // 当前菜单在本层菜单链表中的逻辑位置(第几项) - byItemNum = byMenuNum[bylevel]; // 本层菜单总项数 - - // 当本层菜单总数超过一屏,且当前高亮菜单已经滚动到非第一页时, - // 需要对菜单在当前页中的显示位置进行折算,实现“↑/↓”翻页效果。 - if ((byItemNum > byMaxNum) && (byMenuPos >= byMaxNum)) // 1级菜单项数大于一页且1级菜单当前位置不在第一页 - { - byMenuPos = byMaxNum - (byItemNum - byMenuPos); /* 第一页尾显示↓ 第二页头显示↑ */ - } - - // 根据“上一级菜单框的起始 Y 坐标”和“当前菜单位于本层中的序号”, - // 计算出当前子菜单框在 Y 方向上的起始位置,使子菜单在对应父菜单项的右侧对齐。 - wSPosY = ptIndex[bylevel - 1]->wSPosY + (byMenuPos - 1) * LINE_HEIGHT; // 计算2级菜单框起始坐标 - - byItemNum = byMenuNum[bylevel + 1]; // 下一级(子菜单)项数 - wEPosY = wSPosY + byItemNum * LINE_HEIGHT + MENU_YADD; // 计算2级菜单框终止坐标 = 起始 Y + 所有子项高度 + 边框间距 -#ifdef DEBUG - printf("%d %d %d %d \n", bylevel, byItemNum, wSPosY, wEPosY); -#endif - // 情况 1:子菜单框整体没有超过屏幕的最大 Y 边界,直接采用计算结果 - if (wEPosY < MENU_YMAX) /* 菜单框没有超出边界 */ - { - ptIndex[bylevel]->wSPosY = wSPosY; // 2级菜单矩形框纵起始坐标定位 - ptIndex[bylevel]->wEPosY = wEPosY; // 2级菜单矩形框纵终止坐标定位 - } - else /* 情况 2:子菜单框在向下展开时超出了屏幕底部,需要整体向上“提”一段 */ - { - // 反向计算:尝试让子菜单从当前高亮项“往上展开”, - // 使最后一项与当前父菜单项对齐,从而避免超出底部。 - wEPosY = wSPosY - (byItemNum - 1) * LINE_HEIGHT - MENU_YADD; - if ((wEPosY > LINE_HEIGHT) && (wEPosY < MENU_YMAX)) // 菜单向上不越限 - { - wTemp = wSPosY; - wSPosY = wEPosY; - wEPosY = wTemp + LINE_HEIGHT; - } - else - { - wSPosY = ptIndex[0]->wSPosY; // 2级菜单框起始坐标同1级菜单 - wEPosY = wSPosY + byItemNum * LINE_HEIGHT + MENU_YADD; // 计算2级菜单框终止坐标 - for (uint16_t i = 1; i < (wSPosY / LINE_HEIGHT + 1); i++) - { - if (wEPosY > MENU_YMAX) // wEPosY = CN_YMAX; // 菜单纵向一屏可显示 - { - wSPosY = ptIndex[0]->wSPosY - LINE_HEIGHT * i; - if (wSPosY < LINE_HEIGHT) - { - wSPosY = 0; - } - wEPosY = wSPosY + byItemNum * LINE_HEIGHT + MENU_YADD; // 计算2级菜单框终止坐标 -#ifdef DEBUG - printf("Menu_Sub1PosCal: %d %d %d %d\n", bylevel, byItemNum, wSPosY, wEPosY); -#endif - } - else - { - break; /* 菜单向上不越限,结束 */ - } - } - } - ptIndex[bylevel]->wSPosY = wSPosY; /* 2级菜单矩形框纵起始坐标定位 */ - ptIndex[bylevel]->wEPosY = wEPosY; /* 2级菜单矩形框纵终止坐标定位 */ - } - Menu_Sub2PosCal(2, ptFirst, ptIndex); // 3级菜单框坐标计算 - } - /* 移至同级下一项 */ - ptIndex[bylevel] = ptIndex[bylevel]->ptBehind; - /*如果移至同级下一项后,发现回到了同级菜单的第一个菜单,则说明遍历完了同级菜单的所有菜单,结束*/ - if (ptIndex[bylevel] == ptFirst[bylevel]) - { - break; - } - } -} -/****************************************************************************** - * 函数名: Menu_PositionCal - * 功能: 为菜单树中所有有下级的 0 级菜单项计算并填充其下拉框的显示坐标 - * (wSPosX/Y、wEPosX/Y),供后续绘制矩形框与文字使用 - * 参数: ptMenuHead - 0 级菜单链表头指针(通常为 g_tMenuCtrl.ptHead) - * 返回: 无 - * 说明: 1. 0 级菜单在屏幕顶部横向均分,有下级的项在其下方弹出 1 级下拉框 - * 2. 下拉框左上角 (wSPosX, wSPosY) 从屏幕底部向上推算,避免超出屏幕 - * 3. 下拉框宽度由 Menu_charLenCal 返回的 byMaxLen 与字宽 CN_WITDTH 决定 - * 4. 若下拉框超出屏幕顶部(wEPosY < CN_YMAX),则 wEPosY 取屏高,需分页显示 - * 5. 2 级及以下子菜单坐标由 Menu_Sub1PosCal 递归计算 - * 注意:当前还有一个 bug,没有检查坐标的上下界,导致按照上下界清屏会清不干净 - *****************************************************************************/ -void Menu_PositionCal(tagPMenuItem ptMenuHead) -{ - tagPMenuItem ptFirst[4]; /* 各级首项指针 [0..3] */ - tagPMenuItem ptIndex[4]; /* 各级当前遍历项指针 */ - - uint8_t byMenuNum[4]; /* 各级菜单项数量 */ - uint8_t byMaxLen; /* 1 级菜单项最大字符长度(含右箭头) */ - uint8_t byInterval; /* 0 级菜单项在 X 方向的间隔像素 */ - uint8_t by0LevelNum; /* 0 级菜单项总数 */ - - ptFirst[0] = ptMenuHead; - ptIndex[0] = ptFirst[0]; - byMenuNum[0] = 1; - ptIndex[0]->wPos = 1; - - byInterval = LCD_SIZE_X / g_tMenuCtrl.by0LevelNum; /* 平均分布 */ - by0LevelNum = g_tMenuCtrl.by0LevelNum; - - for (uint16_t wLoop = 0; wLoop < by0LevelNum; wLoop++) /* 遍历 0 级菜单项 */ - { - if (ptIndex[0]->ptLower != NULL) /* 有下级菜单 */ - { - /* 计算 1 级子项最大长度,并为其名称追加右箭头;更新 ptFirst/ptIndex/byMenuNum */ - - byMaxLen = Menu_charLenCal(0, byMenuNum, ptFirst, ptIndex); - /* 左上角坐标计算 */ - /* 1 级下拉框左上角:按 0 级序号横向定位,纵向从屏底向上预留多行高度 */ - ptIndex[0]->wSPosX = (byMenuNum[0] - 1) * byInterval; - ptIndex[0]->wSPosY = LCD_SIZE_Y - LINE_HEIGHT - byMenuNum[1] * LINE_HEIGHT - MENU_YADD; - - /* 右下角坐标计算 */ - /* 下拉框右下角 X:起始 + 最大字符数×字宽 + 边距 */ - ptIndex[0]->wEPosX = ptIndex[0]->wSPosX + byMaxLen * MENU_WITDTH + MENU_XADD; - /* 下拉框右下角 Y:与 0 级菜单底边齐平,若超出屏顶则取屏高(需分页) */ - ptIndex[0]->wEPosY = LCD_SIZE_Y - LINE_HEIGHT; - #ifdef DEBUG - printf("Menu_PositionCal: wLoop = %d, byMaxLen = %d, ptIndex[0]->wEPosX = %d, ptIndex[0]->wEPosY = %d\n", wLoop, byMaxLen, ptIndex[0]->wEPosX, ptIndex[0]->wEPosY); - #endif - - /* 递归计算 2 级及以下子菜单的显示坐标 */ - Menu_Sub1PosCal(1, ptFirst, ptIndex); - } - /* 移至 0 级下一项*/ - ptIndex[0] = ptIndex[0]->ptBehind; - /*如果移至 0 级下一项后,发现回到了0级菜单的第一个菜单,则说明遍历完了0级菜单的所有菜单,结束*/ - if (ptIndex[0] == ptFirst[0]) - { - break; - } - byMenuNum[0]++; /* 0级菜单项数量+1 */ - ptIndex[0]->wPos = byMenuNum[0]; /* 0级菜单项序号 + 1 */ - } -} -/****************************************************************************** - * 函数名: Lcd_Black_Button - * 功能: 在指定矩形区域绘制一个带边框的实心“按钮”效果 - * 参数: left_x - 按钮左上角 X 坐标 - * top_y - 按钮左上角 Y 坐标 - * right_x - 按钮右下角 X 坐标 - * bottom_y - 按钮右下角 Y 坐标 - * 返回: 无 - * 说明: 1. 先用当前前景色填充内部区域(边框内减 1 像素) - * 2. 再用 1 像素宽的水平/垂直线绘制上、下、左、右边框 - * 3. 颜色均使用 g_tCVsr.wFontColor(当前字体/前景颜色) - *****************************************************************************/ - void Menu_BoundaryBox(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y) - { - /* 填充按钮内部区域:Y 从 top_y+1 到 bottom_y-1,X 从 left_x+1 到 right_x-1 */ - for (uint16_t y = top_y; y < bottom_y; y++) - { - for (uint16_t x = left_x; x < right_x; x++) - { - Lcd_SetPixel(x, y, LCD_BACK); /* 设置按钮内部像素为前景色,实现实心效果 */ - } - } - /* 绘制上边框:从 (left_x, top_y) 到 (right_x, top_y),线宽 1 像素 */ - Lcd_LineH(left_x, right_x, top_y, 1, LCD_FONT); - /* 绘制左边框:从 (left_x, top_y) 到 (left_x, bottom_y),线宽 1 像素 */ - Lcd_LineV(top_y, bottom_y, left_x, 1, LCD_FONT); - /* 绘制下边框:从 (left_x, bottom_y) 到 (right_x+1, bottom_y),稍微向右多画 1 像素防止边界漏点 */ - Lcd_LineH(left_x, right_x + 1, bottom_y, 1, LCD_FONT); - /* 绘制右边框:从 (right_x, top_y) 到 (right_x, bottom_y),线宽 1 像素 */ - Lcd_LineV(top_y, bottom_y, right_x, 1, LCD_FONT); -} -/* -这是分页的逻辑,需要保存,不能删除 -if (byMenuNum <= 10) -{ - byPage = 1; -} -else if (byMenuNum <= 20) -{ - byPage = 2; -} -else if (byMenuNum > 20) -{ - byPage = 3; -} - if (byMenuNum > byMaxNum) // 菜单项数大于一页 - { - wPosX = (wSPosX + wEPosX) / 2 - 5; - wLoopMax = byMaxNum - 1; - - if (byMenuPos < byMenuNum - (byMaxNum - 1) + 1) // 由第二页返回第一页 - { - bySecPage = 0; - g_tMenuCtrl.bySecPage = bySecPage; - } - - if ((byMenuPos < byMaxNum) && (0 == bySecPage)) // 当前位置在第一页 - { - byFirstPos = 1; - wPosY = wEPosY - LINE_HEIGHT; - - Menu_ButtonPush(wSPosX + 3, wPosY - 2, wEPosX - 4, wPosY + 14); - Lcd_ShowStr(wPosX, wPosY, (uint8_t*)"↓"); // 第一页尾显示↓ - } - else if ((byMenuPos < (byMaxNum * 2 - 2)) && (byPage > 2)) // 当前位置在第二页 - { - bySecPage = 0xCC; - g_tMenuCtrl.bySecPage = bySecPage; - - byFirstPos = byMaxNum; - wPosY = wSPosY + 2; - - Menu_ButtonPush(wSPosX + 3, wPosY - 1, wEPosX - 4, wPosY + 13); - Lcd_ShowStr(wPosX, wPosY, (uint8_t*)"↑"); // 第二页头显示↑ - - wPosY = wEPosY - LINE_HEIGHT; - - Menu_ButtonPush(wSPosX + 3, wPosY - 2, wEPosX - 4, wPosY + 14); - Lcd_ShowStr(wPosX, wPosY, (uint8_t*)"↓"); // 第一页尾显示↓ - } - else - { - bySecPage = 0x77; - g_tMenuCtrl.bySecPage = bySecPage; - - if (byPage > 2) - { - byFirstPos = byMaxNum * 2 - 2; - } - else - { - byFirstPos = byMenuNum - (byMaxNum - 1) + 1; - } - wPosY = wSPosY + 2; - - Menu_ButtonPush(wSPosX + 3, wPosY - 1, wEPosX - 4, wPosY + 13); - Lcd_ShowStr(wPosX, wPosY, (uint8_t*)"↑"); // 第二页头显示↑ - } - - for (wLoop = 0; wLoop < byMenuNum; wLoop++) - { - if (byFirstPos == ptIndex->wPos) - { - break; - } - ptIndex = ptIndex->ptBehind; - } - } - else - { - bySecPage = 0; - g_tMenuCtrl.bySecPage = bySecPage; - } - if (0xCC == bySecPage) - { - for (wLoop = 0; wLoop < (wLoopMax - 1); wLoop++) - { - byMenuPos = ptIndex->wPos - (byFirstPos - 1); - wPosY = wSPosY + byMenuPos * LINE_HEIGHT + 3; // 计算显示字符位置 - - memcpy(byName, ptIndex->byName, 50); - Lcd_ShowStr(wPosX, wPosY, byName); // 显示菜单字符串 - - if (ptRoute == ptIndex) // 当前菜单设置反显 - { - Lcd_Invert(wSPosX + 2, wPosY - 1, wEPosX - 2, wPosY + 14); - } - ptIndex = ptIndex->ptBehind; // 取下一菜单结点 - } - } - else - { - wPosX = wSPosX + 4; - for (wLoop = 0; wLoop < wLoopMax; wLoop++) - { - if (0x77 == bySecPage) // 显示第二页菜单 - { - byMenuPos = ptIndex->wPos - (byFirstPos - 1); - } - else // 显示第一页菜单 - { - byMenuPos = ptIndex->wPos - 1; - } - wPosY = wSPosY + byMenuPos * LINE_HEIGHT + 3; // 计算显示字符位置 - memcpy(byName, ptIndex->byName, 50); - Lcd_ShowStr(wPosX, wPosY, byName); // 显示菜单字符串 - - if (ptRoute == ptIndex) // 当前菜单设置反显 - { - Lcd_Invert(wSPosX + 2, wPosY - 1, wEPosX - 2, wPosY + 14); - } - ptIndex = ptIndex->ptBehind; // 取下一菜单结点 - } - } -*/ - -/****************************************************************************** - * 函数名: Menu_Show_Other - * 功能: 显示除 0 级以外的其它级别菜单(1 级 / 2 级 / 3 级)的下拉列表 - * 参数: bylevel - 要显示的菜单层级(1 表示 1 级菜单,2 表示 2 级菜单等) - * 返回: 无 - * 说明: 1. 使用 g_tMenuCtrl.ptRoute[bylevel] 获取本级菜单框坐标,并应用水平偏移 - * 2. 调用 Menu_BoundaryBox 绘制当前级菜单的背景边框矩形 - * 3. 以 g_tMenuCtrl.ptRoute[bylevel+1] 为当前“选中项”,逐项绘制所有本级菜单文字 - * 4. 对选中项对应的那一行调用 Lcd_Invert 进行反显高亮 - * 注意: 分页逻辑还没有完成,如果有分页的情况会显示错误 - *****************************************************************************/ -void Menu_Show_Other(uint8_t bylevel) -{ - tagPMenuItem ptIndex; /* 本级菜单遍历指针 */ - tagPMenuItem ptRoute; /* 下一级“选中项”指针(用于确定当前高亮项) */ - - uint16_t wPosX; /* 菜单文本显示的起始 X 坐标 */ - uint16_t wPosY; /* 菜单文本显示的起始 Y 坐标 */ - - uint8_t byName[50]; /* 临时缓冲区,用于拷贝菜单名称 */ - - /* 1. 获取当前层级路径结点,用于绘制本级菜单的背景边框矩形 */ - ptIndex = g_tMenuCtrl.ptRoute[bylevel]; - /* 绘制本级菜单的背景边框矩形 */ - Menu_BoundaryBox(ptIndex->wSPosX, ptIndex->wSPosY, ptIndex->wEPosX, ptIndex->wEPosY); - - /* 2. 取得下一级路径中的当前结点,用于确定哪一项需要高亮 */ - ptRoute = g_tMenuCtrl.ptRoute[bylevel + 1]; /* 进入下一级菜单的选中项 */ - - ptIndex = ptRoute; /* 从选中项开始向后遍历同级菜单 */ - - /* 3. 逐项绘制菜单文字,并对选中项所在行做反显处理 */ - wPosX = g_tMenuCtrl.ptRoute[bylevel]->wSPosX + 4; /* 文本相对左边框右移 4 像素,预留内边距 */ - for (uint16_t index = 0; index < g_tMenuCtrl.ptRoute[bylevel]->wNum; index++) - { - wPosY = g_tMenuCtrl.ptRoute[bylevel]->wSPosY + (ptIndex->wPos - 1) * LINE_HEIGHT + 3; /* 行顶坐标 + 行高 * 行号 + 3 像素微调 */ - memcpy(byName, ptIndex->byName, 50); /* 将菜单名称拷贝到临时缓存 */ - Lcd_ShowStr(wPosX, wPosY, byName); /* 在计算出的坐标位置显示菜单字符串 */ - #ifdef DEBUG - printf("Menu_Show_Other: wPosX = %d, wPosY = %d, byName = %s\n", wPosX, wPosY, byName); - #endif - if(ptRoute == ptIndex) /* 若当前绘制项为“选中项” */ - { - /* 对该行对应的矩形区域执行反显,用于高亮当前选择 */ - /* x轴:起始位置+2,结束位置-2,产生内嵌的感觉 */ - Lcd_Invert(g_tMenuCtrl.ptRoute[bylevel]->wSPosX + 2, wPosY - 1, g_tMenuCtrl.ptRoute[bylevel]->wEPosX - 2, wPosY + 14); - } - ptIndex = ptIndex->ptBehind; /* 沿同级链表向后移动,处理下一项 */ - } -} -/****************************************************************************** - * 函数名: Menu_Show_0Level - * 功能: 绘制 0 级主菜单栏(顶部标题区域),按当前菜单指针依次显示各个 0 级菜单项 - * 参数: 无(依赖全局菜单控制结构 g_tMenuCtrl 与 LCD 显存控制 g_tCVsr) - * 返回: 无 - * 说明: 1. 所有 0 级菜单在 X 方向上按等间距分布 - * 2. 每次循环仅在菜单项仍在屏幕可见区域时才绘制对应标题文本 - * 3. 顶部 0~32 像素区域作为 0 级菜单标题栏背景,会被统一清屏并重绘 - * 4. 调用 Menu_MeiTouPic 绘制“眉头”装饰线条,增强标题栏的视觉效果 - * 5. 示例中固定显示 “当前模式: 无模式”,后续可替换为实际运行模式文本 - *****************************************************************************/ -void Menu_Show_0Level() -{ - /* 先清除顶部 0~32 像素高度区域,作为 0 级菜单标题栏背景 */ - Lcd_FillRect(0, 0, LCD_SIZE_X, 32, LCD_BACK); - /* 绘制“眉头”装饰,使 0 级菜单栏更加立体 */ - Menu_MeiTouPic(16, 2 ); - Lcd_ShowStr(16, 20, (uint8_t*)"当前模式: 无模式" ); -} -/****************************************************************************** - * 函数名: Menu_Show_Proc - * 功能: 根据当前菜单状态刷新 0~3 级菜单显示(增量刷新或整体刷新) - * 情况一:0 级路径发生变化(如 ESC 退回或跨列切换):需要整体刷新 - * 情况二:0 级路径未变,但当前结点发生变化 - 1. 同级内上下移动: 仅重画本级下拉菜单(current->byClass-1 对应的菜单框索引) - 2. 进层: 仅重画本级下拉菜单(current->byClass-1 对应的菜单框索引) - 3. 退层:清屏并重画 0~3 级菜单(这是最简单的操作,但是比较耗费资源) - 所以1. 2. 的逻辑可以合并为:仅重画本级下拉菜单(current->byClass-1 对应的菜单框索引) - * 参数: 无(依赖全局 g_tMenuCtrl、g_tDspCtrl、g_tCVsr) - * 返回: 无 - * 说明: 该函数后续可以改进退层的逻辑,减少计算量 - * 注意: 超出屏幕范围的逻辑还没有处理 - *****************************************************************************/ -void Menu_Show_Proc(void) -{ - uint8_t bNeedFresh; /* 是否需要整体刷新 0~3 级菜单(1=是,0=否) */ - -#ifdef DEBUG - printf("Menu_Show_proc进入\n"); -#endif - - bNeedFresh = 0; /* 默认不整体刷新,仅增量更新 */ - - if (g_tDspCtrl.bFirst) /* 第一次进入菜单层标志 */ - { - g_tDspCtrl.bFirst = 0; /* 清除首次标志 */ - bNeedFresh = 1; /* 强制整体刷新 */ - } - /* 情况一:0 级路径发生变化(如 ESC 退回或跨列切换) */ - if (g_tMenuCtrl.pt0Level != g_tMenuCtrl.ptRoute[0]) - { - bNeedFresh = 1; /* 需要整体刷新 */ - Lcd_FillRect(0, MENU_YMIN, LCD_SIZE_X, MENU_YMAX, LCD_BACK); /* 清除 1~3 级菜单区域 */ - g_tMenuCtrl.pt0Level = g_tMenuCtrl.ptRoute[0]; /* 更新 0 级路径起点 */ - g_tMenuCtrl.ptCurBak = g_tMenuCtrl.ptCurrent; /* 更新备份指针 */ - } - else if (g_tMenuCtrl.ptCurBak != g_tMenuCtrl.ptCurrent) /* 情况二:0 级路径未变,但当前结点发生变化 */ - { - if (g_tMenuCtrl.ptCurrent->byClass >= g_tMenuCtrl.ptCurBak->byClass) /* 2.1 同级内上下移动 或者 进层*/ - { - #ifdef DEBUG - printf("同级内上下移动 或 进层:从 %d 级到 %d 级\n", g_tMenuCtrl.ptCurBak->byClass, g_tMenuCtrl.ptCurrent->byClass); - #endif - Menu_Show_Other(g_tMenuCtrl.ptCurrent->byClass - 1); /* 仅重画本级下拉菜单(byClass-1 对应 1~3 级菜单框索引) */ - /*由于刚开是已经判断了 0 层没有发生变化,因此 ptCurrent->byClass 一定是大于 1 的,所以不需要判断*/ - } - else /* 2.2 级别减小(退层,如从 2 级退回 1 级) */ - { - #ifdef DEBUG - printf("退层:从 %d 级退回 %d 级\n", g_tMenuCtrl.ptCurBak->byClass, g_tMenuCtrl.ptCurrent->byClass); - #endif - Lcd_FillRect(0, MENU_YMIN, LCD_SIZE_X, MENU_YMAX, LCD_BACK); /* 擦除 1~3 级区域(保留 0~32 像素标题栏) */ - bNeedFresh = 1; - } - g_tMenuCtrl.ptCurBak = g_tMenuCtrl.ptCurrent; /* 无论何种情况,更新备份指针为最新当前结点 */ - } - - /* 情况三:需要整体刷新 0~3 级菜单 */ - if (bNeedFresh > 0) - { - Menu_Show_0Level(); /* 显示 0 级菜单 */ - for (uint8_t index = 0; index < g_tMenuCtrl.ptCurrent->byClass; index++) /* 依次显示 1 级、2 级、3 级(直到当前层级) */ - { - Menu_Show_Other(index); - } - } -#ifdef DEBUG - printf("Menu_Show_proc结束\n"); -#endif -} -/****************************************************************************** - * 函数名: Menu_Route - * 功能: 菜单层按键调度:读键并更新当前菜单结点与路径,必要时刷新显示 - * 参数: 无 - * 返回: 无 - * 说明: 1. 首次进入时只调用 Menu_Show_Proc 显示当前路径 - * 2. 上/下键同级移动;左键退上一级或上一列 0 级;右键/确认进入下级或执行叶子结点 - * 3. ESC 在 1 级退回“主界面”并重算布局;在 2~3 级仅退上一级 - * 4. 键值使用 key.h 中的 KEY_U/KEY_D/KEY_L/KEY_R/KEY_ENT/KEY_ESC/KEY_F1/KEY_F2 - *****************************************************************************/ - void Menu_Route(void) - { - tagPMenuItem ptCurrent; /* 当前光标所在菜单结点(可能被按键修改) */ - tagPMenuItem ptHead; /* 0 级菜单头结点,用于 ESC 退回与路径重置 */ - tagPMenuItem ptIndex; /* 路径回溯时的遍历指针 */ - tagPMenuItem *ptRoute; /* 指向 g_tMenuCtrl.ptRoute,便于按层级写入路径 */ - - uint8_t byKeyVal; /* 本次读到的键值(KEY_U/KEY_D/KEY_L/KEY_R/KEY_ENT/KEY_ESC 等) */ - uint8_t bNeedFresh; /* 本次按键是否导致需要重绘菜单(1=是,0=否) */ - - bNeedFresh = 0; /* 默认不刷新,仅当按键改变 ptCurrent 时置 1 */ - - ptCurrent = g_tMenuCtrl.ptCurrent; /* 取出当前选中的菜单结点 */ - ptHead = g_tMenuCtrl.ptHead; /* 取出 0 级头结点 */ - ptRoute = g_tMenuCtrl.ptRoute; /* 取出路径数组指针,用于更新 ptRoute[0..3] */ - - if (g_tDspCtrl.bFirst) /* 第一次进入菜单层:不读键,直接按当前路径刷新显示 */ - { - Menu_Show_Proc(); /* 内部会清 bFirst 并绘制 0~当前级菜单 */ - } - else /* 非首次:读键并根据键值更新 ptCurrent */ - { - byKeyVal = Key_Read(); /* 从 Key_Read 获取键值(无键时为 KEY_NONE) */ - switch (byKeyVal) - { - case KEY_F1: /* F1 当前项目未使用,忽略 */ - break; - case KEY_F2: /* F2 同上 */ - break; - - case KEY_U: /* 上键:切换到同级上一项(ptBefore) */ - ptCurrent = ptCurrent->ptBefore; - bNeedFresh = 1; - break; - - case KEY_D: /* 下键:切换到同级下一项(ptBehind) */ - ptCurrent = ptCurrent->ptBehind; - bNeedFresh = 1; - break; - - case KEY_L: /* 左键:退到上一级路径结点,若已在 0 级则退到上一列 0 级再进其下级 */ - ptCurrent = ptRoute[ptCurrent->byClass - 1]; /* 先退到上一级路径结点 */ - if (0 == ptCurrent->byClass) /* 若退完后是 0 级,则再退一列或两列并进下级 */ - { - if (1 == ptCurrent->wPos) /* 当前在第 1 列:退两列再进下级 */ - { - ptCurrent = ptCurrent->ptBefore->ptBefore; - ptCurrent = ptCurrent->ptLower; - } - else /* 其它列:退一列再进下级 */ - { - ptCurrent = ptCurrent->ptBefore; - ptCurrent = ptCurrent->ptLower; - } - } - bNeedFresh = 1; - break; - - case KEY_R: /* 右键与确认键统一处理 */ - case KEY_ENT: - if (ptCurrent->ptLower != NULL) /* 有下级:进入下一层,光标移到下级首项 */ - { - ptCurrent = ptCurrent->ptLower; - bNeedFresh = 1; - } - else /* 无下级(叶子结点):执行该菜单项绑定的处理函数 */ - { - if (ptCurrent->pfnWinProc != NULL) - { - ptCurrent->pfnWinProc(); /* 如 MenuProc_See_AppInfo、MenuProc_See_YC 等 */ - } - } - break; - - case KEY_ESC: /* 退出键 */ - if (1 == ptCurrent->byClass) /* 当前在 1 级:整体退出到“主界面”,重置 0 级路径与布局 */ - { - g_tMenuCtrl.pt0Level = ptHead; /* 0 级路径起点设为头结点 */ - g_tMenuCtrl.ptRoute[0] = ptHead; /* 路径[0] 指向 0 级头 */ - g_tMenuCtrl.ptCurrent = ptHead->ptLower; /* 当前结点设为 0 级第一个子项 */ - ptCurrent = g_tMenuCtrl.ptCurrent; - g_tMenuCtrl.ptCurBak = ptCurrent; /* 备份当前结点 */ - g_tMenuCtrl.ptRoute[1] = ptCurrent; /* 路径[1] 指向该 1 级项 */ - g_tMenuCtrl.byLeftMove = 0; /* 左移偏移清零 */ - return; /* 直接返回,本次不再调用 Menu_Show_Proc(由下次 Menu_Route 或外部刷新) */ - } - else - { /* 当前在 2 或 3 级:仅退到上一级路径结点 */ - ptCurrent = ptRoute[ptCurrent->byClass - 1]; - bNeedFresh = 1; - } - break; - default: /* 其它键值(含 KEY_NONE)忽略 */ - break; - } - } - if (bNeedFresh) /* 若按键导致菜单位置变化,则更新全局并刷新显示 */ - { - g_tMenuCtrl.ptCurrent = ptCurrent; /* 更新当前选中的菜单项 */ - if (g_tMenuCtrl.ptCurBak != ptCurrent) /* 当前菜单项相对上次备份有变化,需更新路径数组 */ - { - ptIndex = g_tMenuCtrl.ptCurrent; - for (uint16_t index = 0; index < MENU_MAX_ITEM; index++) /* 从当前结点沿 ptHigher 回溯到 0 级 */ - { - if (NULL == ptIndex->ptHigher) /* 无上级(不应出现,保险起见退到 ptBefore) */ - { - ptIndex = ptIndex->ptBefore; - } - else /* 有上级:先回到上级,再将该级路径记录到 ptRoute[byClass] */ - { - ptIndex = ptIndex->ptHigher; - g_tMenuCtrl.ptRoute[ptIndex->byClass] = ptIndex; - } - if (0 == ptIndex->byClass) /* 已回溯到 0 级,结束 */ - { - break; - } - } - g_tMenuCtrl.ptRoute[g_tMenuCtrl.ptCurrent->byClass] = ptCurrent; /* 当前所在层路径指向新结点 */ - } -#ifdef DEBUG - printf("Menu_Route: g_tMenuCtrl.ptCurrent.byClass = %d\n", g_tMenuCtrl.ptCurrent->byClass); - printf("Menu_Route: g_tMenuCtrl.ptCurBak.byClass = %d\n", g_tMenuCtrl.ptCurBak->byClass); - for (uint16_t index = 0; index < 4; index++) - { - printf("Menu_Route: g_tMenuCtrl.ptRoute[%d].byName = %s\n", index, g_tMenuCtrl.ptRoute[index]->byName); - } -#endif - Menu_Show_Proc(); /* 按新路径刷新 0~当前级菜单显示 */ - } -} -void Menu_Init(void) -{ - Lcd_Init(); - g_tDspCtrl.bFirst = 1; - - Menu_0LevelNumCal(); /* 统计 0 级菜单项个数 */ - Menu_Main_Creat(); /* 创建菜单树 */ - g_tMenuCtrl.ptHead = &g_tMenuItem[0]; // 取菜单头结点 - Menu_PositionCal(g_tMenuCtrl.ptHead); /* 菜单框位置计算 */ - - g_tMenuCtrl.ptRoute[0] = &g_tMenuItem[0]; // 0级路径初始化 - g_tMenuCtrl.pt0Level = g_tMenuCtrl.ptRoute[0]; /* 供 Menu_Show_Proc 判断 0 级路径是否变化 */ - g_tMenuCtrl.ptCurrent = g_tMenuCtrl.ptHead->ptLower; /* 菜单当前指针初始化为0级第一个子项 */ - g_tMenuCtrl.ptCurBak = g_tMenuCtrl.ptCurrent; /* 菜单备份指针初始化 */ - g_tMenuCtrl.ptRoute[1] = g_tMenuCtrl.ptCurrent; // 1级路径初始化 - g_tMenuCtrl.ptRoute[2] = g_tMenuCtrl.ptCurrent; // 1级路径初始化 - g_tMenuCtrl.ptRoute[3] = g_tMenuCtrl.ptCurrent; // 1级路径初始化 - g_tMenuCtrl.byLeftMove = 0; - g_tMenuCtrl.bySecPage = 0; -} - - -void MenuProc_See_AppInfo() -{ - printf("MenuProc_See_AppInfo\n"); -} - -void MenuProc_See_YC() -{ - printf("MenuProc_See_YC\n"); -} -void MenuProc_Set_Value() -{ - printf("MenuProc_Set_Value\n"); -} -void MenuProc_Set_Soft() -{ - printf("MenuProc_Set_Soft\n"); -} -void MenuProc_Cfg_Time() -{ - printf("MenuProc_Cfg_Time\n"); -} -void MenuProc_Cfg_RevEvent() -{ - printf("MenuProc_Cfg_RevEvent\n"); -} -void MenuProc_Cfg_ManualWave() -{ - printf("MenuProc_Cfg_ManualWave\n"); -} -void MenuProc_See_Input() -{ - printf("MenuProc_See_Input\n"); -} - -void MenuProc_See_Set() -{ - printf("MenuProc_See_Set\n"); -} - -void MenuProc_See_Soft() -{ - printf("MenuProc_See_Soft\n"); -} -void MenuProc_YX_SetCommType() -{ - printf("MenuProc_YX_SetCommType\n"); -} - -void MenuProc_YX_SetWidth() -{ - printf("MenuProc_YX_SetWidth\n"); -} - -void MenuProc_YX_SetTwin() -{ - printf("MenuProc_YX_SetTwin\n"); -} - -/* ---------- 以下为 display.c 菜单表引用、暂无具体实现的桩函数,避免链接未定义符号 ---------- */ -void Menu_NonPfunc(void) { printf("Menu_NonPfunc\n"); } -void MenuProc_AllInf_Default(void) { printf("MenuProc_AllInf_Default\n"); } -void MenuProc_Para_Default(void) { printf("MenuProc_Para_Default\n"); } -void MenuProc_Set_Default(void) { printf("MenuProc_Set_Default\n"); } -void MenuProc_Resume_Soft(void) { printf("MenuProc_Resume_Soft\n"); } -void MenuProc_Cfg_CellDef(void) { printf("MenuProc_Cfg_CellDef\n"); } -void MenuProc_Cfg_ShowAnaType(void) { printf("MenuProc_Cfg_ShowAnaType\n"); } -void MenuProc_Dbg_XuYX(void) { printf("MenuProc_Dbg_XuYX\n"); } -void MenuProc_Dbg_XuYC(void) { printf("MenuProc_Dbg_XuYC\n"); } -void MenuProc_Dbg_XuEvent(void) { printf("MenuProc_Dbg_XuEvent\n"); } -void MenuProc_Dbg_Relay(void) { printf("MenuProc_Dbg_Relay\n"); } -void MenuProc_See_VersionBoard(void){ printf("MenuProc_See_VersionBoard\n"); } -void MenuProc_Cfg_ClrRec(void) { printf("MenuProc_Cfg_ClrRec\n"); } -void MenuProc_Cfg_ComPara(void) { printf("MenuProc_Cfg_ComPara\n"); } -void MenuProc_Cfg_EditIP(void) { printf("MenuProc_Cfg_EditIP\n"); } -void MenuProc_Cfg_EditSntp(void) { printf("MenuProc_Cfg_EditSntp\n"); } -void MenuProc_Cfg_CellConf(void) { printf("MenuProc_Cfg_CellConf\n"); } -void MenuProc_See_RecSOE(void) { printf("MenuProc_See_RecSOE\n"); } -void MenuProc_See_RecAct(void) { printf("MenuProc_See_RecAct\n"); } -void MenuProc_See_RecOpt(void) { printf("MenuProc_See_RecOpt\n"); } -void MenuProc_See_RecAlm(void) { printf("MenuProc_See_RecAlm\n"); } -void MenuProc_See_RecStart(void) { printf("MenuProc_See_RecStart\n"); } -void MenuProc_See_RecYK(void) { printf("MenuProc_See_RecYK\n"); } -void MenuProc_See_RecChk(void) { printf("MenuProc_See_RecChk\n"); } -void MenuProc_See_RecRun(void) { printf("MenuProc_See_RecRun\n"); } -void MenuProc_See_RecFault(void) { printf("MenuProc_See_RecFault\n"); } -void MenuProc_YC_SetSqValue(void) { printf("MenuProc_YC_SetSqValue\n"); } -void MenuProc_YC_SetAdjCoe(void) { printf("MenuProc_YC_SetAdjCoe\n"); } - diff --git a/src/Drv/menu/app/menu.c b/src/Drv/menu/app/menu.c new file mode 100644 index 0000000..119e142 --- /dev/null +++ b/src/Drv/menu/app/menu.c @@ -0,0 +1,191 @@ +#include "menu.h" + +#include +#include + +#include "../model/display.h" +#include "../../key.h" +#include "../view/menu_layout.h" +#include "../presenter/menu_presenter.h" +#include "../view/menu_render_port.h" +#include "../common/menu_state_types.h" + +/* ------------------------------------------------------------------------- + * 运行态状态收口(App 层私有) + * + * 设计意图: + * 1) 避免全局变量四处散落,统一由 app/menu.c 托管生命周期; + * 2) 外部模块只能经由 MenuApp_* 接口访问,降低耦合; + * 3) Presenter/Model/View 依赖都在初始化时一次性装配。 + * + * 变量职责: + * - s_dspCtrl: + * 显示刷新控制状态(例如首帧标记 bFirst)。 + * - s_menuCtrl: + * 导航与路由控制状态(当前节点、各级 route、0 级入口等)。 + * - s_menuItems[300]: + * 运行时菜单节点池(树构建后常驻内存),由 Model 初始化、View 读取布局/渲染。 + * - g_menuPresenter: + * MVP 调度中枢实例,封装输入处理与刷新协同。 + * - g_menuLayoutConfig: + * 布局参数快照(屏幕尺寸、行高、边距、菜单宽度等),初始化时写入一次。 + * ------------------------------------------------------------------------- */ +static tagDspCtrl s_dspCtrl; +static tagMenuCtrl s_menuCtrl; +static tagMenuItem s_menuItems[300]; + +static MenuPresenter g_menuPresenter; +static MenuLayoutConfig g_menuLayoutConfig; + +void MenuApp_Render(void) +{ + /* 统一渲染入口:由 Presenter 决定全量/增量刷新策略。 */ + MenuPresenter_Refresh(&g_menuPresenter); +} + +void MenuApp_HandleInput(uint8_t keyVal) +{ + /* 单次输入事件入口:按键解释与状态迁移都在 Presenter 内完成。 */ + MenuPresenter_HandleInput(&g_menuPresenter, keyVal); +} + +void MenuApp_PollInput(void) +{ + /* 轮询输入设备(键盘/按键),再复用事件处理入口。 */ + MenuApp_HandleInput(Key_Read()); +} + +void MenuApp_Init(void) +{ + const MenuRenderPort *renderPort = MenuRenderPort_Lcd(); + uint16_t lcdSizeX; + uint16_t lcdSizeY; + + /* 1) 初始化底层渲染设备(LCD/模拟器具体实现由 renderPort 决定)。 */ + renderPort->init(); + + /* 2) 通过渲染端口查询设备能力,避免 app 层直接依赖 lcd.h 宏。 */ + lcdSizeX = renderPort->get_size_x(); + lcdSizeY = renderPort->get_size_y(); + + /* 3) 组装布局配置(后续由 View 布局计算与渲染阶段复用)。 */ + g_menuLayoutConfig.lcdSizeX = lcdSizeX; + g_menuLayoutConfig.lcdSizeY = lcdSizeY; + g_menuLayoutConfig.menuYMin = MENU_YMIN; + g_menuLayoutConfig.menuYMax = MENU_YMAX_FROM_LCD(lcdSizeY); + g_menuLayoutConfig.lineHeight = LINE_HEIGHT; + g_menuLayoutConfig.menuWidth = MENU_WITDTH; + g_menuLayoutConfig.menuXAdd = MENU_XADD; + g_menuLayoutConfig.menuYAdd = MENU_YADD; + + /* 4) 装配 Presenter:注入控制状态、菜单池、渲染端口与布局配置。 */ + MenuPresenter_Setup(&g_menuPresenter, + &s_dspCtrl, + &s_menuCtrl, + s_menuItems, + renderPort, + &g_menuLayoutConfig); + + /* 5) 初始化模型数据并建立初始路由/布局。 */ + MenuPresenter_InitModel(&g_menuPresenter, g_tMenuModelTab, MENU_MAX_ITEM); +} + +const tagMenuItem *MenuApp_GetMenuItems(uint16_t *count) +{ + /* 诊断/测试只读访问接口: + * - 返回内部菜单池首地址; + * - 若 count 非空,同步返回数组容量。 */ + if (count != NULL) + { + *count = (uint16_t)(sizeof(s_menuItems) / sizeof(s_menuItems[0])); + } + return s_menuItems; +} + +void MenuProc_See_AppInfo() +{ + printf("MenuProc_See_AppInfo\n"); +} + +void MenuProc_See_YC() +{ + printf("MenuProc_See_YC\n"); +} +void MenuProc_Set_Value() +{ + printf("MenuProc_Set_Value\n"); +} +void MenuProc_Set_Soft() +{ + printf("MenuProc_Set_Soft\n"); +} +void MenuProc_Cfg_Time() +{ + printf("MenuProc_Cfg_Time\n"); +} +void MenuProc_Cfg_RevEvent() +{ + printf("MenuProc_Cfg_RevEvent\n"); +} +void MenuProc_Cfg_ManualWave() +{ + printf("MenuProc_Cfg_ManualWave\n"); +} +void MenuProc_See_Input() +{ + printf("MenuProc_See_Input\n"); +} + +void MenuProc_See_Set() +{ + printf("MenuProc_See_Set\n"); +} + +void MenuProc_See_Soft() +{ + printf("MenuProc_See_Soft\n"); +} +void MenuProc_YX_SetCommType() +{ + printf("MenuProc_YX_SetCommType\n"); +} + +void MenuProc_YX_SetWidth() +{ + printf("MenuProc_YX_SetWidth\n"); +} + +void MenuProc_YX_SetTwin() +{ + printf("MenuProc_YX_SetTwin\n"); +} + +/* ---------- 以下为 display.c 菜单表引用、暂无具体实现的桩函数,避免链接未定义符号 ---------- */ +void Menu_NonPfunc(void) { printf("Menu_NonPfunc\n"); } +void MenuProc_AllInf_Default(void) { printf("MenuProc_AllInf_Default\n"); } +void MenuProc_Para_Default(void) { printf("MenuProc_Para_Default\n"); } +void MenuProc_Set_Default(void) { printf("MenuProc_Set_Default\n"); } +void MenuProc_Resume_Soft(void) { printf("MenuProc_Resume_Soft\n"); } +void MenuProc_Cfg_CellDef(void) { printf("MenuProc_Cfg_CellDef\n"); } +void MenuProc_Cfg_ShowAnaType(void) { printf("MenuProc_Cfg_ShowAnaType\n"); } +void MenuProc_Dbg_XuYX(void) { printf("MenuProc_Dbg_XuYX\n"); } +void MenuProc_Dbg_XuYC(void) { printf("MenuProc_Dbg_XuYC\n"); } +void MenuProc_Dbg_XuEvent(void) { printf("MenuProc_Dbg_XuEvent\n"); } +void MenuProc_Dbg_Relay(void) { printf("MenuProc_Dbg_Relay\n"); } +void MenuProc_See_VersionBoard(void) { printf("MenuProc_See_VersionBoard\n"); } +void MenuProc_Cfg_ClrRec(void) { printf("MenuProc_Cfg_ClrRec\n"); } +void MenuProc_Cfg_ComPara(void) { printf("MenuProc_Cfg_ComPara\n"); } +void MenuProc_Cfg_EditIP(void) { printf("MenuProc_Cfg_EditIP\n"); } +void MenuProc_Cfg_EditSntp(void) { printf("MenuProc_Cfg_EditSntp\n"); } +void MenuProc_Cfg_CellConf(void) { printf("MenuProc_Cfg_CellConf\n"); } +void MenuProc_See_RecSOE(void) { printf("MenuProc_See_RecSOE\n"); } +void MenuProc_See_RecAct(void) { printf("MenuProc_See_RecAct\n"); } +void MenuProc_See_RecOpt(void) { printf("MenuProc_See_RecOpt\n"); } +void MenuProc_See_RecAlm(void) { printf("MenuProc_See_RecAlm\n"); } +void MenuProc_See_RecStart(void) { printf("MenuProc_See_RecStart\n"); } +void MenuProc_See_RecYK(void) { printf("MenuProc_See_RecYK\n"); } +void MenuProc_See_RecChk(void) { printf("MenuProc_See_RecChk\n"); } +void MenuProc_See_RecRun(void) { printf("MenuProc_See_RecRun\n"); } +void MenuProc_See_RecFault(void) { printf("MenuProc_See_RecFault\n"); } +void MenuProc_YC_SetSqValue(void) { printf("MenuProc_YC_SetSqValue\n"); } +void MenuProc_YC_SetAdjCoe(void) { printf("MenuProc_YC_SetAdjCoe\n"); } diff --git a/src/Drv/menu.h b/src/Drv/menu/app/menu.h similarity index 53% rename from src/Drv/menu.h rename to src/Drv/menu/app/menu.h index d382d98..0706dfc 100644 --- a/src/Drv/menu.h +++ b/src/Drv/menu/app/menu.h @@ -7,30 +7,16 @@ * 目标: * - 用纯 C 语言在控制台模拟嵌入式 HMI 的菜单逻辑 * - 键值映射:W/S/A/D/Enter/Esc → 上/下/左/右/确认/退出 - * - 结构设计尽量贴近原工程的 g_tMenuCtrl / Menu_Route / Menu_Show_Proc + * - 结构采用 MVP:Model(状态) / View(渲染) / Presenter(调度) */ -#include +#include "../common/menu_item_types.h" -#include "lcd/lcd.h" /* MENU_YMAX 依赖 LCD_SIZE_Y */ - -#define CN_HEIGHT 12 /* 菜单汉字高 */ -#define CN_ROWSPACE 2 // 菜单字符行间隔 -#define LINE_HEIGHT (CN_HEIGHT + CN_ROWSPACE) // 字符行间隔 -#define MENU_XADD 4 // 菜单框横坐标增加 -#define MENU_YADD 4 // 菜单框纵坐标增加 -#define MENU_WITDTH 7 /*ASCII字体宽度 7,因此单个字节显示的宽度是 7 个像素点*/ - - - -#define MENU_YMIN 0 // 菜单 Y 坐标的最小值 顶部边界 0 -#define MENU_YMAX (LCD_SIZE_Y - 12) // 菜单 Y 坐标的最大值 底部边界 LCD_SIZE_Y - 16 - - -/* 初始化一棵简单的测试菜单树 */ -void Menu_Init(); -void Menu_Route(void); -void Menu_Show_Proc(void); +void MenuApp_Init(void); +void MenuApp_HandleInput(uint8_t keyVal); +void MenuApp_PollInput(void); +void MenuApp_Render(void); +const tagMenuItem *MenuApp_GetMenuItems(uint16_t *count); /* 非功能键处理 */ void Menu_NonPfunc(); @@ -46,8 +32,6 @@ void MenuProc_Dbg_XuEvent(); void MenuProc_Dbg_Relay(); void MenuProc_See_VersionBoard(); - - void MenuProc_See_AppInfo(); void MenuProc_Set_Value(); void MenuProc_Set_Soft(); @@ -60,7 +44,7 @@ void MenuProc_Cfg_EditIP(); void MenuProc_Cfg_EditSntp(); void MenuProc_Cfg_CellConf(); -void MenuProc_See_RecSOE(); +void MenuProc_See_RecSOE(); void MenuProc_See_RecAct(); void MenuProc_See_RecOpt(); void MenuProc_See_RecAlm(); @@ -78,7 +62,6 @@ void MenuProc_YC_SetSqValue(); void MenuProc_YC_SetAdjCoe(); void MenuProc_YX_SetCommType(); void MenuProc_YX_SetWidth(); -void MenuProc_YX_SetTwin(); - -#endif +void MenuProc_YX_SetTwin(); +#endif diff --git a/src/Drv/menu/common/menu_item_types.h b/src/Drv/menu/common/menu_item_types.h new file mode 100644 index 0000000..c88adc8 --- /dev/null +++ b/src/Drv/menu/common/menu_item_types.h @@ -0,0 +1,52 @@ +#ifndef MENU_ITEM_TYPES_H +#define MENU_ITEM_TYPES_H + +#include "types.h" + +typedef struct MenuItem MenuItem; +typedef MenuItem *tagPMenuItem; +typedef MenuItem tagMenuItem; + +typedef struct +{ + MenuItem *higher; + MenuItem *lower; + MenuItem *before; + MenuItem *behind; +} MenuLinks; + +typedef struct +{ + uint16_t wSPosX; + uint16_t wSPosY; + uint16_t wEPosX; + uint16_t wEPosY; +} MenuRect; + +struct MenuItem +{ + /* 导航关系 */ + MenuItem *ptHigher; + MenuItem *ptLower; + MenuItem *ptBefore; + MenuItem *ptBehind; + + /* 菜单元数据 */ + uint8_t byClass; + uint8_t byName[50]; + uint8_t byTip[50]; + uint8_t byAttrib; + uint16_t wPassword; + uint16_t wPara; + FUNCPTR pfnWinProc; + + /* 视图与布局 */ + uint16_t wPos; + uint16_t wNum; + uint16_t wSPosX; + uint16_t wSPosY; + uint16_t wEPosX; + uint16_t wEPosY; +}; + +#endif diff --git a/src/Drv/menu/common/menu_state_types.h b/src/Drv/menu/common/menu_state_types.h new file mode 100644 index 0000000..5544904 --- /dev/null +++ b/src/Drv/menu/common/menu_state_types.h @@ -0,0 +1,23 @@ +#ifndef MENU_STATE_TYPES_H +#define MENU_STATE_TYPES_H + +#include "menu_item_types.h" + +typedef struct +{ + /* 当前菜单显示只依赖首帧刷新标记 */ + unsigned bFirst : 1; +} tagDspCtrl; + +typedef struct +{ + uint8_t by0LevelNum; + + tagPMenuItem ptHead; + tagPMenuItem ptCurrent; + tagPMenuItem ptRoute[4]; + tagPMenuItem ptCurBak; + tagPMenuItem pt0Level; +} tagMenuCtrl; + +#endif diff --git a/src/Drv/display.c b/src/Drv/menu/model/display.c similarity index 98% rename from src/Drv/display.c rename to src/Drv/menu/model/display.c index f0738f8..16e4ab1 100644 --- a/src/Drv/display.c +++ b/src/Drv/menu/model/display.c @@ -1,8 +1,6 @@ -#include "menu.h" +#include "../app/menu.h" #include "display.h" - - //========================================== // // 字符串代号常量定义 enum _STR_FLAG_ { @@ -74,17 +72,17 @@ enum _STR_FLAG_ { EN_STR_FLAG_HARMOTYPE, EN_STR_FLAG_YXSIN_DI, //设置双点遥信 EN_STR_FLAG_SHOWANATYPE, - EN_STR_FLAG_ANAPOLARTYPE, // 交流通道极性 + EN_STR_FLAG_ANAPOLARTYPE, // 交流通道极性 EN_STR_FLAG_WIRDIAGRAMTYPE // 增加主接线图类型识别.modified by zhanggl 111010 } enumStrType; -/* 静态菜单表 +/* 静态菜单表 注意:该表定义时顺序不能乱,需要从 0 级开始,一级一经按顺序写入 */ const tagMenuModel g_tMenuModelTab[] = { - { 0, "主界面", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 0, "主界面", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, { 1, "装置信息", "查看装置信息", 0, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_AppInfo }, { 1, "实时数据", "装置实时数据", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, { 2, "交流量", "查看遥测交流量", EN_MEA_AC, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_YC }, @@ -146,4 +144,4 @@ const tagMenuModel g_tMenuModelTab[] = { 2, "信号出口", "进入此菜单保护退出", EN_OUTPUT_SIGN, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_Relay }, { 1, "版本信息", "查看板件版本信息", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_See_VersionBoard }, }; -const uint32_t MENU_MAX_ITEM = sizeof(g_tMenuModelTab) / sizeof(tagMenuModel); \ No newline at end of file +const uint32_t MENU_MAX_ITEM = sizeof(g_tMenuModelTab) / sizeof(tagMenuModel); diff --git a/src/Drv/display.h b/src/Drv/menu/model/display.h similarity index 99% rename from src/Drv/display.h rename to src/Drv/menu/model/display.h index 7ee8be1..8e6f847 100644 --- a/src/Drv/display.h +++ b/src/Drv/menu/model/display.h @@ -177,4 +177,4 @@ enum _REC_TYPE_ }; -#endif \ No newline at end of file +#endif diff --git a/src/Drv/menu/model/menu_model.c b/src/Drv/menu/model/menu_model.c new file mode 100644 index 0000000..3c07b2d --- /dev/null +++ b/src/Drv/menu/model/menu_model.c @@ -0,0 +1,52 @@ +#include "menu_model.h" + +#include + +#include "menu_tree_builder.h" + +/* 初始化阶段一次性修饰菜单名称:对有子项的节点追加 '\x10' 指示符。 */ +static void MenuModel_DecorateDisplayNames(tagMenuItem *items, uint32_t maxItem) +{ + for (uint32_t i = 0; i < maxItem; i++) + { + uint16_t nameCapacity = (uint16_t)sizeof(items[i].byName); + uint16_t maxTextLenWithoutNull = (uint16_t)(nameCapacity - 1); + uint16_t len = 0; + if (items[i].ptLower == NULL) + { + continue; + } + while ((len < maxTextLenWithoutNull) && (items[i].byName[len] != '\0')) + { + len++; + } + if ((len == 0) || (items[i].byName[len - 1] != '\x10')) + { + if (len < maxTextLenWithoutNull) + { + items[i].byName[len] = '\x10'; + items[i].byName[len + 1] = '\0'; + } + } + } +} + +void MenuModel_Bootstrap(tagMenuCtrl *menuCtrl, + tagMenuItem *menuItems, + const tagMenuModel *modelTab, + uint32_t modelCount) +{ + MenuTree_0LevelNumCal(menuCtrl, modelTab, modelCount); + MenuTree_MainCreate(menuItems, modelTab, modelCount); + MenuModel_DecorateDisplayNames(menuItems, modelCount); + + menuCtrl->ptHead = &menuItems[0]; + + menuCtrl->ptRoute[0] = &menuItems[0]; + menuCtrl->pt0Level = menuCtrl->ptRoute[0]; + menuCtrl->ptCurrent = menuCtrl->ptHead->ptLower; + menuCtrl->ptCurBak = menuCtrl->ptCurrent; + menuCtrl->ptRoute[1] = menuCtrl->ptCurrent; + menuCtrl->ptRoute[2] = menuCtrl->ptCurrent; + menuCtrl->ptRoute[3] = menuCtrl->ptCurrent; +} diff --git a/src/Drv/menu/model/menu_model.h b/src/Drv/menu/model/menu_model.h new file mode 100644 index 0000000..be67a22 --- /dev/null +++ b/src/Drv/menu/model/menu_model.h @@ -0,0 +1,12 @@ +#ifndef MENU_MODEL_H +#define MENU_MODEL_H + +#include "display.h" +#include "../common/menu_state_types.h" + +void MenuModel_Bootstrap(tagMenuCtrl *menuCtrl, + tagMenuItem *menuItems, + const tagMenuModel *modelTab, + uint32_t modelCount); + +#endif diff --git a/src/Drv/menu/model/menu_tree_builder.c b/src/Drv/menu/model/menu_tree_builder.c new file mode 100644 index 0000000..f551b62 --- /dev/null +++ b/src/Drv/menu/model/menu_tree_builder.c @@ -0,0 +1,119 @@ +#include + +#include "menu_tree_builder.h" + +void MenuTree_0LevelNumCal(tagMenuCtrl *menuCtrl, const tagMenuModel *modelTab, uint32_t maxItem) +{ + uint8_t by0LevelNum = 0; + uint32_t wLoop; + + for (wLoop = 0; wLoop < maxItem; wLoop++) + { + if (modelTab[wLoop].byClass == 0) + { + by0LevelNum++; + } + } + menuCtrl->by0LevelNum = by0LevelNum; +} + +void MenuTree_MainCreate(tagMenuItem *menuItems, const tagMenuModel *modelTab, uint32_t maxItem) +{ + tagPMenuItem ptFirst[4]; + tagPMenuItem ptLast[4]; + tagPMenuItem ptCurrent; + tagPMenuItem ptNextNode; + uint32_t wLoop1; + uint8_t byCurClass; + uint8_t byNextClass; + uint8_t wLoop2; + + ptFirst[0] = &menuItems[0]; + ptLast[0] = ptFirst[0]; + + ptCurrent = &menuItems[0]; + ptCurrent->wPos = 0; + ptCurrent->wNum = 0; + ptCurrent->wSPosX = 0; + ptCurrent->wSPosY = 0; + ptCurrent->wEPosX = 0; + ptCurrent->wEPosY = 0; + + ptCurrent->ptHigher = NULL; + ptCurrent->ptLower = NULL; + ptCurrent->ptBefore = NULL; + ptCurrent->ptBehind = NULL; + + ptCurrent->byClass = modelTab[0].byClass; + ptCurrent->byAttrib = modelTab[0].byAttrib; + ptCurrent->wPassword = modelTab[0].wPassword; + ptCurrent->wPara = modelTab[0].wPara; + ptCurrent->pfnWinProc = modelTab[0].pfnWinProc; + memcpy(ptCurrent->byName, modelTab[0].byName, 50); + memcpy(ptCurrent->byTip, modelTab[0].byTip, 50); + + for (wLoop1 = 1; wLoop1 < maxItem; wLoop1++) + { + ptNextNode = &menuItems[wLoop1]; + + ptNextNode->wPos = 0; + ptNextNode->wNum = 0; + ptNextNode->wSPosX = 0; + ptNextNode->wSPosY = 0; + ptNextNode->wEPosX = 0; + ptNextNode->wEPosY = 0; + + ptNextNode->ptLower = NULL; + ptNextNode->ptBehind = NULL; + ptNextNode->ptBefore = NULL; + ptNextNode->ptHigher = NULL; + + ptNextNode->byClass = modelTab[wLoop1].byClass; + ptNextNode->byAttrib = modelTab[wLoop1].byAttrib; + ptNextNode->wPassword = modelTab[wLoop1].wPassword; + ptNextNode->wPara = modelTab[wLoop1].wPara; + ptNextNode->pfnWinProc = modelTab[wLoop1].pfnWinProc; + memcpy(ptNextNode->byName, modelTab[wLoop1].byName, 50); + memcpy(ptNextNode->byTip, modelTab[wLoop1].byTip, 50); + + byCurClass = ptCurrent->byClass; + byNextClass = ptNextNode->byClass; + + if (byCurClass < byNextClass) + { + ptCurrent->ptLower = ptNextNode; + ptNextNode->ptHigher = ptCurrent; + ptFirst[byNextClass] = ptNextNode; + ptLast[byNextClass] = ptNextNode; + } + else if (byCurClass == byNextClass) + { + ptNextNode->ptBefore = ptCurrent; + ptNextNode->ptHigher = ptCurrent->ptHigher; + ptCurrent->ptBehind = ptNextNode; + ptLast[byNextClass] = ptNextNode; + } + else if (byCurClass > byNextClass) + { + ptNextNode->ptBefore = ptLast[byNextClass]; + ptNextNode->ptHigher = ptFirst[byNextClass]->ptHigher; + ptLast[byNextClass]->ptBehind = ptNextNode; + ptLast[byNextClass] = ptNextNode; + + for (wLoop2 = byCurClass; wLoop2 > byNextClass; wLoop2--) + { + ptLast[wLoop2]->ptBehind = ptFirst[wLoop2]; + ptFirst[wLoop2]->ptBefore = ptLast[wLoop2]; + } + } + + ptCurrent = ptNextNode; + } + + byCurClass = ptCurrent->byClass; + for (wLoop1 = 0; wLoop1 <= byCurClass; wLoop1++) + { + ptLast[wLoop1]->ptBehind = ptFirst[wLoop1]; + ptFirst[wLoop1]->ptBefore = ptLast[wLoop1]; + } +} diff --git a/src/Drv/menu/model/menu_tree_builder.h b/src/Drv/menu/model/menu_tree_builder.h new file mode 100644 index 0000000..66c2400 --- /dev/null +++ b/src/Drv/menu/model/menu_tree_builder.h @@ -0,0 +1,10 @@ +#ifndef MENU_TREE_BUILDER_H +#define MENU_TREE_BUILDER_H + +#include "display.h" +#include "../common/menu_state_types.h" + +void MenuTree_0LevelNumCal(tagMenuCtrl *menuCtrl, const tagMenuModel *modelTab, uint32_t maxItem); +void MenuTree_MainCreate(tagMenuItem *menuItems, const tagMenuModel *modelTab, uint32_t maxItem); + +#endif diff --git a/src/Drv/menu/presenter/menu_nav_types.h b/src/Drv/menu/presenter/menu_nav_types.h new file mode 100644 index 0000000..810d3a3 --- /dev/null +++ b/src/Drv/menu/presenter/menu_nav_types.h @@ -0,0 +1,15 @@ +#ifndef MENU_NAV_TYPES_H +#define MENU_NAV_TYPES_H + +#include "../common/menu_item_types.h" + +typedef struct +{ + tagPMenuItem ptHead; + tagPMenuItem ptCurrent; + tagPMenuItem ptRoute[4]; + tagPMenuItem ptCurBak; + tagPMenuItem pt0Level; +} MenuNavState; + +#endif diff --git a/src/Drv/menu/presenter/menu_navigator.c b/src/Drv/menu/presenter/menu_navigator.c new file mode 100644 index 0000000..9d7d60d --- /dev/null +++ b/src/Drv/menu/presenter/menu_navigator.c @@ -0,0 +1,111 @@ +#include + +#include "menu_navigator.h" + +#include "../../key.h" + +MenuNavResult MenuNavigator_ProcessKey(MenuNavState *navState, uint8_t keyVal) +{ + MenuNavResult result; + tagPMenuItem ptCurrent; + tagPMenuItem ptHead; + tagPMenuItem *ptRoute; + + result.needRefresh = 0; + result.skipRenderThisRound = 0; + + ptCurrent = navState->ptCurrent; + ptHead = navState->ptHead; + ptRoute = navState->ptRoute; + + switch (keyVal) + { + case KEY_F1: + case KEY_F2: + break; + case KEY_U: + ptCurrent = ptCurrent->ptBefore; + result.needRefresh = 1; + break; + case KEY_D: + ptCurrent = ptCurrent->ptBehind; + result.needRefresh = 1; + break; + case KEY_L: + ptCurrent = ptRoute[ptCurrent->byClass - 1]; + if (ptCurrent->byClass == 0) + { + if (ptCurrent->wPos == 1) + { + ptCurrent = ptCurrent->ptBefore->ptBefore; + } + else + { + ptCurrent = ptCurrent->ptBefore; + } + ptCurrent = ptCurrent->ptLower; + } + result.needRefresh = 1; + break; + case KEY_R: + case KEY_ENT: + if (ptCurrent->ptLower != NULL) + { + ptCurrent = ptCurrent->ptLower; + result.needRefresh = 1; + } + else if (ptCurrent->pfnWinProc != NULL) + { + ptCurrent->pfnWinProc(); + } + break; + case KEY_ESC: + if (ptCurrent->byClass == 1) + { + navState->pt0Level = ptHead; + navState->ptRoute[0] = ptHead; + navState->ptCurrent = ptHead->ptLower; + navState->ptCurBak = navState->ptCurrent; + navState->ptRoute[1] = navState->ptCurrent; + result.skipRenderThisRound = 1; + return result; + } + ptCurrent = ptRoute[ptCurrent->byClass - 1]; + result.needRefresh = 1; + break; + default: + break; + } + + navState->ptCurrent = ptCurrent; + return result; +} + +void MenuNavigator_RebuildRoute(MenuNavState *navState, uint32_t maxItem) +{ + tagPMenuItem ptIndex; + + if (navState->ptCurBak == navState->ptCurrent) + { + return; + } + + ptIndex = navState->ptCurrent; + for (uint32_t index = 0; index < maxItem; index++) + { + if (ptIndex->ptHigher == NULL) + { + ptIndex = ptIndex->ptBefore; + } + else + { + ptIndex = ptIndex->ptHigher; + navState->ptRoute[ptIndex->byClass] = ptIndex; + } + if (ptIndex->byClass == 0) + { + break; + } + } + navState->ptRoute[navState->ptCurrent->byClass] = navState->ptCurrent; +} diff --git a/src/Drv/menu/presenter/menu_navigator.h b/src/Drv/menu/presenter/menu_navigator.h new file mode 100644 index 0000000..09e8ce3 --- /dev/null +++ b/src/Drv/menu/presenter/menu_navigator.h @@ -0,0 +1,15 @@ +#ifndef MENU_NAVIGATOR_H +#define MENU_NAVIGATOR_H + +#include "menu_nav_types.h" + +typedef struct +{ + uint8_t needRefresh; + uint8_t skipRenderThisRound; +} MenuNavResult; + +MenuNavResult MenuNavigator_ProcessKey(MenuNavState *navState, uint8_t keyVal); +void MenuNavigator_RebuildRoute(MenuNavState *navState, uint32_t maxItem); + +#endif diff --git a/src/Drv/menu/presenter/menu_presenter.c b/src/Drv/menu/presenter/menu_presenter.c new file mode 100644 index 0000000..fa8d155 --- /dev/null +++ b/src/Drv/menu/presenter/menu_presenter.c @@ -0,0 +1,176 @@ +#include "menu_presenter.h" + +#include "../model/menu_model.h" +#include "menu_navigator.h" + +/* ------------------------------------------------------------------------- + * 函数: MenuPresenter_FillNavState + * 作用: 将“运行时控制结构 tagMenuCtrl”中的导航相关字段复制到 + * “导航临时结构 MenuNavState”。 + * + * 设计原因: + * - MenuNavigator_* 仅关心导航字段,不应直接操作完整控制结构; + * - 通过拷贝到轻量结构,可在 Presenter 内控制读写边界。 + * ------------------------------------------------------------------------- */ +static void MenuPresenter_FillNavState(const tagMenuCtrl *ctrl, MenuNavState *nav) +{ + /* 根节点指针:用于 ESC 回到顶层时复位。 */ + nav->ptHead = ctrl->ptHead; + /* 当前焦点节点:导航计算的主要输入。 */ + nav->ptCurrent = ctrl->ptCurrent; + /* 路径第 0 级:顶层菜单路径缓存。 */ + nav->ptRoute[0] = ctrl->ptRoute[0]; + /* 路径第 1 级:一级菜单路径缓存。 */ + nav->ptRoute[1] = ctrl->ptRoute[1]; + /* 路径第 2 级:二级菜单路径缓存。 */ + nav->ptRoute[2] = ctrl->ptRoute[2]; + /* 路径第 3 级:三级菜单路径缓存。 */ + nav->ptRoute[3] = ctrl->ptRoute[3]; + /* 上一次焦点备份:用于判断是否发生焦点迁移。 */ + nav->ptCurBak = ctrl->ptCurBak; + /* 当前可见的 0 级入口:用于判断是否需要全屏刷新。 */ + nav->pt0Level = ctrl->pt0Level; +} + +/* ------------------------------------------------------------------------- + * 函数: MenuPresenter_ApplyNavState + * 作用: 将导航临时结构 MenuNavState 的结果回写到 tagMenuCtrl。 + * + * 说明: + * - FillNavState + Navigator + ApplyNavState 构成一次输入处理闭环; + * - 这样 Presenter 明确“读输入状态 -> 计算 -> 写回状态”三个阶段。 + * ------------------------------------------------------------------------- */ +static void MenuPresenter_ApplyNavState(tagMenuCtrl *ctrl, const MenuNavState *nav) +{ + /* 回写根节点(通常不变,但保持对称拷贝便于维护)。 */ + ctrl->ptHead = nav->ptHead; + /* 回写当前焦点。 */ + ctrl->ptCurrent = nav->ptCurrent; + /* 回写 0 级路径缓存。 */ + ctrl->ptRoute[0] = nav->ptRoute[0]; + /* 回写 1 级路径缓存。 */ + ctrl->ptRoute[1] = nav->ptRoute[1]; + /* 回写 2 级路径缓存。 */ + ctrl->ptRoute[2] = nav->ptRoute[2]; + /* 回写 3 级路径缓存。 */ + ctrl->ptRoute[3] = nav->ptRoute[3]; + /* 回写焦点备份。 */ + ctrl->ptCurBak = nav->ptCurBak; + /* 回写 0 级入口指针。 */ + ctrl->pt0Level = nav->pt0Level; +} + +/* ------------------------------------------------------------------------- + * 函数: MenuPresenter_Setup + * 作用: 完成 Presenter 的依赖注入和 View 绑定。 + * + * 输入: + * - dspCtrl/menuCtrl/menuItems: 运行态状态与数据池 + * - renderPort: 渲染端口抽象(LCD/模拟器实现) + * - layoutConfig: 布局参数 + * ------------------------------------------------------------------------- */ +void MenuPresenter_Setup(MenuPresenter *presenter, + tagDspCtrl *dspCtrl, + tagMenuCtrl *menuCtrl, + tagMenuItem *menuItems, + const MenuRenderPort *renderPort, + const MenuLayoutConfig *layoutConfig) +{ + /* 保存显示控制状态句柄。 */ + presenter->dspCtrl = dspCtrl; + /* 保存菜单控制状态句柄。 */ + presenter->menuCtrl = menuCtrl; + /* 保存菜单节点池句柄。 */ + presenter->menuItems = menuItems; + /* 保存布局配置句柄。 */ + presenter->layoutConfig = layoutConfig; + /* 初始化 View,并注入具体渲染端口。 */ + MenuView_Init(&presenter->view, renderPort); +} + +/* ------------------------------------------------------------------------- + * 函数: MenuPresenter_InitModel + * 作用: 启动时初始化模型与布局。 + * + * 步骤: + * 1) 设置首帧标志,确保首次进入能触发完整绘制; + * 2) 通过 Model 完成菜单树构建与初始路由; + * 3) 通知 View 计算每个节点的布局坐标。 + * ------------------------------------------------------------------------- */ +void MenuPresenter_InitModel(MenuPresenter *presenter, const tagMenuModel *modelTab, uint32_t modelCount) +{ + /* 标记“首帧未绘制”,后续输入循环会先触发一次刷新。 */ + presenter->dspCtrl->bFirst = 1; + /* 构建运行时菜单树并初始化控制状态。 */ + MenuModel_Bootstrap(presenter->menuCtrl, + presenter->menuItems, + modelTab, + modelCount); + /* 根据模型结果计算显示矩形位置。 */ + MenuView_Layout(&presenter->view, presenter->menuCtrl, presenter->layoutConfig); +} + +/* ------------------------------------------------------------------------- + * 函数: MenuPresenter_Refresh + * 作用: 统一刷新入口,把“当前状态”交给 View 决定具体渲染策略。 + * ------------------------------------------------------------------------- */ +void MenuPresenter_Refresh(MenuPresenter *presenter) +{ + /* View 根据 bFirst / ptCurBak / route 差异决定增量或全量刷新。 */ + MenuView_RenderByState(&presenter->view, presenter->menuCtrl, presenter->dspCtrl); +} + +/* ------------------------------------------------------------------------- + * 函数: MenuPresenter_HandleInput + * 作用: 处理一次输入事件(按键),驱动导航状态迁移与视图刷新。 + * + * 核心流程: + * 1) 首帧短路:先渲染再返回; + * 2) 拷贝导航状态 -> 调用导航器 -> 回写结果; + * 3) 按导航结果决定是否跳过渲染、是否重建路径并刷新。 + * ------------------------------------------------------------------------- */ +void MenuPresenter_HandleInput(MenuPresenter *presenter, uint8_t keyVal) +{ + /* 本轮输入使用的导航状态快照。 */ + MenuNavState navState; + /* 导航器输出:是否需要刷新/是否跳过本轮渲染。 */ + MenuNavResult navResult; + + /* 若仍是首帧阶段:优先做一次渲染,不消费导航逻辑。 */ + if (presenter->dspCtrl->bFirst) + { + /* 执行首帧刷新。 */ + MenuPresenter_Refresh(presenter); + /* 首帧路径到此结束。 */ + return; + } + + /* 从控制结构提取导航子集,作为导航器输入。 */ + MenuPresenter_FillNavState(presenter->menuCtrl, &navState); + /* 基于按键执行导航状态机,得到状态变化与渲染建议。 */ + navResult = MenuNavigator_ProcessKey(&navState, keyVal); + /* 将导航器结果回写到运行时控制结构。 */ + MenuPresenter_ApplyNavState(presenter->menuCtrl, &navState); + + /* 某些按键(如特定 ESC 场景)要求本轮跳过渲染。 */ + if (navResult.skipRenderThisRound) + { + /* 按导航器要求直接返回。 */ + return; + } + + /* 只有导航器明确要求刷新时,才进行路径重建与渲染。 */ + if (navResult.needRefresh) + { + /* 用于“自底向上”重建 route[] 的临时状态。 */ + MenuNavState rebuildState; + /* 以最新控制状态为基准生成重建输入。 */ + MenuPresenter_FillNavState(presenter->menuCtrl, &rebuildState); + /* 重建各级 route 缓存,保证后续渲染路径一致。 */ + MenuNavigator_RebuildRoute(&rebuildState, MENU_MAX_ITEM); + /* 回写重建后的 route 与相关导航状态。 */ + MenuPresenter_ApplyNavState(presenter->menuCtrl, &rebuildState); + /* 触发刷新,让界面体现本次输入结果。 */ + MenuPresenter_Refresh(presenter); + } +} diff --git a/src/Drv/menu/presenter/menu_presenter.h b/src/Drv/menu/presenter/menu_presenter.h new file mode 100644 index 0000000..a8e1616 --- /dev/null +++ b/src/Drv/menu/presenter/menu_presenter.h @@ -0,0 +1,30 @@ +#ifndef MENU_PRESENTER_H +#define MENU_PRESENTER_H + +#include "../model/display.h" +#include "../view/menu_layout.h" +#include "../view/menu_render_port.h" +#include "../common/menu_state_types.h" +#include "../view/menu_view.h" + +typedef struct +{ + tagDspCtrl *dspCtrl; + tagMenuCtrl *menuCtrl; + tagMenuItem *menuItems; + MenuView view; + const MenuLayoutConfig *layoutConfig; +} MenuPresenter; + +void MenuPresenter_Setup(MenuPresenter *presenter, + tagDspCtrl *dspCtrl, + tagMenuCtrl *menuCtrl, + tagMenuItem *menuItems, + const MenuRenderPort *renderPort, + const MenuLayoutConfig *layoutConfig); + +void MenuPresenter_InitModel(MenuPresenter *presenter, const tagMenuModel *modelTab, uint32_t modelCount); +void MenuPresenter_HandleInput(MenuPresenter *presenter, uint8_t keyVal); +void MenuPresenter_Refresh(MenuPresenter *presenter); + +#endif diff --git a/src/Drv/menu/view/menu_layout.c b/src/Drv/menu/view/menu_layout.c new file mode 100644 index 0000000..941da7b --- /dev/null +++ b/src/Drv/menu/view/menu_layout.c @@ -0,0 +1,447 @@ +#include +#include "menu_layout.h" +#include "utf8.h" + +/****************************************************************************** + * 模块: menu_layout.c + * 职责: 根据菜单树结构计算各级菜单在 LCD 上的显示矩形区域 + * + * 设计要点: + * 1) 采用“先算当前层,再递归/级联算下一层”的方式布点; + * 2) 每级菜单都维护: + * - ptFirst[level]: 当前层循环链表的起点; + * - ptIndex[level]: 当前层遍历游标; + * - byMenuNum[level]: 当前层项数量(或当前位置计数); + * 3) 对 Y 方向越界提供多级回退策略(向上翻转/贴顶重排),尽量保证菜单可见。 + *****************************************************************************/ + +/* 读取菜单节点导航指针集合(本文件局部辅助,避免 core 泄漏行为函数)。 */ +static MenuLinks MenuLayout_GetLinks(const MenuItem *item) +{ + MenuLinks links; + links.higher = item->ptHigher; + links.lower = item->ptLower; + links.before = item->ptBefore; + links.behind = item->ptBehind; + return links; +} + +/* 读取菜单节点矩形(本文件局部辅助)。 */ +static MenuRect MenuLayout_GetRect(const MenuItem *item) +{ + MenuRect rect; + rect.wSPosX = item->wSPosX; + rect.wSPosY = item->wSPosY; + rect.wEPosX = item->wEPosX; + rect.wEPosY = item->wEPosY; + return rect; +} + +/* 给菜单项写入矩形坐标的轻量封装,避免到处重复构造 MenuRect。 */ +static void MenuLayout_SetRect(MenuItem *item, uint16_t sx, uint16_t sy, uint16_t ex, uint16_t ey) +{ + item->wSPosX = sx; + item->wSPosY = sy; + item->wEPosX = ex; + item->wEPosY = ey; +} + +/* 将理论挂接行压缩映射到可视区,避免超出可显示行数。 */ +static uint16_t MenuLayout_MapMenuPos(uint16_t menuPos, uint8_t itemNum, uint8_t maxNum) +{ + if ((itemNum > maxNum) && (menuPos >= maxNum)) + { + return (uint16_t)(maxNum - (itemNum - menuPos)); + } + return menuPos; +} + +/* 子菜单 X 方向统一规则:从父框右边展开。 */ +static void MenuLayout_CalcSubMenuX(const MenuRect *parentRect, + uint8_t maxLen, + const MenuLayoutConfig *config, + uint16_t *startX, + uint16_t *endX) +{ + *startX = parentRect->wEPosX; + *endX = (uint16_t)(*startX + maxLen * config->menuWidth + config->menuXAdd); +} + +/* 尝试将菜单框整体翻转到上方显示;成功返回 1,失败返回 0。 */ +static uint8_t MenuLayout_TryFlipUp(uint16_t *startY, + uint16_t *endY, + uint8_t itemNum, + uint16_t flipMinY, + const MenuLayoutConfig *config) +{ + uint16_t candidateEndY; + uint16_t temp; + + candidateEndY = (uint16_t)(*startY - (itemNum - 1) * config->lineHeight - config->menuYAdd); + if ((candidateEndY > flipMinY) && (candidateEndY < config->menuYMax)) + { + temp = *startY; + *startY = candidateEndY; + *endY = (uint16_t)(temp + config->lineHeight); + return 1; + } + return 0; +} + +/* Sub2 兜底:回退到顶层起始 Y,并在底部做截断。 */ +static void MenuLayout_FallbackClampToTop(const tagPMenuItem *ptIndex, + uint8_t itemNum, + uint16_t *startY, + uint16_t *endY, + const MenuLayoutConfig *config) +{ + *startY = ptIndex[0]->wSPosY; + *endY = (uint16_t)(*startY + itemNum * config->lineHeight + config->menuYAdd); + if (*endY > config->menuYMax) + { + *endY = config->menuYMax; + } +} + +/* Sub1 兜底:从顶层起始 Y 逐行上探,直到菜单框不越界。 */ +static void MenuLayout_FallbackProbeUp(const tagPMenuItem *ptIndex, + uint8_t itemNum, + uint16_t *startY, + uint16_t *endY, + const MenuLayoutConfig *config) +{ + *startY = ptIndex[0]->wSPosY; + *endY = (uint16_t)(*startY + itemNum * config->lineHeight + config->menuYAdd); + for (uint16_t i = 1; i < (uint16_t)(ptIndex[0]->wSPosY / config->lineHeight + 1); i++) + { + if (*endY > config->menuYMax) + { + *startY = (uint16_t)(ptIndex[0]->wSPosY - config->lineHeight * i); + if (*startY < config->lineHeight) + { + *startY = 0; + } + *endY = (uint16_t)(*startY + itemNum * config->lineHeight + config->menuYAdd); + } + else + { + break; + } + } +} + +/* 计算单个菜单项“显示宽度”: + * - 中文等多字节字符按 2 列计宽,ASCII 按 1 列; + * - 若该项有下级菜单且名称末尾未带特殊标记 '\x10',额外预留 1 列用于层级指示。 */ +static uint8_t MenuLayout_ItemDisplayLen(const MenuItem *item) +{ + uint8_t displayLen; + displayLen = MenuLayout_Utf8LenCal((uint8_t *)item->byName); + if (item->ptLower != NULL) + { + uint8_t byteLen = 0; + while ((byteLen < 50) && (item->byName[byteLen] != '\0')) + { + byteLen++; + } + if ((byteLen == 0) || (item->byName[byteLen - 1] != '\x10')) + { + displayLen += 1; + } + } + return displayLen; +} + +/* UTF-8 显示长度统计: + * utf8_next() 每次返回一个 Unicode 字符占用的字节数 n, + * 约定 n>1 视为“宽字符”(占 2 列),否则占 1 列。 */ +uint8_t MenuLayout_Utf8LenCal(uint8_t *str) +{ + uint8_t strLen = 0; + uint32_t unicode; + uint8_t index = 0; + uint8_t n = 0; + + while (str[index] != '\0') + { + n = utf8_next(str + index, &unicode); + strLen += (n > 1) ? 2 : 1; + index += n; + } + return strLen; +} + +/****************************************************************************** + * 函数: MenuLayout_CharLenCal + * 作用: + * 1) 统计 bylevel 下一层(bylevel+1)菜单项总数; + * 2) 计算该层中最长显示宽度(用于决定菜单框宽度); + * 3) 为该层每个节点写 wPos(从 1 开始的位置序号); + * 4) 把“下一层项数”写回父节点 wNum。 + * + * 变量字典: + * - bylevel: 输入为父层级,函数内部会自增到“子层级”; + * - byMenuNum[level]: level 层累计项数(同时作为 wPos 的来源); + * - ptFirst[level]: level 层循环链表的起点节点; + * - ptIndex[level]: level 层当前遍历节点; + * - byMaxLen: 本层所有项显示宽度的最大值(单位: 字符列)。 + *****************************************************************************/ +uint8_t MenuLayout_CharLenCal(uint8_t bylevel, uint8_t *byMenuNum, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex) +{ + uint8_t displayLen; + uint8_t byMaxLen = 0; + MenuLinks links; + + /* 进入下一层,从父节点的 lower 作为该层环形链表起点。 */ + ptFirst[bylevel + 1] = ptIndex[bylevel]->ptLower; + bylevel = bylevel + 1; + ptIndex[bylevel] = ptFirst[bylevel]; + + byMenuNum[bylevel] = 1; + ptIndex[bylevel]->wPos = 1; + + /* 固定上限 300 作为保护,避免链表异常导致死循环。 */ + for (uint16_t wLoop = 0; wLoop < 300; wLoop++) + { + links = MenuLayout_GetLinks(ptIndex[bylevel]); + displayLen = MenuLayout_ItemDisplayLen(ptIndex[bylevel]); + + if (byMaxLen < displayLen) + { + byMaxLen = displayLen; + } + + ptIndex[bylevel] = links.behind; + if (ptIndex[bylevel] == ptFirst[bylevel]) + { + break; + } + byMenuNum[bylevel]++; + ptIndex[bylevel]->wPos = byMenuNum[bylevel]; + } + + /* 父节点记录子项数量,供分页/显示逻辑复用。 */ + ptIndex[bylevel - 1]->wNum = byMenuNum[bylevel]; + return byMaxLen; +} + +/****************************************************************************** + * 函数: MenuLayout_Sub2PosCal + * 作用: 计算二级子菜单(及其同层兄弟)的显示矩形 + * 策略: + * - 先按“父项对齐”得到理想 Y; + * - 若底部越界,尝试向上翻转; + * - 仍不满足则使用贴近顶层菜单的兜底摆放。 + * + * 变量字典: + * - byMaxNum: 当前配置下可完整显示的最大行数; + * - byMenuPos: 当前菜单相对父菜单的挂接行(理论位置); + * - byItemNum: 当前层或下一层项数(代码中按阶段复用); + * - wSPosY/wEPosY: 菜单框起止 Y 坐标(含间距边界); + * - parentRect: 父菜单矩形,用于对子菜单做相对定位。 + * + * 关键公式: + * - X 起点 = parentRect.wEPosX(总是从父框右侧展开); + * - X 终点 = X 起点 + byMaxLen * menuWidth + menuXAdd; + * - Y 起点 = parentRect.wSPosY + (byMenuPos - 1) * lineHeight; + * - Y 终点 = Y 起点 + 子项数 * lineHeight + menuYAdd。 + *****************************************************************************/ +void MenuLayout_Sub2PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config) +{ + uint16_t wSPosY; + uint16_t wEPosY; + uint8_t byMaxLen; + uint8_t byMaxNum; + uint16_t byMenuPos; + uint8_t byItemNum; + uint8_t byMenuNum[4]; + MenuLinks links; + MenuRect parentRect; + + /* 当前可完整容纳的最大行数(预留边框/间距)。 */ + byMaxNum = (config->menuYMax - config->menuYMin - 6) / config->lineHeight; + ptIndex[bylevel] = ptFirst[bylevel]; + for (uint16_t wLoop = 0; wLoop < 300; wLoop++) + { + links = MenuLayout_GetLinks(ptIndex[bylevel]); + if (links.lower != NULL) + { + byMaxLen = MenuLayout_CharLenCal(bylevel, byMenuNum, ptFirst, ptIndex); + parentRect = MenuLayout_GetRect(ptIndex[bylevel - 1]); + MenuLayout_CalcSubMenuX(&parentRect, + byMaxLen, + config, + &ptIndex[bylevel]->wSPosX, + &ptIndex[bylevel]->wEPosX); + + /* byMenuPos: 当前项在同层中的理论挂接位置(必要时压缩映射)。 */ + byMenuPos = ptIndex[bylevel]->wPos; + byItemNum = byMenuNum[bylevel]; + byMenuPos = MenuLayout_MapMenuPos(byMenuPos, byItemNum, byMaxNum); + + wSPosY = parentRect.wSPosY + (byMenuPos - 1) * config->lineHeight; + byItemNum = byMenuNum[bylevel + 1]; + wEPosY = wSPosY + byItemNum * config->lineHeight + config->menuYAdd; + + if (wEPosY < config->menuYMax) + { + /* 理想位置不越界,直接应用。 */ + MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY); + } + else + { + /* 底部越界:先尝试“向上翻转”放置整个菜单框。 */ + if (!MenuLayout_TryFlipUp(&wSPosY, &wEPosY, byItemNum, config->menuYMin, config)) + { + /* 仍不满足:回退到顶层菜单起始 Y,再做截断兜底。 */ + MenuLayout_FallbackClampToTop(ptIndex, byItemNum, &wSPosY, &wEPosY, config); + } + MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY); + } + } + ptIndex[bylevel] = links.behind; + if (ptIndex[bylevel] == ptFirst[bylevel]) + { + break; + } + } +} + +/****************************************************************************** + * 函数: MenuLayout_Sub1PosCal + * 作用: 计算一级子菜单(通常是主菜单下拉)的位置,并联动计算二级子菜单 + * 说明: 与 Sub2 逻辑相近,但在兜底时会逐步向上平移,直到菜单框可容纳。 + * + * 变量字典: + * - byMaxNum: 屏幕当前可显示的最大菜单行数; + * - byMenuPos: 当前节点理论挂接行(可能被映射压缩); + * - byItemNum: 当前下拉框条目数; + * - wTemp: 用于“翻转布局”时交换起止 Y; + * - byMenuNum[4]: 分层统计数组,索引与菜单层级一一对应。 + * + * 与 Sub2 的核心差异: + * - Sub1 兜底策略增加“逐行上探”,尝试找到不越界的最靠下位置, + * 视觉上更适合主菜单下拉场景。 + *****************************************************************************/ +void MenuLayout_Sub1PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config) +{ + uint16_t wSPosY; + uint16_t wEPosY; + uint8_t byMenuNum[4]; + uint8_t byMaxLen; + uint8_t byMaxNum; + uint16_t byMenuPos; + uint8_t byItemNum; + MenuLinks links; + MenuRect parentRect; + + byMaxNum = config->menuYMax / config->lineHeight; + ptIndex[bylevel] = ptFirst[bylevel]; + for (uint16_t wLoop = 0; wLoop < 300; wLoop++) + { + links = MenuLayout_GetLinks(ptIndex[bylevel]); + if (links.lower != NULL) + { + byMaxLen = MenuLayout_CharLenCal(bylevel, byMenuNum, ptFirst, ptIndex); + parentRect = MenuLayout_GetRect(ptIndex[bylevel - 1]); + MenuLayout_CalcSubMenuX(&parentRect, + byMaxLen, + config, + &ptIndex[bylevel]->wSPosX, + &ptIndex[bylevel]->wEPosX); + + byMenuPos = ptIndex[bylevel]->wPos; + byItemNum = byMenuNum[bylevel]; + byMenuPos = MenuLayout_MapMenuPos(byMenuPos, byItemNum, byMaxNum); + + wSPosY = parentRect.wSPosY + (byMenuPos - 1) * config->lineHeight; + byItemNum = byMenuNum[bylevel + 1]; + wEPosY = wSPosY + byItemNum * config->lineHeight + config->menuYAdd; + + if (wEPosY < config->menuYMax) + { + MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY); + } + else + { + /* 底部越界:优先尝试一次整体翻转到上方。 */ + if (!MenuLayout_TryFlipUp(&wSPosY, &wEPosY, byItemNum, config->lineHeight, config)) + { + /* 二次兜底:从顶层菜单起始 Y 往上逐行试探,直到不越界。 */ + MenuLayout_FallbackProbeUp(ptIndex, byItemNum, &wSPosY, &wEPosY, config); + } + MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY); + } + /* 一级菜单定位完成后,继续计算二级菜单。 */ + MenuLayout_Sub2PosCal(2, ptFirst, ptIndex, config); + } + ptIndex[bylevel] = links.behind; + if (ptIndex[bylevel] == ptFirst[bylevel]) + { + break; + } + } +} + +/****************************************************************************** + * 函数: MenuLayout_PositionCal + * 作用: 入口函数,计算从 0 级到 2 级菜单的显示坐标 + * 流程: + * 1) 先给 0 级菜单按屏宽等分; + * 2) 每个 0 级项若有下级,则计算其下拉框; + * 3) 再级联计算 1/2 级子菜单。 + * + * 变量字典: + * - by0LevelNum: 顶层菜单数量; + * - byInterval: 顶层菜单等分后的列宽; + * - byMenuNum[0]: 当前顶层节点在同层中的序号(从 1 开始); + * - byMenuNum[1]: 顶层节点的直接子项数量(由 CharLenCal 写入); + * - ptFirst/ptIndex: 各层遍历上下文,在本函数中作为全流程共享状态。 + *****************************************************************************/ +void MenuLayout_PositionCal(tagPMenuItem ptMenuHead, uint8_t by0LevelNum, const MenuLayoutConfig *config) +{ + tagPMenuItem ptFirst[4]; + tagPMenuItem ptIndex[4]; + uint8_t byMenuNum[4]; + uint8_t byMaxLen; + uint8_t byInterval; + MenuLinks links; + + /* 输入保护:头指针/配置为空,或 0 级菜单数量为 0 时直接返回。 */ + if ((ptMenuHead == NULL) || (config == NULL) || (by0LevelNum == 0)) + { + return; + } + + ptFirst[0] = ptMenuHead; + ptIndex[0] = ptFirst[0]; + byMenuNum[0] = 1; + ptIndex[0]->wPos = 1; + /* 顶层菜单按等分宽度布局。 */ + byInterval = config->lcdSizeX / by0LevelNum; + + for (uint16_t wLoop = 0; wLoop < by0LevelNum; wLoop++) + { + links = MenuLayout_GetLinks(ptIndex[0]); + if (links.lower != NULL) + { + /* 先统计该分支子项宽高需求,再写入 0 级菜单框。 */ + byMaxLen = MenuLayout_CharLenCal(0, byMenuNum, ptFirst, ptIndex); + MenuLayout_SetRect( + ptIndex[0], + (byMenuNum[0] - 1) * byInterval, + config->lcdSizeY - config->lineHeight - byMenuNum[1] * config->lineHeight - config->menuYAdd, + (byMenuNum[0] - 1) * byInterval + byMaxLen * config->menuWidth + config->menuXAdd, + config->lcdSizeY - config->lineHeight); + /* 0 级完成后继续展开 1/2 级。 */ + MenuLayout_Sub1PosCal(1, ptFirst, ptIndex, config); + } + ptIndex[0] = links.behind; + if (ptIndex[0] == ptFirst[0]) + { + break; + } + byMenuNum[0]++; + ptIndex[0]->wPos = byMenuNum[0]; + } +} diff --git a/src/Drv/menu/view/menu_layout.h b/src/Drv/menu/view/menu_layout.h new file mode 100644 index 0000000..9887687 --- /dev/null +++ b/src/Drv/menu/view/menu_layout.h @@ -0,0 +1,34 @@ +#ifndef MENU_LAYOUT_H +#define MENU_LAYOUT_H + +#include "../common/menu_item_types.h" + +#define CN_HEIGHT 12 +#define CN_ROWSPACE 2 +#define LINE_HEIGHT (CN_HEIGHT + CN_ROWSPACE) +#define MENU_XADD 4 +#define MENU_YADD 4 +#define MENU_WITDTH 7 + +typedef struct +{ + uint16_t lcdSizeX; + uint16_t lcdSizeY; + uint16_t menuYMin; + uint16_t menuYMax; + uint16_t lineHeight; + uint16_t menuWidth; + uint16_t menuXAdd; + uint16_t menuYAdd; +} MenuLayoutConfig; + +#define MENU_YMIN 0 +#define MENU_YMAX_FROM_LCD(ySize) ((uint16_t)((ySize) - 12U)) + +uint8_t MenuLayout_Utf8LenCal(uint8_t *str); +uint8_t MenuLayout_CharLenCal(uint8_t bylevel, uint8_t *byMenuNum, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex); +void MenuLayout_Sub2PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config); +void MenuLayout_Sub1PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config); +void MenuLayout_PositionCal(tagPMenuItem ptMenuHead, uint8_t by0LevelNum, const MenuLayoutConfig *config); + +#endif diff --git a/src/Drv/menu/view/menu_render_port.h b/src/Drv/menu/view/menu_render_port.h new file mode 100644 index 0000000..253b751 --- /dev/null +++ b/src/Drv/menu/view/menu_render_port.h @@ -0,0 +1,22 @@ +#ifndef MENU_RENDER_PORT_H +#define MENU_RENDER_PORT_H + +typedef struct +{ + void (*init)(void); + unsigned short (*get_size_x)(void); + unsigned short (*get_size_y)(void); + unsigned char (*get_color_font)(void); + unsigned char (*get_color_back)(void); + signed char (*fill_rect)(unsigned short left_x, unsigned short top_y, unsigned short right_x, unsigned short bottom_y, unsigned int color); + signed char (*line_h)(unsigned short x_start, unsigned short x_end, unsigned short y, unsigned short width, unsigned char color); + signed char (*line_v)(unsigned short y_start, unsigned short y_end, unsigned short x, unsigned short width, unsigned char color); + signed char (*line)(unsigned short x_start, unsigned short y_start, unsigned short x_end, unsigned short y_end, unsigned short width, unsigned char color); + signed char (*set_pixel)(unsigned short x, unsigned short y, unsigned char color); + signed char (*invert)(unsigned short left_x, unsigned short top_y, unsigned short right_x, unsigned short bottom_y); + signed char (*show_str)(unsigned short x, unsigned short y, unsigned char *text); +} MenuRenderPort; + +const MenuRenderPort *MenuRenderPort_Lcd(void); + +#endif diff --git a/src/Drv/menu/view/menu_renderer_lcd.c b/src/Drv/menu/view/menu_renderer_lcd.c new file mode 100644 index 0000000..fa58876 --- /dev/null +++ b/src/Drv/menu/view/menu_renderer_lcd.c @@ -0,0 +1,45 @@ +#include "menu_render_port.h" + +#include "lcd.h" +#include "lcd_draw.h" +#include "lcd_text.h" + +static unsigned short MenuRenderPort_LcdSizeX(void) +{ + return LCD_SIZE_X; +} + +static unsigned short MenuRenderPort_LcdSizeY(void) +{ + return LCD_SIZE_Y; +} + +static unsigned char MenuRenderPort_LcdColorFont(void) +{ + return LCD_FONT; +} + +static unsigned char MenuRenderPort_LcdColorBack(void) +{ + return LCD_BACK; +} + +static const MenuRenderPort g_lcd_port = { + .init = Lcd_Init, + .get_size_x = MenuRenderPort_LcdSizeX, + .get_size_y = MenuRenderPort_LcdSizeY, + .get_color_font = MenuRenderPort_LcdColorFont, + .get_color_back = MenuRenderPort_LcdColorBack, + .fill_rect = Lcd_FillRect, + .line_h = Lcd_LineH, + .line_v = Lcd_LineV, + .line = Lcd_Line, + .set_pixel = Lcd_SetPixel, + .invert = Lcd_Invert, + .show_str = Lcd_ShowStr, +}; + +const MenuRenderPort *MenuRenderPort_Lcd(void) +{ + return &g_lcd_port; +} diff --git a/src/Drv/menu/view/menu_view.c b/src/Drv/menu/view/menu_view.c new file mode 100644 index 0000000..d2e1f20 --- /dev/null +++ b/src/Drv/menu/view/menu_view.c @@ -0,0 +1,114 @@ +#include "menu_view.h" + +static void MenuView_DrawMeitou(MenuView *view, uint16_t yStart, uint16_t width) +{ + uint8_t fontColor = view->port->get_color_font(); + view->port->line_h(16, 144, yStart, width, fontColor); + view->port->line(8, yStart - 8, 16, yStart, width, fontColor); + view->port->line(144, yStart, 152, yStart - 8, width, fontColor); +} + +static void MenuView_DrawBoundaryBox(MenuView *view, uint16_t leftX, uint16_t topY, uint16_t rightX, uint16_t bottomY) +{ + uint8_t backColor = view->port->get_color_back(); + uint8_t fontColor = view->port->get_color_font(); + for (uint16_t y = topY; y < bottomY; y++) + { + for (uint16_t x = leftX; x < rightX; x++) + { + view->port->set_pixel(x, y, backColor); + } + } + + view->port->line_h(leftX, rightX, topY, 1, fontColor); + view->port->line_v(topY, bottomY, leftX, 1, fontColor); + view->port->line_h(leftX, rightX + 1, bottomY, 1, fontColor); + view->port->line_v(topY, bottomY, rightX, 1, fontColor); +} + +static void MenuView_ShowOtherLevel(MenuView *view, const tagMenuCtrl *menuCtrl, uint8_t level) +{ + tagPMenuItem ptIndex = menuCtrl->ptRoute[level]; + tagPMenuItem ptRoute; + uint16_t wPosX; + uint16_t wPosY; + + MenuView_DrawBoundaryBox(view, ptIndex->wSPosX, ptIndex->wSPosY, ptIndex->wEPosX, ptIndex->wEPosY); + + ptRoute = menuCtrl->ptRoute[level + 1]; + ptIndex = ptRoute; + wPosX = menuCtrl->ptRoute[level]->wSPosX + 4; + for (uint16_t index = 0; index < menuCtrl->ptRoute[level]->wNum; index++) + { + wPosY = menuCtrl->ptRoute[level]->wSPosY + (ptIndex->wPos - 1) * LINE_HEIGHT + 3; + view->port->show_str(wPosX, wPosY, ptIndex->byName); + if (ptRoute == ptIndex) + { + view->port->invert(menuCtrl->ptRoute[level]->wSPosX + 2, wPosY - 1, menuCtrl->ptRoute[level]->wEPosX - 2, wPosY + 14); + } + ptIndex = ptIndex->ptBehind; + } +} + +static void MenuView_ShowTopLevel(MenuView *view) +{ + uint16_t lcdSizeX = view->port->get_size_x(); + view->port->fill_rect(0, 0, lcdSizeX - 1, 32, view->port->get_color_back()); + MenuView_DrawMeitou(view, 16, 2); + view->port->show_str(16, 20, (uint8_t *)"当前模式: 无模式"); +} + +void MenuView_Init(MenuView *view, const MenuRenderPort *renderPort) +{ + view->port = renderPort; +} + +void MenuView_Layout(MenuView *view, tagMenuCtrl *menuCtrl, const MenuLayoutConfig *config) +{ + (void)view; + MenuLayout_PositionCal(menuCtrl->ptHead, menuCtrl->by0LevelNum, config); +} + +void MenuView_RenderByState(MenuView *view, tagMenuCtrl *menuCtrl, tagDspCtrl *dspCtrl) +{ + uint8_t needFullRefresh = 0; + uint8_t backColor = view->port->get_color_back(); + uint16_t lcdSizeX = view->port->get_size_x(); + uint16_t menuYMax = MENU_YMAX_FROM_LCD(view->port->get_size_y()); + + if (dspCtrl->bFirst) + { + dspCtrl->bFirst = 0; + needFullRefresh = 1; + } + + if (menuCtrl->pt0Level != menuCtrl->ptRoute[0]) + { + needFullRefresh = 1; + view->port->fill_rect(0, MENU_YMIN, lcdSizeX - 1, menuYMax, backColor); + menuCtrl->pt0Level = menuCtrl->ptRoute[0]; + menuCtrl->ptCurBak = menuCtrl->ptCurrent; + } + else if (menuCtrl->ptCurBak != menuCtrl->ptCurrent) + { + if (menuCtrl->ptCurrent->byClass >= menuCtrl->ptCurBak->byClass) + { + MenuView_ShowOtherLevel(view, menuCtrl, menuCtrl->ptCurrent->byClass - 1); + } + else + { + view->port->fill_rect(0, MENU_YMIN, lcdSizeX - 1, menuYMax, backColor); + needFullRefresh = 1; + } + menuCtrl->ptCurBak = menuCtrl->ptCurrent; + } + + if (needFullRefresh > 0) + { + MenuView_ShowTopLevel(view); + for (uint8_t index = 0; index < menuCtrl->ptCurrent->byClass; index++) + { + MenuView_ShowOtherLevel(view, menuCtrl, index); + } + } +} diff --git a/src/Drv/menu/view/menu_view.h b/src/Drv/menu/view/menu_view.h new file mode 100644 index 0000000..61490a1 --- /dev/null +++ b/src/Drv/menu/view/menu_view.h @@ -0,0 +1,17 @@ +#ifndef MENU_VIEW_H +#define MENU_VIEW_H + +#include "menu_layout.h" +#include "menu_render_port.h" +#include "../common/menu_state_types.h" + +typedef struct +{ + const MenuRenderPort *port; +} MenuView; + +void MenuView_Init(MenuView *view, const MenuRenderPort *renderPort); +void MenuView_Layout(MenuView *view, tagMenuCtrl *menuCtrl, const MenuLayoutConfig *config); +void MenuView_RenderByState(MenuView *view, tagMenuCtrl *menuCtrl, tagDspCtrl *dspCtrl); + +#endif diff --git a/src/common/utf8.c b/src/common/utf8.c new file mode 100644 index 0000000..dfdad9d --- /dev/null +++ b/src/common/utf8.c @@ -0,0 +1,36 @@ +#include "utf8.h" + +uint8_t utf8_next(const unsigned char *utf8, uint32_t *out_unicode) +{ + unsigned char c = utf8[0]; + if (c == 0) + { + *out_unicode = 0; + return 0; + } + if (c < 0x80) + { + *out_unicode = c; + return 1; + } + if ((c & 0xE0) == 0xC0) + { + if (utf8[1] == 0) + { + return 0; + } + *out_unicode = (uint32_t)((c & 0x1F) << 6 | (utf8[1] & 0x3F)); + return 2; + } + if ((c & 0xF0) == 0xE0) + { + if (utf8[1] == 0 || utf8[2] == 0) + { + return 0; + } + *out_unicode = (uint32_t)((c & 0x0F) << 12 | (utf8[1] & 0x3F) << 6 | (utf8[2] & 0x3F)); + return 3; + } + *out_unicode = 0; + return 0; +} diff --git a/src/common/utf8.h b/src/common/utf8.h new file mode 100644 index 0000000..0591768 --- /dev/null +++ b/src/common/utf8.h @@ -0,0 +1,9 @@ +#ifndef UTF8_H +#define UTF8_H + +#include "types.h" + +/* 解析 UTF-8 字节流中的一个字符,返回占用字节数(0 表示失败/结束)。 */ +uint8_t utf8_next(const unsigned char *utf8, uint32_t *out_unicode); + +#endif diff --git a/src/main.c b/src/main.c index 83382f9..dc798b7 100644 --- a/src/main.c +++ b/src/main.c @@ -1,4 +1,4 @@ -/* ============================================================================ +/* ============================================================================ * main.c - PC 端 HMI 菜单主程序 * 功能:菜单交互(主线程)+ TCP 服务器(独立线程),按 Q 退出 * ========================================================================== */ @@ -30,7 +30,7 @@ static int getch(void) } #endif -#include "Drv/menu.h" +#include "Drv/menu/app/menu.h" #include "TCP/tcp.h" #include "remoteDisplay.h" #include "Drv/key.h" @@ -63,7 +63,7 @@ int main(void) printf("开始初始化菜单树...\n"); - Menu_Init(); /* 初始化菜单树(运行界面/定值设置/查看数据等) */ + MenuApp_Init(); /* 初始化菜单应用(Model + View + Presenter) */ Key_Init(); /* 初始化按键 */ printf("PC 端 HMI 菜单模拟启动(TCP 服务在单独线程,端口 7003)。\n"); /* 7003 为 RemoDispBus 默认端口,与 remo_disp_server.py 一致 */ @@ -73,13 +73,13 @@ int main(void) } while(1) { - Menu_Route(); + MenuApp_PollInput(); Sleep(20); count++; if(count > 50) { count = 0; - Menu_Show_Proc(); + MenuApp_Render(); } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 79cd86b..6b8dadd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,8 +6,8 @@ set(DTU_TEST_COMMON_SOURCES "${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c" "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c" "${CMAKE_SOURCE_DIR}/src/Drv/key.c" - "${CMAKE_SOURCE_DIR}/src/Drv/menu.c" - "${CMAKE_SOURCE_DIR}/src/Drv/display.c" + "${CMAKE_SOURCE_DIR}/src/Drv/menu/app/menu.c" + "${CMAKE_SOURCE_DIR}/src/Drv/menu/model/display.c" "${CMAKE_SOURCE_DIR}/src/TCP/tcp.c" "${CMAKE_SOURCE_DIR}/src/thread_utils.c" "${CMAKE_SOURCE_DIR}/src/remoteDisplay.c" @@ -20,12 +20,17 @@ set(DTU_TEST_COMMON_SOURCES # - add_test: 注册到 CTest,支持 ctest 统一执行 function(add_dtu_test test_name) add_executable(${test_name} ${ARGN}) + target_sources(${test_name} PRIVATE + "${CMAKE_SOURCE_DIR}/src/common/utf8.c" + ) # 测试目标可见的头文件目录 target_include_directories(${test_name} PRIVATE "${CMAKE_SOURCE_DIR}/include" "${CMAKE_SOURCE_DIR}/src" "${CMAKE_SOURCE_DIR}/src/TCP" "${CMAKE_SOURCE_DIR}/src/Drv" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd" + "${CMAKE_SOURCE_DIR}/src/common" ) # 平台差异:Windows 需要 ws2_32,非 Windows 使用 pthread if(WIN32) @@ -56,14 +61,6 @@ add_dtu_test( "${CMAKE_SOURCE_DIR}/src/TCP/tcp.c" "${CMAKE_SOURCE_DIR}/src/thread_utils.c" ) -add_dtu_test( - test_p0_utf8_next - test_p0_utf8_next.c - "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c" - "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_draw.c" - "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c" - "${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c" -) add_dtu_test( test_p0_utf8_hz12_get test_p0_utf8_hz12_get.c @@ -89,12 +86,35 @@ add_dtu_test( add_dtu_test( test_p1_menu test_p1_menu.c - "${CMAKE_SOURCE_DIR}/src/Drv/display.c" + "${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_tree_builder.c" + "${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_layout.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c" +) +add_dtu_test( + test_p1_menu_nav_legacy + test_p1_menu_nav_legacy.c + "${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_navigator.c" +) +add_dtu_test( + test_p1_menu_navigator + test_p1_menu_navigator.c + "${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_navigator.c" +) +add_dtu_test( + test_p1_menu_tree_builder + test_p1_menu_tree_builder.c + "${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_tree_builder.c" +) +add_dtu_test( + test_p1_menu_layout + test_p1_menu_layout.c + "${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_layout.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c" "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c" "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_draw.c" - "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c" "${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c" - "${CMAKE_SOURCE_DIR}/src/Drv/key.c" ) # ------------------------------------------------------------ @@ -106,3 +126,21 @@ add_dtu_test( "${CMAKE_SOURCE_DIR}/src/TCP/tcp.c" "${CMAKE_SOURCE_DIR}/src/thread_utils.c" ) +add_dtu_test( + test_p2_menu_runtime_startup + test_p2_menu_runtime_startup.c + "${CMAKE_SOURCE_DIR}/src/Drv/menu/app/menu.c" + "${CMAKE_SOURCE_DIR}/src/Drv/menu/model/display.c" + "${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_model.c" + "${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_view.c" + "${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_presenter.c" + "${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_tree_builder.c" + "${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_layout.c" + "${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_navigator.c" + "${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_renderer_lcd.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_draw.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c" + "${CMAKE_SOURCE_DIR}/src/Drv/key.c" +) diff --git a/tests/test_p0_utf8_hz12_get.c b/tests/test_p0_utf8_hz12_get.c index c5a4e2b..f07e51b 100644 --- a/tests/test_p0_utf8_hz12_get.c +++ b/tests/test_p0_utf8_hz12_get.c @@ -1,13 +1,42 @@ +/* ------------------------------------------------------------------------- + * 文件名: test_p0_utf8_hz12_get.c + * 作用: + * 验证中文点阵查询接口 utf8_hz12_get() 的基础行为是否正确。 + * + * 被测接口: + * const uint8_t* utf8_hz12_get(uint32_t unicode); + * + * 测试目标: + * 1) 对于字库中存在的码点,应返回非 NULL 位图指针。 + * 2) 对于字库范围外或不存在的码点,应返回 NULL。 + * + * 说明: + * - 本测试只校验“是否命中”这一契约,不校验位图内容本身。 + * - 若后续需要,可新增逐字节位图校验测试。 + * ------------------------------------------------------------------------- */ #include "../src/Drv/lcd/ascii.h" #include "test_common.h" +/* ------------------------------------------------------------------------- + * 测试入口: + * 按“命中/低位未命中/高位未命中”三个场景依次断言。 + * + * 返回值约定: + * 0 -> 测试通过 + * 非0 -> 断言失败(由 ASSERT_* 宏返回) + * ------------------------------------------------------------------------- */ int main(void) { + /* 命中用例:U+4F60(“你”)通常在当前字库中存在 */ const uint8_t* hit = utf8_hz12_get(0x4F60u); /* 你 */ + /* 低位未命中:非常低的控制区码点,预期不在中文字库 */ const uint8_t* miss_low = utf8_hz12_get(0x0001u); + /* 高位未命中:超高码点,预期不在当前 12 点阵中文表中 */ const uint8_t* miss_high = utf8_hz12_get(0x10FFFFu); + /* 命中必须返回有效指针 */ ASSERT_TRUE(hit != NULL); + /* 非命中必须返回 NULL */ ASSERT_TRUE(miss_low == NULL); ASSERT_TRUE(miss_high == NULL); return 0; diff --git a/tests/test_p0_utf8_next.c b/tests/test_p0_utf8_next.c deleted file mode 100644 index 4764017..0000000 --- a/tests/test_p0_utf8_next.c +++ /dev/null @@ -1,27 +0,0 @@ -#include "../src/Drv/lcd/lcd_text.h" -#include "test_common.h" - -int main(void) -{ - uint32_t unicode = 0; - const unsigned char ascii[] = "A"; - const unsigned char u2[] = {0xC2, 0xA2, 0x00}; /* U+00A2 */ - const unsigned char u3[] = {0xE4, 0xBD, 0xA0, 0x00}; /* U+4F60 */ - const unsigned char invalid[] = {0xF0, 0x00}; /* 4-byte start not supported */ - const unsigned char truncated2[] = {0xC2, 0x00}; - const unsigned char truncated3[] = {0xE4, 0xBD, 0x00}; - - ASSERT_EQ_INT(1, utf8_next(ascii, &unicode)); - ASSERT_EQ_U32(0x41u, unicode); - - ASSERT_EQ_INT(2, utf8_next(u2, &unicode)); - ASSERT_EQ_U32(0x00A2u, unicode); - - ASSERT_EQ_INT(3, utf8_next(u3, &unicode)); - ASSERT_EQ_U32(0x4F60u, unicode); - - ASSERT_EQ_INT(0, utf8_next(invalid, &unicode)); - ASSERT_EQ_INT(0, utf8_next(truncated2, &unicode)); - ASSERT_EQ_INT(0, utf8_next(truncated3, &unicode)); - return 0; -} diff --git a/tests/test_p1_menu.c b/tests/test_p1_menu.c index 01aa99f..db5111b 100644 --- a/tests/test_p1_menu.c +++ b/tests/test_p1_menu.c @@ -1,28 +1,47 @@ #include #include "test_common.h" -#include "../src/Drv/menu.c" + +#include "../src/Drv/menu/view/menu_layout.h" +#include "../src/Drv/menu/model/menu_tree_builder.h" + +static int noop_proc(void) +{ + return 0; +} int main(void) { + tagMenuCtrl ctrl; + tagMenuItem items[4]; uint8_t menu_num[4] = {0}; tagPMenuItem first[4] = {0}; tagPMenuItem index[4] = {0}; uint8_t max_len; - Menu_Init(); - ASSERT_TRUE(g_tMenuCtrl.by0LevelNum > 0); - ASSERT_TRUE(g_tMenuCtrl.ptHead != NULL); - ASSERT_TRUE(g_tMenuCtrl.ptCurrent != NULL); + const tagMenuModel model[4] = { + {0, "Root", "", 0, 0, 0, (FUNCPTR)noop_proc}, + {1, "设置", "", 0, 0, 0, (FUNCPTR)noop_proc}, + {2, "子项", "", 0, 0, 0, (FUNCPTR)noop_proc}, + {1, "查看", "", 0, 0, 0, (FUNCPTR)noop_proc}, + }; - ASSERT_EQ_INT(3, utf8_len_cal((uint8_t*)"ABC")); - ASSERT_EQ_INT(2, utf8_len_cal((uint8_t*)"你")); + memset(&ctrl, 0, sizeof(ctrl)); + memset(items, 0, sizeof(items)); - first[0] = g_tMenuCtrl.ptHead; - index[0] = g_tMenuCtrl.ptHead; - max_len = Menu_charLenCal(0, menu_num, first, index); + MenuTree_0LevelNumCal(&ctrl, model, 4); + ASSERT_EQ_INT(1, ctrl.by0LevelNum); + MenuTree_MainCreate(items, model, 4); + + ASSERT_EQ_INT(3, MenuLayout_Utf8LenCal((uint8_t *)"ABC")); + ASSERT_EQ_INT(2, MenuLayout_Utf8LenCal((uint8_t *)"你")); + + first[0] = &items[0]; + index[0] = &items[0]; + max_len = MenuLayout_CharLenCal(0, menu_num, first, index); ASSERT_TRUE(max_len > 0); ASSERT_TRUE(menu_num[1] > 0); + ASSERT_STREQ("设置", (const char *)items[1].byName); return 0; } diff --git a/tests/test_p1_menu_layout.c b/tests/test_p1_menu_layout.c new file mode 100644 index 0000000..e4272eb --- /dev/null +++ b/tests/test_p1_menu_layout.c @@ -0,0 +1,47 @@ +#include "test_common.h" + +#include + +#include "../src/Drv/lcd/lcd.h" +#include "../src/Drv/menu/app/menu.h" +#include "../src/Drv/menu/view/menu_layout.h" + +int main(void) +{ + tagMenuItem root; + tagMenuItem child; + MenuLayoutConfig config = { + LCD_SIZE_X, + LCD_SIZE_Y, + MENU_YMIN, + MENU_YMAX_FROM_LCD(LCD_SIZE_Y), + LINE_HEIGHT, + MENU_WITDTH, + MENU_XADD, + MENU_YADD}; + + memset(&root, 0, sizeof(root)); + memset(&child, 0, sizeof(child)); + + memcpy(root.byName, "Root", 5); + memcpy(child.byName, "Child", 6); + root.byClass = 0; + child.byClass = 1; + + root.ptLower = &child; + root.ptBehind = &root; + root.ptBefore = &root; + child.ptHigher = &root; + child.ptBehind = &child; + child.ptBefore = &child; + + ASSERT_EQ_INT(3, MenuLayout_Utf8LenCal((uint8_t *)"ABC")); + ASSERT_EQ_INT(2, MenuLayout_Utf8LenCal((uint8_t *)"你")); + + MenuLayout_PositionCal(&root, 1, &config); + ASSERT_TRUE(root.wEPosX > root.wSPosX); + ASSERT_TRUE(root.wEPosY > root.wSPosY); + ASSERT_TRUE(root.wEPosY <= LCD_SIZE_Y); + + return 0; +} diff --git a/tests/test_p1_menu_nav_legacy.c b/tests/test_p1_menu_nav_legacy.c new file mode 100644 index 0000000..6424976 --- /dev/null +++ b/tests/test_p1_menu_nav_legacy.c @@ -0,0 +1,93 @@ +#include + +#include "test_common.h" + +#include "../src/Drv/key.h" +#include "../src/Drv/menu/presenter/menu_navigator.h" + +static int g_exec_count = 0; + +static int on_exec(void) +{ + g_exec_count++; + return 0; +} + +static void build_legacy_like_tree(MenuNavState *nav, tagMenuItem *root, tagMenuItem *m1, tagMenuItem *m2, tagMenuItem *m1_sub) +{ + memset(nav, 0, sizeof(*nav)); + memset(root, 0, sizeof(*root)); + memset(m1, 0, sizeof(*m1)); + memset(m2, 0, sizeof(*m2)); + memset(m1_sub, 0, sizeof(*m1_sub)); + + root->byClass = 0; + root->ptLower = m1; + root->ptBefore = root; + root->ptBehind = root; + root->wPos = 1; + + m1->byClass = 1; + m1->wPos = 1; + m1->ptHigher = root; + m1->ptBefore = m2; + m1->ptBehind = m2; + m1->ptLower = m1_sub; + m1->pfnWinProc = on_exec; + + m2->byClass = 1; + m2->wPos = 2; + m2->ptHigher = root; + m2->ptBefore = m1; + m2->ptBehind = m1; + m2->pfnWinProc = on_exec; + + m1_sub->byClass = 2; + m1_sub->wPos = 1; + m1_sub->ptHigher = m1; + m1_sub->ptBefore = m1_sub; + m1_sub->ptBehind = m1_sub; + m1_sub->pfnWinProc = on_exec; + + nav->ptHead = root; + nav->ptCurrent = m1; + nav->ptCurBak = m1; + nav->ptRoute[0] = root; + nav->ptRoute[1] = m1; +} + +int main(void) +{ + MenuNavState nav; + tagMenuItem root; + tagMenuItem m1; + tagMenuItem m2; + tagMenuItem m1_sub; + MenuNavResult result; + + build_legacy_like_tree(&nav, &root, &m1, &m2, &m1_sub); + + result = MenuNavigator_ProcessKey(&nav, KEY_D); + ASSERT_EQ_INT(1, result.needRefresh); + ASSERT_TRUE(nav.ptCurrent == &m2); + + result = MenuNavigator_ProcessKey(&nav, KEY_U); + ASSERT_EQ_INT(1, result.needRefresh); + ASSERT_TRUE(nav.ptCurrent == &m1); + + result = MenuNavigator_ProcessKey(&nav, KEY_ENT); + ASSERT_EQ_INT(1, result.needRefresh); + ASSERT_TRUE(nav.ptCurrent == &m1_sub); + + result = MenuNavigator_ProcessKey(&nav, KEY_ESC); + ASSERT_EQ_INT(1, result.needRefresh); + ASSERT_TRUE(nav.ptCurrent == &m1); + + g_exec_count = 0; + nav.ptCurrent = &m2; + result = MenuNavigator_ProcessKey(&nav, KEY_ENT); + ASSERT_EQ_INT(0, result.needRefresh); + ASSERT_EQ_INT(1, g_exec_count); + + return 0; +} diff --git a/tests/test_p1_menu_navigator.c b/tests/test_p1_menu_navigator.c new file mode 100644 index 0000000..f06e302 --- /dev/null +++ b/tests/test_p1_menu_navigator.c @@ -0,0 +1,79 @@ +#include "test_common.h" + +#include + +#include "../src/Drv/key.h" +#include "../src/Drv/menu/presenter/menu_navigator.h" + +static int g_exec_count = 0; + +static int on_exec(void) +{ + g_exec_count++; + return 0; +} + +static void build_two_level(MenuNavState *nav, tagMenuItem *root, tagMenuItem *child_a, tagMenuItem *child_b) +{ + memset(nav, 0, sizeof(*nav)); + memset(root, 0, sizeof(*root)); + memset(child_a, 0, sizeof(*child_a)); + memset(child_b, 0, sizeof(*child_b)); + + root->byClass = 0; + root->wPos = 1; + root->ptLower = child_a; + root->ptBefore = root; + root->ptBehind = root; + + child_a->byClass = 1; + child_a->wPos = 1; + child_a->ptHigher = root; + child_a->ptBefore = child_b; + child_a->ptBehind = child_b; + child_a->pfnWinProc = on_exec; + + child_b->byClass = 1; + child_b->wPos = 2; + child_b->ptHigher = root; + child_b->ptBefore = child_a; + child_b->ptBehind = child_a; + child_b->pfnWinProc = on_exec; + + nav->ptHead = root; + nav->ptCurrent = child_a; + nav->ptCurBak = child_a; + nav->ptRoute[0] = root; + nav->ptRoute[1] = child_a; +} + +int main(void) +{ + MenuNavState nav; + tagMenuItem root; + tagMenuItem a; + tagMenuItem b; + MenuNavResult result; + + build_two_level(&nav, &root, &a, &b); + + result = MenuNavigator_ProcessKey(&nav, KEY_D); + ASSERT_EQ_INT(1, result.needRefresh); + ASSERT_TRUE(nav.ptCurrent == &b); + + result = MenuNavigator_ProcessKey(&nav, KEY_U); + ASSERT_EQ_INT(1, result.needRefresh); + ASSERT_TRUE(nav.ptCurrent == &a); + + g_exec_count = 0; + result = MenuNavigator_ProcessKey(&nav, KEY_ENT); + ASSERT_EQ_INT(0, result.needRefresh); + ASSERT_EQ_INT(1, g_exec_count); + + result = MenuNavigator_ProcessKey(&nav, KEY_ESC); + ASSERT_EQ_INT(1, result.skipRenderThisRound); + ASSERT_TRUE(nav.ptCurrent == root.ptLower); + ASSERT_TRUE(nav.ptRoute[0] == &root); + + return 0; +} diff --git a/tests/test_p1_menu_tree_builder.c b/tests/test_p1_menu_tree_builder.c new file mode 100644 index 0000000..0a0d955 --- /dev/null +++ b/tests/test_p1_menu_tree_builder.c @@ -0,0 +1,37 @@ +#include "test_common.h" + +#include + +#include "../src/Drv/menu/model/menu_tree_builder.h" + +static int noop_proc(void) +{ + return 0; +} + +int main(void) +{ + tagMenuCtrl ctrl; + tagMenuItem items[4]; + const tagMenuModel model[4] = { + {0, "Root", "", 0, 0, 0, (FUNCPTR)noop_proc}, + {1, "A", "", 0, 0, 0, (FUNCPTR)noop_proc}, + {1, "B", "", 0, 0, 0, (FUNCPTR)noop_proc}, + {2, "C", "", 0, 0, 0, (FUNCPTR)noop_proc}, + }; + + memset(&ctrl, 0, sizeof(ctrl)); + memset(items, 0, sizeof(items)); + + MenuTree_0LevelNumCal(&ctrl, model, 4); + ASSERT_EQ_INT(1, ctrl.by0LevelNum); + + MenuTree_MainCreate(items, model, 4); + ASSERT_TRUE(items[0].ptLower == &items[1]); + ASSERT_TRUE(items[1].ptBehind == &items[2]); + ASSERT_TRUE(items[2].ptBefore == &items[1]); + ASSERT_TRUE(items[2].ptLower == &items[3]); + ASSERT_TRUE(items[3].ptHigher == &items[2]); + + return 0; +} diff --git a/tests/test_p2_menu_runtime_startup.c b/tests/test_p2_menu_runtime_startup.c new file mode 100644 index 0000000..28e08c9 --- /dev/null +++ b/tests/test_p2_menu_runtime_startup.c @@ -0,0 +1,39 @@ +#include "test_common.h" + +#include "../src/Drv/key.h" +#include "../src/Drv/menu/app/menu.h" + +int main(void) +{ + int decorated_found = 0; + uint16_t itemCount = 0; + const tagMenuItem *menuItems; + + MenuApp_Init(); + Key_Init(); + menuItems = MenuApp_GetMenuItems(&itemCount); + + for (uint16_t i = 0; i < itemCount; i++) + { + if (menuItems[i].ptLower != NULL) + { + uint8_t len = 0; + while ((len < 50) && (menuItems[i].byName[len] != '\0')) + { + len++; + } + ASSERT_TRUE(len > 0); + ASSERT_EQ_INT('\x10', menuItems[i].byName[len - 1]); + decorated_found = 1; + break; + } + } + ASSERT_TRUE(decorated_found == 1); + + /* 首次路由应仅触发首帧绘制,不应崩溃 */ + MenuApp_PollInput(); + + /* 二次刷新路径也不应崩溃 */ + MenuApp_Render(); + return 0; +}