2、LVGL事件基础:lv_event_t结构体、事件注册函数lv_obj_add_event_cb()、事件回调函数签名
好,咱们正式开始啃事件这块硬骨头。说实话,我刚接触LVGL时,最头疼的就是事件系统。你想想看,一个按钮按下去,屏幕要亮,蜂鸣器要响,数据要上传——这些动作怎么串联起来?靠的就是事件。
这一节,咱们把最基础的三样东西讲透:事件结构体、注册函数、回调函数签名。搞懂这三样,后面写状态机就顺了。
2.1 lv_event_t结构体——事件的“身份证”
每个事件在LVGL内部都是一个lv_event_t结构体。你可以把它想象成一张身份证,记录了“谁触发了事件”、“触发了什么类型的事件”、“有没有附带数据”。
我直接贴出核心字段,你感受一下:
typedef struct _lv_event_t {
lv_event_code_t code; // 事件类型码,比如 LV_EVENT_CLICKED
void *user_data; // 用户自定义数据,这个很常用
void *param; // 事件附带参数,比如滚动时的坐标
struct _lv_obj_t *target; // 触发事件的对象(比如你点的那个按钮)
struct _lv_obj_t *current_target; // 当前正在处理事件的对象(冒泡时有用)
lv_event_mark_t mark; // 内部标记,不用管
uint8_t deleted : 1; // 对象是否已被删除
} lv_event_t;
这里我重点说两个字段:
target:谁点的?比如你按了按钮A,target就是按钮A。current_target:谁在处理?如果事件冒泡到父容器,current_target就变成了父容器。
我在项目中遇到过一个问题:一个按钮嵌套在容器里,我想在容器上统一处理点击事件。结果发现target永远是按钮,不是容器。后来才意识到要用current_target来判断当前处理层。嗯,这个坑我踩过,你记住就行。
2.2 lv_obj_add_event_cb()——把事件“挂”到对象上
有了事件结构体,还得告诉LVGL:“嘿,这个按钮被点击时,你要调用我写的这个函数。” 这就是lv_obj_add_event_cb()干的事。
函数签名长这样:
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 |
你要监听哪个对象?比如按钮、标签、滑块 | 别传NULL,会崩 |
event_cb |
回调函数指针,事件发生时调用它 | 函数名前面不用加&,函数名本身就是指针 |
filter |
监听哪种事件?LV_EVENT_ALL表示所有事件 |
尽量精确,别偷懒用ALL,影响性能 |
user_data |
你想传给回调函数的额外数据,任意指针 | 这个太实用了,我后面会演示 |
举个例子,给按钮注册一个点击事件:
static void btn_click_cb(lv_event_t *e) {
// 回调内容稍后讲
}
void my_app_create(void) {
lv_obj_t *btn = lv_btn_create(lv_scr_act());
lv_obj_add_event_cb(btn, btn_click_cb, LV_EVENT_CLICKED, NULL);
}
你看,就这么简单。但注意——同一个对象可以注册多个回调,也可以对同一个事件注册多个回调。它们会按注册顺序依次执行。我个人习惯把UI初始化回调放在前面,业务逻辑回调放在后面,这样好维护。
user_data里传一个按钮ID。这样回调里就能区分是哪个按钮被点了。我经常这么干,省得写一堆重复的回调函数。
2.3 事件回调函数签名——怎么写才规范?
回调函数的签名是固定的,你没法改:
static void my_event_callback(lv_event_t *e);
参数只有一个:lv_event_t *e。所有信息都从这个指针里取。
在回调函数内部,常用的操作有这几个:
- 获取触发对象:
lv_obj_t *obj = lv_event_get_target(e); - 获取当前处理对象:
lv_obj_t *cur = lv_event_get_current_target(e); - 获取事件码:
lv_event_code_t code = lv_event_get_code(e); - 获取用户数据:
void *ud = lv_event_get_user_data(e); - 获取附加参数:比如滑块的值变化时,用
int32_t v = lv_event_get_slider_value(e);
我写一个完整的例子,你一看就明白:
static void btn_click_handler(lv_event_t *e) {
// 1. 拿到是谁触发的
lv_obj_t *btn = lv_event_get_target(e);
// 2. 拿到用户数据(假设传了一个字符串指针)
const char *msg = (const char *)lv_event_get_user_data(e);
// 3. 改变按钮文字
lv_obj_t *label = lv_obj_get_child(btn, 0);
lv_label_set_text(label, msg);
// 4. 打印日志
LV_LOG_USER("按钮被点击,消息:%s", msg);
}
// 注册时传用户数据
lv_obj_add_event_cb(btn, btn_click_handler, LV_EVENT_CLICKED, "你点了我");
这里有个细节:回调函数里不要做耗时操作。比如延时、文件读写、网络请求。为什么?因为LVGL的事件处理是在主循环里同步执行的,你卡住了,整个UI就卡住了。我曾经在一个回调里写了lv_task_delay(1000),结果界面直接冻住,按啥都没反应。后来改成用定时器异步处理,才解决。
lv_obj_del()删除触发事件的对象!否则LVGL内部会访问野指针,直接崩溃。如果非要删除,用lv_event_send(obj, LV_EVENT_DELETE, NULL)来安全删除。
2.4 事件类型过滤——别啥事都接
注册事件时,filter参数可以精确控制监听哪种事件。LVGL预定义了很多事件码,常用的有:
LV_EVENT_CLICKED:点击完成(按下+释放)LV_EVENT_PRESSED:刚按下LV_EVENT_RELEASED:释放LV_EVENT_VALUE_CHANGED:值改变(滑块、下拉列表等)LV_EVENT_SCROLL:滚动中LV_EVENT_SCROLL_END:滚动结束
我建议你养成习惯:只监听你需要的事件。比如按钮只需要LV_EVENT_CLICKED,就别监听LV_EVENT_PRESSED。虽然监听多个也没错,但回调会被频繁调用,浪费CPU。我在一个嵌入式项目里,就因为监听了LV_EVENT_ALL,导致滑动列表时卡顿明显。后来改成只监听LV_EVENT_CLICKED和LV_EVENT_VALUE_CHANGED,流畅多了。
2.5 小结一下
这一节的内容,说白了就是三句话:
- 事件结构体
lv_event_t是事件的载体,里面装着谁触发的、什么类型、带什么数据。 - 注册函数
lv_obj_add_event_cb()把回调函数和对象绑定在一起,告诉LVGL“这事归你管”。 - 回调函数签名 固定为
void func(lv_event_t *),所有信息都从这个参数里取。
下一节,我会讲事件派发流程和冒泡机制。你想想看,一个按钮被点击后,LVGL内部是怎么一层层传递这个事件的?父容器能不能拦截?这些搞懂了,你就能写出更灵活、更解耦的UI代码。
好,先消化这些。有问题随时翻翻LVGL官方文档,或者直接看源码——lv_event.c 里其实写得挺清楚的。