3. 事件处理函数注册:lv_obj_add_event_cb 详解

事件处理函数怎么挂上去的?说白了就是 lv_obj_add_event_cb 这个 API。我刚开始用 LVGL 时,觉得它不就是个注册回调嘛,有啥好研究的。结果后来被坑了好几次,才发现这里面的门道比想象中多。

这一节,咱们就把这个函数掰开揉碎了讲清楚。包括它的参数、回调函数的签名、怎么传用户数据,以及什么时候注册、什么时候销毁。

3.1 函数签名与参数

先看原型:

lv_event_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_obj_t * 要监听事件的对象指针
event_cb lv_event_cb_t 回调函数指针
filter lv_event_code_t 事件过滤器,指定监听哪种事件
user_data void * 用户自定义数据,回调时传回
返回值 lv_event_t * 事件描述符指针,可用于后续移除
小提示: 返回值 lv_event_t * 在 v8.3 之后才稳定可用。如果你还在用 v8.2 或更早版本,返回值可能是 void。建议升级到 v8.3+。

3.2 回调函数签名

回调函数的类型是 lv_event_cb_t,定义如下:

typedef void (*lv_event_cb_t)(lv_event_t * e);

就一个参数 lv_event_t * e。所有信息都从这个指针里取。

常用的取值函数:

  • lv_event_get_target(e) — 获取触发事件的对象(就是注册时的 obj)
  • lv_event_get_current_target(e) — 获取当前处理事件的对象(事件冒泡时有用)
  • lv_event_get_code(e) — 获取事件代码,比如 LV_EVENT_CLICKED
  • lv_event_get_user_data(e) — 获取注册时传入的 user_data
  • lv_event_get_param(e) — 获取事件携带的额外参数

一个典型的回调函数长这样:

static void my_btn_cb(lv_event_t * e) {
    lv_obj_t * btn = lv_event_get_target(e);
    uint32_t * id = (uint32_t *)lv_event_get_user_data(e);
    
    LV_LOG_USER("Button %d clicked", *id);
    
    // 你的业务逻辑
}
重点: 回调函数里不要做耗时操作。LVGL 的事件处理是在主循环里同步执行的,你卡住了,整个 UI 就卡住了。我曾经在一个回调里做了文件读写,结果界面直接冻住 2 秒,用户体验极差。

3.3 用户数据传递

这个 user_data 参数,我用得最多。它是个 void *,你可以传任何东西进去。

常见用法:

  • 传整数 ID:多个按钮共用一个回调,靠 ID 区分
  • 传结构体指针:传递复杂上下文
  • 传字符串:传递标签或标识
  • 传 NULL:不需要额外数据

举个例子,三个按钮共用一个回调:

static void common_btn_cb(lv_event_t * e) {
    int * id = (int *)lv_event_get_user_data(e);
    switch(*id) {
        case 0: // 处理按钮0
            break;
        case 1: // 处理按钮1
            break;
        case 2: // 处理按钮2
            break;
    }
}

// 注册时
int id0 = 0, id1 = 1, id2 = 2;
lv_obj_add_event_cb(btn0, common_btn_cb, LV_EVENT_CLICKED, &id0);
lv_obj_add_event_cb(btn1, common_btn_cb, LV_EVENT_CLICKED, &id1);
lv_obj_add_event_cb(btn2, common_btn_cb, LV_EVENT_CLICKED, &id2);
注意: user_data 传的是指针,你要保证指针指向的内存生命周期比事件回调长。传局部变量地址?那是找死。我见过有人传栈上的变量,函数一返回,指针就悬空了,回调里一访问就崩。

3.4 注册时机与生命周期

什么时候注册事件?这个问题看似简单,但坑不少。

最佳实践: 在创建对象之后、显示对象之前注册。

lv_obj_t * btn = lv_btn_create(lv_scr_act());
lv_obj_add_event_cb(btn, my_cb, LV_EVENT_CLICKED, NULL);
// 然后设置位置、大小等属性
lv_obj_set_pos(btn, 10, 10);

为什么?因为有些事件在对象显示后就会触发(比如 LV_EVENT_DRAW_PART_BEGIN),你注册晚了就错过了。

生命周期规则:

  • 事件回调与对象绑定,对象删除时回调自动移除
  • 你可以手动移除:lv_obj_remove_event_cb(obj, my_cb)
  • 也可以移除所有回调:lv_obj_remove_event_cb(obj, NULL)
  • 对象被删除后,回调函数不会再被调用
经验之谈: 如果你在回调里删除了触发事件的对象,一定要立即 return。因为对象已经没了,继续访问 e 里的任何指针都是未定义行为。我踩过这个坑,debug 了一下午才发现是删除顺序的问题。

3.5 事件注册的完整流程

下面这张图展示了从注册到触发的完整链路:

事件注册与触发流程 创建对象 注册回调 事件发生 查找回调链表 执行回调函数 关键点 • 注册在创建之后立即做 • 一个对象可注册多个回调 • 回调按注册顺序执行 • 对象删除时回调自动清理

3.6 多个回调的注册顺序

一个对象可以注册多个事件回调。它们按注册顺序依次执行。这一点在解决事件冲突时非常关键。

// 先注册的优先执行
lv_obj_add_event_cb(obj, first_cb, LV_EVENT_CLICKED, NULL);
lv_obj_add_event_cb(obj, second_cb, LV_EVENT_CLICKED, NULL);
// 点击时:first_cb 先执行,second_cb 后执行

如果你想控制执行顺序,有两种办法:

  • 调整注册顺序:先注册的先执行
  • 使用事件优先级:LVGL v8.3+ 支持 lv_obj_add_event_cb_priority,可以指定优先级
实战建议: 如果你需要在一个回调里阻止后续回调执行,可以调用 lv_event_stop_bubbling(e)。但要注意,这个函数只阻止事件冒泡,不阻止同一对象上的其他回调。要完全阻止,需要自己维护一个标志位。

3.7 避坑指南

最后,总结几个我踩过的坑:

  • 不要在回调里删除自己:如果一定要删,用 lv_async_call 延迟执行
  • user_data 生命周期:确保指针在回调可能被调用的整个期间都有效
  • 重复注册:同一个回调函数注册多次,会被调用多次。这不是 bug,是特性,但容易误用
  • filter 参数:传 LV_EVENT_ALL 会监听所有事件,性能开销大,非必要别用