开发设备端界面时,最容易被低估的其实是“摆控件”这件事。很多嵌入式工程师写 LVGL 界面,都是靠手算坐标,一个 240×320 的屏,放几行按钮、几个数据卡片,先算 x、再算 y,屏幕分辨率一换,中文字体宽度一变,所有坐标又要重算一遍。这种硬编码方式在可视化需求少的时候还能撑住,一旦界面功能变多,维护成本就会迅速失控。
LVGL 是当前 ESP32 上使用非常广泛的嵌入式图形库,它自带了一套完整的对象模型,并且在 LVGL 8.x 之后提供了接近 Web 前端 Flexbox 和 CSS Grid 的布局系统。通过“容器 + 布局”,我们可以把大量坐标计算交给 LVGL 自动完成,子对象的位置、换行方式、网格划分都能用几条 API 配置出来。本文将围绕“ESP32 + LVGL 容器与布局”这个主题,先讲清楚对象树、容器和布局的关系,再逐个拆解 Flex、Grid 布局的核心用法,最后给出三个可以直接在开发板上运行的示例工程。
无论你是刚接触 LVGL 的新手,还是已经会用lv_obj_set_pos硬堆坐标的开发者,这篇文章都能帮你减少重复劳动。阅读本文不需要太多前置知识,只要你能在 ESP32 上点亮一块 SPI 屏幕,并且跑通过官方最简单的 LVGL 例程,就可以按文章里的步骤继续深入。
1. 背景与核心概念
1.1 为什么嵌入式 UI 需要容器
在 LVGL 里,容器本质上还是一个普通对象,但它承担了“父容器”的职责,可以容纳多个子对象。创建容器之后,子对象默认会以容器为参考系进行定位,容器移动,子对象跟着移动;容器隐藏,子对象也会隐藏;容器本身有裁剪能力,还可以在空间不足时启用滚动。
不使用容器时,所有控件都直接挂在屏幕根对象上,它们之间的关系是扁平的。这种模式的缺点是代码里到处是绝对坐标,改动一个控件的位置,周围控件往往也要连带调整。使用容器后,我们可以把界面划分为多个功能区域,比如顶部标题栏、中间数据区、底部按钮区,每个区域有独立空间,互不干扰。这样做不仅代码可读性更高,也为后续适配不同屏幕分辨率打好了基础。
1.2 LVGL 对象树与父子关系
LVGL 中的所有可见元素都是一个lv_obj_t对象,包括按钮、标签、进度条,也包括容器。整个界面可以看作一棵树,树根是当前屏幕对象,通常用lv_scr_act()获取:
lv_obj_t *scr = lv_scr_act();容器通过lv_obj_create(scr)创建后,就成为了屏幕的子节点;按钮、标签再创建进容器中,就成为了容器的子节点。子对象的位置默认是相对父对象的,这条规则非常关键。很多刚接触 LVGL 的开发者会犯一个错误:把子对象放在屏幕的绝对位置上计算坐标,然后在父容器尺寸改变后,控件位置错乱。如果我们始终让子对象相对父容器布局,这种情况就不会发生。
对象树的深度也不建议过深。一般推荐三层结构:屏幕根对象 → 容器 → 控件。如果容器嵌套多层,虽然 LVGL 能支持,但会增加刷新计算和内存开销,排错时也不容易定位。
1.3 容器与布局的关系
容器是载体,布局是规则。
如果不对容器设置布局,子对象的位置完全由我们自己控制,想放哪里就放哪里;一旦对容器设置布局,子对象的坐标就会交给布局引擎计算。我们可以直接放弃手动设置子对象位置,只需要设置对齐方式、间距、排列方向或行列模板,LVGL 会自动计算每个子对象的位置。
为了更直观地理解,可以把它们的关系总结为下面这样:
| 场景 | 手动布局 | 使用容器 + 布局 |
|---|---|---|
| 添加新控件 | 需要重新计算已有控件坐标 | 布局自动排入下一个位置 |
| 修改屏幕分辨率 | 所有坐标重新核对 | 调整容器尺寸即可 |
| 对齐多个控件 | 逐个计算坐标 | 布局统一处理对齐 |
| 删除某个控件 | 后续控件坐标可能全部要改 | 布局自动填补空位 |
下文所有示例都会遵循“容器 + 布局”的思路,这也是目前 LVGL 工程中最推荐的界面组织方式。
2. 环境准备与版本说明
2.1 硬件与屏幕选择
文章示例以常见的 ESP32 开发板为例,比如 ESP32-WROOM-32 或 ESP32-S3,如果你使用的是其他 ESP32 系列,只要 Flash 和 RAM 满足要求,逻辑基本一致。屏幕方面,推荐使用 SPI 接口屏幕,例如 ST7789、ILI9341、ST7735 等驱动芯片,这些屏幕在 Arduino 生态和 ESP-IDF 生态下都有成熟的驱动库。
需要特别说明的是,屏幕引脚和驱动芯片型号在不同开发板上差异很大,下面的示例不会贴出具体的接线表,因为这部分必须根据你自己的开发板原理图来决定。关键是在 TFT_eSPI 或对应驱动库中正确配置屏幕的 SPI 引脚、背光引脚和屏幕方向。
2.2 软件环境与开发框架
ESP32 上开发 LVGL 有三种常见方式:
- Arduino IDE:适合快速验证界面效果,配合 TFT_eSPI 库可以很快把 LVGL 输出到屏幕。
- PlatformIO + Arduino 框架:比原生 Arduino IDE 更适合工程化管理,多文件项目体验更好。
- ESP-IDF:适合量产级工程,LVGL 官方也提供 ESP-IDF 组件支持,但环境配置门槛稍高。
本文代码以“Arduino 或 PlatformIO + TFT_eSPI + LVGL”为例。这个组合在社区里资料最多,遇到问题也最容易搜索到解决方案。
2.3 LVGL 版本说明
文章代码基于 LVGL 8.3.x 编写,这是目前 ESP32 社区使用最广泛、资料最丰富的版本。LVGL 9.x 以及后续版本已经发布,一些 API 名称有调整,容器和布局的大体设计思路没有变,但细节上存在差异。如果你使用的是 9.x,建议对照官方迁移文档进行调整,不要直接复制 8.x 代码后发现问题又找不到原因。
如果你刚接触 LVGL,我的建议是先使用 8.3.x 稳定版本把基础功能跑通,再根据项目需要评估是否升级。
2.4 显示驱动与 LVGL 初始化骨架
LVGL 本身不直接驱动屏幕,它通过显示驱动回调函数把绘制数据传给屏幕驱动库。下面是一段标准的初始化骨架,负责把 LVGL 和 TFT_eSPI 连接起来:
#include <lvgl.h> #include <TFT_eSPI.h> TFT_eSPI tft = TFT_eSPI(); static lv_disp_draw_buf_t draw