LVGL多级菜单开发经验浅谈

液晶显示界面的开发,不能像简单的OLED或者点阵屏那样去开发,这里用到了菜单树,链表,栈的概念。我按照自己的设计理念做了一个总结:
1,建立一个菜单表,把所有的显示菜单和页面都放在这个表中
2,创建一个结构体 一级,二级,三级页面对应显示内容初步赋值
3,压栈,记录当前的显示菜单和焦点,栈的深度就+1
4,出栈,类似,栈的深度-1
5, 栈的深度对应菜单的等级
6,查当前节点的索引,根据当前节点获取他的父节点,遍历父节点的所有子节点和当前节点相同,记住当前索引
7,把链表转换为数组方便随机跳转。根据当前菜单,找到他的父节点,遍历所有子节点,存入数组
8,菜单跳转:1级进入2级,没有2级直接进入3级,先要判断节点是否有效,无效直接退出,整体流程:先获取节点索引,根据
if (node->child == NULL || TwoLevel[btn_idx].icon_row == NULL)// 若当前菜单没有子菜单,或没有二级图标,直接进入三级全屏页
{
int32_t level3_row = Level2_To_level3_idx[btn_idx]; // 一级和三级做了一个映射
if (level3_row >= 0 && icon_list[level3_row][0] != NULL)
{
push_stack(node); // depth 0→1
push_stack(node); // depth 1→2,补齐跳过的二级层级
curr_menu = node;
show_page((lv_img_dsc_t *)icon_list[level3_row][0]);
printf("Direct to level3: btn_idx=%d, level3_row=%d\n", btn_idx, level3_row);
}
else
{
printf("当前菜单没有子菜单,也没有三级图标\n");
}
return;
}
9, 二级进入三级步骤:当前菜单在二级列表,且不在NULL这一行,记录二级的索引,根据这个索引找到对应的三级。获取当前触摸图标索引,找到三级对应的页面,显示出来
10,更新图标状态
11,创建菜单容器,采用网格方式创建
1>.获取当前活跃屏幕
2>.屏幕上创建一个活动区域
3>.在该区域创建单元格,把参数对应节点链表转换为数组,所有的数组成员填充到网格。创建焦点组,对应焦点的图标显示阴影,所有图标有文字
lv_obj_t *menu_create_item(Icon_Mode *icon_mode) // 创建图标菜单容器
{
lv_obj_t *scr = lv_screen_active(); // 获取当前活动屏幕
lv_obj_t *cont = lv_obj_create(scr); // 在屏幕上创建菜单外层容器cont
lv_obj_set_size(cont, 480, 272); // 设置外层容器尺寸:宽480px,高272px
// 给【整个菜单页的外层Grid根容器】绑"页面配置模板指针":
// 不是绑图标!也不是绑单个按钮!是把"这个页面用哪个Icon_Mode渲染的"这个身份信息挂在根容器上,
// 方便后续回调/调试时快速取回当前页的完整配置(图标行+文字列表+按钮数)
lv_obj_set_user_data(cont, icon_mode);
lv_obj_set_scrollbar_mode(cont, LV_SCROLLBAR_MODE_OFF); // 隐藏容器滚动条(滚动能力保留)

if (menu_group == NULL) // 如果菜单输入组还未创建
menu_group = lv_group_create(); // 创建LVGL输入组,用于键盘/编码器焦点导航

lv_obj_set_layout(cont, LV_LAYOUT_GRID); // 外层容器使用网格Grid布局
static int32_t col_dsc[] = {LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST}; // Grid列定义:3列,等宽均分剩余空间
static int32_t row_dsc[] = {LV_GRID_CONTENT, LV_GRID_CONTENT, LV_GRID_CONTENT, LV_GRID_TEMPLATE_LAST}; // Grid行定义:每行高度由内部内容自适应
lv_obj_set_grid_dsc_array(cont, col_dsc, row_dsc); // 给容器设置网格行列模板
lv_obj_set_grid_align(cont, LV_GRID_ALIGN_CENTER, LV_GRID_ALIGN_CENTER); // 整套网格在cont容器内水平、垂直居中

// 设置容器内边距、网格单元格间隙
lv_obj_set_style_pad_left(cont, 20, LV_PART_MAIN);
lv_obj_set_style_pad_right(cont, 20, LV_PART_MAIN);
lv_obj_set_style_pad_top(cont, 5, LV_PART_MAIN);
lv_obj_set_style_pad_bottom(cont, 5, LV_PART_MAIN);
lv_obj_set_style_pad_column(cont, 10, LV_PART_MAIN); // 网格列之间水平间距10px
lv_obj_set_style_pad_row(cont, 10, LV_PART_MAIN); // 网格行之间垂直间距10px

uint8_t node_count = 0;
MenuNode **node_map = build_node_map(icon_mode, &node_count); // 根据icon_mode构建菜单节点映射数组,返回节点数量

int btn_count = 0;
// 统计有效图标数量,直到图标资源为空或者达到最大按钮数
while (btn_count < icon_mode->btn_count && ICON_GET(icon_mode->icon_row, btn_count) != NULL)
btn_count++;
if (btn_count == 0) // 没有有效图标时强制按钮数量为1,防止异常
btn_count = 1;

lv_obj_t *focus_target = NULL; // 标记需要设置焦点的按钮对象

// 循环创建每一个菜单项(cell + 图标按钮 + 文字)
for (int i = 0; i < btn_count; i++)
{
int row = i / 3; // 计算当前按钮所在网格行号:3列布局,索引整除3得到行
int col = i % 3; // 计算当前按钮所在网格列号:索引取模3得到列

lv_obj_t *cell = lv_obj_create(cont); // 创建cell对象,放到外层cont网格中
lv_obj_set_scrollbar_mode(cell, LV_SCROLLBAR_MODE_OFF); // cell隐藏滚动条
lv_obj_set_grid_cell(cell, LV_GRID_ALIGN_CENTER, col, 1, LV_GRID_ALIGN_CENTER, row, 1); // 将cell放置到网格col列row行,占1列1行,格子内居中
//弹性布局:父容器自动安排内部子控件的位置、大小、间距,不用你手动写坐标
lv_obj_set_layout(cell, LV_LAYOUT_FLEX); // cell内部开启Flex弹性布局,作为flex父容器
//子控件沿着垂直方向,从上到下依次排列
lv_obj_set_flex_flow(cell, LV_FLEX_FLOW_COLUMN); // cell内部子控件垂直排列(图标在上,文字在下)
lv_obj_set_flex_align(cell, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); // cell内部flex:主轴、交叉轴全部居中
lv_obj_set_style_pad_row(cell, 5, LV_PART_MAIN); // cell内部flex子项垂直间距5px(图标和文字间隙)

lv_obj_t btn = lv_imgbtn_create(cell); // 在cell内创建图片按钮
lv_obj_set_size(btn, 80, 80); // 按钮尺寸80
80像素
const void *icon_src = ICON_GET(icon_mode->icon_row, i); // 获取第i个图标资源地址
lv_imgbtn_set_src(btn, LV_IMGBTN_STATE_RELEASED, icon_src, NULL, NULL); // 设置按钮松开态图标,按下/禁用态无图片

lv_obj_set_style_bg_opa(btn, LV_OPA_TRANSP, LV_PART_MAIN); // 默认按钮背景完全透明
lv_obj_set_style_border_width(btn, 0, LV_PART_MAIN); // 默认无边框
lv_obj_set_style_radius(btn, 8, LV_PART_MAIN); // 按钮圆角8px(选中时才会显现)

MenuNode *node = (i < node_count) ? node_map[i] : NULL; // 索引合法则取出菜单节点,否则置空
lv_obj_set_user_data(btn, (void *)node); // 将菜单节点绑定到按钮,点击回调可以读取

// 如果存在待恢复焦点的节点,且当前按钮匹配该节点:设置选中样式(背景+边框)
if (pending_focus_node != NULL && node == pending_focus_node)
{
lv_obj_set_style_bg_color(btn, lv_color_hex(0x4A90D9), LV_PART_MAIN);
lv_obj_set_style_bg_opa(btn, LV_OPA_60, LV_PART_MAIN);
lv_obj_set_style_border_width(btn, 2, LV_PART_MAIN);
lv_obj_set_style_border_color(btn, lv_color_hex(0x2E5C8A), LV_PART_MAIN);
//这里的焦点和选中按键 一般都是一个
selected_btn = btn;
focus_target = btn; // 标记这个按钮为焦点目标
}

// 如果配置了文字列表并且当前文字不为空,则创建文字标签
if (icon_mode->text_list != NULL && icon_mode->text_list[i] != NULL)
{
lv_obj_t *label = lv_label_create(cell); // 在cell内创建文字标签(图标下方)
lv_label_set_text(label, icon_mode->text_list[i]); // 设置标签文字
lv_obj_set_width(label, 80); // 标签宽度80px,和图标对齐
lv_obj_set_style_text_color(label, lv_color_hex(0x000000), LV_PART_MAIN); // 文字黑色
lv_obj_set_style_text_font(label, &fonts_12, LV_PART_MAIN); // 使用12号字体
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN); // 文字居中
}

lv_group_add_obj(menu_group, btn); // 将按钮加入输入组,支持编码器/方向键焦点切换
lv_obj_add_event_cb(btn, menu_click_cb, LV_EVENT_CLICKED, (void *)node); // 绑定点击事件回调,携带菜单节点参数
}

