/* * 模块名称:UI Menu View (菜单显示模型渲染层) * 模块功能:以 LVGL 基元控件实现《菜单页面设计.md》§四⑧"横向级联子框"显示模型: * 右贴靠(子框左缘=父框右缘、共享边框列)+ 框宽内容自适应 + * 顶对齐/底对齐上移;持有全部 LVGL 对象(样式/4 层容器/框体/根框装饰), * 行不是对象——框体 DRAW_MAIN 自绘(节点下标注册 + GET_SELF_SIZE 虚拟滚动); * 不持有任何导航状态——状态由逻辑层 ui_menu.c 经本接口驱动 * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:王建锋 * 创建日期:2026-09-04 * 修改记录: * 2026-09-04 王建锋 创建:自旧 ui_menu.c 单页模型拆分出显示层(高内聚低耦合) * 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/行 对象开销) */ /* 头文件包含区 */ #include #include "ui_menu_view.h" #include "ui_menu_tab.h" #include "lvgl.h" #define DBG_TAG "[UI_MENU_VIEW]" #include "dbg_log.h" /* 私有宏定义区 */ /* * 布局常量(设备像素;参考图 02~07 实测 + 字体度量速查推导): * 汉字步进 12px/字、▶ 步进 10px、行高 15(7 行×15+2 边框=107,与参考图根框一致); * 行内留白取最小值 2px——最深 4 框级联(根 66 + 42 + 42 + 空框 6 - 3 共享边框) * = 153px,须 < 屏宽 160 */ #define MENU_ROW_H 15 /* 行高 */ #define MENU_ROW_TXT_TOP 1 /* 行内文字垂直居中偏移((15-13)/2,字体行高 13) */ #define MENU_ROW_PAD_L 2 /* 行内文字左留白(紧留白,保证级联总宽 < 160) */ #define MENU_ROW_PAD_R 2 /* 行内右侧留白 */ #define MENU_ARROW_GAP 2 /* 行内 文本→箭头 间距 */ #define MENU_FRAME_BORDER 1 /* 框边框宽(子框与父框共享该边框列) */ #define MENU_ROOT_BOX_X 0 /* 根框左(左对齐,贴左缘) */ #define MENU_ROOT_BOX_Y 44 /* 根框顶 */ #define MENU_ROOT_STATUS_X 17 /* 状态行左 */ #define MENU_ROOT_STATUS_Y 20 /* 状态行顶 */ #define MENU_DECO_LEG_W 3 /* 顶部"浅 V"两腿线宽(1bpp 下斜线边缘易被阈值吃掉,需加粗) */ #define MENU_DECO_HORIZ_W 2 /* 顶部"浅 V"水平线宽 */ /* 私有类型定义区 */ /* * 框几何缓存:位置/尺寸是"现算"的(不落导航状态),但子框对齐要用父框刚算出的值, * 故按层缓存一份;sel 为最近一次应用的高亮(子框垂直对齐依据),由逻辑层经 * build/refresh 保持同步 */ typedef struct { uint8_t built; /* 框已建(框体 + 行对象存在):0/1(uint8_t 与系统存储一致) */ uint8_t sel; /* 最近应用的选中行 */ uint8_t item_cnt; /* 行数 */ int16_t x, y; /* 框左上角(含边框;16 位够:y 钳制前可为负) */ int16_t w, h; /* 框外廓尺寸(16 位够:最大 16×15+2=242) */ } menu_frame_t; /* 静态变量定义区 */ 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 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 白边框 */ static lv_style_t s_style_scrollbar; /* 滚动条:白色、4px 宽 */ /* 函数定义区 */ /* * 函数功能:初始化显示层样式(静态样式,仅执行一次) * 入口参数:无 * 返回值:无 * 限定条件:无 * 函数说明: 1. flat 剥离主题给对象附加的卡片样式(边框/圆角/内边距) * 2. frame 为所有级联框(含根框)的 1px 白边框 * 3. 选中行反色不用样式:自绘回调按 sel 直接画白带(行不是对象) */ static void view_styles_init(void) { static bool inited = false; /* 一次性初始化:已初始化则直接返回 */ if (inited) { return; } inited = true; /* flat:透明、无边框/圆角/内边距(剥离主题附加的卡片样式) */ lv_style_init(&s_style_flat); lv_style_set_bg_opa(&s_style_flat, LV_OPA_TRANSP); lv_style_set_border_width(&s_style_flat, 0); lv_style_set_radius(&s_style_flat, 0); lv_style_set_pad_all(&s_style_flat, 0); lv_style_set_pad_gap(&s_style_flat, 0); /* frame:透明底 + 1px 白边框(所有级联框含根框) */ lv_style_init(&s_style_frame); lv_style_set_bg_opa(&s_style_frame, LV_OPA_TRANSP); lv_style_set_border_width(&s_style_frame, MENU_FRAME_BORDER); lv_style_set_border_color(&s_style_frame, lv_color_white()); lv_style_set_radius(&s_style_frame, 0); lv_style_set_pad_all(&s_style_frame, 0); lv_style_set_pad_gap(&s_style_frame, 0); /* scrollbar:白色 4px 宽(行超框高时自动出现) */ lv_style_init(&s_style_scrollbar); lv_style_set_bg_opa(&s_style_scrollbar, LV_OPA_COVER); lv_style_set_bg_color(&s_style_scrollbar, lv_color_white()); lv_style_set_width(&s_style_scrollbar, 4); } /* * 函数功能:计算框外廓宽(内容自适应) * 入口参数:kids - 子节点下标数组(各行内容) const uint8_t * * cnt - 条目数 uint8_t * 返回值:框外廓宽 px int32_t = 最宽行内容 + 左右边框 * 限定条件:LVGL 已初始化(字体度量可用) * 函数说明:行内容宽 = 左留白 + 文本 + (菜单 ? 间距 + ▶ : 0) + 右留白; * 文本/▶ 宽取字体实际步进(汉字 12px、▶ 10px); * 注意本 LVGL 版 lv_text_get_width 的 length=0 表示"不测量",必须传 strlen */ static int32_t view_frame_width(const uint8_t *kids, uint8_t cnt) { const lv_font_t *font = LV_FONT_DEFAULT; int32_t w = 0; /* 遍历各行,取内容宽最大值 */ for (uint8_t i = 0; i < cnt; i++) { const char *name = ui_menu_tab_name(kids[i]); /* 量文本实际步进(本 LVGL 版 length=0 意为"不测量",须传 strlen) */ int32_t tw = lv_text_get_width(name, (uint32_t)strlen(name), font, 0); int32_t cw = MENU_ROW_PAD_L + tw + MENU_ROW_PAD_R; /* 菜单行(有子项):加箭头间距 + ▶ 宽 */ if (ui_menu_tab_has_children(kids[i])) { cw += MENU_ARROW_GAP + lv_text_get_width("▶", (uint32_t)strlen("▶"), font, 0); } /* 取最宽行 */ if (cw > w) { w = cw; } } /* 加左右边框 */ return w + 2 * MENU_FRAME_BORDER; } /* * 函数功能:框体虚拟内容高上报(滚动范围 = 内容高 - 内容区高) * 入口参数:e - 事件指针(user_data = 层深度) lv_event_t * * 返回值:无 * 限定条件:事件为 LV_EVENT_GET_SELF_SIZE,布局阶段由 LVGL 调用 * 函数说明:行不是子对象,经 GET_SELF_SIZE 虚报内容高, * LVGL 据此算滚动范围与自动滚动条(替代旧"行对象撑内容高") */ 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); 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. 选中行滚入可见区(手动计算,无行对象可供 scroll_to_view) * 3. 失效框体:下一帧重绘 */ static void view_apply_sel(uint8_t depth, uint8_t sel) { menu_frame_t *f = &s_frame[depth]; f->sel = sel; view_scroll_to_sel(s_frame_box[depth], f); /* 选中行必在可见区内 */ lv_obj_invalidate(s_frame_box[depth]); /* 重绘框体(反色带/文字色绘制时决定) */ } /* * 函数功能:配置框体滚动行为(自绘行无子对象,仅配置自动滚动条) * 入口参数:box - 框体 lv_obj_t * * 返回值:无 * 限定条件:box 已创建 * 函数说明:行内容超框高时框内滚动(本菜单最深 9 行不触发,机制为更长列表保留) */ static void view_box_layout(lv_obj_t *box) { /* 自动滚动条:仅当行内容超框高时出现(框内滚动) */ 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:根框框体已存在,仅(重)注册行数据 * 2. d>0:右贴靠父框右缘;顶边=父框选中行行顶,超屏底则 * 上移使本框底边对齐父框选中行行底,仍超顶则钳制顶部并改框内滚动 * 3. 框宽 = 最宽行(文本 + ▶ + 左右留白)+ 边框;条目现算不落地 */ void ui_menu_view_frame_build(uint8_t depth, uint8_t node, uint8_t sel, uint16_t scroll) { menu_frame_t *f = &s_frame[depth]; lv_obj_t *box; uint8_t kids[MENU_ITEM_MAX]; uint8_t cnt = ui_menu_tab_children(node, kids, MENU_ITEM_MAX); if (depth == 0u) { box = s_frame_box[0]; /* 根框框体持久(view_init 已建) */ } else { if (f->built) { ui_menu_view_frame_destroy(depth); /* 防御:重入 = 先毁后建 */ } const menu_frame_t *p = &s_frame[depth - 1u]; /* X:右贴靠——子框左缘 = 父框右缘(共享边框列、零间隙) */ f->x = p->x + p->w - MENU_FRAME_BORDER; /* Y:顶对齐父框"选中行"行顶;(顶边+框高) 超屏底 → 上移使本框底边 * 对齐父框"选中行"行底(顶部不钳制,可叠到状态行区); * 仍超顶(比整屏高)→ 钳制顶部,框高截到可见区、行改框内滚动 */ int32_t row_top = p->y + MENU_FRAME_BORDER + (int32_t)p->sel * MENU_ROW_H; int32_t h = (int32_t)cnt * MENU_ROW_H + 2 * MENU_FRAME_BORDER; f->y = row_top; if (f->y + h > MENU_SCREEN_H) { f->y = row_top + MENU_ROW_H - h; } if (f->y < 0) { f->y = 0; } if (f->y + h > MENU_SCREEN_H) { h = MENU_SCREEN_H - f->y; } f->h = h; 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]); if (box == NULL) { return; /* LVGL 池耗尽:框未建,状态回滚 */ } lv_obj_add_style(box, &s_style_flat, 0); lv_obj_add_style(box, &s_style_frame, 0); lv_obj_add_flag(box, LV_OBJ_FLAG_IGNORE_LAYOUT); 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; } /* 更新框几何缓存(子框对齐的依据) */ f->built = 1u; f->item_cnt = cnt; f->sel = sel; /* 行数据:条目现算(= parent==node 过滤),注册节点下标、自绘(无对象) */ for (uint8_t i = 0; i < MENU_ITEM_MAX; i++) { s_row_node[depth][i] = 0u; /* 清旧(超 cnt 的行不绘制) */ } for (uint8_t i = 0; i < cnt; i++) { s_row_node[depth][i] = kids[i]; } /* 滚动位:有保存先还原,再跟随滚动保证选中行可见 */ if (scroll > 0u) { lv_obj_scroll_to_y(box, (int32_t)scroll, LV_ANIM_OFF); } view_apply_sel(depth, sel); /* 选中 + 跟随滚动 + 失效重绘 */ } /* * 函数功能:销毁指定深度的框(框体连自绘回调整体删除,容器保留) * 入口参数:depth - 深度 uint8_t * 返回值:无 * 限定条件:depth>0(根框持久不可销毁) * 函数说明:返回(关最右框)时调用,LVGL 池内存随即回落 */ void ui_menu_view_frame_destroy(uint8_t depth) { menu_frame_t *f = &s_frame[depth]; if (depth == 0u || !f->built) { return; /* 根框持久不可销毁 */ } lv_obj_delete(s_frame_box[depth]); /* 框体(含自绘回调)整体删除,容器保留 */ s_frame_box[depth] = NULL; /* 清空行数据与几何缓存 */ for (uint8_t i = 0; i < MENU_ITEM_MAX; i++) { s_row_node[depth][i] = 0u; } f->built = 0u; f->item_cnt = 0u; f->sel = 0u; f->x = 0; f->y = 0; f->w = 0; f->h = 0; } /* * 函数功能:刷新指定深度框的选中高亮 * 入口参数:depth - 深度 uint8_t * 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]; /* 未建或越界直接返回 */ if (!f->built || depth >= MENU_PAGE_MAX || sel >= f->item_cnt) { return; } view_apply_sel(depth, sel); /* 选中数据 + 跟随滚动 + 失效(自绘决定反色带) */ } /* * 函数功能:应用指定深度框的滚动偏移 * 入口参数:depth - 深度 uint8_t * scroll - 滚动偏移(px) uint16_t * 返回值:无 * 限定条件:该层框已建 * 函数说明:行对象重建后按此还原用户滚动位置 */ void ui_menu_view_set_scroll(uint8_t depth, uint16_t scroll) { /* 未建框直接返回 */ if (depth >= MENU_PAGE_MAX || !s_frame[depth].built || s_frame_box[depth] == NULL) { return; } lv_obj_scroll_to_y(s_frame_box[depth], (int32_t)scroll, LV_ANIM_OFF); } /* * 函数功能:读取指定深度框当前滚动偏移 * 入口参数:depth - 深度 uint8_t * 返回值:滚动偏移(px) uint16_t 未建框返回 0 * 限定条件:无 * 函数说明:跟随滚动后由逻辑层读回,写入导航状态持久化 */ uint16_t ui_menu_view_get_scroll(uint8_t depth) { /* 未建框返回 0 */ if (depth >= MENU_PAGE_MAX || !s_frame[depth].built || s_frame_box[depth] == NULL) { return 0u; } return (uint16_t)lv_obj_get_scroll_y(s_frame_box[depth]); } /* * 函数功能:焦点移至指定深度的容器 * 入口参数:depth - 深度 uint8_t * 返回值:无 * 限定条件:ui_menu_view_init() 已完成且容器已加入输入组 * 函数说明:焦点=最右框;进/退时由逻辑层调用 */ void ui_menu_view_focus(uint8_t depth) { if (depth < MENU_PAGE_MAX && s_cont[depth] != NULL) { /* 焦点移到目标深度容器 */ lv_group_focus_obj(s_cont[depth]); } } /* * 函数功能:取指定深度的持久容器 * 入口参数:depth - 深度 0..MENU_PAGE_MAX-1 uint8_t * 返回值:容器对象 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; } /* * 函数功能:应用主界面顶部"浅 V"装饰线段的公共样式 * 入口参数:line - lv_line 对象;width - 线宽(px) * 返回值:无 * 限定条件:line 已创建且已设点集 * 函数说明:绝对定位于 (0,0)、忽略布局、白色实线、指定线宽 */ 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); } /* * 函数功能:初始化显示层(样式 + 4 层持久容器 + 根框装饰 + 根框框体) * 入口参数:无 * 返回值:0 - 成功,其他 - 失败(对象创建失败) * 限定条件:LVGL 已初始化且活动屏幕存在(ui_port_init / sim_port_init 已完成) * 函数说明:1. 4 层容器每深度一个、全屏覆盖、持久不删(焦点/键入口,防悬空) * 2. 根框装饰(浅 V + 状态行)与根框框体只属根层(d=0) * 3. 行数据由逻辑层经 ui_menu_view_frame_build 注册,框体自绘行 */ int ui_menu_view_init(void) { /* 取活动屏幕:NULL = ui_port_init/sim_port_init 未完成、LVGL 未就绪 */ lv_obj_t *scr = lv_screen_active(); if (scr == NULL) { return -1; } /* 样式一次性初始化(静态样式、常驻,仅此处建一次) */ view_styles_init(); /* 读屏幕实际分辨率(不硬编码,MCU/模拟器两端共用本代码) */ lv_display_t *disp = lv_display_get_default(); int32_t sw = lv_display_get_horizontal_resolution(disp); int32_t sh = lv_display_get_vertical_resolution(disp); /* 屏幕根容器:铺满屏幕;子对象全部绝对定位(级联框须同屏叠加,不用 flex 堆叠) */ lv_obj_t *menu = lv_obj_create(scr); if (menu == NULL) { return -1; } 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) { 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, "当前模式 : 无模式"); /* 静态字符串,不拷内存 */ /* 根框框体:持久;宽 = 内容自适应(主界面 node 0 的子项) */ uint8_t kids[MENU_ITEM_MAX]; uint8_t cnt = ui_menu_tab_children(0u, kids, MENU_ITEM_MAX); /* 主界面子项(7 个 L1 条目) */ 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]); if (s_frame_box[0] == NULL) { return -1; } lv_obj_add_style(s_frame_box[0], &s_style_flat, 0); /* 透明底 */ lv_obj_add_style(s_frame_box[0], &s_style_frame, 0); /* 1px 白边框 */ 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]); /* 自动滚动条(行自绘,无子对象) */ /* 自绘 + 虚拟内容高:根框持久,回调仅此处挂一次 */ 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].x = MENU_ROOT_BOX_X; s_frame[0].y = MENU_ROOT_BOX_Y; s_frame[0].w = w; s_frame[0].h = h; DBG_INFO("view init: root frame w=%d h=%d", (int)w, (int)h); return 0; }