4. 事件注册与回调机制
事件冒泡,说白了就是事件在对象树里往上“冒”的过程。你点了一个按钮,这个按钮没处理的事件,会一层层往上找,直到根对象。这个机制,我刚开始接触 LVGL 时觉得挺绕,后来发现它其实很符合直觉——就像你在公司里,小事自己解决,解决不了就找上级。
4.1 事件传播机制:从子到父的“向上汇报”
LVGL 的事件传播默认是冒泡模式。什么意思呢?
假设你有一个屏幕(screen),上面放了一个面板(panel),面板里又放了一个按钮(btn)。当你点击按钮时,事件触发的顺序是:
- 按钮本身(btn)
- 按钮的父对象——面板(panel)
- 面板的父对象——屏幕(screen)
- 屏幕的父对象——默认的根对象(lv_scr_act() 的父级)
这个过程,我习惯叫它“向上冒泡”。每个父对象都有机会处理这个事件,只要它注册了对应的事件回调。
核心要点:事件冒泡是 LVGL 的默认行为。你不需要额外配置,它就会自动从子对象向父对象传播。
为什么会设计成冒泡?你想想看,如果你在面板上放了一排按钮,你既想单独处理每个按钮的点击,又想在面板层面统一处理一些逻辑(比如记录点击日志),冒泡机制就能让你轻松做到。我在做智能家居面板项目时,就用这个特性在父容器里统一处理了所有子控件的“点击反馈动画”,省了不少代码。
4.2 lv_obj_set_event_cb 与 lv_obj_add_event_cb 的区别
这两个函数,名字很像,但用法完全不同。我见过不少新手在这上面栽跟头。
| 对比项 | lv_obj_set_event_cb | lv_obj_add_event_cb |
|---|---|---|
| 注册方式 | 设置单个回调 | 添加多个回调 |
| 是否覆盖 | 会覆盖之前设置的回调 | 不会覆盖,追加到回调列表 |
| 支持多个回调 | 不支持,只能有一个 | 支持,可以注册多个 |
| 推荐使用 | 旧版兼容,简单场景 | 新版推荐,灵活可控 |
| 事件过滤 | 无,所有事件都进同一个回调 | 可以指定只监听特定事件类型 |
lv_obj_set_event_cb 是 LVGL 早期版本留下的函数。它简单粗暴:你传一个回调函数进去,它就记住了。但如果你再调用一次,之前的回调就被覆盖了。我早期做项目时,有一次在初始化代码里调了两次 lv_obj_set_event_cb,结果第一个回调莫名其妙不执行了,查了半天才发现是被覆盖了。
// 旧方式:会被覆盖
lv_obj_set_event_cb(btn, btn_callback_1);
lv_obj_set_event_cb(btn, btn_callback_2); // btn_callback_1 被覆盖了!
lv_obj_add_event_cb 是 LVGL v7+ 引入的新方式。它不会覆盖,而是把回调函数添加到对象的回调列表里。你可以给同一个对象注册多个回调,每个回调只处理自己关心的事件类型。
// 新方式:可以注册多个
lv_obj_add_event_cb(btn, btn_click_handler, LV_EVENT_CLICKED, NULL);
lv_obj_add_event_cb(btn, btn_press_handler, LV_EVENT_PRESSED, NULL);
lv_obj_add_event_cb(btn, btn_release_handler, LV_EVENT_RELEASED, NULL);
// 三个回调共存,互不干扰
我的建议:新项目一律用 lv_obj_add_event_cb。它更灵活,也更容易维护。如果你在维护老项目,看到 lv_obj_set_event_cb 也别慌,知道它会被覆盖就行。
还有一个细节:lv_obj_add_event_cb 的最后一个参数是 user_data,你可以传一个指针进去,在回调里通过 lv_event_get_user_data() 取出来。这个功能很实用,我在做多语言切换时,就用它把语言 ID 传给了回调函数。
4.3 事件停止传播:lv_event_stop_bubbling
有时候,你不想让事件继续往上冒。比如你点击了一个“删除”按钮,这个按钮自己处理了删除逻辑,你不想让父容器也响应这个点击事件。这时候就需要停止事件传播。
LVGL 提供了 lv_event_stop_bubbling() 函数。在回调里调用它,事件就不会再往父对象传播了。
static void btn_delete_handler(lv_event_t * e)
{
// 处理删除逻辑
lv_obj_t * obj = lv_event_get_target(e);
lv_obj_del(obj);
// 停止事件冒泡,不让父容器也响应
lv_event_stop_bubbling(e);
}
// 注册回调
lv_obj_add_event_cb(btn, btn_delete_handler, LV_EVENT_CLICKED, NULL);
注意:lv_event_stop_bubbling() 只阻止事件向父对象传播,不会阻止同一对象上其他回调的执行。如果你注册了多个回调,它们都会被执行,只是父对象收不到这个事件了。
我曾经在一个仪表盘项目里踩过这个坑。我在一个容器上注册了“点击切换页面”的回调,又在容器里的按钮上注册了“点击执行操作”的回调。结果每次点按钮,页面也跟着切换了。后来加上 lv_event_stop_bubbling() 才解决。
还有一个函数叫 lv_event_stop_processing(),它更狠——不仅停止冒泡,还停止当前对象上后续回调的执行。不过这个用得少,一般场景下 lv_event_stop_bubbling() 就够了。
4.4 事件传播流程图
下面这张图,把事件从触发到停止的完整路径画出来了。你可以对照着理解。
4.5 实际项目中的选择策略
说了这么多,到底什么时候用哪个?我总结了几条经验:
- 简单场景:一个按钮只有一个功能,用
lv_obj_set_event_cb也行,但我不推荐。万一以后要加功能,还得改代码。 - 复杂场景:一个对象需要响应多种事件(点击、长按、释放),用
lv_obj_add_event_cb分别注册,代码清晰好维护。 - 需要停止冒泡:子对象处理完事件后,调用
lv_event_stop_bubbling(),防止父对象误触发。 - 全局监听:在根对象上注册事件回调,监听所有子对象冒泡上来的事件。比如统计用户点击行为。
小技巧:在回调里,用 lv_event_get_target(e) 获取触发事件的对象,用 lv_event_get_current_target(e) 获取当前正在处理事件的对象。这两个在冒泡过程中可能不一样——前者是子对象,后者是父对象。我刚开始老搞混,后来记住一句话:target 是“谁干的”,current_target 是“谁在处理”。
嗯,事件冒泡和捕获这块,说白了就是理解事件在对象树里的流动方向。你掌握了 lv_obj_add_event_cb 的用法,再配合 lv_event_stop_bubbling 控制传播,大部分交互场景都能搞定。我在实际项目中,90% 的事件处理都是用这两个函数完成的。