前言
在 LVGL 事件开发中,开发者经常使用 lv_obj_add_event_cb 绑定回调函数。多数初学者无法理解:回调函数如何被控件保存、事件参数从何而来、两种函数指针的区别是什么。本文从零、零基础、标准化讲解整套原理。
一、普通函数指针
1. 定义
不依赖任何结构体、独立存在的函数指针,称为普通函数指针。
LVGL 官方对事件回调统一类型定义:
typedef void (*lv_event_cb_t)(lv_event_t * e);
该类型仅规定函数格式,不属于任何结构体,属于标准普通函数指针类型。
2. 实例
用户自定义的回调函数,其地址即为普通函数指针:
void test_cb(lv_event_t * e) { }
3. 特点
-
独立存在,不归属任何控件或结构体
-
仅提供函数执行地址,等待被调用
-
开发者传入
lv_obj_add_event_cb的回调函数,均为普通函数指针
二、结构体函数指针
1. 定义
将函数指针作为结构体成员进行保存,该指针称为结构体函数指针。
普通函数指针是"裸函数地址",结构体函数指针是"被结构体收纳保存的函数地址"。
2. LVGL 底层真实结构(简化)
LVGL 并不会直接在 lv_obj_t 中存放回调,而是通过独立事件结构体管理:
cs
// 事件描述结构体:保存单个回调信息
typedef struct lv_event_dsc_t
{
lv_event_cb_t cb;
// 结构体函数指针(真正保存回调的位置)
uint32_t event_type;
// 事件类型
void * user_data;
// 用户数据
}
lv_event_dsc_t;
// 控件结构体
typedef struct lv_obj_t
{
// 控件坐标、样式等基础属性
lv_event_dsc_t * event_list;
// 控件挂载的所有事件
}
lv_obj_t;
3. 特点
-
必须依托结构体存在,不能独立使用
-
每一个控件拥有独立的事件结构体,控件之间回调互不干扰
-
控件能够绑定多个事件,依靠事件结构体链表实现
三、lv_obj_add_event_cb 工作原理
1. 函数作用
lv_obj_add_event_cb 的核心功能:将用户自定义的普通函数指针,赋值绑定到目标控件所属事件结构体的函数指针成员中,实现回调函数与控件事件的关联。
2. 执行流程
`lv_obj_add_event_cb(btn, test_cb, LV_EVENT_CLICKED, NULL);`
-
test_cb:用户提供的普通函数指针 -
LVGL 为
btn控件创建/取出对应的lv_event_dsc_t事件结构体 -
将普通函数指针
test_cb赋值给事件结构体内部的cb成员,完成回调绑定 -
最终回调被保存在控件内部,等待事件触发
四、回调参数 lv_event_t *e 的来源(核心难点)
1. 参数来源说明
开发者从未定义、从未创建 lv_event_t 变量,但回调可以正常接收参数。
原因:事件对象由 LVGL 内核自动生成,不属于用户代码。
2. 底层触发逻辑
当按钮被点击时,LVGL 内核会自动执行事件分发逻辑。为方便读者直观理解完整执行流程,下文为教学简化伪代码,仅用于原理演示,并非工程可编译代码,也不会与 LVGL 内核源码冲突。所有变量均为演示用途,真实逻辑由 LVGL 内核私有代码实现。
bash
// 教学伪代码:仅用于原理解释,用户代码不能使用、无需定义
// dsc 为模拟内核临时指针,仅演示逻辑,非用户可用变量
lv_event_dsc_t *dsc = btn->event_list;
// 1. 模拟内核:自动创建局部事件对象
lv_event_t evt;
// 2. 模拟内核:自动填充本次点击的所有事件信息
// 3. 模拟内核:通过结构体函数指针调用用户回调
dsc->cb(&evt);
3. 参数传递关系
-
内核局部变量
evt:系统自动生成 -
&evt:系统自动传递给回调函数 -
用户回调形参
lv_event_t *e:接收系统传入的地址
4. 变量名无关性
参数传递仅传递内存地址,与变量名称无关:
-
内核内部事件变量名称由官方源码固定定义,不可修改;
-
仅用户侧回调形参名称可自定义,修改为
e、ev、event均不影响程序运行;
五、最终核心总结
1. 普通函数指针:用户编写的回调函数,独立存在,无归属。
2. 结构体函数指针:存放在控件事件结构体内部的指针,用于保存回调。
3. 绑定原理:API 将用户的普通函数指针,保存进控件的结构体函数指针中。
4. 参数原理:事件触发时,LVGL 内核在自身私有作用域中自动创建事件对象、完成参数传递,用户仅需通过回调形参接收事件数据即可。