3、LVGL基础架构:对象树、屏幕与父对象、事件驱动模型初识
好,咱们今天聊点硬核的。LVGL 的架构,说白了就是一套「套娃」系统。你想想看,一个屏幕上有按钮,按钮里可能还有图标,图标上还能叠文字……这些东西怎么组织?靠的就是对象树。
3.1 对象树:一切控件的根基
LVGL 里每个可见的东西,都是一个对象(Object)。对象之间是父子关系。一个父对象可以有多个子对象,但一个子对象只能有一个父对象。这就像公司里的组织架构——一个部门经理可以带好几个员工,但每个员工只向一个经理汇报。
我刚开始用 LVGL 时,犯过一个低级错误:把两个屏幕上的控件挂到了同一个父对象下。结果切换屏幕时,控件全乱了。嗯,这里要注意:每个屏幕都有自己的对象树,根节点就是屏幕本身。
核心概念:
- 根节点:每个屏幕(lv_obj_t *)是独立的对象树根
- 父子关系:子对象的位置、可见性、裁剪都受父对象影响
- 兄弟关系:同一父对象下的子对象,按创建顺序决定 Z 轴层级
举个例子,创建一个按钮并挂到当前屏幕上:
lv_obj_t *btn = lv_btn_create(lv_scr_act()); // 父对象是当前活动屏幕
lv_obj_t *label = lv_label_create(btn); // 父对象是按钮
你看,label 的父对象是 btn,不是屏幕。这样当按钮移动时,label 会跟着动。这就是对象树的魅力——位置继承。
3.2 屏幕与父对象:谁才是老大?
LVGL 里可以有多个屏幕,但同一时刻只有一个活动屏幕。活动屏幕就是 lv_scr_act() 返回的那个。所有新创建的控件,如果不指定父对象,默认就挂到活动屏幕上。
我个人习惯把屏幕比作「画布」,父对象比作「画框」。画框可以嵌套,但画布只有一张。你想想看,如果每个控件都直接挂在屏幕上,那屏幕对象会变得极其臃肿。所以实际项目中,我们会用容器(lv_cont)或面板(lv_panel)来做中间层。
| 对象类型 | 作用 | 典型父对象 |
|---|---|---|
| 屏幕 (lv_obj) | 根节点,管理整个显示区域 | 无(由 LVGL 内部管理) |
| 容器 (lv_cont) | 布局容器,自动排列子对象 | 屏幕或其他容器 |
| 控件 (btn, label 等) | 具体交互元素 | 屏幕或容器 |
避坑指南:我曾经在创建控件时忘了指定父对象,结果控件跑到了默认屏幕上。后来切换屏幕时,那个控件就「消失」了。其实它还在,只是被新屏幕挡住了。所以创建控件时,务必显式指定父对象。
3.3 事件驱动模型初识:谁动了我的按钮?
LVGL 是事件驱动的。说白了,就是「你点一下,我响应一下」。事件驱动模型的核心是:事件源 → 事件队列 → 事件处理函数。
事件源可以是触摸、按键、定时器,甚至是代码主动触发。LVGL 内部维护了一个事件队列,所有事件都排队等待处理。处理函数就是回调函数——你写一个函数,告诉 LVGL:「当按钮被点击时,调用我这个函数」。
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_t *btn = lv_btn_create(lv_scr_act());
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL);
这里要注意:事件回调是在 LVGL 的主循环中执行的。不要在回调里做耗时操作,否则界面会卡顿。我遇到过有人把文件写入操作放在事件回调里,结果界面卡了 2 秒才响应。嗯,正确的做法是把耗时操作放到任务或线程里。
重要提醒:事件回调中不要调用 lv_task_handler() 或 lv_timer_handler(),否则会造成递归调用,轻则卡死,重则栈溢出。我曾经因为这个 bug 调试了一整天……
3.4 事件传递:冒泡还是捕获?
LVGL 的事件传递机制和浏览器 DOM 事件类似,但更简单。事件从子对象向父对象传递,这叫「冒泡」。如果你在父对象上绑定了事件,子对象触发的事件也会传到父对象。
举个例子:一个容器里有多个按钮。你想在容器层面统一处理所有按钮的点击事件,就可以在容器上绑定 LV_EVENT_CLICKED。这样不管点哪个按钮,容器都能收到事件。
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_obj(e); // 当前处理事件的对象
LV_LOG_USER("目标对象: %p, 当前对象: %p", (void*)target, (void*)current);
}
lv_obj_t *cont = lv_obj_create(lv_scr_act());
lv_obj_add_event_cb(cont, container_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_t *btn1 = lv_btn_create(cont);
lv_obj_t *btn2 = lv_btn_create(cont);
点击 btn1 时,事件先到 btn1,然后冒泡到 cont。如果 btn1 本身也绑定了事件,两个回调都会执行。执行顺序是:先子后父。
小技巧:如果你想阻止事件继续冒泡,可以调用 lv_event_stop_bubbling(e)。这在某些场景下很有用,比如你只想让子对象处理事件,不让父对象干扰。
3.5 总结一下
对象树、屏幕、父对象、事件驱动——这四个概念是 LVGL 的基石。对象树决定了控件的组织方式,屏幕是独立的根节点,父对象影响子对象的行为,事件驱动让交互变得灵活。
我个人建议初学者先画一张对象树图,把屏幕、容器、控件的关系理清楚。然后再写代码,这样思路会清晰很多。下一章我们会深入事件系统的细节,包括事件类型、自定义事件、以及如何高效管理事件回调。
嗯,今天就到这里。记住:对象树是骨架,事件是血肉。两者结合,才能做出真正可用的 GUI。