3、LVGL基础入门:LVGL架构与核心概念
好,从这一章开始,我们正式进入LVGL的世界。说实话,我第一次接触LVGL的时候,也被它那一堆概念搞得有点懵——对象、图层、事件、样式……感觉像在学一个新的操作系统。但别担心,咱们一步步来,把这些东西拆开揉碎了讲清楚。
我个人习惯,学一个新框架之前,先看它的整体架构。就像你要去一个陌生的城市,先看地图,再走街串巷,心里才有底。
3.1 LVGL的整体架构
LVGL的架构,说白了就是三层结构:
- 底层:硬件驱动层,负责跟屏幕、触摸、定时器打交道。
- 中间层:核心库层,对象管理、事件分发、绘图引擎都在这里。
- 上层:控件层,按钮、标签、滑块、图表……你看到的UI元素。
你想想看,这三层各司其职。底层把像素点亮,中间层决定怎么画、画什么,上层提供现成的“积木块”。
核心要点: LVGL的架构设计遵循“分层解耦”原则。你写应用代码时,基本只跟上层控件打交道,底层驱动配置好一次就不用再动了。
我在项目中遇到过一个问题:有人把底层驱动和UI逻辑混在一起写,结果换了个屏幕,整个代码都要重写。嗯,这就是没理解分层的好处。
3.2 核心概念:对象、图层、事件
这三个概念,是LVGL的基石。搞懂了它们,你就掌握了LVGL的一半。
3.2.1 对象(lv_obj_t)
LVGL里的一切都是对象。按钮是对象,标签是对象,甚至屏幕本身也是一个对象。每个对象都有自己的属性:位置、大小、颜色、透明度……
对象之间是父子关系。比如,一个按钮对象是屏幕对象的子对象。子对象会继承父对象的一些属性,比如裁剪区域、透明度等。
// 创建一个屏幕对象
lv_obj_t * scr = lv_obj_create(NULL);
// 在屏幕上创建一个按钮对象
lv_obj_t * btn = lv_btn_create(scr);
// 在按钮上创建一个标签对象
lv_obj_t * label = lv_label_create(btn);
lv_label_set_text(label, "点击我");
这段代码,就是典型的“对象树”结构。屏幕是根,按钮是子,标签是孙。你想想看,这种树形结构管理UI,是不是很清晰?
小技巧: 我习惯把对象树画在纸上,尤其是复杂界面。先画树,再写代码,逻辑不会乱。
3.2.2 图层(Layer)
图层这个概念,很多人容易忽略。LVGL默认有三个图层:
| 图层 | 名称 | 用途 |
|---|---|---|
| 底层 | LV_LAYER_TYPE_BOTTOM | 背景、壁纸等 |
| 主层 | LV_LAYER_TYPE_TOP | 普通UI控件 |
| 顶层 | LV_LAYER_TYPE_SYS | 系统级覆盖层(如弹窗) |
说白了,图层就是Z轴顺序。底层在最下面,顶层在最上面。你平时创建的控件默认都在主层。如果你想让某个控件永远在最前面,比如一个“加载中”的提示,可以把它放到顶层。
我曾经踩过一个坑:弹窗被其他控件挡住了。后来才发现,弹窗默认在主层,而主层里控件的Z轴顺序是按创建时间排的。解决办法很简单,把弹窗放到顶层图层就好了。
// 创建一个顶层图层对象
lv_obj_t * sys_layer = lv_layer_sys_create();
// 在顶层图层上创建弹窗
lv_obj_t * msgbox = lv_msgbox_create(sys_layer, NULL);
3.2.3 事件(Event)
事件,就是用户操作和系统状态变化的“通知”。点击、长按、滑动、释放……这些都是事件。
LVGL的事件处理机制,我特别喜欢。它用的是“事件监听+回调”模式。你给一个对象注册一个事件回调函数,当事件发生时,LVGL自动调用这个函数。
// 事件回调函数
static void btn_event_cb(lv_event_t * e) {
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_CLICKED) {
LV_LOG_USER("按钮被点击了!");
}
}
// 注册事件回调
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL);
这里要注意,事件是可以冒泡的。子对象的事件会向上传递给父对象。你想想看,这有什么用?比如,你有一个容器,里面有很多按钮。你不需要给每个按钮都注册事件,只需要给容器注册一个事件,然后判断事件源是谁就行了。
避坑指南: 我曾经在事件回调里做了耗时操作,导致界面卡死。记住,事件回调里不要做延时、循环、文件读写等阻塞操作。如果必须做,请使用LVGL的任务系统(lv_task)或异步处理。
3.3 创建第一个LVGL工程
理论说完了,咱们动手。创建一个LVGL工程,其实就三步:
- 下载源码:从GitHub或官网下载LVGL源码。
- 配置工程:把源码文件添加到你的IDE工程里。
- 编写初始化代码:调用lv_init()初始化库,然后配置显示驱动和输入驱动。
我建议你直接用LVGL官方提供的模板工程,省时省力。如果你用的是STM32,官方有STM32的模板;如果你用的是ESP32,也有对应的模板。
#include "lvgl.h"
int main(void) {
// 1. 初始化LVGL库
lv_init();
// 2. 初始化显示驱动(后面详细讲)
my_display_init();
// 3. 初始化输入驱动(触摸或按键)
my_input_init();
// 4. 创建UI
lv_obj_t * label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Hello, LVGL!");
lv_obj_center(label);
// 5. 主循环
while(1) {
lv_timer_handler(); // 处理LVGL任务
my_delay(5); // 延时5ms
}
}
这段代码,就是LVGL工程的“骨架”。你以后所有的项目,都是在这个骨架基础上填充内容。
小提示: lv_timer_handler() 是LVGL的心脏,它负责处理所有定时任务、动画、事件等。你必须定期调用它,一般每5ms调用一次。我习惯在主循环里放一个5ms的延时,然后调用它。
3.4 屏幕与显示驱动配置
显示驱动配置,是LVGL移植中最关键的一步。说白了,就是告诉LVGL:你的屏幕长什么样,怎么往上面画像素。
LVGL的显示驱动,核心是一个结构体:lv_disp_drv_t。你需要填充这个结构体,然后注册它。
| 字段 | 说明 | 必填 |
|---|---|---|
| hor_res | 屏幕水平分辨率 | 是 |
| ver_res | 屏幕垂直分辨率 | 是 |
| flush_cb | 刷新回调函数 | 是 |
| draw_buf | 绘图缓冲区 | 是 |
| user_data | 用户自定义数据 | 否 |
最重要的就是 flush_cb,这个函数负责把LVGL绘制好的缓冲区内容,刷新到屏幕上。
// 显示缓冲区
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[LV_HOR_RES_MAX * 10]; // 10行缓冲区
// 刷新回调函数
static void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) {
// 将color_p指向的缓冲区数据,刷新到屏幕的area区域
// 这里调用你的屏幕驱动函数
my_display_fill(area->x1, area->y1, area->x2, area->y2, color_p);
// 通知LVGL刷新完成
lv_disp_flush_ready(disp_drv);
}
// 显示驱动初始化
void my_display_init(void) {
// 初始化绘图缓冲区
lv_disp_draw_buf_init(&draw_buf, buf, NULL, LV_HOR_RES_MAX * 10);
// 初始化显示驱动
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = 320; // 屏幕宽度
disp_drv.ver_res = 240; // 屏幕高度
disp_drv.flush_cb = my_flush_cb;
disp_drv.draw_buf = &draw_buf;
// 注册显示驱动
lv_disp_drv_register(&disp_drv);
}
这里有个细节要注意:lv_disp_flush_ready() 必须调用。如果不调用,LVGL会认为刷新还没完成,就不会继续绘制下一帧。我曾经忘了加这一句,结果屏幕一直黑着,查了半天才发现。
关键点: 显示缓冲区的大小直接影响性能。缓冲区越大,刷新效率越高,但占用内存也越多。对于资源受限的MCU,我建议使用“行缓冲区”,比如10行。这样内存占用小,性能也够用。
嗯,到这里,LVGL的基础架构和核心概念就讲完了。你可能会觉得内容有点多,但别急,这些都是基本功。下一节我们会用这些知识,真正跑起来一个LVGL程序。