前言

日常使用 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 底层触摸硬件上报逻辑

  1. 触摸屏驱动轮询 / 中断读取触摸坐标(x,y)、状态PRESSED/RELEASED
  2. 调用lv_indev_set_pointlv_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()分支处理脏区:

  1. 取出所有脏区(本次是 label_1 的矩形)
  2. 调用标签类专属绘制回调lv_label_class_draw
  3. 字体渲染器将字符串"123456"转为像素点阵,写入显示缓冲区
  4. 底层显示驱动推送缓冲区到屏幕,肉眼看到文字更新

标签绘制回调核心逻辑

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);
}

完整底层时序汇总(从头到尾执行链路)

  1. 初始化阶段events_init_screenlv_obj_add_event_cb → 分配lv_event_cb_dsc_t节点挂载到btn_1->event_list,保存 handler 函数指针
  2. 硬件触摸阶段:触摸屏驱动上报 (x,y) 坐标与松开状态 → 存入 LVGL 输入设备缓冲区
  3. 主循环轮询阶段lv_task_handler()_lv_indev_process()
    • 遍历控件树,通过坐标匹配找到点击对象btn_1
    • 判定松开动作,生成LV_EVENT_CLICKED事件码
  4. 事件分发阶段lv_obj_send_event(btn_1, LV_EVENT_CLICKED)
    • 遍历按钮事件链表,匹配LV_EVENT_ALL过滤条件
    • 通过函数指针直接调用screen_btn_1_event_handler(&e)
  5. 业务逻辑执行:进入 handler,匹配LV_EVENT_CLICKED,调用lv_label_set_text(label, "123456")
    • 更新标签内存中的文本字符串
    • lv_obj_invalidate(label)标记标签区域为脏区
  6. 屏幕渲染阶段:主循环渲染分支处理脏区
    • 调用标签绘制函数,渲染123456像素点阵
    • 刷入显示缓冲区,屏幕更新文字

关键底层知识点总结

  1. LVGL 事件为纯同步机制:无消息队列、无操作系统信号、无线程,仅通过函数指针同步调用,handler 阻塞在lv_obj_send_event内部。
  2. 回调提前挂载:初始化时就将函数指针存入控件,点击仅触发执行,不是点击时才绑定回调。
  3. 每个控件独立事件链表:按钮、标签各自维护独立event_list,不同控件事件互不干扰。
  4. 文字更新≠立即绘图lv_label_set_text仅修改内存 + 标记脏区,真正渲染统一在下一帧lv_task_handler渲染分支执行。
  5. 一切交互依赖 lv_task_handler:如果不周期调用该函数,输入、事件、渲染全部停滞,界面卡死。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