From 9ead5679a16eb2322954d69dfb2a7b219ba91273 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 8 Sep 2026 22:24:45 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E4=BC=98=E5=8C=96=E8=8F=9C?= =?UTF-8?q?=E5=8D=95RAM=E9=99=8D=E4=BD=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Drivers/BSP/UI/ui_menu.c | 40 +++---- Drivers/BSP/UI/ui_menu_view.c | 187 ++++++++++++++++++-------------- Drivers/BSP/UI/ui_menu_view.h | 20 ++-- docs/UI 设计/UI_界面制作流程.md | 8 +- docs/UI 设计/UI_设计架构.md | 42 +++---- docs/UI 设计/菜单页面设计.md | 64 ++++++----- simulator/main.c | 6 + 7 files changed, 195 insertions(+), 172 deletions(-) diff --git a/Drivers/BSP/UI/ui_menu.c b/Drivers/BSP/UI/ui_menu.c index 923eba53..87abd878 100644 --- a/Drivers/BSP/UI/ui_menu.c +++ b/Drivers/BSP/UI/ui_menu.c @@ -10,6 +10,8 @@ * 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 调用去参) */ /* 头文件包含区 */ @@ -69,7 +71,7 @@ static void menu_open(uint8_t child) { ui_menu_view_frame_build(d + 1u, child, s_sel[d + 1u], (uint16_t)(same ? s_scroll[d + 1u] : 0u)); - ui_menu_view_focus(d + 1u); + ui_menu_view_focus(); } /* @@ -77,13 +79,13 @@ static void menu_open(uint8_t child) { * 入口参数:无 * 返回值:无 * 限定条件:s_depth > 0 - * 函数说明: 1. 销毁最右框行对象(既有屏上框不动、无需重建) + * 函数说明: 1. 销毁最右框(框体+行数据;既有屏上框不动、无需重建) * 2. 焦点回父框;父层选中/滚动原样保留(选中不跳) */ static void menu_back(void) { ui_menu_view_frame_destroy(s_depth); s_depth--; - ui_menu_view_focus(s_depth); + ui_menu_view_focus(); } /* @@ -135,18 +137,15 @@ static void menu_key_handle(uint8_t depth, lv_key_t key) { } /* - * 函数功能:页面容器按键事件入口(每层容器各挂一个,user_data=层深度) - * 入口参数:e - 事件指针 lv_event_t * + * 函数功能:页面容器按键事件入口(唯一入口;深度取 s_depth) + * 输入参数:e - 事件指针 lv_event_t * * 返回值:无 * 限定条件:容器已加入默认输入组并获焦 * 函数说明:键值经默认输入组送达焦点容器;统一分发到 menu_key_handle + * (焦点恒为持久容器,焦点框深度即 s_depth,无需 user_data 传层) */ static void menu_key_cb(lv_event_t *e) { - uint8_t depth = (uint8_t)(uintptr_t)lv_event_get_user_data(e); - - if (depth < MENU_PAGE_MAX) { - menu_key_handle(depth, (lv_key_t)lv_event_get_key(e)); - } + menu_key_handle(s_depth, (lv_key_t)lv_event_get_key(e)); } /* @@ -156,7 +155,7 @@ static void menu_key_cb(lv_event_t *e) { * 限定条件:ui_port_init()/sim_port_init() 已成功调用,活动屏幕存在 * 函数说明: 1. 数据表自检(parent 下标均有效,捕获表内笔误) * 2. 重置导航状态 → 初始化显示层 - * 3. 各层容器加入默认输入组并挂键回调(按键由逻辑层统一处理) + * 3. 持久容器加入默认输入组并挂键回调(唯一入口;深度取 s_depth) * 4. 建根框(主界面 node 0 子项)并置焦点 * 5. 构建前后监控 LVGL 池内存并打印 */ @@ -182,25 +181,22 @@ int ui_menu_init(void) { (unsigned)mon.total_size, (unsigned)mon.free_size, (unsigned)mon.free_biggest_size); - /* 初始化显示层:样式 + 4 层持久容器 + 根框框体/装饰(失败 = 对象创建失败、池耗尽) */ + /* 初始化显示层:样式 + 持久容器 + 根框框体(失败 = 对象创建失败、池耗尽) */ if (ui_menu_view_init() != 0) { return -1; } - /* 键接线:各层容器加入默认输入组;按键一律由逻辑层处理(显示层纯渲染) */ - for (uint8_t d = 0u; d < MENU_PAGE_MAX; d++) { - lv_obj_t *cont = ui_menu_view_container(d); - if (cont == NULL) { - return -1; - } - lv_group_add_obj(lv_group_get_default(), cont); /* 容器入组:焦点可轮到它、能收键事件 */ - /* 容器挂键回调;层深度 d 装入 user_data 指针(menu_key_cb 内拆回) */ - lv_obj_add_event_cb(cont, menu_key_cb, LV_EVENT_KEY, (void *)(uintptr_t)d); + /* 键接线:持久容器入默认输入组;按键一律由逻辑层处理(显示层纯渲染) */ + 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(0u); /* 焦点置根框:按键首先进根框 */ + ui_menu_view_focus(); /* 焦点置持久容器:按键进入菜单 */ /* 读构建后内存、打印增量(对照上方基线) */ lv_mem_monitor(&mon); diff --git a/Drivers/BSP/UI/ui_menu_view.c b/Drivers/BSP/UI/ui_menu_view.c index 21eb73b6..80d1fb98 100644 --- a/Drivers/BSP/UI/ui_menu_view.c +++ b/Drivers/BSP/UI/ui_menu_view.c @@ -2,7 +2,7 @@ * 模块名称:UI Menu View (菜单显示模型渲染层) * 模块功能:以 LVGL 基元控件实现《菜单页面设计.md》§四⑧"横向级联子框"显示模型: * 右贴靠(子框左缘=父框右缘、共享边框列)+ 框宽内容自适应 + - * 顶对齐/底对齐上移;持有全部 LVGL 对象(样式/4 层容器/框体/根框装饰), + * 顶对齐/底对齐上移;持有全部 LVGL 对象(样式/持久容器/框体), * 行不是对象——框体 DRAW_MAIN 自绘(节点下标注册 + GET_SELF_SIZE 虚拟滚动); * 不持有任何导航状态——状态由逻辑层 ui_menu.c 经本接口驱动 * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) @@ -16,6 +16,9 @@ * 2026-09-08 王建锋 行对象→自绘重写:删行容器/文本/箭头 label 对象,框体 * DRAW_MAIN 自绘行(反色带/文本/▶),GET_SELF_SIZE 虚报 * 内容高实现虚拟滚动(省约 2 KB/行 对象开销) + * 2026-09-08 王建锋 4 层容器→1 个持久容器(焦点/键唯一入口;帧深度由逻辑层 + * s_depth 决定,键回调 user_data=depth 冗余);根框装饰 + * (浅 V)+状态行改容器自绘(删 3 lv_line + 1 label 对象) */ /* 头文件包含区 */ @@ -69,14 +72,11 @@ typedef struct { /* 静态变量定义区 */ -static lv_obj_t *s_cont[MENU_PAGE_MAX]; /* 4 层持久容器(每深度一个;焦点/键入口,永不删) */ +static lv_obj_t *s_cont; /* 单一持久全屏容器(焦点/键唯一入口;各层框均建其内;永不删) */ static lv_obj_t *s_frame_box[MENU_PAGE_MAX]; /* 框体:d=0 持久(根框);d>=1 进入时建、返回时毁(含自绘回调) */ static uint8_t s_row_node[MENU_PAGE_MAX][MENU_ITEM_MAX]; /* 行数据:节点下标(名称/箭头绘制时现查,无对象) */ static menu_frame_t s_frame[MENU_PAGE_MAX]; /* 每层框几何缓存:位置/尺寸/选中行(现算不落状态,供子框对齐;见上方类型注释) */ -static lv_obj_t *s_root_deco = NULL; /* 根框装饰:顶部"浅 V"水平段(两腿为临时对象,样式同源) */ -static lv_obj_t *s_root_status = NULL; /* 根框状态行 */ - /* 样式(静态,一次性初始化,常驻) */ static lv_style_t s_style_flat; /* 通用:透明、无边框/圆角/内边距(剥离主题 card) */ static lv_style_t s_style_frame; /* 框体:透明底 + 1px 白边框 */ @@ -163,6 +163,23 @@ static int32_t view_frame_width(const uint8_t *kids, uint8_t cnt) { return w + 2 * MENU_FRAME_BORDER; } +/* + * 函数功能:判断两区域是否相交(等价 lv_area_intersect 非空判断;避免依赖 LVGL 私有头) + * 入口参数:a - 区域 1 const lv_area_t * + * b - 区域 2 const lv_area_t * + * 返回值:true - 相交;false - 不相交 + * 限定条件:无 + * 函数说明:四边取 min/max 成交集,任一边为空即不相交(与 lv_area.c 同语义) + */ +static bool areas_intersect(const lv_area_t *a, const lv_area_t *b) { + int32_t x1 = (a->x1 > b->x1) ? a->x1 : b->x1; + int32_t y1 = (a->y1 > b->y1) ? a->y1 : b->y1; + int32_t x2 = (a->x2 < b->x2) ? a->x2 : b->x2; + int32_t y2 = (a->y2 < b->y2) ? a->y2 : b->y2; + + return (x1 <= x2 && y1 <= y2); +} + /* * 函数功能:框体虚拟内容高上报(滚动范围 = 内容高 - 内容区高) * 入口参数:e - 事件指针(user_data = 层深度) lv_event_t * @@ -210,7 +227,6 @@ static void view_frame_draw_cb(lv_event_t *e) { for (i = i_first; i <= i_last; i++) { int32_t row_y = area.y1 + i * MENU_ROW_H - sy; lv_area_t row_area; - lv_area_t clip; const char *name = ui_menu_tab_name(s_row_node[depth][i]); bool is_sel = (i == (int32_t)f->sel); lv_color_t color = is_sel ? lv_color_black() : lv_color_white(); @@ -220,13 +236,9 @@ static void view_frame_draw_cb(lv_event_t *e) { row_area.y1 = row_y; row_area.y2 = row_y + MENU_ROW_H - 1; - /* 行 ∩ 重绘区(min/max 裁剪,与 lv_area_intersect 等价;空则跳过 culling) */ - clip.x1 = row_area.x1 > layer->_clip_area.x1 ? row_area.x1 : layer->_clip_area.x1; - clip.y1 = row_area.y1 > layer->_clip_area.y1 ? row_area.y1 : layer->_clip_area.y1; - clip.x2 = row_area.x2 < layer->_clip_area.x2 ? row_area.x2 : layer->_clip_area.x2; - clip.y2 = row_area.y2 < layer->_clip_area.y2 ? row_area.y2 : layer->_clip_area.y2; - if (clip.x1 > clip.x2 || clip.y1 > clip.y2) { - continue; /* 行不在重绘区:跳过(culling) */ + /* 行不在重绘区:跳过(culling) */ + if (!areas_intersect(&row_area, &layer->_clip_area)) { + continue; } /* 选中行:整行反色(白带) */ @@ -383,7 +395,7 @@ void ui_menu_view_frame_build(uint8_t depth, uint8_t node, uint8_t sel, uint16_t f->w = (cnt > 0u) ? view_frame_width(kids, cnt) : (2 * MENU_FRAME_BORDER + MENU_ROW_PAD_L + MENU_ROW_PAD_R); - box = lv_obj_create(s_cont[depth]); + box = lv_obj_create(s_cont); if (box == NULL) { return; /* LVGL 池耗尽:框未建,状态回滚 */ } @@ -498,53 +510,93 @@ uint16_t ui_menu_view_get_scroll(uint8_t depth) { } /* - * 函数功能:焦点移至指定深度的容器 - * 入口参数:depth - 深度 uint8_t + * 函数功能:焦点置于持久容器 + * 入口参数:无 * 返回值:无 * 限定条件:ui_menu_view_init() 已完成且容器已加入输入组 - * 函数说明:焦点=最右框;进/退时由逻辑层调用 + * 函数说明:焦点=唯一键入口(框深度由逻辑层 s_depth 决定); + * 进/退时由逻辑层调用(防御性:保证容器恒为焦点对象) */ -void ui_menu_view_focus(uint8_t depth) { - if (depth < MENU_PAGE_MAX && s_cont[depth] != NULL) { - /* 焦点移到目标深度容器 */ - lv_group_focus_obj(s_cont[depth]); +void ui_menu_view_focus(void) { + if (s_cont != NULL) { + lv_group_focus_obj(s_cont); } } /* - * 函数功能:取指定深度的持久容器 - * 入口参数:depth - 深度 0..MENU_PAGE_MAX-1 uint8_t - * 返回值:容器对象 lv_obj_t * 越界返回 NULL + * 函数功能:取持久容器 + * 入口参数:无 + * 返回值:容器对象 lv_obj_t * 未初始化返回 NULL * 限定条件:ui_menu_view_init() 已完成 * 函数说明:逻辑层在容器上绑定输入组与键事件(显示层不碰输入) */ -lv_obj_t *ui_menu_view_container(uint8_t depth) { - return (depth < MENU_PAGE_MAX) ? s_cont[depth] : NULL; +lv_obj_t *ui_menu_view_container(void) { + return s_cont; } /* - * 函数功能:应用主界面顶部"浅 V"装饰线段的公共样式 - * 入口参数:line - lv_line 对象;width - 线宽(px) - * 返回值:无 - * 限定条件:line 已创建且已设点集 - * 函数说明:绝对定位于 (0,0)、忽略布局、白色实线、指定线宽 + * 根框装饰(浅 V)点集与自绘区域:容器 DRAW_MAIN 自绘(无对象); + * culling 区域覆盖原 lv_line 对象范围((0,0) 处 160×20)与状态行文本区 */ -static void view_deco_line_apply(lv_obj_t *line, int32_t width) { - lv_obj_add_flag(line, LV_OBJ_FLAG_IGNORE_LAYOUT); - lv_obj_set_pos(line, 0, 0); - lv_obj_set_size(line, MENU_SCREEN_W, 20); - lv_obj_set_style_line_width(line, width, 0); - lv_obj_set_style_line_color(line, lv_color_white(), 0); - lv_obj_set_style_line_opa(line, LV_OPA_COVER, 0); +static const lv_point_precise_t s_deco_leg_l[2] = { {7, 7}, {16, 17} }; /* 左腿:自顶端斜下 */ +static const lv_point_precise_t s_deco_hoz[2] = { {16, 17}, {145, 17} }; /* 水平线:接两腿底端 */ +static const lv_point_precise_t s_deco_leg_r[2] = { {158, 7}, {145, 17} }; /* 右腿 */ +static const lv_point_precise_t *const s_deco_segs[3] = {s_deco_leg_l, s_deco_leg_r, s_deco_hoz}; +static const int32_t s_deco_seg_w[3] = {MENU_DECO_LEG_W, MENU_DECO_LEG_W, MENU_DECO_HORIZ_W}; + +static const lv_area_t s_deco_band = {0, 0, MENU_SCREEN_W - 1, 19}; /* 浅 V culling 区(与原线段对象范围一致) */ +static const lv_area_t s_status_area = {MENU_ROOT_STATUS_X, MENU_ROOT_STATUS_Y, 130, 33}; /* 状态行 culling 区(文本约 104px 宽 + 余量) */ + +/* + * 函数功能:自绘根框装饰 + 状态行(容器 DRAW_MAIN;替代旧 4 个 LVGL 对象) + * 入口参数:e - 事件指针 lv_event_t * + * 返回值:无 + * 限定条件:事件为 LV_EVENT_DRAW_MAIN,容器已建 + * 函数说明: 1. 浅 V:两腿 3px 粗、水平线 2px 细(1bpp 斜线边缘易被阈值吃掉,腿须比水平线粗) + * 2. 绘制次序 = 旧对象创建序(左腿、右腿、水平线),重叠像素与旧渲染一致 + * 3. 状态行:静态字符串、白色,与旧 label 同字体同位置 + * 4. 重绘区不覆盖对应 culling 区则跳过(culling) + */ +static void view_deco_draw_cb(lv_event_t *e) { + lv_layer_t *layer = lv_event_get_layer(e); + + /* "浅 V"装饰:3 段线(线宽各异,逐段画) */ + if (areas_intersect(&s_deco_band, &layer->_clip_area)) { + for (uint8_t i = 0; i < 3; i++) { + lv_draw_line_dsc_t ld; + + lv_draw_line_dsc_init(&ld); + ld.base.layer = layer; + ld.color = lv_color_white(); + ld.width = s_deco_seg_w[i]; + ld.p1 = s_deco_segs[i][0]; + ld.p2 = s_deco_segs[i][1]; + lv_draw_line(layer, &ld); + } + } + + /* 状态行:当前模式(静态文本) */ + if (areas_intersect(&s_status_area, &layer->_clip_area)) { + lv_draw_label_dsc_t sd; + + lv_draw_label_dsc_init(&sd); + sd.base.layer = layer; + sd.text = "当前模式 : 无模式"; + sd.text_static = 1; /* Flash 字符串:不拷内存 */ + sd.color = lv_color_white(); + sd.ofs_x = 0; /* 区域原点即状态行左上 (17,20) */ + sd.ofs_y = 0; + lv_draw_label(layer, &sd, &s_status_area); + } } /* - * 函数功能:初始化显示层(样式 + 4 层持久容器 + 根框装饰 + 根框框体) + * 函数功能:初始化显示层(样式 + 持久容器 + 根框框体;装饰/状态行自绘) * 入口参数:无 * 返回值:0 - 成功,其他 - 失败(对象创建失败) * 限定条件:LVGL 已初始化且活动屏幕存在(ui_port_init / sim_port_init 已完成) - * 函数说明:1. 4 层容器每深度一个、全屏覆盖、持久不删(焦点/键入口,防悬空) - * 2. 根框装饰(浅 V + 状态行)与根框框体只属根层(d=0) + * 函数说明:1. 持久容器:全屏覆盖、持久不删(焦点/键唯一入口;各层框均建其内) + * 2. 根框装饰(浅 V + 状态行)由容器自绘(无对象);根框框体持久 * 3. 行数据由逻辑层经 ui_menu_view_frame_build 注册,框体自绘行 */ int ui_menu_view_init(void) { @@ -571,49 +623,18 @@ int ui_menu_view_init(void) { lv_obj_add_style(menu, &s_style_flat, 0); /* 透明、无边框:纯定位层,不可见 */ lv_obj_set_size(menu, sw, sh); - /* 4 层持久容器:每深度一个、全屏覆盖、永不删(焦点/键入口,防悬空) */ - for (uint8_t d = 0; d < MENU_PAGE_MAX; d++) { - s_cont[d] = lv_obj_create(menu); - if (s_cont[d] == NULL) { - return -1; - } - lv_obj_add_style(s_cont[d], &s_style_flat, 0); /* 透明、无边框 */ - lv_obj_add_flag(s_cont[d], LV_OBJ_FLAG_IGNORE_LAYOUT); /* 绝对定位,不参与父级 flex 布局 */ - lv_obj_set_pos(s_cont[d], 0, 0); - lv_obj_set_size(s_cont[d], sw, sh); /* 全屏覆盖 */ - lv_obj_add_flag(s_cont[d], LV_OBJ_FLAG_CLICK_FOCUSABLE); /* 可获焦:键事件入口 */ - } - - /* 根框装饰:顶部"开口浅 V"(两腿加粗、水平线细)+ 状态行(绝对定位于 d=0 容器) - * 1bpp 下斜线边缘易被阈值吃掉,腿须比水平线粗才接近参考图 */ - { - static const lv_point_precise_t deco_leg_l[] = { {7, 7}, {16, 17} }; /* 左腿:自顶端斜下 */ - static const lv_point_precise_t deco_hoz[] = { {16, 17}, {145, 17} }; /* 水平线:接两腿底端 */ - static const lv_point_precise_t deco_leg_r[] = { {158, 7}, {145, 17} }; /* 右腿 */ - - lv_obj_t *leg_l = lv_line_create(s_cont[0]); - lv_obj_t *leg_r = lv_line_create(s_cont[0]); - s_root_deco = lv_line_create(s_cont[0]); /* 仅水平线留存句柄;两腿建后无需再引用 */ - if (leg_l == NULL || leg_r == NULL || s_root_deco == NULL) { - return -1; - } - lv_line_set_points(leg_l, deco_leg_l, 2); - lv_line_set_points(leg_r, deco_leg_r, 2); - lv_line_set_points(s_root_deco, deco_hoz, 2); - view_deco_line_apply(leg_l, MENU_DECO_LEG_W); /* 腿:3px 粗 */ - view_deco_line_apply(leg_r, MENU_DECO_LEG_W); - view_deco_line_apply(s_root_deco, MENU_DECO_HORIZ_W); /* 水平线:2px 细 */ - } - - /* 状态行:当前模式(顶部区域,根框上方) */ - s_root_status = lv_label_create(s_cont[0]); - if (s_root_status == NULL) { + /* 持久容器:全屏覆盖、永不删(焦点/键唯一入口;各层框均建其内,防悬空) */ + s_cont = lv_obj_create(menu); + if (s_cont == NULL) { return -1; } - lv_obj_add_style(s_root_status, &s_style_flat, 0); - lv_obj_add_flag(s_root_status, LV_OBJ_FLAG_IGNORE_LAYOUT); - lv_obj_set_pos(s_root_status, MENU_ROOT_STATUS_X, MENU_ROOT_STATUS_Y); - lv_label_set_text_static(s_root_status, "当前模式 : 无模式"); /* 静态字符串,不拷内存 */ + lv_obj_add_style(s_cont, &s_style_flat, 0); /* 透明、无边框 */ + lv_obj_add_flag(s_cont, LV_OBJ_FLAG_IGNORE_LAYOUT); /* 绝对定位,不参与父级 flex 布局 */ + lv_obj_set_pos(s_cont, 0, 0); + lv_obj_set_size(s_cont, sw, sh); /* 全屏覆盖 */ + lv_obj_add_flag(s_cont, LV_OBJ_FLAG_CLICK_FOCUSABLE); /* 可获焦:键事件入口 */ + /* 根框装饰(浅 V + 状态行):容器自绘(无对象),回调仅此处挂一次 */ + lv_obj_add_event_cb(s_cont, view_deco_draw_cb, LV_EVENT_DRAW_MAIN, NULL); /* 根框框体:持久;宽 = 内容自适应(主界面 node 0 的子项) */ uint8_t kids[MENU_ITEM_MAX]; @@ -621,7 +642,7 @@ int ui_menu_view_init(void) { int32_t w = view_frame_width(kids, cnt); /* 宽 = 最宽行内容 + 左右边框(当前 66) */ int32_t h = (int32_t)cnt * MENU_ROW_H + 2 * MENU_FRAME_BORDER; /* 高 = 行数×行高 + 上下边框(当前 107) */ - s_frame_box[0] = lv_obj_create(s_cont[0]); + s_frame_box[0] = lv_obj_create(s_cont); if (s_frame_box[0] == NULL) { return -1; } diff --git a/Drivers/BSP/UI/ui_menu_view.h b/Drivers/BSP/UI/ui_menu_view.h index 320ac41a..5e354b26 100644 --- a/Drivers/BSP/UI/ui_menu_view.h +++ b/Drivers/BSP/UI/ui_menu_view.h @@ -2,7 +2,7 @@ * 模块名称:UI Menu View (菜单显示模型渲染层) * 模块功能:以 LVGL 基元控件实现《菜单页面设计.md》§四⑧"横向级联子框"显示模型: * 右贴靠(子框左缘=父框右缘、共享边框列)+ 框宽内容自适应 + - * 顶对齐/底对齐上移;持有全部 LVGL 对象(样式/4 层容器/框/根框装饰), + * 顶对齐/底对齐上移;持有全部 LVGL 对象(样式/持久容器/框), * 不持有任何导航状态——状态由逻辑层 ui_menu.c 经本接口驱动 * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:王建锋 @@ -10,6 +10,8 @@ * 修改记录: * 2026-09-04 王建锋 创建:自旧 ui_menu.c 单页模型拆分出显示层(高内聚低耦合) * 2026-09-08 王建锋 行改自绘(节点下标注册 + 框体 DRAW_MAIN 自绘 + 虚拟滚动),接口不变 + * 2026-09-08 王建锋 4 层容器→单一持久容器(container/focus 去 depth 参数); + * 根框装饰/状态行改容器自绘 */ #ifndef UI_MENU_VIEW_H #define UI_MENU_VIEW_H @@ -37,13 +39,13 @@ int ui_menu_view_init(void); /* - * 函数功能:取指定深度的持久容器 - * 入口参数:depth - 深度 0..MENU_PAGE_MAX-1 uint8_t - * 返回值:容器对象 lv_obj_t * 越界返回 NULL + * 函数功能:取持久容器 + * 入口参数:无 + * 返回值:容器对象 lv_obj_t * 未初始化返回 NULL * 限定条件:ui_menu_view_init() 已完成 * 函数说明:逻辑层在容器上绑定输入组与键事件(显示层不碰输入) */ -lv_obj_t *ui_menu_view_container(uint8_t depth); +lv_obj_t *ui_menu_view_container(void); /* * 函数功能:构建指定深度的框(几何现算 + 行数据注册 + 选中/滚动应用) @@ -99,12 +101,12 @@ void ui_menu_view_set_scroll(uint8_t depth, uint16_t scroll); uint16_t ui_menu_view_get_scroll(uint8_t depth); /* - * 函数功能:焦点移至指定深度的容器 - * 入口参数:depth - 深度 uint8_t + * 函数功能:焦点置于持久容器 + * 入口参数:无 * 返回值:无 * 限定条件:ui_menu_view_init() 已完成且容器已加入输入组 - * 函数说明:焦点=最右框;进/退时由逻辑层调用 + * 函数说明:焦点=唯一键入口(框深度由逻辑层 s_depth 决定);进/退时由逻辑层调用 */ -void ui_menu_view_focus(uint8_t depth); +void ui_menu_view_focus(void); #endif /* UI_MENU_VIEW_H */ diff --git a/docs/UI 设计/UI_界面制作流程.md b/docs/UI 设计/UI_界面制作流程.md index e61d2a59..71a6c614 100644 --- a/docs/UI 设计/UI_界面制作流程.md +++ b/docs/UI 设计/UI_界面制作流程.md @@ -204,9 +204,9 @@ flowchart TD ### 5.3 翻译纪律 1. **逐参数翻译**:代码中每个 px 值必须能在样机"布局参数表"中找到出处;禁止"看着办" -2. 样式沿用现有 `s_style_flat` / `s_style_selected` 模式(静态样式、一次性初始化、后加优先覆盖主题) +2. 样式沿用现有 `s_style_flat` 模式(静态样式、一次性初始化、后加优先覆盖主题) 3. 焦点对象恒为**页面容器**;条目不入输入组(架构 §5.4) -4. 容器**永不删除**(indev 释放事件 use-after-free,架构 §5.2);只建/毁条目 +4. 容器**永不删除**(indev 释放事件 use-after-free,架构 §5.2);只建/毁框体(行自绘、无对象) 5. 编码纪律(C6):Edit 工具精确替换;文件保持 UTF-8 BOM 不变 6. 注释按代码规范 V1.0:文件头模块块 + 函数头块(功能/参数/返回/限定/说明) @@ -301,7 +301,7 @@ flowchart TD 1. **试点先行**:01 主界面走完全流程,校准各阶段耗时、翻译损耗点、内存基线(试点已完成 P1,后续阶段数据回填本文档) 2. **按界面族批量**: - - 列表页(复用现有条目行架构)→ 数据显示页(数值行框架)→ 参数编辑页(编辑/确认交互)→ 曲线/状态页 + - 列表页(复用现有级联框自绘架构)→ 数据显示页(数值行框架)→ 参数编辑页(编辑/确认交互)→ 曲线/状态页 - 每族先做**族内第一屏**产出可复用的页面框架,再批量铺同族界面 3. **每批收尾**:字体统一重生成(各屏字符集**并集**,一次生成)→ 全量回归(已完成界面重跑 @test) 4. 字符集只增不减;字体文件每次**全量重生成**,禁止手工拼合 @@ -331,7 +331,7 @@ flowchart TD | 字体缺字形(如宋体无 ▶) | 该字符显示成方框 □ | `make_font.py` 已内置 `merge_arrow_glyph()` 自动补回;其余缺字形同法(从有该字的字体提取位图合并,§5.4) | | 新 .c 只注册了 Keil(或只改了 CMake) | 单端编译缺函数/静默缺失 | 双端注册(C9) | | 模拟器主循环调 `SDL_PollEvent` | 键盘全部无响应 | 事件只由 LVGL SDL 驱动内部轮询(架构 §5.7) | -| 选中行"白底白字" | 选中文字不可见 | 条目行用 label 自身携带 CHECKED 态(架构 §5.5) | +| 选中行"白底白字" | 选中文字不可见 | 历史坑(2026-09-02):子 label 自身 card 样式总命中、继承不到父选中色;现行行自绘(绘制时给色、无样式继承)已根除(架构 §5.5) | | 按键处理里删除焦点容器 | use-after-free 崩溃 | 容器持久不删(架构 §5.2) | | LVGL 池爆 40KB | 对象创建失败/画面残缺 | §5.5 预算 + 按需建毁 | | 极性配反 | 黑屏/白屏/不可读 | 模拟器 mono 主题 `dark_mode` 参数 + MCU 端背景色一致 | diff --git a/docs/UI 设计/UI_设计架构.md b/docs/UI 设计/UI_设计架构.md index 8579f729..2d1594f0 100644 --- a/docs/UI 设计/UI_设计架构.md +++ b/docs/UI 设计/UI_设计架构.md @@ -131,7 +131,7 @@ SDL 键盘 → LVGL SDL 驱动内部 5ms 定时器(sdl_event_handler) | 层 | 文件 | 职责 | 持 LVGL 对象? | |---|---|---|---| | 数据层 | `ui_menu_tab.c/h`(生成物) | `g_menu_tab[]` 父子索引表 + 查询 API(`children/has_children/name/valid`) | 否 | -| 显示层 | `ui_menu_view.c/h` | 级联框渲染:样式 / 4 层容器 / 框体 / 行 / 根框装饰;几何现算按层缓存 | 是(全部) | +| 显示层 | `ui_menu_view.c/h` | 级联框渲染:样式 / 持久容器 / 框体(行与根框装饰、状态行均自绘、无对象);几何现算按层缓存 | 是(全部) | | 逻辑层 | `ui_menu.c/h` | 导航状态(`s_path/s_sel/s_depth/s_scroll`,共 17 B)+ 键语义;单向驱动显示层建/毁/刷新 | 否 | - 依赖方向:逻辑 → 显示,两者都用数据查询 API;显示层无导航状态、逻辑层无 LVGL 对象 @@ -148,13 +148,12 @@ SDL 键盘 → LVGL SDL 驱动内部 5ms 定时器(sdl_event_handler) ``` screen └─ menu(铺满屏幕,flat) - ├─ cont[0] 全屏容器(depth 0):根框装饰"浅 V" + 状态行 + 根框体(持久) - ├─ cont[1] 全屏容器(depth 1):子框体进入时建、返回时毁 - ├─ cont[2] 全屏容器(depth 2) - └─ cont[3] 全屏容器(depth 3) + └─ cont 全屏持久容器(唯一焦点/键入口;各层框体均建其内,永不删) + ├─ 根框体(持久)+ 根框装饰"浅 V" + 状态行(容器自绘,无对象) + └─ 子框体进入时建、返回时毁 ``` -- 4 层容器**持久存在**永不删(焦点/键入口,见 §5.2);子对象全部绝对定位(级联框须同屏叠加,层间不用 flex 堆叠) +- 持久容器**永不删**(唯一焦点/键入口,见 §5.2);子对象全部绝对定位(级联框须同屏叠加,层间不用 flex 堆叠) - 框体 = `lv_obj`(透明底 + 1px 白边框 + 自动滚动条),行在框体内自绘(无子对象) ### 4.4 条目行与选中态 @@ -168,12 +167,12 @@ screen ``` ENTER/→ 进子框: 现算几何(x = 父框右缘 − 1 共享边框列;y = 父"选中行"行顶,超屏底则上移使本框底对齐该行行底) - → cont[depth+1] 建框体 + 注册行数据 → 焦点移至新容器 + → cont 建框体 + 注册行数据(框深度由逻辑层 s_depth 决定,焦点恒在持久容器) ←/Esc 返回: - 销毁最右框(框体连自绘回调整体删除,容器保留)→ 焦点回父容器;父层选中/滚动位原样保留 + 销毁最右框(框体连自绘回调整体删除,容器保留)→ 焦点恒在持久容器;父层选中/滚动位原样保留 ``` -- **容器持久、框动态**:行自绘无对象,池内存 = 屏上框体 + 装饰(实测根页约 1~2 KB,远低于 40KB 池) +- **容器持久、框动态**:行自绘无对象,池内存 = 屏上框体(实测根框 4872 B、最深四级级联 6128 B,远低于 40KB 池) - 同节点再次进入:还原该框上次选中与滚动位(`s_sel/s_scroll`);换节点则清零 - 叶子节点(无子项):ENTER 建空白终点框(w=6、h=2 占位),仅 ESC 可返回 - 深度保护:depth+1 ≥ 4 不再下钻 @@ -197,27 +196,27 @@ ENTER/→ 进子框: - keypad 输入设备在**按下时**捕获焦点对象,**释放时**向其回发 RELEASED/CLICKED - 若在按键处理器内删除焦点对象(如"返回时销毁当前页"),释放事件将落在已释放内存上 → use-after-free -- 容器永不删除,回发事件始终落在存活对象上,天然安全;要释放的只是容器内的条目 +- 容器永不删除,回发事件始终落在存活对象上,天然安全;要释放的只是容器内的框体(行自绘、无对象) ### 5.3 框按需建/销 - LVGL 池仅 40KB(`LV_MEM_SIZE`),全量预建 42 个节点的所有页面必然爆内存 -- 只建"当前路径"上的框:内存 = 屏上各框行数之和(实测根页约 13KB,最坏级联远低于池上限) +- 只建"当前路径"上的框:行自绘无对象,池内存 = 屏上框体(实测根框 used=4872 B、最深四级级联 used=6128 B,远低于 40KB 池) - 返回即 `lv_obj_delete` 释放,内存随时回落到根框水位 -### 5.4 容器是唯一焦点对象 +### 5.4 持久容器是唯一焦点对象 -- v9 输入组不自动处理方向键(无默认键回调),焦点移动完全由焦点对象自己实现 -- 容器集中处理按键:逻辑单点、行为可预测、无多对象焦点争抢 -- 条目行不入组:行只负责"显示选中态",不参与焦点/点击,避免行与容器抢键 +- v9 输入组不自动处理方向键(无默认键回调),框深度无需由焦点对象携带 +- 持久容器为组内唯一焦点对象(初始化时入输入组并获焦,永不删、焦点永不迁移);容器集中处理按键:逻辑单点、行为可预测、无多对象焦点争抢 +- 框深度由逻辑层 `s_depth` 决定(4 容器时代的 `user_data=depth` 已冗余,去掉) +- 框体(含自绘行)不入组:纯展示,不参与焦点/点击,避免与容器抢键 -### 5.5 条目行用 label 而非 button+子 label +### 5.5 条目行自绘而非对象化 -- 样式解析规则(`lv_obj_style.c`):**先查对象自身样式**,查不到才向父对象继承(继承时用**父对象**的状态) -- 子 label 自带主题 card 样式(文字色固定为白),自身样式总能命中 → 永远继承不到 - 父按钮的选中态文字色 → 选中行"白底白字",文字不可见(2026-09-02 实际踩坑) -- label 行**自身**携带 CHECKED 态:选中样式(白底黑字)对自身精确匹配,直接生效 -- 行不需要按钮语义(按压反馈/点击):输入由容器统一处理,行是纯展示 +- 每行对象有"机制税":`lv_obj_t` 结构体 + 样式向量 + 子 label(文本/箭头)状态,约 2 KB/行;屏上 2~3 框 × 多行会逼近 40 KB 池 → 行降级为**数据**(节点下标 `s_row_node[4][16]`,1 B/行) +- 框体 `LV_EVENT_DRAW_MAIN` 自绘:选中行整行白带(`lv_draw_rect`),每行文本/箭头经 `lv_draw_label`(与 label 同字体光栅化路径,像素级一致) +- 行不需要对象语义(焦点/点击/按压反馈):输入由容器统一处理,行是纯展示 +- 自绘同时根除了旧对象模型的两类问题:样式继承"白底白字"坑(2026-09-02 实际踩坑)、行对象池内存膨胀 ### 5.6 flat 样式剥离主题 card @@ -241,3 +240,4 @@ ENTER/→ 进子框: | 2026-09-02 | 菜单重构:弃用 lv_menu → 基元控件页面导航(§4/§5);字体 83→78 字(补「返/回」);修复模拟器主循环吞事件致键盘失效(§5.7);修复选中行白底白字(§5.5) | | 2026-09-03 | 关键约束 7:`LV_DRAW_SW_COMPLEX` 须=1(顶部"浅 V"斜线依赖);两端 lv_conf 显式固化,`LV_DRAW_SW_CIRCLE_CACHE_SIZE`=4 防零长数组 | | 2026-09-04 | 菜单重构为"横向级联框"模型(见 `菜单页面设计.md` §四⑧):数据/显示/逻辑三层拆分(新增 `ui_menu_view` 显示层、`ui_menu_tab` 生成物,`ui_menu` 瘦身为纯状态机);字体重生成补缺字形(78 字,含菜单树新增的整/控/软/板等);模拟器加 `--keys ` 无头注键截图验证 | +| 2026-09-08 | 菜单 RAM 收敛两连:行对象→框体自绘(行降为数据,§4.4/§5.5);4 层容器→单一持久容器(焦点恒在其上,框深度由逻辑层 `s_depth` 决定,§4.3/§5.4)+ 根框装饰/状态行容器自绘(无对象);LVGL 池实测 根框 4872 B / 最深四级级联 6128 B | diff --git a/docs/UI 设计/菜单页面设计.md b/docs/UI 设计/菜单页面设计.md index ae98d5d3..153e713f 100644 --- a/docs/UI 设计/菜单页面设计.md +++ b/docs/UI 设计/菜单页面设计.md @@ -75,8 +75,8 @@ sequenceDiagram participant V as 显示层 ui_menu_view participant T as 数据层 ui_menu_tab K->>G: 键码 - Note over G: 焦点 = 最右(最深)框容器 s_cont[d](同组 4 容器,仅最右获焦) - G->>L: LV_EVENT_KEY → menu_key_cb(user_data=depth)→ menu_key_handle + Note over G: 焦点 = 持久容器 s_cont(唯一键入口;帧深度由逻辑层 s_depth 决定) + G->>L: LV_EVENT_KEY → menu_key_cb(深度=s_depth)→ menu_key_handle L->>L: 状态变更:移动 s_sel[d] / 进入 s_depth+1+s_path / 退出 s_depth−1 L->>V: frame_refresh(高亮+跟滚)/ frame_build(建新框)/ frame_destroy(拆最右框)+ focus(移焦点) V->>T: children / has_children / name(条目现算不落盘) @@ -106,15 +106,15 @@ sequenceDiagram T-->>L: 通过 end L->>L: 清零 17 B 导航状态(s_path/s_sel/s_scroll=0,s_depth=0) - L->>V: ui_menu_view_init()(样式 → 屏幕根容器 → 4 层容器 → 根框装饰 + 状态行 → 根框本体) - L->>V: 各层容器 lv_group_add_obj(默认组)+ 挂 menu_key_cb(user_data=depth) - L->>V: ui_menu_view_frame_build(0) 建根框行 + focus(0) + L->>V: ui_menu_view_init()(样式 → 屏幕根容器 → 持久容器(装饰/状态行自绘回调) → 根框本体(含自绘回调)) + L->>V: 持久容器 lv_group_add_obj(默认组)+ 挂 menu_key_cb(深度取 s_depth) + L->>V: ui_menu_view_frame_build(0) 注册根框行数据 + focus() Note over L: lv_mem_monitor 打印建前/建后 LVGL 池 L-->>U: 0(成功) ``` - 日志标记:`UI menu init: OK`(uiTask 打印)——`@test` 回归可作 Required 标记。 -- LVGL 池实测(模拟器 40 KB 池):根框建完 `used=13312 B`;最深三级级联增量远小于池余量。 +- LVGL 池实测(模拟器 40 KB 池):根框建完 `used=4872 B`(旧行对象实现为 13312 B,自绘 + 容器收敛省 8.2 KB);最深四级级联 `used=6128 B`(增量 1256 B = 3 个子框体),远小于池余量。 ### 1.6 层间不变量(每层"绝不做") @@ -146,7 +146,7 @@ sequenceDiagram | **菜单节点** | 行尾 **▶** 箭头 | `ENTER` 进入 → 展开下一级子页 | | **叶子节点** | 无箭头 | `ENTER` 进入 → **终点**(本阶段仅占位,内容页后续设计) | -> 根页默认选中**第 1 项"装置信息"**(整行反色白底黑字)。 +> 根页默认选中**第 1 项"装置信息"**(整行反色:白带、黑字)。 ### 2.2 完整层级树 @@ -369,8 +369,7 @@ bool ui_menu_tab_valid(void); /* uint8_t kids[MENU_ITEM_MAX]; uint8_t cnt = ui_menu_tab_children(node, kids, MENU_ITEM_MAX); /* node = s_path[depth] */ for (uint8_t i = 0; i < cnt; i++) { - /* 在框体内建一行:文本 = ui_menu_tab_name(kids[i]); - 箭头 = ui_menu_tab_has_children(kids[i])(有子项才带 ▶) */ + /* 注册一行:s_row_node[depth][i] = kids[i](框体自绘时现查名称/箭头) */ } ``` @@ -456,19 +455,19 @@ python tools/menu_gen/gen_menu.py ### 4.4 选中态与焦点 -- **选中态**:整行反色(白底黑字);菜单行有 **▶**,叶子行无;**选中行保留 ▶**(箭头随反色变黑,参考图 02/04 实测);非选中行透明底白字。 +- **选中态**:整行反色(白带、黑字);菜单行有 **▶**,叶子行无;**选中行保留 ▶**(箭头随反色变黑,参考图 02/04 实测);非选中行黑底白字(白箭头)。 - **焦点** = 最右(最深)框;框无页头/返回按钮(框即紧凑列表,返回靠 `←`);仅根框带主界面装饰(顶部"浅 V" + 状态行"当前模式 : 无模式")。 --- ## 五、显示层:ui_menu_view(持有全部 LVGL 对象) -> **一句话职责**:按"状态 + 数据表"渲染级联框——持有**全部** LVGL 对象(样式 / 4 层容器 / 框体 / 行 / 箭头 / 根框装饰),由逻辑层经 8 个接口(§5.5)驱动建/毁/刷新。 +> **一句话职责**:按"状态 + 数据表"渲染级联框——持有**全部** LVGL 对象(样式 / 持久容器 / 框体;根框装饰与状态行由容器自绘、无对象),由逻辑层经 8 个接口(§5.5)驱动建/毁/刷新。行不是对象,由框体自绘。 > **绝不做**:持有导航状态(17 B 在逻辑层);解释按键(输入回调挂在逻辑层)。 ### 5.1 职责边界 -- 持有全部 LVGL 对象(样式/容器/框体/行/箭头/根框装饰) +- 持有全部 LVGL 对象(样式/持久容器/框体);根框装饰与状态行由容器自绘(无对象) - 按 `frame_build/destroy/refresh` 调用建/毁/刷新(依据"状态 + 数据表") - 现算框几何(§5.3 公式)并按层缓存(`s_frame[]`) - 提供滚动位读写(`set_scroll` / `get_scroll`)供逻辑层持久化 @@ -490,7 +489,7 @@ python tools/menu_gen/gen_menu.py ### 5.3 几何计算(`ui_menu_view_frame_build()`) -`frame_build` 拿到 (depth, node, sel, scroll) 后,先**算三个数**——框宽 w、位置 (x, y)、框高 h——再照数建对象。四步各配一格图(下图 ①~④): +`frame_build` 拿到 (depth, node, sel, scroll) 后,先**算三个数**——框宽 w、位置 (x, y)、框高 h——再照数建框体、注册行数据、应用选中/滚动。四步各配一格图(下图 ①~④): ![级联框几何计算四步:① 行剖面定框宽 ② X 贴父框右缘 ③ Y 顶对齐选中行 ④ Y 溢出上移](images/级联框几何计算.svg) @@ -546,26 +545,26 @@ y + h > 160 → y = 选中行行顶 + ROW_H − h;y < 0 → y = 0(截断/框 ### 5.4 对象模型 -- 屏幕根容器(全屏 flat)→ 4 层**持久全屏容器** `s_cont[0..3]`(每深度一个,`LV_OBJ_FLAG_IGNORE_LAYOUT` + 绝对定位,透明,永不删;级联框须同屏叠加,层间不用 flex 堆叠) -- 根框体 `s_frame_box[0]` 持久(位置 (0,44)、宽自适应);子框体进入时建 / 返回时毁(`lv_obj_create(s_cont[d])` + flat + 边框样式,`IGNORE_LAYOUT` 绝对定位) +- 屏幕根容器(全屏 flat)→ **单一持久全屏容器** `s_cont`(各层框均建其内;`LV_OBJ_FLAG_IGNORE_LAYOUT` + 绝对定位,透明,永不删;级联框须同屏叠加) +- 根框体 `s_frame_box[0]` 持久(位置 (0,44)、宽自适应);子框体进入时建 / 返回时毁(`lv_obj_create(s_cont)` + flat + 边框样式 + 自绘回调,`IGNORE_LAYOUT` 绝对定位) - 框体:自动滚动条(`LV_SCROLLBAR_MODE_AUTO`);行自绘(无子对象),行内容超框高才框内滚动(内容高经 `LV_EVENT_GET_SELF_SIZE` 虚报) - 行 = **数据**(节点下标注册 `s_row_node[d][i]`,无对象);框体 `LV_EVENT_DRAW_MAIN` 自绘:选中行 = 整行白带(`lv_draw_rect`),每行 = `lv_draw_label` 文本(左留白 2)+ 箭头(紧跟文本、间距 2;叶子无箭头),静态字符串(Flash 表、不拷贝);按滚动偏移枚举可见行、重绘区外的行跳过(culling) - 选中行 = 整行反色(白带、黑字),**箭头随反色变黑**(§4.4);非选中行黑底白字(白箭头) -- 根框装饰:两腿"浅 V"(`lv_line` 宽 3,点集 (7,7)-(16,17) / (158,7)-(145,17))+ 水平线(宽 2,(16,17)-(145,17))+ 状态行 label(17,20) +- 根框装饰(浅 V + 状态行):容器 `DRAW_MAIN` 自绘(3 段线 `lv_draw_line` + 1 行文本 `lv_draw_label`,无对象;两腿 3px、水平线 2px) - 空终点框:叶子 ENTER 后建 w=6、h=2 的占位框(无行),仅 ESC 可返回 ### 5.5 公开 API | 接口 | 作用 | 调用方 | |---|---|---| -| `int ui_menu_view_init(void)` | 样式 + 4 层容器 + 根框装饰 + 根框框体(一次性) | 逻辑层 `ui_menu_init` | -| `lv_obj_t *ui_menu_view_container(uint8_t depth)` | 取该深度持久容器(逻辑层在其上绑输入组/键回调) | 逻辑层 | +| `int ui_menu_view_init(void)` | 样式 + 持久容器(装饰/状态行自绘回调)+ 根框框体(含自绘回调)(一次性) | 逻辑层 `ui_menu_init` | +| `lv_obj_t *ui_menu_view_container(void)` | 取持久容器(焦点/键唯一入口;逻辑层在其上绑输入组/键回调) | 逻辑层 | | `void ui_menu_view_frame_build(uint8_t depth, uint8_t node, uint8_t sel, uint16_t scroll)` | 建该深度框:现算几何 + 注册行数据 + 应用选中/滚动(d=0 仅(重)注册行) | 逻辑层(进 / 初始化) | -| `void ui_menu_view_frame_destroy(uint8_t depth)` | 毁该深度框(框体+行,容器保留;根框 d=0 不可毁) | 逻辑层(退) | -| `void ui_menu_view_frame_refresh(uint8_t depth, uint8_t sel)` | 刷选中高亮 + 跟随滚动(不重建对象) | 逻辑层(移选后) | -| `void ui_menu_view_set_scroll(uint8_t depth, uint16_t scroll)` | 应用滚动偏移(行重建后按此还原) | 逻辑层 | +| `void ui_menu_view_frame_destroy(uint8_t depth)` | 毁该深度框(框体连自绘回调整体删除、行数据清空;容器保留;根框 d=0 不可毁) | 逻辑层(退) | +| `void ui_menu_view_frame_refresh(uint8_t depth, uint8_t sel)` | 刷选中(选中数据 + 跟随滚动 + 重绘框体,不重建框) | 逻辑层(移选后) | +| `void ui_menu_view_set_scroll(uint8_t depth, uint16_t scroll)` | 应用滚动偏移(行数据重注册后按此还原) | 逻辑层 | | `uint16_t ui_menu_view_get_scroll(uint8_t depth)` | 读当前滚动偏移(供逻辑层回读持久化) | 逻辑层 | -| `void ui_menu_view_focus(uint8_t depth)` | 焦点移到该深度容器(焦点=最右框) | 逻辑层(进/退) | +| `void ui_menu_view_focus(void)` | 焦点置持久容器(唯一键入口;框深度由 `s_depth` 决定) | 逻辑层(进/退) | > 公共常量:`MENU_PAGE_MAX 4`(深度上限 = 根框 + 3 级子框)、`MENU_ITEM_MAX 16`(单框最大条目数)、`MENU_SCREEN_W/H 160`(与 `ui_port.h` / 模拟器一致)。 @@ -573,9 +572,8 @@ y + h > 160 → y = 选中行行顶 + ROW_H − h;y < 0 → y = 0(截断/框 | 字段 | 类型 | 大小 | 用途 | |---|---|---|---| -| `s_cont[4]` | `lv_obj_t*`×4 | 16 B | 4 个全屏容器(每深度一个);透明常驻、**永不删**(框在容器内建/毁,防焦点对象悬空) | +| `s_cont` | `lv_obj_t*` | 4 B | 单一全屏持久容器(焦点/键唯一入口;各层框建其内);透明常驻、**永不删**(防焦点对象悬空) | | `s_row_node[4][16]` | `uint8_t` | 64 B | 行数据:节点下标(名称/箭头绘制时现查,无对象);**仅屏上框注册** | -| `s_root_deco` / `s_root_status` | `lv_obj_t*`×2 | 8 B | 根框装饰:"浅 V"水平段(两腿为局部对象,不落句柄)+ 状态行 | | `s_style_flat/frame/scrollbar` | `lv_style_t`×3 | ~300 B | 样式(一次性初始化,常驻) | | `s_frame_box[4]` | `lv_obj_t*`×4 | 16 B | 框体(d=0 持久;d>0 进入时建、返回时毁) | | `s_frame[4]` | `menu_frame_t`(built/sel/item_cnt/x/y/w/h) | 48 B | 每框几何缓存:子框对齐要用**父框刚算出**的 x/w/选中行,故按层留一份;随时可由 `s_path`/`s_sel` 重算,**不属于导航状态** | @@ -679,7 +677,7 @@ s_path[0]=0 ─▶ s_path[1]=6 ─▶ s_path[2]=7 ─▶ s_path[3]=8 |---|---| | `↑` / `↓` | 当前(最右)框内上/下移动选中(**首尾环绕**);到边缘且该框可滚则**滚动该框** | | `→` 或 `ENTER` | 选中**菜单节点** → 其右侧**弹出子框**;选中**叶子节点** → 进终点(空框) | -| `←` 或 `ESC` | 返回:**关最右框**(销毁其行)、焦点回父框、选中态保持(不跳) | +| `←` 或 `ESC` | 返回:**关最右框**(毁框体+行数据)、焦点回父框、选中态保持(不跳) | | 焦点 / 框头 | 焦点 = 最右(最深)框;**无逐框页头/返回按钮**(框即紧凑列表,返回靠 `←`);仅根框带主界面装饰 | **进入/返回示例**(路径,`→`/`←` = 进/退动作方向): @@ -714,15 +712,15 @@ ESC/LEFT : d>0 → menu_back() (根框 d=0 不响 | 键 | 逻辑层(17 B 状态) | 显示层(画面) | |---|---|---| | **移** ↑/↓ | `s_sel[d] = (s_sel[d] ∓ 1 + cnt) % cnt`(首尾环绕);`cnt=0`(空框)不变 | `frame_refresh(d, s_sel[d])`:改选中数据 + 跟随滚动 + 重绘框体(不重建);随后逻辑层 `get_scroll(d)` 回读入 `s_scroll[d]` | -| **进** →/ENTER | `s_depth+1`;`s_path[d+1]=kids[s_sel[d]]`;`s_sel[d+1]`(同节点还原原选中/滚动,换节点清零) | `frame_build(d+1, …)`:现算几何(贴父框右缘、顶对齐/底对齐上移)→ 注册行数据 → 应用选中/滚动;`focus(d+1)`;叶子 → 空终点框(w=6, h=2) | -| **退** ←/ESC | `s_depth−1`(根框 d=0 不响应) | `frame_destroy(d)`:毁框体(含自绘回调,容器保留,池内存即回落);`focus(d−1)`;父框不动、无需重建,`s_sel`/`s_scroll` 原样保留 | +| **进** →/ENTER | `s_depth+1`;`s_path[d+1]=kids[s_sel[d]]`;`s_sel[d+1]`(同节点还原原选中/滚动,换节点清零) | `frame_build(d+1, …)`:现算几何(贴父框右缘、顶对齐/底对齐上移)→ 注册行数据 → 应用选中/滚动;`focus()`(焦点恒在持久容器);叶子 → 空终点框(w=6, h=2) | +| **退** ←/ESC | `s_depth−1`(根框 d=0 不响应) | `frame_destroy(d)`:毁框体(含自绘回调,容器保留,池内存即回落);`focus()`(焦点恒在持久容器);父框不动、无需重建,`s_sel`/`s_scroll` 原样保留 | ### 7.2 框生命周期(进建退毁) - **进时建**(`frame_build`):现算几何(§5.3;父框几何取父层 `s_frame` 缓存)→ 注册行数据(行 = 子项,现算不落地)→ 应用选中高亮 + 滚动位。 - **退时毁**(`frame_destroy`):框体(含自绘回调)整体销毁、容器保留(永不删,防焦点对象悬空);LVGL 池内存随即回落。 - **根框持久**(`ui_menu_view_init` 建一次,永不毁);子框随进/退建/毁。 -- **焦点移动**:4 层容器同组、任一时刻仅最右框容器获焦;进/退时 `focus()` 显式移动,无焦点争抢。 +- **焦点**:持久容器为唯一焦点对象(初始化时入输入组并获焦,永不删、焦点不迁移);框深度由逻辑层 `s_depth` 决定,无焦点争抢。 - **空终点框**:叶子 ENTER 后建 w=6、h=2 占位框(无行),仅 ESC 可返回。 ### 7.3 存 vs 算(关键区分) @@ -763,13 +761,12 @@ ESC/LEFT : d>0 → menu_back() (根框 d=0 不响 |---|---|---| | 导航历史 `s_path`+`s_sel`+`s_depth` | 逻辑层 | 9 B | | 每框滚动 `s_scroll[4]` | 逻辑层 | 8 B | -| 容器 `s_cont[4]` | 显示层 | 16 B | +| 容器 `s_cont` | 显示层 | 4 B | | 行数据 `s_row_node[d][i]` `[4][16]` | 显示层 | 64 B | -| 根框装饰指针(2 个:水平段 + 状态行) | 显示层 | 8 B | | 框体句柄 `s_frame_box[4]` | 显示层 | 16 B | | 框几何缓存 `s_frame[4]`(`menu_frame_t`) | 显示层 | 48 B | | 样式 `s_style_*`(3 个,一次性) | 显示层 | ~300 B | -| **小计** | | **~0.5 KB**(逻辑层 17 B + 显示层 ~0.5 KB) | +| **小计** | | **~0.45 KB**(逻辑层 17 B + 显示层 ~0.4 KB) | **LVGL 池(动态,`LV_MEM` 40 KB 预算内)**:行自绘无对象,池只占**屏上框体**(2~3 个 `lv_obj`)+ 根框装饰/状态行, 典型 ≈ **1~2 KB**,仍远小于 40 KB(池可再缩减,见 `docs/待处理内容/RAM使用分析与优化方案.md`)。 @@ -782,7 +779,7 @@ ESC/LEFT : d>0 → menu_back() (根框 d=0 不响 | 环节 | 方法 | |---|---| -| 模拟器无头 | `lvgl_sim.exe --screenshot shot.bin` / `--keys shot.bin`;I1 像素(102400 B)转 PNG(PIL)目检 | +| 模拟器无头 | `lvgl_sim.exe --screenshot shot.bin` / `--keys shot.bin`;I1 原始像素(160×160 = 3200 B)转 PNG(PIL)目检 | | 对照参考图 | 7 状态(根页 + 6 个一级子框)逐一对照参考图 01~07;反色行/框几何用像素统计核验(偏差见 §8.2 注 5) | | 编译 | `@build`(ARMCC,0 错 0 警,强制) | | 真机 | `@test`(Required 含 `UI port init: OK;UI menu init: OK`;屏幕内容仍需人工目检) | @@ -827,4 +824,5 @@ ESC/LEFT : d>0 → menu_back() (根框 d=0 不响 | 2026-09-08 | §5.3 几何计算重写为图文并茂:新增 `images/级联框几何计算.svg` 四格图(① 行剖面定框宽 ② X 共享边框列 ③ Y 顶对齐 ④ Y 溢出上移,含虚线幽灵框 + 超屏红区示意),正文改为四步叙述 + 实测框宽表 + 公式汇总(五式内容不变) | | 2026-09-08 | `menu_frame_t` 瘦身:`built` bool→`uint8_t`(与系统存储类型一致);`x/y/w/h` `int32_t`→`int16_t`(值域 0..160 / ≤242,16 位够;y 需有符号——钳制前可为负);`s_frame[4]` 96 B→48 B,省 48 B RAM(§5.4 对象模型表 / §7.5 内存预算表同步) | | 2026-09-08 | 行 label 指针合并:删 `s_row_text` / `s_row_arrow` 两数组(省 512 B);文本/箭头颜色改在行容器 `s_row` 上设置、子 label 继承(LVGL `text_color` 为可继承属性,`lv_style.c` 属性标志表证实);§5.4 对象模型表 / §7.5 内存预算表同步(768 B→256 B) | -| 2026-09-08 | **行对象 → 自绘重写(方案1,省 LVGL 池"每行对象机制税")**:行不再是 LVGL 对象——行数据改 `s_row_node[4][16]`(`uint8_t` 节点下标,256 B→64 B),框体 `LV_EVENT_DRAW_MAIN` 自绘(选中行白带 `lv_draw_rect` + 每行 `lv_draw_label` 文本/箭头,按滚动偏移枚举可见行、重绘区外行 culling);框内滚动改 `LV_EVENT_GET_SELF_SIZE` 虚报内容高 + 手动跟随滚动(`view_scroll_to_sel`,替代 `lv_obj_scroll_to_view`);删 `s_style_selected`、`view_add_row` 与 flex 堆行;`frame_build/destroy/refresh` 改行数据注册/清空/重绘;公开 API 签名不变。§5.4/§5.6/§6.2/§7.1~7.5 同步;静态显示层 ~1.4→~0.5 KB、LVGL 池典型 4~8→1~2 KB。验证:`@build` 0 错 0 警 + 代码规范 PASS + 模拟器 8 状态像素回归(白带/边框/根框不变性 + 3 组往返态逐像素一致)全 PASS | +| 2026-09-08 | **行对象 → 自绘重写(方案1,省 LVGL 池"每行对象机制税")**:行不再是 LVGL 对象——行数据改 `s_row_node[4][16]`(`uint8_t` 节点下标,256 B→64 B),框体 `LV_EVENT_DRAW_MAIN` 自绘(选中行白带 `lv_draw_rect` + 每行 `lv_draw_label` 文本/箭头,按滚动偏移枚举可见行、重绘区外行 culling);框内滚动改 `LV_EVENT_GET_SELF_SIZE` 虚报内容高 + 手动跟随滚动(`view_scroll_to_sel`,替代 `lv_obj_scroll_to_view`);删 `s_style_selected`、`view_add_row` 与 flex 堆行;`frame_build/destroy/refresh` 改行数据注册/清空/重绘。§5.4/§5.6/§6.2/§7.1~7.5 同步。验证:`@build` 0 错 0 警 + 代码规范 PASS + 模拟器 8 状态像素回归(白带/边框/根框不变性 + 3 组往返态逐像素一致)全 PASS | +| 2026-09-08 | **4 层容器→单一持久容器 + 根框装饰/状态行自绘(RAM 收敛)**:焦点/键唯一入口 = 持久容器(框深度由逻辑层 `s_depth` 决定,键回调去掉 `user_data=depth`,`container`/`focus` 接口去参);根框装饰(浅 V)与状态行改容器 `DRAW_MAIN` 自绘(3 段 `lv_draw_line` + 1 行 `lv_draw_label`,删 3 `lv_line` + 1 label + 3 容器对象);静态显示层 ~0.5→~0.4 KB。验证:`@build` 0 错 0 警 + 代码规范 PASS + 模拟器 8 状态像素回归全 PASS(装饰/状态行像素级一致);LVGL 池实测 根框 7168→4872 B、最深四级级联 8488→6128 B(省 ~2.3 KB) | diff --git a/simulator/main.c b/simulator/main.c index 41c370c6..74037a86 100644 --- a/simulator/main.c +++ b/simulator/main.c @@ -129,6 +129,12 @@ static int run_headless(const char * keys, const char * path) lv_group_send_data(g, key); } + /* 注键后 LVGL 池占用(无头模式;内存回归用,对照 ui_menu_init 基线) */ + lv_mem_monitor_t mon; + lv_mem_monitor(&mon); + printf("pool after keys: used=%u free=%u\n", + (unsigned)(mon.total_size - mon.free_size), (unsigned)mon.free_size); + /* 用 snapshot 把整屏渲进独立 I1 draw_buf(避开 I1+FULL 的渲染死循环) */ lv_draw_buf_t *snap = lv_snapshot_take(lv_screen_active(), LV_COLOR_FORMAT_I1); if (snap == NULL) {