3、LVGL事件注册与注销:lv_obj_add_event_cb()函数详解、lv_obj_remove_event_cb()函数详解、事件优先级与执行顺序
好,咱们进入第三章。这一章我打算把LVGL里事件注册和注销的细节彻底讲透。说白了,就是搞清楚怎么给控件“绑定”一个事件处理函数,以及怎么把它“解绑”。
你想想看,一个按钮按下去,总得干点啥吧?这个“干点啥”就是通过事件注册来实现的。而注销,则是当你不再需要某个功能时,把它清理掉,避免内存泄漏或者误触发。
3.1 lv_obj_add_event_cb() 函数详解
这个函数是LVGL事件驱动的核心入口。它的作用就是把一个事件处理函数(回调函数)挂载到指定的对象上。
先看原型:
uint32_t lv_obj_add_event_cb(lv_obj_t * obj, lv_event_cb_t event_cb, lv_event_code_t filter, void * user_data);
参数解释一下:
- obj:你要绑定事件的对象指针。比如一个按钮
lv_btn_create(lv_scr_act())返回的指针。 - event_cb:回调函数指针。类型是
lv_event_cb_t,也就是void (*)(lv_event_t *)。 - filter:事件过滤器。你想监听哪种事件?比如
LV_EVENT_CLICKED、LV_EVENT_VALUE_CHANGED。传LV_EVENT_ALL就是监听所有事件。 - user_data:用户自定义数据指针。这个很实用,你可以传一个结构体进去,在回调里取出来用。
返回值是一个 uint32_t 类型的事件ID。这个ID后面注销时会用到。
重点:同一个对象可以注册多个事件回调,同一个回调也可以注册到多个对象上。LVGL内部维护了一个链表来管理这些回调。
举个例子:
static void btn_click_cb(lv_event_t * e) {
lv_obj_t * btn = lv_event_get_target(e);
LV_LOG_USER("按钮被点击了!");
}
void my_create_ui(void) {
lv_obj_t * btn = lv_btn_create(lv_scr_act());
lv_obj_center(btn);
// 注册点击事件
lv_obj_add_event_cb(btn, btn_click_cb, LV_EVENT_CLICKED, NULL);
}
嗯,这里要注意一点:回调函数的第一个参数是 lv_event_t *,通过它你可以获取事件目标、当前对象、用户数据等信息。我个人习惯在回调开头先调用 lv_event_get_target() 拿到触发事件的对象。
3.2 lv_obj_remove_event_cb() 函数详解
有注册就有注销。当你不再需要某个事件处理时,记得把它移除掉。否则对象销毁时,回调函数可能还在,造成野指针问题。
函数原型:
bool lv_obj_remove_event_cb(lv_obj_t * obj, lv_event_cb_t event_cb);
参数很简单:
- obj:对象指针。
- event_cb:要移除的回调函数指针。
返回值是 bool,成功返回 true,失败返回 false。
还有一种更精确的移除方式,通过事件ID来移除:
bool lv_obj_remove_event_cb_with_id(lv_obj_t * obj, lv_event_cb_t event_cb, uint32_t event_id);
这个 event_id 就是之前 lv_obj_add_event_cb() 返回的那个ID。为什么要用ID?因为同一个回调函数可能被注册了多次,用ID可以精确移除某一个。
我曾经踩过这个坑:在动态创建和销毁控件时,忘了移除事件回调。结果对象销毁后,事件系统还保留着回调指针,一旦触发就崩溃。所以,养成好习惯——对象销毁前,先移除它上面注册的所有事件回调。
示例:
static uint32_t g_event_id = 0;
static void my_event_handler(lv_event_t * e) {
// 处理事件
}
void register_event(void) {
lv_obj_t * obj = lv_obj_create(lv_scr_act());
g_event_id = lv_obj_add_event_cb(obj, my_event_handler, LV_EVENT_CLICKED, NULL);
}
void unregister_event(void) {
lv_obj_t * obj = lv_obj_create(lv_scr_act()); // 假设你知道是哪个对象
lv_obj_remove_event_cb_with_id(obj, my_event_handler, g_event_id);
}
3.3 事件优先级与执行顺序
这个知识点很多人容易忽略。LVGL的事件处理是有顺序的,不是随便乱来的。
当一个事件发生时(比如点击),LVGL会按照以下顺序调用回调:
- 对象自身注册的回调:按注册的先后顺序执行。
- 对象的父对象注册的回调:如果父对象也监听了该事件,会继续向上传递。
- 全局事件处理:通过
lv_event_send()手动发送的事件。
但这里有个细节:同一个对象上注册了多个回调,它们的执行顺序是怎样的?
答案是:按注册顺序执行。先注册的先执行,后注册的后执行。
那有没有办法改变优先级呢?
LVGL 8.3 之后引入了 lv_obj_add_event_cb_priority() 函数,可以指定优先级。数值越小,优先级越高,越先执行。
uint32_t lv_obj_add_event_cb_priority(lv_obj_t * obj, lv_event_cb_t event_cb, lv_event_code_t filter, void * user_data, int32_t priority);
优先级范围通常是 0 到 INT32_MAX。默认是 0。
建议:除非你有特殊需求,否则不要轻易修改优先级。默认的注册顺序已经能满足大部分场景。我在项目中遇到过需要拦截事件的情况,那时才用到了优先级——把拦截器的优先级设成负数,让它先于其他回调执行。
来看一个优先级示例:
static void high_priority_cb(lv_event_t * e) {
LV_LOG_USER("高优先级回调执行");
}
static void low_priority_cb(lv_event_t * e) {
LV_LOG_USER("低优先级回调执行");
}
void demo_priority(void) {
lv_obj_t * btn = lv_btn_create(lv_scr_act());
// 先注册低优先级
lv_obj_add_event_cb_priority(btn, low_priority_cb, LV_EVENT_CLICKED, NULL, 100);
// 后注册高优先级
lv_obj_add_event_cb_priority(btn, high_priority_cb, LV_EVENT_CLICKED, NULL, 0);
// 点击按钮时,输出顺序是:
// "高优先级回调执行"
// "低优先级回调执行"
}
为什么会这样?因为优先级数值小的先执行。0 比 100 小,所以高优先级的先跑。
最后,总结一下事件执行顺序的完整流程:
| 步骤 | 说明 |
|---|---|
| 1 | 按优先级排序(从小到大) |
| 2 | 同优先级按注册顺序执行 |
| 3 | 事件向上冒泡到父对象 |
| 4 | 父对象同样按优先级和注册顺序执行 |
嗯,这一章的内容就这些。记住三个核心点:注册用 lv_obj_add_event_cb(),注销用 lv_obj_remove_event_cb(),优先级用 lv_obj_add_event_cb_priority() 控制。下一章咱们聊聊事件传递机制,那个更有意思。