UI 菜单初步完成

This commit is contained in:
2026-09-07 10:48:18 +08:00
parent 89462d8aa1
commit 2b41bdccb5
30 changed files with 2226 additions and 859 deletions
+116 -556
View File
@@ -1,449 +1,129 @@
/*
* 模块名称:UI Menu (菜单页面构建)
* 模块功能:基于 LVGL 基元控件(页面导航)复现 DTU-HMI 树形菜单,支持方向键导航与返回
* 适用平台:STM32F407ZGTx
* 模块名称:UI Menu (菜单导航逻辑层)
* 模块功能:g_menu_tab 父子索引表驱动的横向级联菜单导航——持有导航状态
* (路径/选中/深度/滚动,共 17 B)与键语义(↑↓移选、→/ENTER 进、←/ESC 退),
* 单向驱动显示层 ui_menu_view 建/毁/刷新级联框;本层不持有任何 LVGL 对象
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-08-29
* 修改记录:
* 2026-08-29 王建锋 创建初始版本
* 2026-09-02 王建锋 弃用已废弃的 lv_menu 控件,改为基元控件页面导航
* 2026-09-02 王建锋 修复选中行文字不可见(白底白字):行改用 label 承载,选中态样式直接覆盖文字色
* 2026-09-03 王建锋 主界面按设计稿重构:顶部开口浅 V 装饰 + 状态行 + 左对齐带框 7 行列表(选中行整行反色、无箭头,其余行带 ▶);
* 根页条目收敛为一级 7 项(排除"厂家设置"子树);重命名 定值参数→参数定值、遥测量→三遥设置、通讯设置→通讯参数
* 2026-09-02 王建锋 修复选中行文字不可见(白底白字)
* 2026-09-03 王建锋 主界面按设计稿重构(根页 7 项/装饰/状态行)
* 2026-09-04 王建锋 重构为"横向级联框"模型(菜单页面设计.md):拆出显示层
* ui_menu_view 与数据层 ui_menu_tab;本层只留导航状态机
*/
#include "ui_menu.h"
#include "ui_menu_view.h"
#include "ui_menu_tab.h"
#include <stdint.h>
#include "lvgl.h"
#define DBG_TAG "[UI_MENU]"
#include "dbg_log.h"
/* 菜单节点表(简化自 DTU-HMI menuTab):层级 + 名称 */
typedef struct {
uint8_t level; /* 0=根页面, 1..3=子层次 */
const char *name;
} ui_menu_node_t;
static const ui_menu_node_t g_menu_tab[] = {
{0, "主界面"},
{1, "装置信息"},
{1, "实时数据"},
{2, "交流量"},
{2, "直流量"},
{2, "开入量"},
{1, "参数定值"},
{2, "定值"},
{3, "定值"},
{3, "遥调"},
{3, "零压"},
{3, "电压"},
{2, "查看"},
{3, "定值"},
{3, "遥调"},
{3, "零压"},
{3, "电压"},
{1, "三遥设置"},
{2, "测量值"},
{2, "遥调系数"},
{2, "遥测系数"},
{2, "双字节"},
{2, "装置遥调"},
{1, "装置维护"},
{2, "时间设置"},
{2, "强制复归"},
{2, "手动录波"},
{2, "自动录波"},
{1, "通讯参数"},
{2, "通讯参数"},
{2, "网络配置"},
{2, "SNTP设置"},
{1, "记录查询"},
{2, "SOE记录"},
{2, "事故记录"},
{2, "操作记录"},
{2, "告警记录"},
{2, "变位告警"},
{2, "遥信记录"},
{2, "自检记录"},
{2, "定值记录"},
{2, "录波记录"},
{0, "厂家设置"},
{1, "元件设置"},
{1, "恢复默认"},
{2, "全部恢复"},
{2, "默认参数"},
{2, "默认定值"},
{2, "默认系数"},
{2, "电压"},
{2, "元件参数"},
{1, "版本显示"},
{1, "装置遥调"},
{2, "测量遥调"},
{2, "装置遥调"},
{2, "直流遥调"},
{2, "开出遥调"},
{2, "装置记录"},
{2, "告警记录"},
{2, "遥调记录"},
{2, "信号复归"},
{1, "版本信息"},
};
#define MENU_NODE_COUNT (sizeof(g_menu_tab) / sizeof(g_menu_tab[0]))
#define MENU_NODE_ROOT 0xFFFF /* 虚拟根节点(根页面) */
#define MENU_PAGE_MAX 4 /* 页面深度上限(根页 + 3 级子页) */
#define MENU_ITEM_MAX 16 /* 单页最大条目数 */
#define MENU_HEADER_H 18 /* 子页页头高度(px) */
/* 主界面(根页)布局(设备像素,依据设计稿 mockup 实测) */
#define MENU_ROW_H 15 /* 行高 */
#define MENU_ROW_TXT_TOP 1 /* 行内文字垂直居中偏移((15-13)/2,字体行高 13) */
#define MENU_ROW_PAD_L 8 /* 行内文字左留白 */
#define MENU_ROW_ARROW_X 58 /* 行内箭头 x(紧跟 4 字文本之后) */
#define MENU_ROOT_BOX_X 0 /* 列表框左(左对齐,贴左缘) */
#define MENU_ROOT_BOX_Y 44 /* 列表框顶 */
#define MENU_ROOT_BOX_W 99 /* 列表框宽 */
#define MENU_ROOT_BOX_H 107 /* 列表框高(7 行 × 15 + 上下边框 2) */
#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"水平线宽(≈原图 ~2px) */
/*
* 页面:持久容器 + 按导航动态建毁的条目
* 容器常驻对象树(焦点对象不可删除,避免输入设备悬空指针),
* 条目按钮进入页面时创建、返回时销毁,内存占用 = 当前路径
*
* 每条目 = 行容器(整行背景)+ 文本 label + 可选箭头 label:
* - 行容器承载整行反色高亮(选中态白底)
* - 文本/箭头为子 label(绝对定位);选中态文字色须显式置黑
* (主题对 label 直接给白字,父级继承不可靠,见 2026-09-02 记录)
* 导航状态:唯一"必须持久"的导航历史(共 17 B,见设计 §7.2);
* 条目列表/框几何均可由 g_menu_tab + 本状态随时重算,不落 RAM
*/
typedef struct {
lv_obj_t *cont; /* 页面容器(持久) */
lv_obj_t *row[MENU_ITEM_MAX]; /* 行容器(动态) */
lv_obj_t *row_text[MENU_ITEM_MAX]; /* 行文本 label */
lv_obj_t *row_arrow[MENU_ITEM_MAX]; /* 行箭头 label(NULL=无) */
uint16_t item_node[MENU_ITEM_MAX]; /* 各条目对应 g_menu_tab 下标 */
uint16_t node; /* 本页展示节点(MENU_NODE_ROOT=虚拟根) */
uint8_t item_cnt; /* 条目数 */
uint8_t sel; /* 当前选中条目下标 */
uint8_t depth; /* 页面深度(0=根页) */
} ui_menu_page_t;
static ui_menu_page_t s_pages[MENU_PAGE_MAX];
static ui_menu_page_t *s_cur = NULL; /* 当前显示页面 */
static lv_obj_t *s_header = NULL; /* 子页页头容器(返回按钮 + 标题);根页隐藏 */
static lv_obj_t *s_back_btn = NULL; /* 页头返回按钮 */
static lv_obj_t *s_title = NULL; /* 页头标题 */
static lv_obj_t *s_root_deco = NULL; /* 主界面顶部装饰(开口浅 V) */
static lv_obj_t *s_root_status = NULL; /* 主界面状态行 */
static lv_obj_t *s_root_box = NULL; /* 主界面列表框(1px 边框) */
/* 样式(静态,一次性初始化) */
static lv_style_t s_style_flat; /* 通用:透明、无边框/圆角/内边距 */
static lv_style_t s_style_selected; /* 选中态:整行反色(白底) */
static lv_style_t s_style_back; /* 返回按钮:细边框、小内边距 */
static lv_style_t s_style_scrollbar; /* 滚动条:白色、4px 宽 */
static lv_style_t s_style_root_box; /* 主界面列表框:透明底 + 1px 白边框 */
static uint8_t s_path[MENU_PAGE_MAX]; /* 路径:s_path[d]=第 d 层展示节点(s_path[0]=0 主界面) */
static uint8_t s_sel[MENU_PAGE_MAX]; /* 各层选中下标(返回时还原,选中不跳) */
static uint8_t s_depth; /* 当前深度(0=根框)= 焦点所在(最右)框 */
static uint16_t s_scroll[MENU_PAGE_MAX]; /* 各框滚动偏移(px);框销毁再进入时还原 */
/*
* 函数功能:初始化菜单样式(静态样式,仅执行一次)
* 函数功能:移选后的层刷新(高亮 + 跟随滚动 + 滚动位回读持久化)
* 入口参数:depth - 层深度 uint8_t
* 返回值:无
* 限定条件:该层框已建
* 函数说明:显示层跟随滚动后,滚动位写回 s_scroll 持久(框重建时还原)
*/
static void menu_refresh_layer(uint8_t depth)
{
ui_menu_view_frame_refresh(depth, s_sel[depth]);
s_scroll[depth] = ui_menu_view_get_scroll(depth);
}
/*
* 函数功能:进入选中条目(压层)
* 入口参数:child - 选中子节点下标 uint8_t
* 返回值:无
* 限定条件:s_depth+1 < MENU_PAGE_MAX
* 函数说明: 1. 叶子(无子项)同样压层,生成空白终点框(占位,无条目)
* 2. 同节点再次进入:还原上次选中与滚动位;换节点则清零
*/
static void menu_open(uint8_t child)
{
uint8_t d = s_depth;
bool same = (s_path[d + 1u] == child);
s_depth = (uint8_t)(d + 1u);
s_path[d + 1u] = child;
s_sel[d + 1u] = same ? s_sel[d + 1u] : 0u;
ui_menu_view_frame_build(d + 1u, child, s_sel[d + 1u],
(uint16_t)(same ? s_scroll[d + 1u] : 0u));
ui_menu_view_focus(d + 1u);
}
/*
* 函数功能:关闭最右框(弹层)
* 入口参数:无
* 返回值:无
* 限定条件:无
* 函数说明: 1. flat 用于剥离主题给对象附加的卡片样式(边框/圆角/内边距)
* 2. selected 为选中条目整行反色高亮(1bpp:白底;文字色在刷新时显式置黑)
* 3. 样式后加优先,可覆盖主题在创建时附加的样式
* 限定条件:s_depth > 0
* 函数说明: 1. 销毁最右框行对象(既有屏上框不动、无需重建)
* 2. 焦点回父框;父层选中/滚动原样保留(选中不跳)
*/
static void menu_styles_init(void)
static void menu_back(void)
{
static bool inited = false;
if (inited) {
return;
}
inited = true;
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);
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());
lv_style_init(&s_style_back);
lv_style_set_bg_opa(&s_style_back, LV_OPA_TRANSP);
lv_style_set_border_width(&s_style_back, 1);
lv_style_set_border_color(&s_style_back, lv_color_white());
lv_style_set_radius(&s_style_back, 0);
lv_style_set_pad_all(&s_style_back, 1);
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);
lv_style_init(&s_style_root_box);
lv_style_set_bg_opa(&s_style_root_box, LV_OPA_TRANSP);
lv_style_set_border_width(&s_style_root_box, 1);
lv_style_set_border_color(&s_style_root_box, lv_color_white());
lv_style_set_radius(&s_style_root_box, 0);
lv_style_set_pad_all(&s_style_root_box, 0);
/* mono 主题 card 样式自带 pad_gap=PAD_DEF(4),须显式清零,否则行间距被撑大导致溢出 */
lv_style_set_pad_gap(&s_style_root_box, 0);
ui_menu_view_frame_destroy(s_depth);
s_depth--;
ui_menu_view_focus(s_depth);
}
/*
* 函数功能:向页面添加一个条目(行容器 + 文本 + 可选箭头)
* 入口参数:page - 页面结构指针 ui_menu_page_t *
* parent - 条目父容器 lv_obj_t * 根页=s_root_box,子页=page->cont
* node - g_menu_tab 节点下标 uint16_t
* has_arrow - 是否创建箭头 label bool
* 返回值:无
* 限定条件:page->item_cnt < MENU_ITEM_MAX
* 函数说明:1. 行容器整行宽、高 MENU_ROW_H,承载选中反色高亮
* 2. 文本 label 左留白 MENU_ROW_PAD_L;箭头 label 位于 MENU_ROW_ARROW_X
* 3. 文本/箭头子 label 加 IGNORE_LAYOUT 做行内绝对定位
*/
static void menu_page_add_item(ui_menu_page_t *page, lv_obj_t *parent,
uint16_t node, bool has_arrow)
{
if (page->item_cnt >= MENU_ITEM_MAX) {
return;
}
uint8_t idx = page->item_cnt;
lv_obj_t *row = lv_obj_create(parent);
if (row == NULL) {
return;
}
lv_obj_add_style(row, &s_style_flat, 0);
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);
lv_obj_t *lbl = lv_label_create(row);
if (lbl == NULL) {
lv_obj_delete(row);
return;
}
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);
lv_label_set_text_static(lbl, g_menu_tab[node].name);
lv_obj_t *ar = NULL;
if (has_arrow) {
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_ARROW_X, MENU_ROW_TXT_TOP);
lv_label_set_text_static(ar, "▶");
}
}
page->row[idx] = row;
page->row_text[idx] = lbl;
page->row_arrow[idx] = ar;
page->item_node[idx] = node;
page->item_cnt++;
}
/* 前向声明(menu_page_build 先于其定义调用) */
static void menu_page_refresh_sel(ui_menu_page_t *page);
/*
* 函数功能:构建页面条目列表
* 入口参数:page - 页面结构指针 ui_menu_page_t *
* node - 展示节点 uint16_t MENU_NODE_ROOT 表示虚拟根
* 返回值:无
* 限定条件:根页时 s_root_box 已创建
* 函数说明:1. 根页:一级节点 7 项(至第二个根"厂家设置"前截断),带箭头,父容器=s_root_box
* 2. 子页:node+1 起 level 为 (level+1) 的连续段,无箭头,父容器=page->cont
* 3. 先清空父容器旧条目再重建,默认选中首项
*/
static void menu_page_build(ui_menu_page_t *page, uint16_t node)
{
bool is_root = (node == MENU_NODE_ROOT);
lv_obj_t *parent = is_root ? s_root_box : page->cont;
bool has_arrow = is_root;
lv_obj_clean(parent);
page->node = node;
page->item_cnt = 0;
page->sel = 0;
if (is_root) {
for (uint32_t i = 1; i < MENU_NODE_COUNT; i++) {
if (g_menu_tab[i].level == 0) {
break; /* 抵达第二个根(厂家设置),主界面仅取其之前的一级项 */
}
if (g_menu_tab[i].level == 1) {
menu_page_add_item(page, parent, (uint16_t)i, has_arrow);
}
}
}
else {
uint8_t lvl = (uint8_t)(g_menu_tab[node].level + 1);
for (uint32_t i = node + 1; i < MENU_NODE_COUNT; i++) {
if (g_menu_tab[i].level != lvl) {
break;
}
menu_page_add_item(page, parent, (uint16_t)i, has_arrow);
}
}
if (page->item_cnt > 0) {
menu_page_refresh_sel(page);
}
}
/*
* 函数功能:更新选中高亮、文字色与箭头显隐
* 入口参数:page - 页面结构指针 ui_menu_page_t *
* 返回值:无
* 限定条件:page->item_cnt > 0
* 函数说明:1. 选中行整行反色(白底),文字/箭头显式置黑并隐藏箭头
* 2. 非选中行透明底,文字/箭头置白并显示箭头
* 3. 滚动使选中条目进入可视区
*/
static void menu_page_refresh_sel(ui_menu_page_t *page)
{
if (page->item_cnt == 0) {
return;
}
for (uint8_t i = 0; i < page->item_cnt; i++) {
bool sel = (i == page->sel);
if (sel) {
lv_obj_add_state(page->row[i], LV_STATE_CHECKED);
}
else {
lv_obj_clear_state(page->row[i], LV_STATE_CHECKED);
}
lv_obj_set_style_text_color(page->row_text[i],
sel ? lv_color_black() : lv_color_white(), 0);
if (page->row_arrow[i] != NULL) {
if (sel) {
lv_obj_add_flag(page->row_arrow[i], LV_OBJ_FLAG_HIDDEN);
}
else {
lv_obj_remove_flag(page->row_arrow[i], LV_OBJ_FLAG_HIDDEN);
}
lv_obj_set_style_text_color(page->row_arrow[i],
sel ? lv_color_black() : lv_color_white(), 0);
}
}
lv_obj_scroll_to_view(page->row[page->sel], LV_ANIM_OFF);
}
/*
* 函数功能:切换显示页面
* 入口参数:page - 目标页面 ui_menu_page_t *
* 函数功能:按键语义分发(移/进/退)
* 入口参数:depth - 焦点所在层深度 uint8_t
* key - LVGL 键值 lv_key_t
* 返回值:无
* 限定条件:ui_menu_init() 已完成
* 函数说明:1. 隐藏旧页、显示新页(仅切换 HIDDEN 标志)
* 2. 页头仅子页显示;根页隐藏页头(让位顶部装饰 + 状态行,内容区撑满 160)
* 3. 焦点移至新页容器(按键输入入口)
* 函数说明: 1. UP/DOWN:移选(首尾环绕);选中越出可见区由显示层跟随滚动
* 2. ENTER/RIGHT:进子框(叶子 → 空白终点框)
* 3. ESC/LEFT:关最右框(根框不响应)
* 4. 空框(终点页 0 条目)只响应返回
*/
static void menu_show_page(ui_menu_page_t *page)
static void menu_key_handle(uint8_t depth, lv_key_t key)
{
if (s_cur != NULL && s_cur != page) {
lv_obj_add_flag(s_cur->cont, LV_OBJ_FLAG_HIDDEN);
}
s_cur = page;
lv_obj_clear_flag(page->cont, LV_OBJ_FLAG_HIDDEN);
if (page->depth == 0) {
lv_obj_add_flag(s_header, LV_OBJ_FLAG_HIDDEN);
}
else {
lv_obj_remove_flag(s_header, LV_OBJ_FLAG_HIDDEN);
lv_label_set_text_static(s_title, g_menu_tab[page->node].name);
}
lv_group_focus_obj(page->cont);
}
/*
* 函数功能:打开选中条目的子页面
* 入口参数:page - 当前页面 ui_menu_page_t *
* 返回值:无
* 限定条件:page->item_cnt > 0
* 函数说明:1. 在下一深度页面容器上重建条目并显示
* 2. 叶子节点(无子节点)打开空白占位页
* 3. 深度保护:超过 MENU_PAGE_MAX 不再下钻
*/
static void menu_open_selected(ui_menu_page_t *page)
{
if (page->item_cnt == 0) {
return;
}
if (page->depth + 1 >= MENU_PAGE_MAX) {
return;
}
ui_menu_page_t *child = &s_pages[page->depth + 1];
menu_page_build(child, page->item_node[page->sel]);
menu_show_page(child);
}
/*
* 函数功能:返回上级页面
* 入口参数:page - 当前页面 ui_menu_page_t *
* 返回值:无
* 限定条件:page->depth > 0
* 函数说明:1. 销毁本页条目释放 LVGL 池内存(容器保留)
* 2. 显示父页并恢复焦点(父页选中状态保持不变)
*/
static void menu_go_back(ui_menu_page_t *page)
{
if (page->depth == 0) {
return;
}
lv_obj_clean(page->cont);
menu_show_page(&s_pages[page->depth - 1]);
}
/*
* 函数功能:页面容器按键事件处理
* 入口参数:e - 事件指针 lv_event_t *
* 返回值:无
* 限定条件:容器已加入默认输入组并获焦
* 函数说明:1. UP/DOWN:移动选中(环绕)
* 2. ENTER/RIGHT:打开选中条目子页
* 3. ESC/LEFT:返回上级页面
*/
static void menu_page_key_cb(lv_event_t *e)
{
ui_menu_page_t *page = (ui_menu_page_t *)lv_event_get_user_data(e);
lv_key_t key = (lv_key_t)lv_event_get_key(e);
uint8_t kids[MENU_ITEM_MAX];
uint8_t cnt = ui_menu_tab_children(s_path[depth], kids, MENU_ITEM_MAX);
switch (key) {
case LV_KEY_UP:
if (page->item_cnt > 0) {
page->sel = (uint8_t)((page->sel + page->item_cnt - 1) % page->item_cnt);
menu_page_refresh_sel(page);
if (cnt > 0u) {
s_sel[depth] = (uint8_t)((s_sel[depth] + (uint8_t)cnt - 1u) % cnt);
menu_refresh_layer(depth);
}
break;
case LV_KEY_DOWN:
if (page->item_cnt > 0) {
page->sel = (uint8_t)((page->sel + 1) % page->item_cnt);
menu_page_refresh_sel(page);
if (cnt > 0u) {
s_sel[depth] = (uint8_t)((s_sel[depth] + 1u) % cnt);
menu_refresh_layer(depth);
}
break;
case LV_KEY_ENTER:
case LV_KEY_RIGHT:
menu_open_selected(page);
if (cnt > 0u && depth + 1u < MENU_PAGE_MAX) {
menu_open(kids[s_sel[depth]]);
}
break;
case LV_KEY_ESC:
case LV_KEY_LEFT:
menu_go_back(page);
if (depth > 0u) {
menu_back();
}
break;
default:
break;
@@ -451,56 +131,45 @@ static void menu_page_key_cb(lv_event_t *e)
}
/*
* 函数功能:返回按钮点击回调(预留触摸输入)
* 函数功能:页面容器按键事件入口(每层容器各挂一个,user_data=层深度)
* 入口参数:e - 事件指针 lv_event_t *
* 返回值:无
* 限定条件:ui_menu_init() 已完成
* 函数说明:返回上级页面;按键输入由页面容器 ESC/LEFT 处理
* 限定条件:容器已加入默认输入组并获焦
* 函数说明:键值经默认输入组送达焦点容器;统一分发到 menu_key_handle
*/
static void menu_back_btn_cb(lv_event_t *e)
static void menu_key_cb(lv_event_t *e)
{
(void)e;
if (s_cur != NULL) {
menu_go_back(s_cur);
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));
}
}
/*
* 函数功能:应用主界面顶部"浅 V"装饰线段的公共样式
* 入口参数:line - lv_line 对象;width - 线宽(px)
* 返回值:无
* 限定条件:line 已创建且已设点集
* 函数说明:绝对定位于 (0,0)、忽略布局、白色实线、指定线宽
*/
static void 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, 160, 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);
}
/*
* 函数功能:构建菜单(基元控件页面导航)
* 函数功能:构建菜单(横向级联框)
* 入口参数:无
* 返回值:0 - 成功,其他 - 失败(对象创建失败)
* 返回值:0 - 成功,其他 - 失败(表自检失败/对象创建失败)
* 限定条件:ui_port_init()/sim_port_init() 已成功调用,活动屏幕存在
* 函数说明: 1. 子页页头(返回按钮 + 标题)+ 内容区,4 个页面容器(每深度一个,持久)
* 2. 主界面(根页):顶部开口浅 V 装饰 + 状态行 + 左对齐带框 7 行列表
* 3. 页面按需构建:ENTER 时建、返回时毁,内存 = 当前路径
* 4. 页面容器加入默认输入组,按键由容器统一处理
* 函数说明: 1. 数据表自检(parent 下标均有效,捕获表内笔误)
* 2. 重置导航状态 → 初始化显示层
* 3. 各层容器加入默认输入组并挂键回调(按键由逻辑层统一处理)
* 4. 建根框(主界面 node 0 子项)并置焦点
* 5. 构建前后监控 LVGL 池内存并打印
*/
int ui_menu_init(void)
{
lv_obj_t *scr = lv_screen_active();
if (scr == NULL) {
if (!ui_menu_tab_valid()) {
DBG_ERROR("menu table: INVALID (parent index out of range)");
return -1;
}
menu_styles_init();
for (uint8_t d = 0u; d < MENU_PAGE_MAX; d++) {
s_path[d] = 0u;
s_sel[d] = 0u;
s_scroll[d] = 0u;
}
s_depth = 0u;
lv_mem_monitor_t mon;
lv_mem_monitor(&mon);
@@ -508,132 +177,23 @@ int ui_menu_init(void)
(unsigned)mon.total_size,
(unsigned)mon.free_size, (unsigned)mon.free_biggest_size);
/* 根容器:铺满屏幕,纵向布局 */
lv_display_t *disp = lv_display_get_default();
lv_obj_t *menu = lv_obj_create(scr);
if (menu == NULL) {
if (ui_menu_view_init() != 0) {
return -1;
}
lv_obj_add_style(menu, &s_style_flat, 0);
lv_obj_set_size(menu, lv_display_get_horizontal_resolution(disp),
lv_display_get_vertical_resolution(disp));
lv_obj_set_flex_flow(menu, LV_FLEX_FLOW_COLUMN);
/* 子页页头:返回按钮 + 标题(根页隐藏) */
s_header = lv_obj_create(menu);
if (s_header == NULL) {
return -1;
}
lv_obj_add_style(s_header, &s_style_flat, 0);
lv_obj_set_width(s_header, lv_pct(100));
lv_obj_set_height(s_header, MENU_HEADER_H);
lv_obj_set_flex_flow(s_header, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(s_header, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_gap(s_header, 4, 0);
s_back_btn = lv_button_create(s_header);
if (s_back_btn == NULL) {
return -1;
}
lv_obj_add_style(s_back_btn, &s_style_back, 0);
lv_obj_set_size(s_back_btn, 28, 16);
lv_obj_set_flex_flow(s_back_btn, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(s_back_btn, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t *back_lbl = lv_label_create(s_back_btn);
if (back_lbl == NULL) {
return -1;
}
lv_obj_add_style(back_lbl, &s_style_flat, 0);
lv_label_set_text_static(back_lbl, "返回");
lv_obj_add_event_cb(s_back_btn, menu_back_btn_cb, LV_EVENT_CLICKED, NULL);
s_title = lv_label_create(s_header);
if (s_title == NULL) {
return -1;
}
lv_obj_add_style(s_title, &s_style_flat, 0);
lv_label_set_text_static(s_title, "主界面");
lv_obj_add_flag(s_header, LV_OBJ_FLAG_HIDDEN); /* 默认根页:隐藏页头 */
/* 内容区:占满剩余高度(根页隐藏页头时撑满 160) */
lv_obj_t *content = lv_obj_create(menu);
if (content == NULL) {
return -1;
}
lv_obj_add_style(content, &s_style_flat, 0);
lv_obj_set_width(content, lv_pct(100));
lv_obj_set_flex_grow(content, 1);
lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN);
/* 页面容器:每深度一个,持久存在(条目按导航建毁) */
for (uint8_t i = 0; i < MENU_PAGE_MAX; i++) {
s_pages[i].cont = lv_obj_create(content);
if (s_pages[i].cont == NULL) {
/* 键接线:各层容器加入默认输入组;按键一律由逻辑层处理(显示层纯渲染) */
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_obj_add_style(s_pages[i].cont, &s_style_flat, 0);
lv_obj_set_size(s_pages[i].cont, lv_pct(100), lv_pct(100));
lv_obj_add_flag(s_pages[i].cont, LV_OBJ_FLAG_CLICK_FOCUSABLE);
lv_group_add_obj(lv_group_get_default(), s_pages[i].cont);
lv_obj_add_event_cb(s_pages[i].cont, menu_page_key_cb, LV_EVENT_KEY, &s_pages[i]);
s_pages[i].depth = i;
s_pages[i].node = MENU_NODE_ROOT;
if (i > 0) {
/* 子页容器:纵向堆叠条目 + 自动滚动条 */
lv_obj_set_flex_flow(s_pages[i].cont, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(s_pages[i].cont, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_START);
lv_obj_set_scrollbar_mode(s_pages[i].cont, LV_SCROLLBAR_MODE_AUTO);
lv_obj_add_style(s_pages[i].cont, &s_style_scrollbar, LV_PART_SCROLLBAR);
lv_obj_add_flag(s_pages[i].cont, LV_OBJ_FLAG_HIDDEN);
}
lv_group_add_obj(lv_group_get_default(), cont);
lv_obj_add_event_cb(cont, menu_key_cb, LV_EVENT_KEY, (void *)(uintptr_t)d);
}
/* 主界面(根页):顶部"开口浅 V"装饰 + 状态行 + 列表框(绝对定位于 s_pages[0].cont)
* 拆 3 段:两腿加粗(1bpp 下斜线边缘易被阈值吃掉,需比水平线粗才接近原图 ~4px),水平线保持细线 */
{
static const lv_point_precise_t g_deco_leg_l[] = { {7, 7}, {16, 17} };
static const lv_point_precise_t g_deco_hoz[] = { {16, 17}, {145, 17} };
static const lv_point_precise_t g_deco_leg_r[] = { {158, 7}, {145, 17} };
lv_obj_t * leg_l = lv_line_create(s_pages[0].cont);
lv_obj_t * leg_r = lv_line_create(s_pages[0].cont);
s_root_deco = lv_line_create(s_pages[0].cont);
if (leg_l == NULL || leg_r == NULL || s_root_deco == NULL) {
return -1;
}
lv_line_set_points(leg_l, g_deco_leg_l, 2);
lv_line_set_points(leg_r, g_deco_leg_r, 2);
lv_line_set_points(s_root_deco, g_deco_hoz, 2);
deco_line_apply(leg_l, MENU_DECO_LEG_W);
deco_line_apply(leg_r, MENU_DECO_LEG_W);
deco_line_apply(s_root_deco, MENU_DECO_HORIZ_W);
}
s_root_status = lv_label_create(s_pages[0].cont);
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, "当前模式 : 无模式");
s_root_box = lv_obj_create(s_pages[0].cont);
if (s_root_box == NULL) {
return -1;
}
lv_obj_add_style(s_root_box, &s_style_root_box, 0);
lv_obj_add_flag(s_root_box, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(s_root_box, MENU_ROOT_BOX_X, MENU_ROOT_BOX_Y);
lv_obj_set_size(s_root_box, MENU_ROOT_BOX_W, MENU_ROOT_BOX_H);
lv_obj_set_flex_flow(s_root_box, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(s_root_box, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_START);
/* 构建根页并显示 */
menu_page_build(&s_pages[0], MENU_NODE_ROOT);
menu_show_page(&s_pages[0]);
/* 根框:主界面(node 0)子项;建一次、持久 */
ui_menu_view_frame_build(0u, 0u, 0u, 0u);
ui_menu_view_focus(0u);
lv_mem_monitor(&mon);
DBG_INFO("menu build done: total=%u free=%u big=%u used=%u",