ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LVGL容器与布局实战:ESP32上Flex与Grid自适应UI

LVGL容器与布局实战:ESP32上Flex与Grid自适应UI 这次我们来看 LVGL 在 ESP32 上真正容易卡住的一块容器与布局。屏幕一旦超过三五个控件继续用绝对坐标一个个lv_obj_set_pos摆放就会变得很难维护。小屏幕还好一旦遇到分辨率不同的屏幕、横竖屏切换或者后期要加按钮坐标全部要重算。LVGL 的容器和布局机制就是为了把这件事系统化把“每个控件放在哪”交给布局规则去算。先说它最核心的几个能力容器支持嵌套可以像 Web 前端的div一样把屏幕划分成若干区域内置 Flex 布局类似 CSS Flexbox可以控制子对象沿横向或纵向排列、换行、居中对齐内置 Grid 布局类似 CSS Grid可以定义行和列的尺寸比例把子对象精确放进网格单元lv_obj本身就是容器容器任意控件都可以作为父对象承载其他控件父子关系决定了样式继承和事件传递布局不依赖屏幕分辨率写死坐标换屏或旋转后只需重新计算布局适配成本低很多。这篇文章会从 LVGL 容器的本质讲起再分别给出 Flex 布局和 Grid 布局的完整代码示例最后带一个 ESP32 上的综合布局实战和踩坑排查清单。如果你正在用 ESP-IDF 或 Arduino LVGL 做屏幕 UI这篇可以直接对照着抄。1. LVGL 容器与布局核心能力速览能力项说明适用平台ESP32、ESP32-S3 等支持 LVGL 移植的嵌入式平台前置依赖LVGL 图形库、显示屏驱动、输入设备驱动可选核心功能普通容器创建、Flex 布局、Grid 布局、容器嵌套、样式继承、事件传递布局 APIlv_obj_set_flex_flow、lv_obj_set_flex_align、lv_obj_set_grid_dsc_array、lv_obj_set_grid_cell等资源占用由屏幕分辨率、缓冲区大小和对象数量决定布局本身不增加明显额外显存支持版本本文代码以 LVGL 8.3 语法为例9.x 需要对照官方 API 迁移启动方式集成到 ESP-IDF / Arduino 工程后随固件启动是否支持批量任务支持多个同结构子对象可批量创建并统一交给布局管理适合场景仪表盘、菜单页、设置页、多按钮控制面板、卡片式信息展示从实际界面开发角度看容器和布局解决的是“控件之间的相对关系”。这个关系一旦由布局算法维护就不需要你在代码里关心某个按钮的像素坐标也方便后续调整间距、边距和对齐方式。2. 理解 LVGL 容器lv_obj 本身就是容器先建立一个重要认知LVGL 里没有一种专门叫“容器”的特殊控件确切地说lv_obj是所有控件的基类同时它本身就是一个通用容器。你在屏幕上创建任何一个对象它都可以承载子对象。常见的容器创建方式/* 创建一个普通容器作为父容器 */ lv_obj_t *cont lv_obj_create(lv_scr_act()); lv_obj_set_size(cont, 240, 120); lv_obj_align(cont, LV_ALIGN_CENTER, 0, 0);把子对象加进容器后子对象位置可以手动设置也可以交给布局管理/* 在容器内手动添加一个按钮不启用布局时坐标完全手动控制 */ lv_obj_t *btn lv_btn_create(cont); lv_obj_set_pos(btn, 10, 10);这正是很多人刚开始用 LVGL 时的默认做法。手动控制坐标在小界面、固定屏幕下没问题但它有两个隐患屏幕旋转或分辨率变化时坐标需要大量重算子对象数量变多后父子对象的坐标管理容易混乱尤其是继承pad边距时。容器的价值在于当你把子对象放进去并给容器设置了 Flex 或 Grid 布局子对象的坐标就不再需要你干预。布局算法会在容器内部按规则排列子对象并根据容器尺寸自动计算每个子对象的位置和尺寸。3. 启用心须的配置lv_conf.h 里的布局开关在写代码之前先检查 LVGL 的配置文件lv_conf.h。布局功能不是默认全部开启的如果宏没有正确打开布局 API 会被编译成空操作或者直接报未定义。LVGL 8.3 中需要确认#define LV_USE_FLEX 1 #define LV_USE_GRID 1LV_USE_FLEX控制 Flex 布局LV_USE_GRID控制 Grid 布局。如果是从官方仓库拉取 LVGL通常需要先复制一份lv_conf_template.h为lv_conf.h。ESP-IDF 工程里一般放在main目录下或components/lvgl中。Arduino LVGL 库的情况下lv_conf.h同样需要手动放入 Arduino 库目录的src目录或用户指定的配置目录。遗漏开关的表现往往是调用lv_obj_set_flex_flow后界面没有任何变化编译也能通过因为部分情况下宏关掉后 API 直接不生效。遇到布局不生效第一步先查这个开关。4. Flex 布局横排、竖排、换行与对齐Flex 布局适合用在工具栏、按钮组、状态栏这类“一排元素按顺序摆放”的场景。它的核心是把父容器变成一个弹性盒子子对象沿主轴排列。4.1 基础横向排列下面代码创建一个横向排列三个按钮的容器/* 创建父容器 */ lv_obj_t *cont lv_obj_create(lv_scr_act()); lv_obj_set_size(cont, 240, 60); lv_obj_align(cont, LV_ALIGN_TOP_MID, 0, 10); /* 启用 Flex 布局设置主轴为横向、不换行 */ lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); /* 连续添加三个按钮 */ for (int i 0; i 3; i) { lv_obj_t *btn lv_btn_create(cont); lv_obj_set_size(btn, 60, 40); lv_obj_t *label lv_label_create(btn); lv_label_set_text_fmt(label, B%d, i 1); lv_obj_center(label); }lv_obj_set_flex_flow的可选值LV_FLEX_FLOW_ROW横向排列不换行LV_FLEX_FLOW_ROW_WRAP横向排列超出换行LV_FLEX_FLOW_COLUMN纵向排列不换行LV_FLEX_FLOW_COLUMN_WRAP纵向排列超出换行。lv_obj_set_flex_align的三个参数分别控制主轴上的对齐方式、交叉轴上的对齐方式和交叉轴多行时的对齐方式。三个参数都填LV_FLEX_ALIGN_CENTER是常见的居中配置。4.2 自动换行排列如果你的界面需要展示一组间距均匀的标签列表可以用LV_FLEX_FLOW_ROW_WRAP再配合pad边距让子对象之间有间隔lv_obj_t *tag_cont lv_obj_create(lv_scr_act()); lv_obj_set_size(tag_cont, 200, 120); lv_obj_align(tag_cont, LV_ALIGN_CENTER, 0, 0); lv_obj_set_flex_flow(tag_cont, LV_FLEX_FLOW_ROW_WRAP); lv_obj_set_flex_align(tag_cont, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START); /* 设置容器内边距控制子对象与容器边缘的间距 */ lv_obj_set_style_pad_row(tag_cont, 8, 0); lv_obj_set_style_pad_column(tag_cont, 8, 0); for (int i 0; i 8; i) { lv_obj_t *lbl lv_label_create(tag_cont); lv_label_set_text_fmt(lbl, Tag %d, i 1); lv_obj_set_style_pad_all(lbl, 4, 0); lv_obj_set_style_bg_color(lbl, lv_palette_lighten(LV_PALETTE_BLUE, 3), 0); lv_obj_set_style_bg_opa(lbl, LV_OPA_COVER, 0); }pad_row和pad_column是 Flex 布局下控制行间距和列间距的关键属性。注意这两个 API 是 LVGL 8.3 的写法9.x 中部分样式属性更名的所以以你当前使用的 LVGL 版本手册为准。4.3 Flex 布局中的 grow 属性Flex 布局还有一个非常有用的特性子对象可以设置flex_grow让它自动拉伸占满剩余空间。lv_obj_t *btn_ok lv_btn_create(cont); lv_obj_set_size(btn_ok, 80, 40); lv_obj_set_flex_grow(btn_ok, 1); /* 占据剩余空间 */ lv_obj_t *btn_cancel lv_btn_create(cont); lv_obj_set_size(btn_cancel, 80, 40);这段代码会让“确定”按钮占据剩余的空间宽度而“取消”按钮保持固定宽度。在自适应宽度场景下非常实用。5. Grid 布局按行列网格定位子对象Grid 布局适合需要精确控制行列的界面比如键盘、数字小键盘、仪表盘中的网格卡片区。它的用法比 Flex 更繁琐一些但控制力更强。5.1 基础 3x3 网格/* 创建网格容器 */ lv_obj_t *grid_cont lv_obj_create(lv_scr_act()); lv_obj_set_size(grid_cont, 240, 200); lv_obj_align(grid_cont, LV_ALIGN_CENTER, 0, 0); /* 定义列描述每列的宽度策略 */ static lv_coord_t col_dsc[] {LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST}; /* 定义行描述三行内容行最后加结束标记 */ static lv_coord_t row_dsc[] {LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST}; lv_obj_set_grid_dsc_array(grid_cont, col_dsc, row_dsc);这里LV_GRID_FR(1)表示按比例分配剩余空间三列各占 1/3。LV_GRID_TEMPLATE_LAST是必须的结束标记。除了LV_GRID_FR还可以用固定像素值static lv_coord_t col_dsc[] {60, LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST};这表示第一列固定 60 像素第二列占剩余空间。5.2 把子对象放进网格单元列和行描述定义好后每个子对象需要告诉容器它要放在第几列、第几行for (int row 0; row 3; row) { for (int col 0; col 3; col) { lv_obj_t *btn lv_btn_create(grid_cont); lv_obj_set_size(btn, 50, 40); lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_CENTER, col, 1, LV_GRID_ALIGN_CENTER, row, 1); lv_obj_t *label lv_label_create(btn); lv_label_set_text_fmt(label, %d%d, row, col); lv_obj_center(label); } }lv_obj_set_grid_cell的参数含义很重要lv_obj_set_grid_cell(obj, LV_GRID_ALIGN_CENTER, /* 水平对齐方式 */ col, /* 所在列起点 */ 1, /* 跨几列 */ LV_GRID_ALIGN_CENTER, /* 垂直对齐方式 */ row, /* 所在行起点 */ 1); /* 跨几行 */只要行列号不超出容器描述的范围子对象就会被放进对应网格单元。5.3 网格跨列合并Grid 布局支持合并单元格比如顶部一个跨三列的标题栏下面三列再放三个按钮lv_obj_t *grid_cont lv_obj_create(lv_scr_act()); lv_obj_set_size(grid_cont, 240, 200); lv_obj_align(grid_cont, LV_ALIGN_CENTER, 0, 0); static lv_coord_t col_dsc[] {LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST}; static lv_coord_t row_dsc[] {50, LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST}; lv_obj_set_grid_dsc_array(grid_cont, col_dsc, row_dsc); /* 跨三列的标题 */ lv_obj_t *header lv_label_create(grid_cont); lv_label_set_text(header, Control Panel); lv_obj_set_grid_cell(header, LV_GRID_ALIGN_CENTER, 0, 3, LV_GRID_ALIGN_CENTER, 0, 1); /* 三个按钮分别位于第二行三列 */ for (int col 0; col 3; col) { lv_obj_t *btn lv_btn_create(grid_cont); lv_obj_set_size(btn, 50, 40); lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_CENTER, col, 1, LV_GRID_ALIGN_CENTER, 1, 1); }header使用跨 3 列的方式一行代码就实现了顶部通栏效果。这在 Flex 布局里实现起来要绕一些而 Grid 是天然支持的。6. 容器嵌套构建真正的仪表盘布局单一布局模式在真实 UI 中往往不够用。实际界面通常是“左侧菜单 右侧内容区”“顶部状态栏 底部按钮区”这样的结构。这时需要容器嵌套。下面是一个典型的 ESP32 仪表盘布局顶部状态栏显示 Wi-Fi、电量等中间左侧纵向菜单栏中间右侧内容卡片区底部三个操作按钮。6.1 创建根容器并启用 Flex 纵向布局/* 根容器铺满全屏 */ lv_obj_t *root lv_obj_create(lv_scr_act()); lv_obj_set_size(root, 240, 320); lv_obj_align(root, LV_ALIGN_TOP_LEFT, 0, 0); lv_obj_set_style_pad_all(root, 0, 0); /* 纵向排列三个子区域状态栏、中间区域、底部按钮区 */ lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(root, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);6.2 顶部状态栏lv_obj_t *status_bar lv_obj_create(root); lv_obj_set_size(status_bar, 240, 30); lv_obj_set_style_pad_all(status_bar, 4, 0); lv_obj_t *wifi_icon lv_label_create(status_bar); lv_label_set_text(wifi_icon, Wi-Fi ON); lv_obj_align(wifi_icon, LV_ALIGN_LEFT_MID, 5, 0); lv_obj_t *battery_label lv_label_create(status_bar); lv_label_set_text(battery_label, BAT 78%); lv_obj_align(battery_label, LV_ALIGN_RIGHT_MID, -5, 0);状态栏内部没有启用 Flex而是用lv_obj_align做了左、右两个锚点。这说明容器布局并非所有场景都要用 Flex混合使用往往更高效。6.3 中间区域Flex 横向分成左菜单和右内容lv_obj_t *middle lv_obj_create(root); lv_obj_set_size(middle, 240, 220); lv_obj_set_style_pad_all(middle, 0, 0); lv_obj_set_flex_flow(middle, LV_FLEX_FLOW_ROW); /* 左侧菜单容器 */ lv_obj_t *menu_panel lv_obj_create(middle); lv_obj_set_size(menu_panel, 70, 220); lv_obj_set_flex_flow(menu_panel, LV_FLEX_FLOW_COLUMN); const char *menu_items[] {Home, Data, Set}; for (int i 0; i 3; i) { lv_obj_t *menu_btn lv_btn_create(menu_panel); lv_obj_set_size(menu_btn, 60, 40); lv_obj_t *menu_label lv_label_create(menu_btn); lv_label_set_text(menu_label, menu_items[i]); lv_obj_center(menu_label); } /* 右侧内容容器 */ lv_obj_t *content_panel lv_obj_create(middle); lv_obj_set_size(content_panel, 170, 220); lv_obj_set_flex_flow(content_panel, LV_FLEX_FLOW_COLUMN); for (int i 1; i 3; i) { lv_obj_t *card lv_obj_create(content_panel); lv_obj_set_size(card, 140, 50); lv_obj_t *card_label lv_label_create(card); lv_label_set_text_fmt(card_label, Sensor %d, i); lv_obj_center(card_label); }6.4 底部按钮区lv_obj_t *bottom_bar lv_obj_create(root); lv_obj_set_size(bottom_bar, 240, 50); lv_obj_set_flex_flow(bottom_bar, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(bottom_bar, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); const char *bottom_btns[] {Start, Stop, Reset}; for (int i 0; i 3; i) { lv_obj_t *btn lv_btn_create(bottom_bar); lv_obj_set_size(btn, 60, 40); lv_obj_t *lbl lv_label_create(btn); lv_label_set_text(lbl, bottom_btns[i]); lv_obj_center(lbl); }这个嵌套结构下根容器负责纵向排三个区中间区再横向分成两个子容器子容器内部再各自使用 Flex。整个界面没有出现任何手工像素坐标替换成 320x240 屏幕时只需要改根容器和中间容器的尺寸内部排布自动适配。7. ESP32 上的显示效果与资源验证布局代码写完之后关键问题是它在 ESP32 上能不能流畅运行。验证过程需要围绕三个方面7.1 确认屏幕驱动正常布局的显示效果依赖底层屏幕驱动。建议先跑 LVGL 自带的 demo 或一个最简单的“创建一个 label 放在屏幕中间”样例确认屏幕能正常点亮和刷新再叠加容器布局代码。如果直接上完整布局屏幕不亮时很难分清是驱动问题还是布局问题。7.2 检查刷新率和帧率LVGL 在 ESP32 上可以通过日志观察刷新状态。在lv_conf.h中确认#define LV_USE_LOG 1 #define LV_LOG_LEVEL LV_LOG_LEVEL_INFO然后调用lv_timer_handler();通常放在while(1)或esp_timer回调里每个周期执行一次。观察日志中是否有异常比如刷新超时、碎片跳过等。如果界面掉帧明显优先检查屏幕驱动的 SPI 时钟频率是否过高图形缓冲区大小是否过小是否有其他高优先级任务频繁打断 LVGL 刷新。7.3 观察内存占用ESP32 上的图形缓冲区是最关键的内存消耗点。常见的双缓冲配置会直接占用2 * 缓冲区宽度 * 缓冲区高度 * 每个像素字节数。一张 240x320、RGB565 的屏幕如果配置双缓冲大约要占用 300KB 以上的 PSRAM 或内部 RAM。LVGL 可以通过以下方式打印内存状态lv_mem_monitor_t mon; lv_mem_monitor(mon); ESP_LOGI(LVGL, used: %d, free: %d, frag: %d%%, (int)mon.total_size - (int)mon.free_size, (int)mon.free_size, (int)mon.frag_pct);在布局代码里每创建一个容器或按钮used都会增长。如果free持续走低、frag_pct升高说明你在界面创建阶段频繁分配内存后续可以考虑将创建的对象定义为static避免堆上反复分配释放减少不必要的lv_obj_create嵌套层级为字体、图片声明静态资源而不是动态解码使用样式复用相同样式的组件共享lv_style_t不要让每个对象都创建独立样式。7.4 验证不同坐标系下的布局适配ESP32 常见屏幕尺寸有 1.3 寸、1.8 寸、2.4 寸、3.5 寸等。同一个布局代码切换到不同尺寸屏幕时重点检查根容器尺寸是否跟着屏幕尺寸调整最外层容器是否设置了LV_ALIGN_TOP_LEFT 满宽满高还是使用了lv_obj_set_align(lv_scr_act(), ...)内部容器是否使用了lv_obj_set_size固定值导致小屏幕上被裁剪。更稳妥的做法是把屏幕宽高定义成宏#define SCREEN_HOR_RES 240 #define SCREEN_VER_RES 320 lv_obj_set_size(root, SCREEN_HOR_RES, SCREEN_VER_RES);换屏幕时只需要改宏不用动布局关系。8. 常见问题与排查方法问题现象可能原因排查方式解决方案Flex API 调用后无任何效果lv_conf.h中LV_USE_FLEX为 0检查宏开关打开LV_USE_FLEX后重新编译Grid API 调用后无任何效果LV_USE_GRID为 0检查宏开关打开LV_USE_GRID后重新编译子对象位置重叠在一起容器创建后未设置尺寸或者子对象未设置尺寸打印容器和子对象的get_width/get_height先设置容器尺寸再创建子对象子对象排列顺序错乱在启用 Flex 后手动设置lv_obj_set_pos坐标被布局覆盖检查代码中是否混用布局和手动坐标Flex 布局下不要手动设置坐标统一交给布局管理间距过大或过小pad_row/pad_column未设置或设置过大检查lv_obj_set_style_pad_*参数调小间距值Grid 子对象不在预期位置行列描述数组长度和lv_obj_set_grid_cell中的行列号不匹配逐个调试子对象的 grid cell 参数统一行列号确保不超出描述范围容器背景盖住子对象容器默认有背景色和边框且不透明检查容器样式设置lv_obj_set_style_bg_opa(cont, LV_OPA_TRANSP, 0)布局切换后对象不刷新布局 API 调用后未触发刷新检查是否有调用lv_obj_invalidate一般创建完成后调用lv_obj_invalidate可强制重绘屏幕显示异常花屏SPI 速率过快或缓冲区配置不合理降低 SPI 频率、调整缓冲区大小先降到较低频率验证再逐步提高内存逐步耗尽布局中反复创建销毁对象没有释放用lv_mem_monitor监控内存复用对象避免动态创建销毁LVGL 9.x 编译报错布局 API 名称和参数与 8.3 不同对照 9.x 官方文档使用当前版本的 API 或回退到 8.3其中最容易忽略的是容器默认样式。lv_obj_create创建的容器默认带背景色、圆角和边框多个容器嵌套时界面上会看到明显的灰色方块。要制作透明面板需要显式关闭背景lv_obj_set_style_bg_opa(cont, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(cont, 0, 0);这不会影响子对象的布局位置但会直接影响视觉效果。9. 最佳实践与使用建议9.1 先从固定尺寸小容器开始验证不要一次性把整个屏幕都做成嵌套布局。先在屏幕中间创建一个 200x100 的容器放 5 个按钮验证 Flex 排列效果。再逐步改成 Grid、增加嵌套层级。这样出问题时定位范围很小。9.2 区分“布局型容器”和“视觉型容器”同一个lv_obj_create创建的对象既可以承担布局职责又可以被用作视觉面板。建议在工程中明确布局型容器只负责排列子对象不需要背景色、边框视觉型容器作为卡片或分区面板可以带背景、圆角、阴影。代码中可以用两条初始化路径分开处理避免每次创建容器都要重复写样式清除代码。9.3 样式尽量复用LVGL 中每个对象独立设置样式会消耗额外内存。对按钮、卡片这类重复组件建议创建共享样式static lv_style_t style_btn; lv_style_init(style_btn); lv_style_set_bg_color(style_btn, lv_palette_main(LV_PALETTE_BLUE)); lv_style_set_radius(style_btn, 6); /* 创建多个按钮时统一使用该样式 */ lv_obj_add_style(btn, style_btn, 0);9.4 配合事件回调处理点击布局排布只解决显示问题交互还需要事件回调。用lv_obj_add_event_cb为按钮绑定回调static void btn_event_cb(lv_event_t *e) { lv_event_code_t code lv_event_get_code(e); if (code LV_EVENT_CLICKED) { ESP_LOGI(UI, button clicked); } } lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_ALL, NULL);多个同结构按钮可以绑定同一个回调通过lv_event_get_target(e)区分具体是哪个按钮被点击。9.5 布局变更后及时刷新如果在运行期间动态增删子对象、修改容器尺寸记得在修改后调用lv_obj_invalidate或lv_obj_report_style_change触发重新布局和刷新。否则界面上可能出现残留对象或排列错误。9.6 合规与安全边界提醒ESP32 LVGL 常用于智能家居控制面板、数据采集终端、工业显示器等场景。如果你开发的界面涉及用户信息展示、摄像头画面、蓝牙配网、云平台数据上传等能力请务必注意设备只允许访问用户授权范围内的数据不能通过后门获取未授权信息涉及人脸、定位等敏感信息时必须明确告知用户并取得授权开源代码和第三方库要保留原作者版权声明按对应许可证合规使用产品发布前需要做安全测试避免调试接口和默认口令直接暴露到公网。10. 总结与下一步LVGL 的容器和布局是嵌入式 GUI 开发中从“能画控件”到“能设计界面”的关键一步。容器提供了父子对象管理和区域划分能力Flex 布局解决了顺序排列和自动换行的问题Grid 布局解决了行列对齐和网格排版的问题。三者结合起来即使你手里只有一块小屏也能写出结构清晰、可维护的 UI 代码。建议最先验证的是打开LV_USE_FLEX和LV_USE_GRID在屏幕中间创建一个 240x120 的容器用 Flex 横向放三个按钮再换成 Grid 的 3x3 网格观察效果。这一步跑通后再往上叠加嵌套布局和事件回调。最容易踩的坑集中在三处lv_conf.h布局开关没打开、容器尺寸为 0 导致子对象不可见、Flex 布局下混用手动坐标导致排列错乱。遇到奇怪现象时优先检查这三个点。下一步你可以继续深入把容器布局和lv_timer结合起来做动态刷新数据的面板用lv_obj_set_grid_cell实现多页面菜单切换或者把布局代码封装成函数输入一组标题和数据自动生成卡片列表。LVGL 的布局体系一旦熟悉起来后续做菜单、设置页、仪表盘都会快很多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进