3、LVGL深度解析(上):架构、对象树、事件驱动与样式系统
LVGL 是目前嵌入式圈子里最活跃的 GUI 库之一。我最早接触它是在一个智能家居项目里,当时要在 2MB Flash 的芯片上跑一套带动画的温控界面。说实话,一开始我挺担心资源不够的。但深入看完它的架构后,心里就有底了。
这一章,我们先把 LVGL 的骨架拆开看看。理解它的设计哲学,后面写代码才能游刃有余。
3.1 整体架构:分层清晰,各司其职
LVGL 的架构其实不复杂。它像一座三层小楼,每层只管自己的事。
核心分层:
- 硬件层(HAL): 负责跟显示屏、触摸屏、定时器打交道。你只需要实现几个回调函数,比如 flush_cb、read_cb。
- 核心层(Core): 对象管理、事件分发、样式计算、动画引擎。这是 LVGL 的心脏。
- 组件层(Widgets): 按钮、标签、滑块、图表等现成控件。它们都基于核心层构建。
我个人习惯把 HAL 层看作「适配器」。不管你的屏幕是 SPI 接口还是 RGB 接口,只要把 flush 函数写好,LVGL 就能跑起来。我曾在 STM32F429 上接了一块 800x480 的 RGB 屏,HAL 层只花了半天就调通了。
我的建议: 移植 LVGL 时,先跑一次 lv_demo_widgets。如果界面能正常显示,说明 HAL 层没问题。剩下的就是业务逻辑了。
3.2 对象树:一切皆对象,父子相承
LVGL 里每个可见元素都是一个 lv_obj_t 对象。屏幕是根节点,按钮、标签都是它的孩子。这就构成了一个树形结构。
你想想看,为什么用树?因为 GUI 天然就是树。一个窗口里有面板,面板里有按钮,按钮里有图标。父子关系决定了位置、可见性和事件传递。
// 创建一个屏幕(根对象)
lv_obj_t *scr = lv_scr_act();
// 在屏幕上创建一个按钮(子对象)
lv_obj_t *btn = lv_btn_create(scr);
lv_obj_set_pos(btn, 50, 50);
// 在按钮上创建一个标签(孙对象)
lv_obj_t *label = lv_label_create(btn);
lv_label_set_text(label, "Click Me");
这里有个关键点:子对象的位置是相对于父对象的。我刚开始用的时候,以为坐标是屏幕绝对坐标,结果按钮跑到了奇怪的地方。后来才反应过来——父对象移动,子对象跟着动。
避坑指南: 删除父对象时,所有子对象会被自动删除。我曾经在动态创建大量控件后,直接删了父容器,结果子对象指针全成了野指针。如果你有外部引用,记得先置 NULL。
3.3 事件驱动模型:谁触发了,谁处理
LVGL 的事件机制很灵活。它不绑定特定硬件,而是用事件驱动的方式处理用户交互。
事件传递分三个阶段:
- 捕获阶段: 从根节点向目标对象传递。
- 目标阶段: 事件到达触发对象。
- 冒泡阶段: 从目标对象向上冒泡回根节点。
说白了,就是事件先从上往下走,到了目标处理完,再原路返回。你可以在任意阶段拦截事件。
// 给按钮添加点击事件
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL);
static void btn_event_cb(lv_event_t *e) {
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t *target = lv_event_get_target(e);
if (code == LV_EVENT_CLICKED) {
lv_label_set_text(label, "Clicked!");
}
}
我记得有个项目需要实现「长按加速」功能。利用 LV_EVENT_PRESSING 事件,每次触发时累加一个计数器,超过阈值就加速。这个机制比轮询优雅多了。
小技巧: 如果你想让多个对象响应同一个事件,可以把事件监听器挂在它们的父容器上。利用冒泡机制,父容器能捕获所有子对象的事件。这在实现「全局手势」时特别有用。
3.4 样式系统(Style):灵活但需要理解
LVGL 的样式系统是我见过最灵活的嵌入式 GUI 样式方案之一。它用结构体描述外观,然后应用到对象上。
每个 lv_style_t 可以设置背景色、边框、圆角、阴影、文字颜色等属性。样式可以叠加,后应用的样式会覆盖前面的。
// 创建一个样式
static lv_style_t style_btn;
lv_style_init(&style_btn);
lv_style_set_bg_color(&style_btn, lv_color_hex(0x2196F3));
lv_style_set_radius(&style_btn, 8);
lv_style_set_border_width(&style_btn, 2);
lv_style_set_border_color(&style_btn, lv_color_hex(0x1976D2));
// 应用到按钮
lv_obj_add_style(btn, &style_btn, 0);
这里有个概念叫「状态」。按钮有 LV_STATE_DEFAULT、LV_STATE_PRESSED、LV_STATE_DISABLED 等状态。你可以为不同状态定义不同样式。
| 状态 | 说明 | 典型样式变化 |
|---|---|---|
| LV_STATE_DEFAULT | 默认状态 | 蓝色背景 |
| LV_STATE_PRESSED | 按下状态 | 深蓝色背景,缩小 5% |
| LV_STATE_DISABLED | 禁用状态 | 灰色背景,降低透明度 |
| LV_STATE_FOCUSED | 焦点状态 | 边框高亮 |
// 为按下状态定义样式
static lv_style_t style_btn_pr;
lv_style_init(&style_btn_pr);
lv_style_set_bg_color(&style_btn_pr, lv_color_hex(0x1565C0));
lv_style_set_transform_zoom(&style_btn_pr, 256); // 缩小(256 = 100%)
// 应用状态样式
lv_obj_add_style(btn, &style_btn_pr, LV_STATE_PRESSED);
避坑指南: 样式变量必须是静态或全局的。我曾在函数内定义局部样式,函数返回后样式被销毁,结果按钮显示异常。嗯,这个坑我踩过两次才记住。
3.5 主题(Theme):一键换肤
主题是样式的集合。LVGL 内置了几套主题:默认主题、暗色主题、Material 主题等。你可以直接切换,也可以自定义。
主题的作用是统一管理全局样式。比如你希望所有按钮都是圆角蓝色,所有标签都是 16px 字体。定义一次主题,全局生效。
// 启用内置暗色主题
lv_theme_t *th = lv_theme_default_init(
lv_disp_get_default(),
lv_palette_main(LV_PALETTE_BLUE),
lv_palette_main(LV_PALETTE_RED),
false, // 暗色模式
&lv_font_montserrat_14
);
lv_disp_set_theme(lv_disp_get_default(), th);
我个人习惯在项目初期就定义好主题。这样后期调整配色时,只需要改主题文件,不用逐个控件去改。有一次客户要求从蓝色主题改成绿色主题,我只改了主题初始化代码,半小时搞定。
主题 vs 样式:
- 主题: 全局默认样式,影响所有同类控件。
- 样式: 局部覆盖,只影响特定对象。
- 优先级: 局部样式 > 主题样式。
如果你需要自定义主题,可以继承内置主题,只修改部分属性。这样既保留了主题的完整性,又实现了定制需求。
// 自定义主题:只修改按钮背景色
lv_theme_t *my_theme = lv_theme_default_init(...);
lv_theme_set_style(my_theme, LV_THEME_BTN, LV_STATE_DEFAULT, &my_btn_style);