完成一个主界面开发流程
This commit is contained in:
+533
-38
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user