3、LVGL基础架构:组件体系、对象树与父子关系、事件驱动模型、显示与输入设备抽象层
学LVGL,说白了就是学它的骨架。骨架搭对了,后面填肉就顺了。这一章我们聊聊LVGL的四大核心:组件体系、对象树、事件驱动、显示与输入抽象。这四块搞明白,你基本就摸清LVGL的脾气了。
3.1 组件体系:万物皆对象
LVGL里所有可见的东西,都叫“对象”(lv_obj_t)。按钮是对象,标签是对象,滑块是对象,连屏幕本身也是对象。我刚开始接触时觉得这概念有点虚,后来发现——这恰恰是LVGL最聪明的地方。
每个对象都继承自一个基础结构,这个结构里存了坐标、大小、样式、事件回调等通用信息。你创建一个按钮,本质上就是创建了一个lv_obj_t,然后给它套上按钮的行为和外观。
核心组件分类
- 基础组件:lv_btn(按钮)、lv_label(标签)、lv_img(图片)
- 输入组件:lv_textarea(文本框)、lv_dropdown(下拉菜单)、lv_keyboard(键盘)
- 容器组件:lv_cont(容器)、lv_page(页面)、lv_tabview(标签页)
- 可视化组件:lv_chart(图表)、lv_canvas(画布)、lv_led(指示灯)
- 其他:lv_slider(滑块)、lv_switch(开关)、lv_bar(进度条)
每个组件都有自己专属的创建函数,比如lv_btn_create()、lv_label_create()。但注意,它们都返回lv_obj_t *指针。这意味着你可以用同一套API操作所有组件——设置大小、位置、样式,完全通用。
// 创建一个按钮
lv_obj_t *btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 100, 40);
lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0);
// 创建一个标签
lv_obj_t *label = lv_label_create(btn);
lv_label_set_text(label, "点击我");
lv_obj_center(label);
你看,按钮和标签的创建方式几乎一样。这就是“万物皆对象”的好处——学习成本低,复用性强。
3.2 对象树与父子关系
LVGL的界面不是平铺的,而是一棵树。每个对象都有一个父对象,父对象可以有多个子对象。这棵树从根节点——活动屏幕(lv_scr_act())开始,向下生长。
我做过一个智能家居面板,界面层级深到第四层。当时如果没理解透父子关系,布局早就乱套了。嗯,这里有个关键点:子对象的位置是相对于父对象的。父对象移动,子对象跟着动;父对象隐藏,子对象全看不见。
个人习惯:我一般把屏幕当作根,第一层放容器(比如lv_cont),第二层放具体组件。这样调整布局时,只需要移动容器,里面的组件自动跟着走。
对象树的遍历也很重要。LVGL提供了几个常用函数:
// 获取父对象
lv_obj_t *parent = lv_obj_get_parent(child);
// 获取第一个子对象
lv_obj_t *first_child = lv_obj_get_child(parent, 0);
// 获取下一个兄弟对象
lv_obj_t *sibling = lv_obj_get_sibling(child);
为什么要遍历?举个例子:你想批量修改某个容器下所有按钮的样式。用循环遍历子对象,一行代码搞定。否则你得一个一个找,累死。
我曾经踩过的坑:删除对象时,如果忘了处理父子关系,子对象会变成“孤儿”——内存泄漏的元凶。正确做法是用lv_obj_del_async(),它会安全地递归删除所有子对象。
下面这张图帮你理清对象树的结构:
3.3 事件驱动模型
LVGL是事件驱动的。什么意思?就是界面不会主动干活,只有你点了、滑了、按了,它才响应。每个对象都可以绑定事件回调,当特定事件发生时,回调函数被触发。
事件类型很多,常用的有:
| 事件宏 | 触发时机 | 典型用途 |
|---|---|---|
LV_EVENT_CLICKED |
点击完成(松开后) | 按钮响应 |
LV_EVENT_PRESSING |
正在按下 | 长按检测 |
LV_EVENT_VALUE_CHANGED |
值发生变化 | 滑块、开关 |
LV_EVENT_KEY |
键盘按键 | 文本输入 |
LV_EVENT_DRAW_PART_BEGIN |
开始绘制某部分 | 自定义绘制 |
绑定事件很简单:
static void btn_event_cb(lv_event_t *e) {
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_CLICKED) {
LV_LOG_USER("按钮被点击了!");
}
}
// 绑定事件
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL);
这里有个细节:同一个对象可以绑定多个事件回调。比如一个按钮,既想处理点击,又想处理长按,那就绑两个回调。LVGL内部会按注册顺序依次调用。
我建议:事件回调里不要做耗时操作。比如读写SD卡、网络请求,这些会卡住UI。正确的做法是发一个消息给任务队列,让后台线程去处理。
事件传递还有个“冒泡”机制。子对象的事件没被处理,会向上传递给父对象。你想想看,这有什么用?比如你有一个容器,里面放了10个按钮。你可以在容器上绑一个事件,统一处理所有按钮的点击,省得每个按钮单独绑。
// 在容器上统一处理子按钮的点击
static void container_event_cb(lv_event_t *e) {
lv_obj_t *target = lv_event_get_target(e); // 实际被点击的对象
lv_obj_t *current = lv_event_get_current_target(e); // 当前处理事件的对象(容器)
if(lv_event_get_code(e) == LV_EVENT_CLICKED) {
LV_LOG_USER("容器内的 %p 被点击了", (void*)target);
}
}
lv_obj_add_event_cb(container, container_event_cb, LV_EVENT_CLICKED, NULL);
3.4 显示与输入设备抽象层
LVGL能跑在那么多硬件上,靠的就是抽象层。显示抽象层叫“显示驱动”,输入抽象层叫“输入设备驱动”。你只需要实现几个底层函数,LVGL就能接管一切。
显示驱动
核心就是告诉LVGL:你的屏幕怎么画像素。通常需要实现一个“刷新回调”:
static void my_disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
// 将 color_p 中的像素数据写入屏幕的 area 区域
// 比如用SPI发送数据到LCD
lv_disp_flush_ready(disp_drv); // 通知LVGL刷新完成
}
void my_display_init(void) {
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.flush_cb = my_disp_flush;
disp_drv.hor_res = 320; // 屏幕宽度
disp_drv.ver_res = 240; // 屏幕高度
lv_disp_drv_register(&disp_drv);
}
注意那个lv_disp_flush_ready(),必须调用。我曾经忘了加,结果屏幕死活不显示——嗯,LVGL以为刷新还没完,一直在等。
输入设备驱动
触摸屏、按键、编码器,都算输入设备。你需要实现一个“读取回调”:
static void my_touchpad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) {
// 从硬件读取触摸坐标
int16_t x, y;
bool pressed = touchpad_get_xy(&x, &y);
if(pressed) {
data->state = LV_INDEV_STATE_PR;
data->point.x = x;
data->point.y = y;
} else {
data->state = LV_INDEV_STATE_REL;
}
}
void my_touchpad_init(void) {
lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER; // 触摸屏类型
indev_drv.read_cb = my_touchpad_read;
lv_indev_drv_register(&indev_drv);
}
输入设备类型有几种:
| 类型 | 说明 | 常见硬件 |
|---|---|---|
LV_INDEV_TYPE_POINTER |
指针设备(触摸屏、鼠标) | 电阻屏、电容屏 |
LV_INDEV_TYPE_KEYPAD |
键盘/矩阵按键 | 4x4矩阵键盘 |
LV_INDEV_TYPE_ENCODER |
旋转编码器 | EC11旋钮 |
LV_INDEV_TYPE_BUTTON |
外部物理按键 | GPIO按键 |
我曾经踩过的坑:触摸屏的坐标和屏幕的显示方向不一致。比如屏幕是横屏,触摸屏坐标却是竖屏的。解决办法是在read_cb里做坐标映射,或者用lv_disp_set_rotation()统一旋转。
抽象层的好处很明显:换屏幕或换触摸芯片,只需要改驱动文件,上层UI代码完全不用动。我做过一个项目,从ILI9341换成ST7789,只改了显示驱动,界面代码一行没改——这就是抽象层的威力。
3.5 小结
这一章我们聊了LVGL的四大基础架构:
- 组件体系:万物皆对象,所有组件共用一套基础API
- 对象树:父子层级管理,布局和生命周期都依赖它
- 事件驱动:用户操作触发回调,支持冒泡传递
- 抽象层:显示和输入设备通过驱动接口与硬件解耦
这四块是LVGL的基石。后面章节我们会基于这些概念,一步步搭建真正的嵌入式GUI界面。你先把这些基础消化掉,后面学起来会轻松很多。