3. LVGL基础入门

LVGL(Light and Versatile Graphics Library)是一款面向嵌入式系统的开源图形库,以轻量、可裁剪、控件丰富著称。它并不依赖操作系统,也不绑定具体显示芯片,只要提供一块可写的显存缓冲区和周期性心跳,就能在 MCU、Linux 帧缓冲、甚至 PC 模拟器上运行。对于 Qt 与 LVGL 混合界面集成而言,理解 LVGL 的对象树、样式继承和驱动注册方式,是后续把 LVGL 画面嵌入 Qt 窗口的前提。

一、核心架构:从心跳到刷屏

LVGL 的运行可以概括为“一个心跳、两棵树、三层驱动”。心跳由 lv_timer_handler() 驱动,通常每 5ms 调用一次,负责处理动画、输入、重绘和定时任务。两棵树分别指对象树(控件父子关系)和样式树(样式继承与状态)。三层驱动则是显示驱动、输入驱动和文件/图像解码等可选驱动。

显示驱动通过 lv_disp_drv_t 注册,核心是 flush_cb 回调:LVGL 把渲染好的区域交给回调,由开发者搬运到真实屏幕。输入驱动通过 lv_indev_drv_t 注册,常见类型有触摸屏、鼠标、键盘和编码器。LVGL 内部维护一个“无效区域”列表,只有被标记为脏的区域才会重绘,这是它在低端硬件上依然流畅的关键。

LVGL 不直接操作硬件寄存器,所有硬件差异都被 flush_cb 和 read_cb 两个回调隔离,这也是它能同时跑在模拟器和真实板卡上的原因。

二、对象模型:父子树与坐标系统

LVGL 中一切可见元素都是 lv_obj_t 对象,包括按钮、标签、图表、甚至屏幕本身。对象之间通过父子关系组成一棵树,父对象移动、隐藏或删除时,子对象会随之联动。屏幕对象 lv_scr_act() 是当前活动屏幕,也是这棵树的根。

坐标默认相对于父对象,使用 lv_obj_set_pos() 或 lv_obj_align() 定位。LVGL 提供 Flex 和 Grid 两种布局,可让子对象自动排列,减少手工计算。对象还带有标志位(如可点击、可滚动、可聚焦)和事件回调,事件机制是交互逻辑的入口。

lv_obj_t *btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 120, 50);
lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0);

lv_obj_t *label = lv_label_create(btn);
lv_label_set_text(label, "Click");
lv_obj_center(label);

lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL);

三、样式系统:状态、继承与局部覆盖

样式系统是 LVGL 区别于“裸画点”库的重要特征。一个 lv_style_t 可以描述背景色、边框、圆角、阴影、文字字体、内边距等属性。样式不是直接绑定到对象,而是通过“添加样式”的方式叠加,后添加的样式优先级更高,从而支持局部覆盖。

LVGL 为对象定义了多种状态,如默认态、按下态、聚焦态、禁用态和选中态。同一个对象在不同状态下可以呈现不同外观,例如按钮按下时背景变深。样式还支持继承:子对象若未显式设置某属性,会沿对象树向上查找父对象的样式值,这让整体换肤变得容易。

概念作用常用接口
lv_style_t描述一组视觉属性lv_style_init / lv_style_set_bg_color
状态区分默认、按下、聚焦等外观LV_STATE_PRESSED / LV_STATE_FOCUSED
局部样式只作用于单个对象lv_obj_add_style
主题全局统一风格lv_theme_default_init

四、显示与输入驱动:注册与回调

显示驱动注册流程通常为:初始化缓冲区、初始化 lv_disp_drv_t、设置 flush_cb、调用 lv_disp_drv_register()。缓冲区可以是一块、两块或部分屏幕大小,双缓冲能减少撕裂。输入驱动注册流程类似,设置 read_cb 后注册,LVGL 会在心跳中轮询输入状态。

static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf1[SCREEN_W * 40];

lv_disp_draw_buf_init(&draw_buf, buf1, NULL, SCREEN_W * 40);

static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.draw_buf = &draw_buf;
disp_drv.flush_cb = my_flush_cb;
lv_disp_drv_register(&disp_drv);

在 Qt 混合集成场景中,flush_cb 往往不直接写 LCD,而是把像素数据拷贝到 Qt 的 QImage 或纹理中,再由 Qt 负责最终呈现。输入回调则把 Qt 的鼠标、触摸事件转换为 LVGL 的坐标和按下状态。

五、模拟器使用:先在 PC 上跑通

LVGL 官方提供基于 SDL 的模拟器,可在 Windows、Linux、macOS 上直接运行。模拟器使用与真实设备相同的 LVGL 源码,只是把显示和输入驱动替换为 SDL 实现。建议先在模拟器中完成界面布局、样式调试和事件逻辑,再移植到目标板,能显著降低调试成本。

典型模拟器工程包含 lv_conf.h、main.c、lv_drivers 和 lvgl 源码目录。编译后运行可看到窗口,鼠标即触摸点。若使用 Qt 做宿主,也可以把 LVGL 编译为静态库,由 Qt 工程链接,再用 QWidget 或 QQuickItem 承载画面。

模拟器阶段建议开启 LV_USE_LOG 和 LV_USE_ASSERT,很多对象越界、样式未初始化的问题会在日志中直接暴露。

六、常见误区与复盘

  • 把 flush_cb 写成阻塞式整屏拷贝,导致心跳被拖慢;应只拷贝 LVGL 指定的区域。
  • 忘记在 flush_cb 末尾调用 lv_disp_flush_ready(),画面会停止刷新。
  • 在非 LVGL 线程中直接调用 LVGL API,造成对象树竞争;应统一在主循环或加锁。
  • 样式对象定义在函数栈上,函数返回后样式失效;样式应为静态或全局生命周期。
  • 输入坐标未做屏幕旋转和缩放换算,触摸位置偏移。

下面用一张结构图梳理 LVGL 的核心组成与数据流:心跳驱动定时器与输入,输入事件进入对象树,对象树结合样式树完成渲染,渲染结果经显示驱动 flush_cb 输出到屏幕或 Qt 宿主。

LVGL 核心架构与数据流 心跳 lv_timer_handler() 输入驱动 read_cb / indev 定时器 / 动画 anim / timer 对象树 lv_obj_t 父子关系 屏幕 / 按钮 / 标签 事件回调 / 标志位 Flex / Grid 布局 无效区域标记 样式树叠加与状态 渲染引擎 绘制脏区域 合成到 draw_buf 显示驱动 flush_cb 回调 LCD / Qt 宿主 模拟器 / 目标板 SDL 窗口 / 真实屏幕 驱动 事件 刷新 提交 像素 输出

掌握以上内容后,LVGL 就不再是一个“黑盒控件库”,而是一套可拆解、可替换驱动的图形框架。后续在 Qt 中集成时,本质上就是替换 flush_cb 和 read_cb 两个回调,把 LVGL 的渲染结果接入 Qt 的绘制体系。