完成一个主界面开发流程

This commit is contained in:
2026-09-03 23:08:16 +08:00
parent 52d7dc7d4d
commit 89462d8aa1
21 changed files with 2177 additions and 198 deletions
+533 -38
View File
@@ -1,11 +1,15 @@
/*
* 模块名称:UI Menu (菜单页面构建)
* 模块功能:基于 lv_menu 复现 DTU-HMI 树形菜单,支持方向键导航与返回
* 模块功能:基于 LVGL 基元控件(页面导航)复现 DTU-HMI 树形菜单,支持方向键导航与返回
* 适用平台:STM32F407ZGTx
* 作者:王建锋
* 创建日期:2026-08-29
* 修改记录:
* 2026-08-29 王建锋 创建初始版本
* 2026-09-02 王建锋 弃用已废弃的 lv_menu 控件,改为基元控件页面导航
* 2026-09-02 王建锋 修复选中行文字不可见(白底白字):行改用 label 承载,选中态样式直接覆盖文字色
* 2026-09-03 王建锋 主界面按设计稿重构:顶部开口浅 V 装饰 + 状态行 + 左对齐带框 7 行列表(选中行整行反色、无箭头,其余行带 ▶);
* 根页条目收敛为一级 7 项(排除"厂家设置"子树);重命名 定值参数→参数定值、遥测量→三遥设置、通讯设置→通讯参数
*/
#include "ui_menu.h"
@@ -27,7 +31,7 @@ static const ui_menu_node_t g_menu_tab[] = {
{2, "交流量"},
{2, "直流量"},
{2, "开入量"},
{1, "定值参数"},
{1, "参数定值"},
{2, "定值"},
{3, "定值"},
{3, "遥调"},
@@ -38,7 +42,7 @@ static const ui_menu_node_t g_menu_tab[] = {
{3, "遥调"},
{3, "零压"},
{3, "电压"},
{1, "遥测量"},
{1, "三遥设置"},
{2, "测量值"},
{2, "遥调系数"},
{2, "遥测系数"},
@@ -49,7 +53,7 @@ static const ui_menu_node_t g_menu_tab[] = {
{2, "强制复归"},
{2, "手动录波"},
{2, "自动录波"},
{1, "通讯设置"},
{1, "通讯参数"},
{2, "通讯参数"},
{2, "网络配置"},
{2, "SNTP设置"},
@@ -85,31 +89,418 @@ static const ui_menu_node_t g_menu_tab[] = {
{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) */
/*
* 函数功能:构建菜单(lv_menu 页面树),复现参考项目菜单结构
* 页面:持久容器 + 按导航动态建毁的条目
* 容器常驻对象树(焦点对象不可删除,避免输入设备悬空指针),
* 条目按钮进入页面时创建、返回时销毁,内存占用 = 当前路径
*
* 每条目 = 行容器(整行背景)+ 文本 label + 可选箭头 label:
* - 行容器承载整行反色高亮(选中态白底)
* - 文本/箭头为子 label(绝对定位);选中态文字色须显式置黑
* (主题对 label 直接给白字,父级继承不可靠,见 2026-09-02 记录)
*/
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 白边框 */
/*
* 函数功能:初始化菜单样式(静态样式,仅执行一次)
* 入口参数:无
* 返回值:无
* 限定条件:无
* 函数说明: 1. flat 用于剥离主题给对象附加的卡片样式(边框/圆角/内边距)
* 2. selected 为选中条目整行反色高亮(1bpp:白底;文字色在刷新时显式置黑)
* 3. 样式后加优先,可覆盖主题在创建时附加的样式
*/
static void menu_styles_init(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);
}
/*
* 函数功能:向页面添加一个条目(行容器 + 文本 + 可选箭头)
* 入口参数: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 *
* 返回值:无
* 限定条件:ui_menu_init() 已完成
* 函数说明:1. 隐藏旧页、显示新页(仅切换 HIDDEN 标志)
* 2. 页头仅子页显示;根页隐藏页头(让位顶部装饰 + 状态行,内容区撑满 160)
* 3. 焦点移至新页容器(按键输入入口)
*/
static void menu_show_page(ui_menu_page_t *page)
{
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);
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);
}
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);
}
break;
case LV_KEY_ENTER:
case LV_KEY_RIGHT:
menu_open_selected(page);
break;
case LV_KEY_ESC:
case LV_KEY_LEFT:
menu_go_back(page);
break;
default:
break;
}
}
/*
* 函数功能:返回按钮点击回调(预留触摸输入)
* 入口参数:e - 事件指针 lv_event_t *
* 返回值:无
* 限定条件:ui_menu_init() 已完成
* 函数说明:返回上级页面;按键输入由页面容器 ESC/LEFT 处理
*/
static void menu_back_btn_cb(lv_event_t *e)
{
(void)e;
if (s_cur != NULL) {
menu_go_back(s_cur);
}
}
/*
* 函数功能:应用主界面顶部"浅 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 - 成功,其他 - 失败(对象创建失败)
* 限定条件:ui_port_init() 已成功调用,活动屏幕存在
* 函数说明:1. 仅构建主页面(根页 + 一级菜单项),子树暂不构建以控制内存占用
* 2. 文本全部使用静态字符串(不 strdup),减少 LVGL 堆分配
* 3. 构建中监控 LVGL 池空闲内存,不足时中止而非崩溃
* 限定条件:ui_port_init()/sim_port_init() 已成功调用,活动屏幕存在
* 函数说明: 1. 子页页头(返回按钮 + 标题)+ 内容区,4 个页面容器(每深度一个,持久)
* 2. 主界面(根页):顶部开口浅 V 装饰 + 状态行 + 左对齐带框 7 行列表
* 3. 页面按需构建:ENTER 时建、返回时毁,内存 = 当前路径
* 4. 页面容器加入默认输入组,按键由容器统一处理
* 5. 构建前后监控 LVGL 池内存并打印
*/
int ui_menu_init(void)
{
lv_obj_t *menu = lv_menu_create(lv_screen_active());
if (menu == NULL) {
lv_obj_t *scr = lv_screen_active();
if (scr == NULL) {
return -1;
}
lv_obj_set_size(menu, 160, 160);
lv_menu_set_mode_root_back_button(menu, LV_MENU_ROOT_BACK_BUTTON_ENABLED);
lv_group_add_obj(lv_group_get_default(), menu);
/* 仅构建主页面:根页 + 一级菜单项(静态文本,不建子树,省内存) */
lv_obj_t *root_page = lv_menu_page_create(menu, NULL);
if (root_page == NULL) {
return -1;
}
lv_menu_set_page_title_static(root_page, "主界面");
menu_styles_init();
lv_mem_monitor_t mon;
lv_mem_monitor(&mon);
@@ -117,28 +508,132 @@ int ui_menu_init(void)
(unsigned)mon.total_size,
(unsigned)mon.free_size, (unsigned)mon.free_biggest_size);
uint32_t n = sizeof(g_menu_tab) / sizeof(g_menu_tab[0]);
for (uint32_t i = 1; i < n; i++) {
if (g_menu_tab[i].level != 1) {
continue; /* 只显示一级菜单项,子树暂不构建 */
}
/* 根容器:铺满屏幕,纵向布局 */
lv_display_t *disp = lv_display_get_default();
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, lv_display_get_horizontal_resolution(disp),
lv_display_get_vertical_resolution(disp));
lv_obj_set_flex_flow(menu, LV_FLEX_FLOW_COLUMN);
/* 内存警戒:空闲池不足则中止,避免 LVGL 内部 NULL 解引用崩溃 */
lv_mem_monitor(&mon);
if (mon.free_biggest_size < 1024) {
DBG_ERROR("menu mem low: abort at item %u (free=%u big=%u)",
(unsigned)i, (unsigned)mon.free_size,
(unsigned)mon.free_biggest_size);
break;
}
/* 子页页头:返回按钮 + 标题(根页隐藏) */
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);
const char *name = g_menu_tab[i].name;
lv_obj_t *item = lv_menu_cont_create(root_page);
lv_obj_t *lbl = lv_label_create(item);
lv_label_set_text_static(lbl, name);
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) {
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_menu_set_page(menu, root_page);
/* 主界面(根页):顶部"开口浅 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]);
lv_mem_monitor(&mon);
DBG_INFO("menu build done: total=%u free=%u big=%u used=%u",