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_CLICKEDlv_event_get_user_data(e)— 获取注册时传入的 user_datalv_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会监听所有事件,性能开销大,非必要别用