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_CLICKEDLV_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会按照以下顺序调用回调:

  1. 对象自身注册的回调:按注册的先后顺序执行。
  2. 对象的父对象注册的回调:如果父对象也监听了该事件,会继续向上传递。
  3. 全局事件处理:通过 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);

优先级范围通常是 0INT32_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() 控制。下一章咱们聊聊事件传递机制,那个更有意思。