深度拆解 LVGL:按键点击到标签刷新完整底层源码流程
·
前言
日常使用 LVGL 最经典的 Demo:点击按钮,标签显示123456,绝大多数开发者只会调用 API,但不清楚从触摸硬件到屏幕文字更新的完整底层链路。 本文结合 LVGL 原生源码,基于 Guider 自动生成工程代码,完整梳理硬件输入→事件注册→事件分发→回调执行→控件重绘全流程。
一、业务基础代码(Guider 生成示例)
1. 事件绑定初始化代码
void events_init_screen (lv_ui *ui)
{
// 给btn_1注册事件回调,第四个参数ui作为user_data传入回调
lv_obj_add_event_cb(ui->screen_btn_1, screen_btn_1_event_handler, LV_EVENT_ALL, ui);
}
2. 按键点击回调 handler
static void screen_btn_1_event_handler (lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
switch (code) {
case LV_EVENT_CLICKED:
{
// Guider自动封装全局UI结构体guider_ui,直接访问label控件
lv_label_set_text(guider_ui.screen_label_1, "123456");
break;
}
default:
break;
}
}
阶段 1:界面初始化,回调函数挂载到按钮对象(lv_obj_add_event_cb 底层)
1.1 LVGL 控件基类结构体 lv_obj_t
所有按钮、标签等控件都继承该结构体,内置事件链表头:
typedef struct _lv_obj_t {
const lv_obj_class_t * class_p; // 控件类(按钮类、标签类)
struct _lv_obj_t * parent;
// ... 坐标、样式、图层等冗余字段省略
lv_event_list_t * event_list; // 【关键】事件回调链表头
} lv_obj_t;
1.2 事件链表节点结构体
存储单条回调的全部信息:函数指针、监听事件、自定义参数
typedef struct _lv_event_cb_dsc_t {
lv_event_cb_t cb; // 你写的 screen_btn_1_event_handler 函数指针
lv_event_code_t filter; // 监听的事件(LV_EVENT_ALL)
void * user_data; // 注册传入的 ui 指针
struct _lv_event_cb_dsc_t * next; // 下一个回调节点
} lv_event_cb_dsc_t;
1.3 lv_obj_add_event_cb 底层源码逻辑
void lv_obj_add_event_cb(lv_obj_t * obj, lv_event_cb_t cb, lv_event_code_t filter, void * user_data)
{
// 1. 给按钮分配一个事件节点
lv_event_cb_dsc_t * dsc = lv_malloc(sizeof(lv_event_cb_dsc_t));
dsc->cb = cb; // 保存handler函数地址
dsc->filter = filter; // LV_EVENT_ALL
dsc->user_data = user_data; // 保存ui指针
dsc->next = NULL;
// 2. 如果按钮没有事件链表,新建链表
if(obj->event_list == NULL) {
obj->event_list = lv_malloc(sizeof(lv_event_list_t));
obj->event_list->head = NULL;
}
// 3. 把节点插入按钮的event_list链表尾部
_lv_event_list_append(obj->event_list, dsc);
}
本阶段结论:仅内存挂载函数指针,无任何函数执行,只是建立「按钮→回调」的映射关系。
阶段 2:触摸硬件输入 → LVGL 内核识别点击按钮(lv_task_handler 主循环)
2.1 LVGL 程序核心主循环
所有 LVGL 应用必须周期调用,输入、事件、渲染全部在这里处理
while(1) {
lv_task_handler(); // 所有输入、渲染、事件分发都在这里处理
delay_ms(10);
}
lv_task_handler 内部执行三步:输入处理 → 任务执行 → 屏幕渲染
2.2 底层触摸硬件上报逻辑
- 触摸屏驱动轮询 / 中断读取触摸坐标
(x,y)、状态PRESSED/RELEASED - 调用
lv_indev_set_point、lv_indev_set_state存入 LVGL 输入缓冲区
2.3 输入处理核心源码分支
void lv_task_handler(void)
{
// 第一步:处理所有输入设备(触摸、外设按键)
_lv_indev_process();
// 第二步:执行延时任务、动画任务
_lv_task_exec();
// 第三步:刷新脏区域渲染屏幕
_lv_disp_refr_process();
}
2.3.1 遍历输入设备,读取触摸数据
static void _lv_indev_process(void)
{
lv_indev_t * indev;
LV_LLIST_FOREACH(indev, LV_GLOBAL_INDEV_HEAD) {
if(indev->driver->read_cb) {
indev->driver->read_cb(indev, &indev->proc_data); // 调用底层驱动读取触摸
// 根据触摸坐标遍历控件树,查找被点击的 obj(btn_1)
_lv_indev_search_obj(indev);
// 根据按下/松开动作生成对应事件码
_lv_indev_send_event(indev);
}
}
}
2.3.2 遍历控件树,精准命中按钮btn_1
_lv_indev_search_obj递归遍历屏幕所有子控件,判断触摸坐标是否落在控件矩形区域内:
static void _lv_indev_search_obj(lv_indev_t * indev)
{
lv_area_t touch_area;
lv_obj_t * scr = lv_disp_get_scr_act(indev->disp);
lv_area_set(&touch_area, indev->proc_data.point.x, indev->proc_data.point.y,
indev->proc_data.point.x, indev->proc_data.point.y);
// 递归查找最上层、包含触摸坐标的控件
indev->act_obj = lv_obj_get_child_at(scr, &touch_area, true);
// 点击btn_1后,act_obj = guider_ui.screen_btn_1
}
2.3.3 生成LV_EVENT_CLICKED点击事件
手指松开时,LVGL 内核判定一次完整点击,生成对应事件码并派发事件:
static void _lv_indev_send_event(lv_indev_t * indev)
{
lv_obj_t * target = indev->act_obj;
lv_event_code_t event_code;
if(indev->proc_data.state == LV_INDEV_STATE_RELEASED) {
// 按下后松开,判定为点击,事件码赋值 LV_EVENT_CLICKED
event_code = LV_EVENT_CLICKED;
// 【核心】向按钮对象派发事件
lv_obj_send_event(target, event_code, NULL);
}
}
阶段 3:lv_obj_send_event底层遍历事件链表,调用 handler 回调
3.1 事件分发核心源码
lv_res_t lv_obj_send_event(lv_obj_t * obj, lv_event_code_t event_code, void * param)
{
if(obj->event_list == NULL) return LV_RES_OK; // 无注册回调,直接返回
// 1. 初始化 lv_event_t 事件结构体,携带全部上下文
lv_event_t e;
e.target = obj; // 触发对象:btn_1
e.code = event_code; // 事件码:LV_EVENT_CLICKED
e.param = param;
e.user_data = NULL; // 临时占位,遍历节点时会覆盖
e.current_target = obj;
// 2. 遍历按钮 event_list 链表所有回调节点
lv_event_cb_dsc_t * dsc;
LV_LLIST_FOREACH(dsc, obj->event_list->head) {
// 判断当前事件是否匹配注册时的 filter(LV_EVENT_ALL 全部匹配)
if((dsc->filter & event_code) || dsc->filter == LV_EVENT_ALL) {
e.user_data = dsc->user_data; // 填充注册时传入的 ui 指针
// 【最关键一步:函数指针直接调用你的handler】
dsc->cb(&e);
// 等价执行 screen_btn_1_event_handler(&e);
}
}
return LV_RES_OK;
}
3.2 进入自定义 handler 执行业务逻辑
static void screen_btn_1_event_handler (lv_event_t *e)
{
// 从事件结构体取出事件码 e->code = LV_EVENT_CLICKED
lv_event_code_t code = lv_event_get_code(e);
switch (code) {
case LV_EVENT_CLICKED:
{
// 调用标签修改文本接口,进入下一阶段
lv_label_set_text(guider_ui.screen_label_1, "123456");
break;
}
default: break;
}
}
lv_event_get_code(e)只是简单封装读取e->code字段,无复杂逻辑。
阶段 4:lv_label_set_text底层通知标签刷新、渲染出 123456
修改文字不会立刻绘图,分为更新内存字符串 → 标记脏区 → 下一帧统一渲染三步。
4.1 lv_label_set_text 源码
void lv_label_set_text(lv_obj_t * obj, const char * text)
{
// 校验对象必须是标签类
LV_ASSERT_OBJ(obj, MY_CLASS);
lv_label_t * label = (lv_label_t *)obj;
// 1. 释放旧字符串内存,分配新内存存储 "123456"
if(label->text != NULL) lv_free(label->text);
label->text = lv_strdup(text);
// 2. 【关键】标记控件区域为「脏区」,告诉LVGL这块画面需要重绘
lv_obj_invalidate(obj);
}
4.1.1 lv_obj_invalidate 底层标记脏区
void lv_obj_invalidate(lv_obj_t * obj)
{
lv_area_t area;
lv_obj_get_coords(obj, &area); // 获取标签在屏幕上的坐标矩形
// 将标签矩形加入显示器脏区链表,等待渲染阶段刷新
_lv_disp_add_invalid_area(obj->disp, &area);
}
脏区机制作用:LVGL 仅重绘标记过的区域,避免全屏刷新节省性能。
4.2 主循环渲染阶段绘制文字
回到lv_task_handler()的_lv_disp_refr_process()分支处理脏区:
- 取出所有脏区(本次是 label_1 的矩形)
- 调用标签类专属绘制回调
lv_label_class_draw - 字体渲染器将字符串
"123456"转为像素点阵,写入显示缓冲区 - 底层显示驱动推送缓冲区到屏幕,肉眼看到文字更新
标签绘制回调核心逻辑
static void draw_label(lv_obj_t * obj, lv_draw_ctx_t * draw_ctx)
{
lv_label_t * label = (lv_label_t *)obj;
lv_draw_label_dsc_t label_dsc;
lv_draw_label_dsc_init(&label_dsc);
label_dsc.text = label->text; // 读取刚更新的 "123456"
// 在标签坐标处绘制文本像素
lv_draw_label(draw_ctx, &label_dsc, &obj->coords, label->text, NULL);
}
完整底层时序汇总(从头到尾执行链路)
- 初始化阶段:
events_init_screen→lv_obj_add_event_cb→ 分配lv_event_cb_dsc_t节点挂载到btn_1->event_list,保存 handler 函数指针 - 硬件触摸阶段:触摸屏驱动上报 (x,y) 坐标与松开状态 → 存入 LVGL 输入设备缓冲区
- 主循环轮询阶段:
lv_task_handler()→_lv_indev_process()- 遍历控件树,通过坐标匹配找到点击对象
btn_1 - 判定松开动作,生成
LV_EVENT_CLICKED事件码
- 遍历控件树,通过坐标匹配找到点击对象
- 事件分发阶段:
lv_obj_send_event(btn_1, LV_EVENT_CLICKED)- 遍历按钮事件链表,匹配
LV_EVENT_ALL过滤条件 - 通过函数指针直接调用
screen_btn_1_event_handler(&e)
- 遍历按钮事件链表,匹配
- 业务逻辑执行:进入 handler,匹配
LV_EVENT_CLICKED,调用lv_label_set_text(label, "123456")- 更新标签内存中的文本字符串
lv_obj_invalidate(label)标记标签区域为脏区
- 屏幕渲染阶段:主循环渲染分支处理脏区
- 调用标签绘制函数,渲染
123456像素点阵 - 刷入显示缓冲区,屏幕更新文字
- 调用标签绘制函数,渲染
关键底层知识点总结
- LVGL 事件为纯同步机制:无消息队列、无操作系统信号、无线程,仅通过函数指针同步调用,handler 阻塞在
lv_obj_send_event内部。 - 回调提前挂载:初始化时就将函数指针存入控件,点击仅触发执行,不是点击时才绑定回调。
- 每个控件独立事件链表:按钮、标签各自维护独立
event_list,不同控件事件互不干扰。 - 文字更新≠立即绘图:
lv_label_set_text仅修改内存 + 标记脏区,真正渲染统一在下一帧lv_task_handler渲染分支执行。 - 一切交互依赖 lv_task_handler:如果不周期调用该函数,输入、事件、渲染全部停滞,界面卡死。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)