继续优化菜单RAM降低

This commit is contained in:
2026-09-08 22:24:45 +08:00
parent 6662f3a455
commit 9ead5679a1
7 changed files with 195 additions and 172 deletions
+18 -22
View File
@@ -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);
+104 -83
View File
@@ -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;
}
+11 -9
View File
@@ -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 */
+4 -4
View File
@@ -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 端背景色一致 |
+21 -21
View File
@@ -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 <seq>` 无头注键截图验证 |
| 2026-09-08 | 菜单 RAM 收敛两连:行对象→框体自绘(行降为数据,§4.4/§5.5);4 层容器→单一持久容器(焦点恒在其上,框深度由逻辑层 `s_depth` 决定,§4.3/§5.4)+ 根框装饰/状态行容器自绘(无对象);LVGL 池实测 根框 4872 B / 最深四级级联 6128 B |
+31 -33
View File
@@ -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 <seq> shot.bin`;I1 像素(102400 B)转 PNG(PIL)目检 |
| 模拟器无头 | `lvgl_sim.exe --screenshot shot.bin` / `--keys <seq> 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) |
+6
View File
@@ -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) {