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_CLICKEDLV_EVENT_VALUE_CHANGED,流畅多了。

2.5 小结一下

这一节的内容,说白了就是三句话:

  1. 事件结构体 lv_event_t 是事件的载体,里面装着谁触发的、什么类型、带什么数据。
  2. 注册函数 lv_obj_add_event_cb() 把回调函数和对象绑定在一起,告诉LVGL“这事归你管”。
  3. 回调函数签名 固定为 void func(lv_event_t *),所有信息都从这个参数里取。

下一节,我会讲事件派发流程和冒泡机制。你想想看,一个按钮被点击后,LVGL内部是怎么一层层传递这个事件的?父容器能不能拦截?这些搞懂了,你就能写出更灵活、更解耦的UI代码。

好,先消化这些。有问题随时翻翻LVGL官方文档,或者直接看源码——lv_event.c 里其实写得挺清楚的。