/* * 模块名称:UI Menu (菜单导航逻辑层) * 模块功能:g_menu_tab 父子索引表驱动的横向级联菜单导航——持有导航状态 * (路径/选中/深度/滚动,共 17 B)与键语义(↑↓移选、→/ENTER 进、←/ESC 退), * 单向驱动显示层 ui_menu_view 建/毁/刷新级联框;本层不持有任何 LVGL 对象 * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:王建锋 * 创建日期:2026-08-29 * 修改记录: * 2026-09-04 王建锋 重构为"横向级联框"模型(菜单页面设计.md):拆出显示层 * ui_menu_view 与数据层 ui_menu_tab;本层只留导航状态机 * 2026-09-08 王建锋 按代码规范 V1.0 补全分区注释与关键行注释;函数定义括号改 K&R 风格(同行) * 2026-09-08 王建锋 4 层容器→单一持久容器(焦点/键唯一入口;键回调深度改取 * s_depth,去掉 user_data=depth;container/focus 调用去参) */ /* 头文件包含区 */ #include #include "ui_menu.h" #include "ui_menu_view.h" #include "ui_content_page.h" #include "ui_menu_tab.h" #include "lvgl.h" #define DBG_TAG "[UI_MENU]" #include "dbg_log.h" /* 静态变量定义区 */ /* * 导航状态:唯一"必须持久"的导航历史(共 17 B,见设计 §7.2); * 条目列表/框几何均可由 g_menu_tab + 本状态随时重算,不落 RAM */ static uint8_t s_path[MENU_PAGE_MAX]; /* 路径:s_path[d]=第 d 层展示节点(s_path[0]=0 主界面) */ static uint8_t s_sel[MENU_PAGE_MAX]; /* 各层选中下标(返回时还原,选中不跳) */ static uint8_t s_depth; /* 当前深度(0=根框)= 焦点所在(最右)框 */ static uint16_t s_scroll[MENU_PAGE_MAX]; /* 各框滚动偏移(px);框销毁再进入时还原 */ /* 函数定义区 */ /* * 函数功能:移选后的层刷新(高亮 + 跟随滚动 + 滚动位回读持久化) * 入口参数:depth - 层深度 uint8_t * 返回值:无 * 限定条件:该层框已建 * 函数说明:显示层跟随滚动后,滚动位写回 s_scroll 持久(框重建时还原) */ static void menu_refresh_layer(uint8_t depth) { ui_menu_view_frame_refresh(depth, s_sel[depth]); s_scroll[depth] = ui_menu_view_get_scroll(depth); } /* * 函数功能:进入选中条目(压层) * 入口参数:child - 选中子节点下标 uint8_t * 返回值:无 * 限定条件:s_depth+1 < MENU_PAGE_MAX * 函数说明: 1. 叶子(无子项)同样压层,生成空白终点框(占位,无条目) * 2. 同节点再次进入:还原上次选中与滚动位;换节点则清零 */ static void menu_open(uint8_t child) { uint8_t d = s_depth; bool same = (s_path[d + 1u] == child); s_depth = (uint8_t)(d + 1u); s_path[d + 1u] = child; s_sel[d + 1u] = same ? s_sel[d + 1u] : 0u; ui_menu_view_frame_build(d + 1u, child, s_sel[d + 1u], (uint16_t)(same ? s_scroll[d + 1u] : 0u)); ui_menu_view_focus(); } /* * 函数功能:关闭最右框(弹层) * 入口参数:无 * 返回值:无 * 限定条件:s_depth > 0 * 函数说明: 1. 销毁最右框(框体+行数据;既有屏上框不动、无需重建) * 2. 焦点回父框;父层选中/滚动原样保留(选中不跳) */ static void menu_back(void) { ui_menu_view_frame_destroy(s_depth); s_depth--; ui_menu_view_focus(); } /* * 函数功能:查叶子节点对应的内容页(无内容页返回哨兵值) * 入口参数:node - 菜单节点下标 uint8_t * 返回值:内容页 ID content_page_id_t;无内容页 = CONTENT_PAGE_COUNT * 限定条件:无 * 函数说明:仅部分叶子有内容页(参考图 10/20/21);下标对应 ui_menu_tab.c * node 1=装置信息、3=交流量、4=直流量(表序变更时须同步本映射) */ static content_page_id_t menu_content_page_for(uint8_t node) { switch (node) { case 1u: return CONTENT_PAGE_DEVICE_INFO; /* 装置信息(L1 叶子) */ case 3u: return CONTENT_PAGE_AC; /* 交流量(实时数据子项) */ case 4u: return CONTENT_PAGE_DC; /* 直流量(实时数据子项) */ default: return (content_page_id_t)CONTENT_PAGE_COUNT; /* 无内容页 */ } } /* * 函数功能:按键语义分发(移/进/退) * 入口参数:depth - 焦点所在层深度 uint8_t * key - LVGL 键值 lv_key_t * 返回值:无 * 限定条件:ui_menu_init() 已完成 * 函数说明: 1. UP/DOWN:移选(首尾环绕);选中越出可见区由显示层跟随滚动 * 2. ENTER/RIGHT:叶子有内容页 → 切内容屏(不压层);否则进子框(叶子 → 空白终点框) * 3. ESC/LEFT:关最右框(根框不响应) * 4. 空框(终点页 0 条目)只响应返回 */ static void menu_key_handle(uint8_t depth, lv_key_t key) { uint8_t kids[MENU_ITEM_MAX]; /* 本框条目列表:由 g_menu_tab 按 parent 现算,不落 RAM */ uint8_t cnt = ui_menu_tab_children(s_path[depth], kids, MENU_ITEM_MAX); /* 条目数;0 = 空白终点框(进入叶子) */ switch (key) { case LV_KEY_UP: if (cnt > 0u) { /* 空框(0 条目)不移选;同时防对 0 取模 */ /* 移选上移、首尾环绕:先加 cnt 再减 1 保证无符号下不为负,% cnt 绕回首行 */ s_sel[depth] = (uint8_t)((s_sel[depth] + (uint8_t)cnt - 1u) % cnt); menu_refresh_layer(depth); /* 刷新高亮 + 跟随滚动 + 滚动位回读持久化 */ } break; case LV_KEY_DOWN: if (cnt > 0u) { /* 空框(0 条目)不移选;同时防对 0 取模 */ /* 移选下移、首尾环绕:% cnt 越尾自动绕回首行 */ s_sel[depth] = (uint8_t)((s_sel[depth] + 1u) % cnt); menu_refresh_layer(depth); /* 刷新高亮 + 跟随滚动 + 滚动位回读持久化 */ } break; case LV_KEY_ENTER: case LV_KEY_RIGHT: { /* 进子框:空框无条目可进;最深层(depth=MAX-1)无子层可再开 */ if (cnt > 0u) { content_page_id_t pid = menu_content_page_for(kids[s_sel[depth]]); if (pid < CONTENT_PAGE_COUNT) { /* 叶子有内容页:切内容屏(不压层,导航状态原样保留;ESC 返回即还原) */ ui_content_page_open(pid); } else if (depth < MENU_PAGE_MAX - 1u) { menu_open(kids[s_sel[depth]]); /* 压层:建新框 + 焦点移到新框(叶子 → 空白终点框) */ } } break; } case LV_KEY_ESC: case LV_KEY_LEFT: if (depth > 0u) { /* 根框(d=0)不响应返回 */ menu_back(); /* 弹层:毁最右框,焦点回父框(选中/滚动不跳) */ } break; default: break; /* 其余键值忽略 */ } } /* * 函数功能:页面容器按键事件入口(唯一入口;深度取 s_depth) * 输入参数:e - 事件指针 lv_event_t * * 返回值:无 * 限定条件:容器已加入默认输入组并获焦 * 函数说明:键值经默认输入组送达焦点容器;统一分发到 menu_key_handle * (焦点恒为持久容器,焦点框深度即 s_depth,无需 user_data 传层) */ static void menu_key_cb(lv_event_t *e) { menu_key_handle(s_depth, (lv_key_t)lv_event_get_key(e)); } /* * 函数功能:构建菜单(横向级联框) * 入口参数:无 * 返回值:0 - 成功,其他 - 失败(表自检失败/对象创建失败) * 限定条件:ui_port_init()/sim_port_init() 已成功调用,活动屏幕存在 * 函数说明: 1. 数据表自检(parent 下标均有效,捕获表内笔误) * 2. 重置导航状态 → 初始化显示层 * 3. 持久容器加入默认输入组并挂键回调(唯一入口;深度取 s_depth) * 4. 建根框(主界面 node 0 子项)并置焦点 * 5. 构建前后监控 LVGL 池内存并打印 */ int ui_menu_init(void) { /* 数据表自检:各节点 parent 下标必须有效,提前捕获表内笔误(失败即返回、不建任何对象) */ if (!ui_menu_tab_valid()) { DBG_ERROR("menu table: INVALID (parent index out of range)"); return -1; } /* 重置导航状态:各层从主界面(node 0)第 0 行开始、无滚动 */ for (uint8_t d = 0u; d < MENU_PAGE_MAX; d++) { s_path[d] = 0u; s_sel[d] = 0u; s_scroll[d] = 0u; } s_depth = 0u; /* 起始于根框 */ /* 取 LVGL 池内存基线(与构建后对照,量化菜单对象开销) */ lv_mem_monitor_t mon; lv_mem_monitor(&mon); DBG_INFO("menu build start: total=%u free=%u big=%u", (unsigned)mon.total_size, (unsigned)mon.free_size, (unsigned)mon.free_biggest_size); /* 初始化显示层:样式 + 持久容器 + 根框框体(失败 = 对象创建失败、池耗尽) */ if (ui_menu_view_init() != 0) { return -1; } /* 键接线:持久容器入默认输入组;按键一律由逻辑层处理(显示层纯渲染) */ lv_obj_t *cont = ui_menu_view_container(); if (cont == NULL) { return -1; } lv_group_add_obj(lv_group_get_default(), cont); /* 容器入组:焦点可轮到它、能收键事件 */ lv_obj_add_event_cb(cont, menu_key_cb, LV_EVENT_KEY, NULL); /* 根框:主界面(node 0)子项;建一次、持久 */ ui_menu_view_frame_build(0u, 0u, 0u, 0u); /* depth=0、node=0(主界面)、sel=0、scroll=0 */ ui_menu_view_focus(); /* 焦点置持久容器:按键进入菜单 */ /* 读构建后内存、打印增量(对照上方基线) */ lv_mem_monitor(&mon); DBG_INFO("menu build done: total=%u free=%u big=%u used=%u", (unsigned)mon.total_size, (unsigned)mon.free_size, (unsigned)mon.free_biggest_size, (unsigned)(mon.total_size - mon.free_size)); return 0; }