修改菜单显示方式

This commit is contained in:
2026-09-08 21:07:04 +08:00
parent 891c9ef0d9
commit 6662f3a455
4 changed files with 212 additions and 158 deletions
+173 -121
View File
@@ -2,7 +2,8 @@
* 模块名称:UI Menu View (菜单显示模型渲染层)
* 模块功能:以 LVGL 基元控件实现《菜单页面设计.md》§四⑧"横向级联子框"显示模型:
* 右贴靠(子框左缘=父框右缘、共享边框列)+ 框宽内容自适应 +
* 顶对齐/底对齐上移;持有全部 LVGL 对象(样式/4 层容器/框/行/根框装饰),
* 顶对齐/底对齐上移;持有全部 LVGL 对象(样式/4 层容器/框体/根框装饰),
* 行不是对象——框体 DRAW_MAIN 自绘(节点下标注册 + GET_SELF_SIZE 虚拟滚动);
* 不持有任何导航状态——状态由逻辑层 ui_menu.c 经本接口驱动
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
@@ -12,6 +13,9 @@
* 2026-09-08 王建锋 按代码规范 V1.0 补全:分区注释、公共函数注释、关键行注释
* 2026-09-08 王建锋 menu_frame_t 瘦身(uint8_t/int16_t,96→48 B/层);
* s_row_text/s_row_arrow 并入 s_row(省 512 B,行 label 颜色继承)
* 2026-09-08 王建锋 行对象→自绘重写:删行容器/文本/箭头 label 对象,框体
* DRAW_MAIN 自绘行(反色带/文本/▶),GET_SELF_SIZE 虚报
* 内容高实现虚拟滚动(省约 2 KB/行 对象开销)
*/
/* 头文件包含区 */
@@ -66,8 +70,8 @@ typedef struct {
/* 静态变量定义区 */
static lv_obj_t *s_cont[MENU_PAGE_MAX]; /* 4 层持久容器(每深度一个;焦点/键入口,永不删) */
static lv_obj_t *s_frame_box[MENU_PAGE_MAX]; /* 框体:d=0 持久(根框);d>=1 进入时建、返回时毁 */
static lv_obj_t *s_row[MENU_PAGE_MAX][MENU_ITEM_MAX]; /* 行容器(整行反色高亮的载体;文本/箭头是其子对象,不单独存) */
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"水平段(两腿为临时对象,样式同源) */
@@ -75,7 +79,6 @@ static lv_obj_t *s_root_status = NULL; /* 根框状态行 */
/* 样式(静态,一次性初始化,常驻) */
static lv_style_t s_style_flat; /* 通用:透明、无边框/圆角/内边距(剥离主题 card) */
static lv_style_t s_style_selected; /* 选中态:整行反色(白底) */
static lv_style_t s_style_frame; /* 框体:透明底 + 1px 白边框 */
static lv_style_t s_style_scrollbar; /* 滚动条:白色、4px 宽 */
@@ -88,7 +91,7 @@ static lv_style_t s_style_scrollbar; /* 滚动条:白色、4px 宽 */
* 限定条件:无
* 函数说明: 1. flat 剥离主题给对象附加的卡片样式(边框/圆角/内边距)
* 2. frame 为所有级联框(含根框)的 1px 白边框
* 3. selected 为选中行整行反色(1bpp 白底);文字色在刷新时显式置黑
* 3. 选中行反色不用样式:自绘回调按 sel 直接画白带(行不是对象)
*/
static void view_styles_init(void) {
static bool inited = false;
@@ -107,12 +110,6 @@ static void view_styles_init(void) {
lv_style_set_pad_all(&s_style_flat, 0);
lv_style_set_pad_gap(&s_style_flat, 0);
/* selected:选中行整行反色(白底黑字) */
lv_style_init(&s_style_selected);
lv_style_set_bg_opa(&s_style_selected, LV_OPA_COVER);
lv_style_set_bg_color(&s_style_selected, lv_color_white());
lv_style_set_text_color(&s_style_selected, lv_color_black());
/* frame:透明底 + 1px 白边框(所有级联框含根框) */
lv_style_init(&s_style_frame);
lv_style_set_bg_opa(&s_style_frame, LV_OPA_TRANSP);
@@ -167,120 +164,184 @@ static int32_t view_frame_width(const uint8_t *kids, uint8_t cnt) {
}
/*
* 函数功能:在框体上创建一条条目行(行容器 + 文本 + 可选箭头)
* 入口参数:box - 所属框体 lv_obj_t *
* d - 框所在深度 uint8_t
* i - 行下标 uint8_t
* node - 条目节点下标 uint8_t
* has_arrow - 是否创建箭头(=条目有子项) bool
* 返回值:true - 成功;false - 对象创建失败(LVGL 池耗尽,调用方停止建行)
* 限定条件:d < MENU_PAGE_MAX,i < MENU_ITEM_MAX
* 函数说明: 1. 行容器整行宽、高 MENU_ROW_H,承载选中整行反色高亮
* 2. 文本左留白 MENU_ROW_PAD_L;箭头紧跟文本(间距 MENU_ARROW_GAP,自适应)
* 3. 文本/箭头子 label 加 IGNORE_LAYOUT 做行内绝对定位,是行的子对象、不单独存
* 函数功能:框体虚拟内容高上报(滚动范围 = 内容高 - 内容区高)
* 入口参数:e - 事件指针(user_data = 层深度) lv_event_t *
* 返回值:无
* 限定条件:事件为 LV_EVENT_GET_SELF_SIZE,布局阶段由 LVGL 调用
* 函数说明:行不是子对象,经 GET_SELF_SIZE 虚报内容高,
* LVGL 据此算滚动范围与自动滚动条(替代旧"行对象撑内容高")
*/
static bool view_add_row(lv_obj_t *box, uint8_t d, uint8_t i, uint8_t node, bool has_arrow) {
/* 行容器:整行宽、行高 MENU_ROW_H(整行反色高亮的载体) */
lv_obj_t *row = lv_obj_create(box);
if (row == NULL) {
return false;
}
lv_obj_add_style(row, &s_style_flat, 0);
static void view_frame_self_size_cb(lv_event_t *e) {
uint8_t depth = (uint8_t)(uintptr_t)lv_event_get_user_data(e);
lv_point_t *p = lv_event_get_param(e);
/* 挂选中行反色样式(CHECKED 态时触发白底) */
lv_obj_add_style(row, &s_style_selected, LV_STATE_CHECKED);
lv_obj_set_width(row, lv_pct(100));
lv_obj_set_height(row, MENU_ROW_H);
/* 文本 label:左留白,行内绝对定位(IGNORE_LAYOUT) */
lv_obj_t *lbl = lv_label_create(row);
if (lbl == NULL) {
lv_obj_delete(row);
return false;
}
lv_obj_add_style(lbl, &s_style_flat, 0);
lv_obj_add_flag(lbl, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(lbl, MENU_ROW_PAD_L, MENU_ROW_TXT_TOP);
/* 静态字符串:直接指向 ROM 表,不拷贝 */
lv_label_set_text_static(lbl, ui_menu_tab_name(node));
/* 箭头:仅菜单行(有子项),紧跟文本(间距 MENU_ARROW_GAP) */
lv_obj_t *ar = NULL;
if (has_arrow) {
const char *name = ui_menu_tab_name(node);
/* 箭头 x = 左留白 + 实测文本宽 + 间距 */
int32_t tw = lv_text_get_width(name, (uint32_t)strlen(name), LV_FONT_DEFAULT, 0);
ar = lv_label_create(row);
if (ar != NULL) {
lv_obj_add_style(ar, &s_style_flat, 0);
lv_obj_add_flag(ar, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(ar, MENU_ROW_PAD_L + tw + MENU_ARROW_GAP, MENU_ROW_TXT_TOP);
lv_label_set_text_static(ar, "▶");
}
}
/* 只存行容器:文本/箭头是其子对象(颜色在行上设置、子对象继承) */
s_row[d][i] = row;
return true;
p->x = 0; /* 无横向虚拟内容(行整宽,无横向滚动) */
p->y = (int32_t)s_frame[depth].item_cnt * MENU_ROW_H; /* 内容高 = 行数×行高 */
}
/*
* 函数功能:应用指定深度框的选中高亮(整行反色 + 文字/箭头颜色)
* 函数功能:框体自绘(选中行反色带 + 行文本 + 箭头)
* 入口参数:e - 事件指针(user_data = 层深度) lv_event_t *
* 返回值:无
* 限定条件:事件为 LV_EVENT_DRAW_MAIN,框体已建、行已注册
* 函数说明: 1. 行不是对象:按滚动偏移枚举可见行号,逐行画白带/文本/▶
* 2. 选中行反色(白底黑字)、非选中黑底白字(与旧 label 渲染同
* 字体光栅化路径,1bpp 下像素级一致)
* 3. 不在重绘区的行跳过(culling)
*/
static void view_frame_draw_cb(lv_event_t *e) {
uint8_t depth = (uint8_t)(uintptr_t)lv_event_get_user_data(e);
const menu_frame_t *f = &s_frame[depth];
lv_obj_t *box = s_frame_box[depth];
lv_layer_t *layer = lv_event_get_layer(e);
lv_area_t area;
int32_t i_first, i_last, i;
lv_obj_get_content_coords(box, &area); /* 内容区(框内、边框之内) */
int32_t sy = lv_obj_get_scroll_y(box); /* 纵向滚动偏移(px) */
/* 可见行范围:行 i 占内容 y ∈ [i*15, i*15+15) */
i_first = sy / MENU_ROW_H;
i_last = (sy + lv_obj_get_content_height(box) - 1) / MENU_ROW_H;
if (i_last >= (int32_t)f->item_cnt) {
i_last = (int32_t)f->item_cnt - 1;
}
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();
row_area.x1 = area.x1;
row_area.x2 = area.x2;
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) */
}
/* 选中行:整行反色(白带) */
if (is_sel) {
lv_draw_rect_dsc_t fd;
lv_draw_rect_dsc_init(&fd);
fd.base.layer = layer;
fd.bg_color = lv_color_white();
fd.bg_opa = LV_OPA_COVER;
lv_draw_rect(layer, &fd, &row_area);
}
/* 行文本:左留白、垂直居中偏移(与旧 label 同位) */
lv_draw_label_dsc_t ld;
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = name;
ld.text_static = 1; /* Flash 字符串:不拷内存 */
ld.color = color; /* 选中黑 / 非选中白 */
ld.ofs_x = MENU_ROW_PAD_L;
ld.ofs_y = MENU_ROW_TXT_TOP;
lv_draw_label(layer, &ld, &row_area);
/* 箭头:仅菜单行(有子项),紧跟文本(间距自适应) */
if (ui_menu_tab_has_children(s_row_node[depth][i])) {
int32_t tw = lv_text_get_width(name, (uint32_t)strlen(name), LV_FONT_DEFAULT, 0);
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = "▶";
ld.text_static = 1;
ld.color = color;
ld.ofs_x = MENU_ROW_PAD_L + tw + MENU_ARROW_GAP;
ld.ofs_y = MENU_ROW_TXT_TOP;
lv_draw_label(layer, &ld, &row_area);
}
}
}
/*
* 函数功能:把选中行滚入可见区(手动跟随滚动,替代 scroll_to_view)
* 入口参数:box - 框体 lv_obj_t *
* f - 框几何缓存(含 sel) const menu_frame_t *
* 返回值:无
* 限定条件:box 已创建且已虚报内容高
* 函数说明:选中行在可见区之上 → 顶对齐;之下 → 底对齐;已在区内 → 不动
* ANIM_OFF:回读滚动位 = 最终值(逻辑层靠回读持久化)
*/
static void view_scroll_to_sel(lv_obj_t *box, const menu_frame_t *f) {
int32_t sy;
int32_t row_top;
int32_t view_h;
if (f->item_cnt == 0u) {
return; /* 空框无行可跟随 */
}
sy = lv_obj_get_scroll_y(box);
row_top = (int32_t)f->sel * MENU_ROW_H;
view_h = lv_obj_get_content_height(box); /* 内容区高(框内、边框之内) */
if (row_top < sy) {
sy = row_top; /* 选中行在可见区之上:顶对齐 */
}
else if (row_top + MENU_ROW_H > sy + view_h) {
sy = row_top + MENU_ROW_H - view_h; /* 选中行在可见区之下:底对齐 */
}
if (sy < 0) {
sy = 0;
}
if (sy != lv_obj_get_scroll_y(box)) {
lv_obj_scroll_to_y(box, sy, LV_ANIM_OFF);
}
}
/*
* 函数功能:应用指定深度框的选中(选中数据 + 跟随滚动 + 失效重绘)
* 入口参数:depth - 深度 uint8_t
* sel - 选中行 uint8_t
* 返回值:无
* 限定条件:该层行对象已建
* 函数说明:1. 选中行整行反色(白底黑字),箭头随反色变黑(参考图实测:选中行保留 ▶)
* 2. 非选中行透明底白字、白色箭头
* 3. 文本/箭头颜色设在行容器上,子 label 继承(text_color 为可继承属性)
* 限定条件:该层框体已建
* 函数说明:1. 行不是对象:选中只是数据,自绘回调绘制时定反色带/文字色
* 2. 选中行滚入可见区(手动计算,无行对象可供 scroll_to_view)
* 3. 失效框体:下一帧重绘
*/
static void view_apply_sel(uint8_t depth, uint8_t sel) {
menu_frame_t *f = &s_frame[depth];
/* 逐行置/清 CHECKED 态(选中行经样式整行反色) */
for (uint8_t i = 0; i < f->item_cnt; i++) {
bool is_sel = (i == sel);
if (is_sel) {
lv_obj_add_state(s_row[depth][i], LV_STATE_CHECKED);
}
else {
lv_obj_clear_state(s_row[depth][i], LV_STATE_CHECKED);
}
/* 文本色设在行上:文本/箭头子对象继承(选中黑、非选中白,箭头随反色) */
lv_obj_set_style_text_color(s_row[depth][i],
is_sel ? lv_color_black() : lv_color_white(), 0);
}
f->sel = sel;
view_scroll_to_sel(s_frame_box[depth], f); /* 选中行必在可见区内 */
lv_obj_invalidate(s_frame_box[depth]); /* 重绘框体(反色带/文字色绘制时决定) */
}
/*
* 函数功能:配置框体布局(纵向 flex 堆行 + 自动滚动条)
* 函数功能:配置框体滚动行为(自绘行无子对象,仅配置自动滚动条)
* 入口参数:box - 框体 lv_obj_t *
* 返回值:无
* 限定条件:box 已创建
* 函数说明:行内容超框高时框内滚动(本菜单最深 9 行不触发,机制为更长列表保留)
*/
static void view_box_layout(lv_obj_t *box) {
/* 纵向 flex 堆行、顶对齐(行自框顶依次排列) */
lv_obj_set_flex_flow(box, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(box, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
/* 自动滚动条:仅当行内容超框高时出现(框内滚动) */
lv_obj_set_scrollbar_mode(box, LV_SCROLLBAR_MODE_AUTO);
lv_obj_add_style(box, &s_style_scrollbar, LV_PART_SCROLLBAR);
}
/*
* 函数功能:构建指定深度的框(几何现算 + 行对象创建 + 选中/滚动应用)
* 函数功能:构建指定深度的框(几何现算 + 行数据注册 + 选中/滚动应用)
* 入口参数:depth - 深度(0=根框) uint8_t
* node - 本框展示节点(行 = 其子项) uint8_t
* sel - 本框选中行 uint8_t
* scroll - 本框滚动偏移(px,0=顶部) uint16_t
* 返回值:无
* 限定条件:depth>0 时 depth-1 层框已建(父框几何是其对齐依据)
* 函数说明: 1. d=0:根框框体已存在,仅(重)建行对象
* 函数说明: 1. d=0:根框框体已存在,仅(重)注册行数据
* 2. d>0:右贴靠父框右缘;顶边=父框选中行行顶,超屏底则
* 上移使本框底边对齐父框选中行行底,仍超顶则钳制顶部并改框内滚动
* 3. 框宽 = 最宽行(文本 + ▶ + 左右留白)+ 边框;条目现算不落地
@@ -332,6 +393,9 @@ void ui_menu_view_frame_build(uint8_t depth, uint8_t node, uint8_t sel, uint16_t
lv_obj_set_pos(box, f->x, f->y);
lv_obj_set_size(box, f->w, f->h);
view_box_layout(box);
/* 自绘 + 虚拟内容高:回调 user_data 装层深度(回调内拆回) */
lv_obj_add_event_cb(box, view_frame_draw_cb, LV_EVENT_DRAW_MAIN, (void *)(uintptr_t)depth);
lv_obj_add_event_cb(box, view_frame_self_size_cb, LV_EVENT_GET_SELF_SIZE, (void *)(uintptr_t)depth);
s_frame_box[depth] = box;
}
@@ -340,35 +404,23 @@ void ui_menu_view_frame_build(uint8_t depth, uint8_t node, uint8_t sel, uint16_t
f->item_cnt = cnt;
f->sel = sel;
/* 行对象:条目现算(= parent==node 过滤),进时建、退时毁 */
lv_obj_clean(box);
/* 清空旧行指针(旧对象已随框体删除) */
/* 行数据:条目现算(= parent==node 过滤),注册节点下标、自绘(无对象) */
for (uint8_t i = 0; i < MENU_ITEM_MAX; i++) {
s_row[depth][i] = NULL;
s_row_node[depth][i] = 0u; /* 清旧(超 cnt 的行不绘制) */
}
/* 逐行建行对象 */
for (uint8_t i = 0; i < cnt; i++) {
if (!view_add_row(box, depth, i, kids[i], ui_menu_tab_has_children(kids[i]))) {
break; /* LVGL 池耗尽:停止建行(池有界,见设计 §7.5) */
}
s_row_node[depth][i] = kids[i];
}
/* 选中高亮 + 滚动位 */
view_apply_sel(depth, sel);
/* 滚动位:有保存先还原,再跟随滚动保证选中行可见 */
if (scroll > 0u) {
/* 有保存的滚动位:还原 */
lv_obj_scroll_to_y(box, (int32_t)scroll, LV_ANIM_OFF);
}
else if (cnt > 0u) {
/* 无保存滚动位:选中行对齐到可见区 */
lv_obj_scroll_to_view(s_row[depth][sel], LV_ANIM_OFF);
}
view_apply_sel(depth, sel); /* 选中 + 跟随滚动 + 失效重绘 */
}
/*
* 函数功能:销毁指定深度的框(框体连同行对象,容器保留)
* 函数功能:销毁指定深度的框(框体连自绘回调整体删除,容器保留)
* 入口参数:depth - 深度 uint8_t
* 返回值:无
* 限定条件:depth>0(根框持久不可销毁)
@@ -380,12 +432,12 @@ void ui_menu_view_frame_destroy(uint8_t depth) {
if (depth == 0u || !f->built) {
return; /* 根框持久不可销毁 */
}
lv_obj_delete(s_frame_box[depth]); /* 框体连行对象整体删除(容器保留) */
lv_obj_delete(s_frame_box[depth]); /* 框体(含自绘回调)整体删除,容器保留 */
s_frame_box[depth] = NULL;
/* 清空行指针与几何缓存 */
/* 清空行数据与几何缓存 */
for (uint8_t i = 0; i < MENU_ITEM_MAX; i++) {
s_row[depth][i] = NULL;
s_row_node[depth][i] = 0u;
}
f->built = 0u;
f->item_cnt = 0u;
@@ -402,7 +454,7 @@ void ui_menu_view_frame_destroy(uint8_t depth) {
* sel - 新选中行 uint8_t
* 返回值:无
* 限定条件:ui_menu_view_frame_build(depth,…) 已完成
* 函数说明:只改行状态与文字色 + 跟随滚动,不重建对象
* 函数说明:只改选中数据 + 跟随滚动、重绘框体,不重建对象
*/
void ui_menu_view_frame_refresh(uint8_t depth, uint8_t sel) {
menu_frame_t *f = &s_frame[depth];
@@ -411,10 +463,7 @@ void ui_menu_view_frame_refresh(uint8_t depth, uint8_t sel) {
if (!f->built || depth >= MENU_PAGE_MAX || sel >= f->item_cnt) {
return;
}
f->sel = sel;
view_apply_sel(depth, sel);
/* 选中变化只改行状态 + 跟随滚动,不重建对象 */
lv_obj_scroll_to_view(s_row[depth][sel], LV_ANIM_OFF);
view_apply_sel(depth, sel); /* 选中数据 + 跟随滚动 + 失效(自绘决定反色带) */
}
/*
@@ -496,7 +545,7 @@ static void view_deco_line_apply(lv_obj_t *line, int32_t width) {
* 限定条件:LVGL 已初始化且活动屏幕存在(ui_port_init / sim_port_init 已完成)
* 函数说明:1. 4 层容器每深度一个、全屏覆盖、持久不删(焦点/键入口,防悬空)
* 2. 根框装饰(浅 V + 状态行)与根框框体只属根层(d=0)
* 3. 行对象不在此建,由逻辑层调 ui_menu_view_frame_build 按需建
* 3. 行数据由逻辑层经 ui_menu_view_frame_build 注册,框体自绘行
*/
int ui_menu_view_init(void) {
/* 取活动屏幕:NULL = ui_port_init/sim_port_init 未完成、LVGL 未就绪 */
@@ -581,10 +630,13 @@ int ui_menu_view_init(void) {
lv_obj_add_flag(s_frame_box[0], LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(s_frame_box[0], MENU_ROOT_BOX_X, MENU_ROOT_BOX_Y); /* 左上 (0,44):装饰/状态行之下 */
lv_obj_set_size(s_frame_box[0], w, h);
view_box_layout(s_frame_box[0]); /* 纵向 flex 堆行 + 自动滚动条(行对象后由 frame_build 添加) */
view_box_layout(s_frame_box[0]); /* 自动滚动条(行自绘,无子对象) */
/* 自绘 + 虚拟内容高:根框持久,回调仅此处挂一次 */
lv_obj_add_event_cb(s_frame_box[0], view_frame_draw_cb, LV_EVENT_DRAW_MAIN, (void *)(uintptr_t)0u);
lv_obj_add_event_cb(s_frame_box[0], view_frame_self_size_cb, LV_EVENT_GET_SELF_SIZE, (void *)(uintptr_t)0u);
/* 写根框几何缓存:子框右贴靠其右缘、顶对齐其选中行,均以此为准 */
s_frame[0].built = 1u; /* 框体就绪;行对象由逻辑层 frame_build(0) 创建 */
s_frame[0].built = 1u; /* 框体就绪;行数据由逻辑层 frame_build(0) 注册 */
s_frame[0].x = MENU_ROOT_BOX_X;
s_frame[0].y = MENU_ROOT_BOX_Y;
s_frame[0].w = w;