// 没有待恢复焦点节点,并且按钮数量大于0:默认选中第一个按钮
if (focus_target == NULL && pending_focus_node == NULL && btn_count > 0)
{
lv_obj_t *first_btn = lv_obj_get_child(lv_obj_get_child(cont, 0), 0); // 获取第一个cell里面的第一个子对象(imgbtn)
if (first_btn)
{
lv_obj_set_style_bg_color(first_btn, lv_color_hex(0x4A90D9), LV_PART_MAIN);
lv_obj_set_style_bg_opa(first_btn, LV_OPA_60, LV_PART_MAIN);
lv_obj_set_style_border_width(first_btn, 2, LV_PART_MAIN);
lv_obj_set_style_border_color(first_btn, lv_color_hex(0x2E5C8A), LV_PART_MAIN);
selected_btn = first_btn;
focus_target = first_btn;
if (node_count > 0)
curr_menu = node_map[0]; // 当前菜单节点更新为第一个节点
}
}
else if (focus_target != NULL) // 如果找到需要恢复焦点的按钮
{
curr_menu = pending_focus_node; // 更新当前菜单节点为待恢复焦点节点
}

pending_focus_node = NULL; // 清空待恢复焦点标记,防止下次创建菜单复用旧值

if (selected_btn != NULL && menu_group != NULL)
lv_group_focus_obj(selected_btn); // 将输入组焦点切换到选中按钮(编码器导航生效)

return cont; // 返回外层网格容器,外部负责管理/销毁这个菜单
}
12. 初始化菜单组,将所有的输入的设备 绑定到这个菜单组
13.按键回调:根据事件获取按键id,根据id获取按键文字,根据文字调用 上下,左右的函数

posted on 2026-10-09 17:29  一起疯狂  阅读(14)  评论(0)    收藏  举报

导航