尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

LVGL菜单控件实战:嵌入式设备UI开发高效指南

发布时间:2026/9/28 16:23:08

资讯中心
01
ARTICLE

LVGL菜单控件实战:嵌入式设备UI开发高效指南

LVGL菜单控件实战:嵌入式设备UI开发高效指南
做嵌入式开发这几年我越来越觉得UI设计是一道绕不过去的坎。不管是仪表盘、智能家电还是手持设备用户第一眼看到的就是界面菜单做得好不好直接决定产品给人的印象。早期我用过几款商业GUI方案效果是不错但一谈到授权费和内存占用就只能劝退。后来转到开源方案对比了一圈LVGL在性能和资源消耗之间平衡得最好而且具备一套设计完善的控件库。不过真正让我决定深入研究的是它的菜单控件在复杂界面切换时依然能保持流畅刷新这一点对小内存单片机来说非常关键。这篇内容我打算围绕“LVGL菜单控件实战”来写从整体构思到具体落地把菜单控件的设计思路和完整代码细节拆开讲清楚。语言尽量大白话凡是编译不过、运行异常、显示不对的地方我都会把排查思路和坑点一并放出来。文章里的代码可以直接作为一个基础模板使用稍加扩展就能套到自己的项目中。如果你正打算在嵌入式设备上做UI界面、想让界面交互更顺手、或者刚开始接触LVGL移植和控件使用这篇应该能省下你不少摸索时间。1. 内容整体设计与思路拆解1.1 为什么选LVGL做嵌入式菜单界面先聊一个朴素的问题嵌入式设备上的界面和手机或者电脑上的界面有什么本质区别核心区别在资源。手机动不动就是八核CPU加几GB内存渲染界面可以非常随意但嵌入式设备经常只有几百KB内存CPU主频也有限甚至有的还要求界面刷新能实时跟上传感器数据变化。这就决定了嵌入式UI必须同时满足两个条件渲染足够快内存足够省。LVGL在这两点上做得相当出色。它的整个绘制流程基于脏矩形和裁剪算法意思是每一帧只绘制变化的部分而不是把整个屏幕重新刷一遍。举个最简单的例子数字时钟界面里只有秒钟在变那一帧就只会重绘秒数的区域。这个机制对降低CPU负载和功耗都有直接帮助。菜单控件其实承担了整个界面系统的“骨架”功能——无论是设置项、音量调节还是模式选择几乎都能归纳成一组菜单项用户选中某个选项后进入二级页面或者执行动作。所以把菜单控件玩熟基本上就掌握了LVGL交互的入门钥匙。而且LVGL的菜单控件支持复杂的层级跳转、滚动动画、按键和触摸响应写起来不需要从零造轮子。我在实际项目里还发现LVGL的community非常活跃各种平台都有移植示例比如在FreeRTOS任务里跑LVGL、在Linux的VSCode环境里直接模拟调试、还有一些现成的9.x模拟器工程基本能避开很多从环境搭建就开始踩坑的问题。1.2 菜单方案选型与整体架构LVGL本身提供了不少控件tab控件适合做多页签切换list控件适合做普通列表基于这两个控件就可以组合出一套高度可复用的菜单框架。更复杂的项目还可以用lv_menu控件直接生成多级菜单它具备自动展开、滚动条记忆、页面动画等高级能力。不过说实话lv_menu的API封装层级比较深上手成本会高一点。对大多数产品场景来说用tab控件加list控件亲手搭一套菜单框架反而更可控也方便针对自己的需求植入动画和主题。整套菜单框架我建议分成四层每一层的职责尽量单一第一层是设备初始化层负责屏幕、触摸和定时心跳确保LVGL可以稳定运行。第二层是样式与主题层定义菜单项的颜色、边距、字体、圆角以及聚焦高亮时的变化效果。第三层是菜单数据层将不同菜单项所需的文字和图标以数组形式组织起来便于后期扩展。第四层是事件交互层处理用户通过触摸或按键选中某个菜单项后的跳转、刷新和回调逻辑。这种分层方式的好处很明显换屏幕尺寸时只需调整样式层加功能模块时只需在数据层追加条目不用反复改动主流程代码。这算是做嵌入式UI的一条基础经验尽可能把界面数据和界面逻辑解耦后面维护起来会轻松很多。2. 核心细节解析与实操要点2.1 菜单项的数据结构与控件选择一个务实的菜单数据模型不应该只有一串字符串。因为真实产品中一个菜单项通常还包含图标、英文标识、绑定的功能ID、是否可选中、次级页面类型等。基于这个考虑我设计了一个非常简单的结构体实际项目中你完全可以在此基础上扩充。typedef struct { const char *name; // 菜单名称如“亮度调节” const void *icon; // 图标或图片指针可以存空 uint16_t id; // 功能ID用于区分点击回调 uint8_t is_selectable; // 是否允许被选中灰色项不能选 } menu_item_t;这个结构体在存储紧张的单片机上也可以跑因为指针只占4字节Cortex-M平台为例字符串本身则存放在flash区不会占用运行内存。一个小技巧是在字符串定义时尽量用const修饰并放在常量区避免拷贝到RAM中。控件选择上lv_list是相对容易操作的基础控件。它的每一行都可以绑定一个图标和文本并且自带活动状态管理。如果界面需要的是底部导航或者横滑页签lv_tabview会更顺手它天然支持多个页面之间的滑动切换动画效果也自带。我的经验是层级菜单需求用lv_list底部导航式用lv_tabview两者也可以混合使用。比如某款智能家居面板底部是三个tab页每个tab页内用list承载具体设备项这在实际项目中非常常见。2.2 样式、主题与字体处理LVGL从8.x开始引入主题机制用户可以通过lv_theme_default_init快速应用一套默认主题也可以自定义每个控件的样式。菜单UI观感好不好很大程度取决于空间节奏和焦点高亮设计而不是单纯的颜色花哨。我常用的聚焦高亮做法是给列表项添加一个独立的过渡色。比如默认状态背景色为白色文字为深灰色当某一项被选中时背景色切换为品牌蓝文字变为白色同时让背景色带一点渐变效果。LVGL里设置渐变非常直接只需为背景添加lv_grad_dsc_t描述并在lv_style_set_bg_opa中开启即可。但要注意渐变会增加一小部分计算量如果CPU太弱又只是简单UI尽量不用免得为了眼感牺牲流畅度。字体也是个容易踩坑的地方。LVGL的默认字体往往不含中文字符运行中会直接显示乱码或方框。解决思路通常有三种一是用LVGL官方提供的字体转换工具把TTF字体转为C数组并嵌入工程二是只嵌入项目用到的几十个汉字体积能压得很小三是使用片外flash或文件系统动态加载字体适合字体集合特别大的场景。我个人的建议是如果是200个以内的固定汉字直接通过转换工具生成自定义字体既不占用运行时内存也不影响刷新速度。还有一个细节是圆角处理。小屏幕设备上过于尖锐的直角会显得不精致但圆角过大又可能在圆角区域出现锯齿。一个安全做法是设置radius为8到12像素这是大部分嵌入式小屏上观感与性能的折中区间。同时在图片或圆角背景上尽量避免半透明效果因为半透明混合会带来额外绘制开销。3. 实操过程与核心环节实现3.1 初始化LVGL环境与屏幕驱动要用LVGL第一步是把它跑到一个具体的硬件环境或PC模拟器上。我平常最常用的是PC模拟器做UI逻辑验证因为编译快、调试方便可以直接用VSCode打开项目环境搭好后点击运行就能看到界面。PC模拟器的项目入口本质上和单片机LLVM一致底层只是替换了显示输出和输入设备驱动大部分UI代码可以直接复用或移植到目标板。下面是一段标准初始化代码适配了较新的LVGL 9.x API。如果你还在用LVGL 8.x差别主要是对象创建时的lv_obj_class调用方式会有小差异。#include lvgl.h #include lv_drivers/display/monitor.h #include lv_drivers/indev/mouse.h int main(void) { lv_init(); monitor_init(); mouse_init(); lv_display_t *disp lv_display_create(MONITOR_HOR_RES, MONITOR_VER_RES); lv_display_set_flush_cb(disp, monitor_flush); lv_indev_t *mouse_indev lv_indev_create(); lv_indev_set_type(mouse_indev, LV_INDEV_TYPE_POINTER); lv_indev_set_read_cb(mouse_indev, mouse_read); lv_indev_set_display(mouse_indev, disp); /* 创建主界面 */ create_menu_screen(); while (1) { uint32_t time_till_next lv_timer_handler(); SDL_Delay(time_till_next); } }如果在单片机上运行流程也差不多。只需要把monitor_flush换成自己的LCD屏flush回调再把mouse_read换成触摸屏或按键设备读取。lv_timer_handler()会被放在主循环或者FreeRTOS的低优先级任务中保持周期性调用它就是整个LVGL的心跳。3.2 创建菜单列表与填充数据有了环境我直接创建一个基于lv_list的主菜单。在实际产品中这个页面可能是“系统设置”、“设备状态”、“关于”等入口的集合。菜单数据我按上一节的结构体来定义static const menu_item_t main_menu_items[] { {亮度调节, icon_brightness, 1, 1}, {音量设置, icon_volume, 2, 1}, {设备信息, icon_info, 3, 1}, {恢复出厂, icon_reset, 4, 0}, }; #define MAIN_MENU_ITEM_COUNT (sizeof(main_menu_items) / sizeof(main_menu_items[0]))然后创建列表并填充内容。这段代码要注意的地方是列表项的点击事件最终都汇聚到同一个回调函数中通过item-id来区分具体执行的动作。这也是嵌入式UI比较推荐的事件分发写法避免为每个菜单项单独注册回调。static void menu_event_cb(lv_event_t *e) { lv_obj_t *obj lv_event_get_current_target(e); uint16_t id (uint16_t)(uintptr_t)lv_obj_get_user_data(obj); lv_obj_t *scr lv_obj_get_screen(obj); switch (id) { case 1: /* 跳转到亮度设置子页面 */ create_brightness_screen(scr); break; case 2: /* 跳转到音量设置子页面 */ create_volume_screen(scr); break; case 3: /* 弹窗显示设备信息 */ lv_label_set_text(info_label, Device: IoT-Board\nVersion: 1.0.0); lv_obj_clear_flag(info_popup, LV_OBJ_FLAG_HIDDEN); break; case 4: /* 二次确认后恢复出厂 */ break; default: break; } } static void create_menu_screen(void) { lv_obj_t *scr lv_scr_act(); lv_obj_t *list lv_list_create(scr); lv_obj_set_size(list, LV_PCT(100), LV_PCT(100)); lv_obj_set_style_pad_all(list, 12, 0); for (int i 0; i MAIN_MENU_ITEM_COUNT; i) { lv_obj_t *btn lv_list_add_button(list, main_menu_items[i].icon, main_menu_items[i].name); lv_obj_set_user_data(btn, (void *)(uintptr_t)main_menu_items[i].id); lv_obj_add_event_cb(btn, menu_event_cb, LV_EVENT_CLICKED, NULL); } }3.3 子页面返回与导航逻辑实际菜单一定会有二级页面那就必须处理返回逻辑。LVGL没有默认的系统级导航栈但我们可以用手动方式维护一个简单栈。我的做法是用一个数组记录当前打开的页面对象返回时删除或隐藏当前页面即可。static lv_obj_t *page_stack[8]; static uint8_t page_stack_depth 0; static void page_push(lv_obj_t *page) { if (page_stack_depth 8) { page_stack[page_stack_depth] page; } } static void page_pop(void) { if (page_stack_depth 0) return; lv_obj_t *page page_stack[--page_stack_depth]; lv_obj_delete(page); }在create_brightness_screen等子页面函数中把新建的页面对象压栈并在页面内放置一个返回按钮点击时调用page_pop()。这个思路虽然简单但在大多数中小型项目中足够用不需要引入复杂的路由框架。3.4 键盘或实体按键控制与焦点管理很多嵌入式设备不依赖触摸而是通过实体按键上下选择、确认返回。LVGL对键盘和按键输入支持得比较到位核心机制是焦点focus。为了让按键能控制列表项需要先确保列表对象可以获得焦点然后将输入设备绑定到屏幕以ST7920按键屏也有相似的处理方式。下面这段代码演示了如何把读到的按键编码转换为LVGL键盘事件。static void keypad_read(lv_indev_t *indev, lv_indev_data_t *data) { uint8_t key read_keypad_gpio(); static uint8_t last_key 0; >static void slide_in_anim(void *obj, int32_t v) { lv_obj_set_x(obj, v); } static void anim_create_slide_in(lv_obj_t *obj) { lv_anim_t a; lv_anim_init(a); lv_anim_set_var(a, obj); lv_anim_set_values(a, LV_HOR_RES, 0); lv_anim_set_time(a, 250); lv_anim_set_exec_cb(a, slide_in_anim); lv_anim_set_path_cb(a, lv_anim_path_ease_out); lv_anim_start(a); }还有一类低成本的视觉提升方法给页面容器加上合理的背景色和阴影给菜单列表加上细分割线适当使用lv_obj_set_style_bg_opa和lv_obj_set_style_border_width等属性调整细节。这些不会对性能带来明显压力但会让整个UI显得更“产品化”。4. 常见问题与排查技巧实录4.1 中文乱码问题中文乱码基本是LVGL新手最容易遇到的问题因为LVGL默认英文字体里没有中文字形。查看方式很简单运行程序后看屏幕上的汉字是不是一堆方框或者“?”。解决办法就是按我前面说的嵌入自定义中文字体。用官方转换工具时记得把需要包含的字符集放到工具里再生成然后放到项目中通过以下代码注册到LVGL的全局字体配置lv_obj_set_style_text_font(list, my_cn_font_16, 0);这个步骤很容易忽略。如果忘了在全局theme里设置中文字体后续所有控件依然会调用默认字体导致所有地方都乱码。比较稳妥的做法是在lv_theme_default_init之后再统一设置全局默认字体lv_display_t *disp lv_display_get_default(); lv_theme_t *th lv_theme_default_init(disp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_dark(LV_PALETTE_BLUE), false, my_cn_font_16); lv_display_set_theme(disp, th);4.2 编译内存溢出与优化方法嵌入式环境下最容易的说法是RAM不够因为LVGL每个对象都有结构体内存对象一多堆占用会明显上升。排查方法有两个一是打开LVGL自带的内存监控函数lv_mem_monitor打印当前堆使用情况二是检查是否有对象创建后没有删除造成内存泄漏。菜单页如果反复进入和退出我建议确认lv_obj_delete被正确调用否则这些页面虽然不可见但内存一直被占用。另一个常见问题是大量使用lv_label_set_text时多次触发字符串动态分配导致碎片。解决思路是预先设置相同长度的文本buffer或者用lv_label_set_text_fmt时尽量做格式限制。实际的嵌入式优化中还可以调整LV_MEM_SIZE宏定义来适配自己的项目但不要无脑增大要根据实际内存监控结果做配置。4.3 触摸漂移或点击位置不准触摸屏校准不准确会出现点不到目标菜单项的情况。LVGL的输入设备回调里可以增加坐标偏移和比例校正在read_cb中当场处理data-point.x (lv_coord_t)(raw_x * 1.2 5);>lv_obj_set_size(list, LV_PCT(100), LV_PCT(100)); lv_obj_set_style_pad_row(list, 8, 0);如果容器里还有额外的header、footer就需要用lv_obj_set_size(list, LV_PCT(100), LV_PCT(85))这类方式留出空间。滚动条最好保持默认开启用户滑到哪个位置会有明确反馈。5. 扩展思考从菜单到完整嵌入式UI框架5.1 菜单只是一个起点能跑通菜单控件意味着你已经掌握了LVGL中最关键的交互模式。再往后无论是做曲线、仪表盘、弹窗、键盘输入都不需要从零开始。LVGL生态把这些高级控件都封装得很完整比如图表控件可直接绘制动态波形仪表盘控件适合做速度表键盘控件则在密码输入场景非常方便。所以菜单项目是你建立自己嵌入式UI组件库的“地基”之后就好办了。5.2 多任务协作与界面刷新嵌入式设备通常不只有一个界面任务同时还要采集传感器、执行控制逻辑。如果这些任务都堆积在一个循环里界面会卡顿数据更新也会不及时。常见做法是LVGL跑在单独任务或主循环其他业务逻辑跑在独立任务中通过队列或共享变量的方式与UI交互。比如传感器数据到了放到一个环形缓冲区UI层定时读取并刷新图表各司其职稳定很多。5.3 项目目录结构建议工程越来越大的时候我通常建议按模块拆分源文件而不是把所有代码堆在一个main.c里。比如ui/放所有界面相关文件比如screen_menu.c、screen_setting.cui/fonts/放字体和图片资源ui/components/放自定义可复用控件比如自定义滑动条、弹窗app/放业务逻辑driver/放屏幕、触摸、按键等底层驱动这样新同事接手项目时能快速定位到对应模块自己过几个月再回来看代码也不会一头雾水。6. 我的个人踩坑与最终建议我刚开始接触LVGL时最大的教训是急着堆需求、贪大求全结果界面做得看起来很复杂但在低主频芯片上卡成PPT。后面逐渐懂得了一个道理嵌入式UI的成功不在于功能多少而在于交互是否顺滑、内存是否够用、代码是否可控。每完成一个页面我都会在目标平台上实测一次帧率和内存发现问题马上改而不是积累到最后一起处理。在做菜单项目的过程中我还摸索出一个经验先在PC模拟器上用纯鼠标把交互逻辑全部调好再移植到单片机。PC模拟器里调试效率和可视化都比硬件好太多等硬件回来后重点专注驱动适配和性能优化整体开发周期可以压缩一大半。另外对于LVGL的版本建议初学者直接上较新的9.x版本虽然网上大量资料还停留在8.x但9.x的API更规范后续维护也更长远。如果遇到某个函数不生效先去官方docs确认对应版本用法别盲抄旧教程。我自己的很多坑都是因为版本和代码不匹配才踩出来的。最后再分享一个小技巧在做菜单UI时可以把常用颜色、间距、字体统一定义成宏或常量例如#define UI_COLOR_MAIN lv_color_hex(0x1E90FF) #define UI_COLOR_BG lv_color_hex(0xF5F5F5) #define UI_RADIUS_DEFAULT 10 #define UI_PAD_STANDARD 12整体界面风格调起来会非常高效也容易在不同产品间复用不必每次打开源文件逐个改样式。这套火锅式的经验我用了多个项目至少能让你的排错时间缩减一半。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。