3、LVGL基础入门:LVGL架构、对象树概念、第一个Hello World界面
好,咱们正式开始动手了。
前面两章聊了嵌入式GUI的选型和环境搭建,算是热身。从这一章开始,我们要真正进入LVGL的世界。我个人觉得,理解一个框架,最好的方式不是先啃文档,而是先搞清楚它长什么样、怎么组织代码、然后跑起来第一个例子。今天我们就干这三件事。
3.1 LVGL的整体架构
先看一张图,在心里搭个框架。
LVGL的架构其实不复杂,从上到下大概分三层:
- 应用层:你写的代码,创建按钮、标签、处理点击事件。
- 核心层:LVGL自己的库,管理对象、绘图、事件、动画等。
- 驱动层:连接硬件,比如屏幕的显示驱动、触摸驱动、定时器。
说白了,你写应用层代码,调用核心层的API,核心层再通过驱动层把内容画到屏幕上。
核心要点:LVGL本身不直接操作硬件寄存器。它通过一组“接口”与底层打交道。你只需要实现这几个接口(比如 flush 函数、读取触摸点),剩下的绘图、事件处理全交给LVGL。
我在项目中遇到过一种情况:有人把LVGL的绘图逻辑和硬件操作混在一起写,结果换了个屏幕驱动,代码改得面目全非。嗯,这里要记住——分层是嵌入式开发的命根子。
3.2 对象树概念
这是LVGL里最重要的概念之一,没有“之一”。
LVGL里所有的界面元素,都叫“对象”(lv_obj_t)。按钮是对象,标签是对象,进度条也是对象。这些对象不是孤立的,它们以树形结构组织在一起。
想象一下:
- 最顶层有一个“根对象”,叫屏幕(screen)。
- 屏幕上可以放一个“面板”(panel),面板是屏幕的子对象。
- 面板上再放一个“按钮”,按钮是面板的子对象。
- 按钮上再放一个“标签”,显示文字。
这就形成了一棵树:
屏幕 (screen)
└── 面板 (panel)
└── 按钮 (btn)
└── 标签 (label)
为什么要搞这么复杂?你想想看,如果所有控件都平铺在屏幕上,那移动一个面板,它上面的按钮和标签都得跟着动。有了父子关系,你只需要移动父对象,子对象自动跟着走。这就是继承位置和变换。
小技巧:我习惯把逻辑上相关的控件放在同一个父对象下。比如一个设置页面,所有控件都放在一个“设置面板”里。这样后期调整布局、隐藏或删除整个页面,只需要操作父对象就行。
还有一个细节:每个对象都有一个“父指针”和“子链表”。LVGL内部通过链表维护这些关系。你创建对象时,如果不指定父对象,默认父对象就是当前活动的屏幕。
注意:删除父对象时,所有子对象也会被自动删除。我曾经在项目中不小心删了一个面板,结果上面的十几个控件全没了,排查了半天才发现是父对象被提前释放了。所以,管理好对象的生命周期。
3.3 第一个Hello World界面
理论说完了,咱们直接写代码。
目标:在屏幕上显示一个标签,上面写着“Hello, LVGL!”。
先看完整代码:
#include "lvgl.h"
void my_gui_init(void)
{
/* 1. 创建一个标签对象,父对象是当前屏幕 */
lv_obj_t *label = lv_label_create(lv_scr_act());
/* 2. 设置标签的文本 */
lv_label_set_text(label, "Hello, LVGL!");
/* 3. 让标签居中显示 */
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
}
就这么几行。我们来拆解一下:
lv_scr_act():获取当前活动的屏幕对象。每个LVGL应用至少有一个屏幕。lv_label_create():创建一个标签对象。第一个参数是父对象,这里传的是屏幕。lv_label_set_text():设置标签显示的文本。lv_obj_align():对齐函数。这里让标签相对于父对象居中,偏移量为0。
你可能会问:就这么简单?对,就这么简单。LVGL的设计哲学就是“用最少的代码完成最常用的事情”。
个人经验:我第一次在手表上跑这个例子时,屏幕亮起来显示“Hello, LVGL!”的那一刻,说实话挺激动的。但紧接着就遇到了问题——文字太小了,根本看不清。所以,在实际项目中,你通常还需要设置字体大小:
lv_obj_set_style_local_text_font(label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &lv_font_montserrat_20);
或者直接用更简单的方式:
lv_label_set_text(label, "Hello, LVGL!");
lv_obj_set_style_text_font(label, &lv_font_montserrat_20, 0);
3.4 让界面动起来
光显示一个静态文字太无聊了。我们加点动画,让文字慢慢变大再恢复,模拟心跳效果。
void my_gui_init(void)
{
lv_obj_t *label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Hello, LVGL!");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
/* 创建一个缩放动画 */
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, label);
lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_scale);
lv_anim_set_values(&a, 100, 130); /* 从100%缩放到130% */
lv_anim_set_time(&a, 500); /* 持续500ms */
lv_anim_set_playback_time(&a, 500);/* 回放500ms */
lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); /* 无限循环 */
lv_anim_start(&a);
}
你看,LVGL的动画API也很直观。设置目标对象、执行回调、起始值、结束值、时长,然后启动。就这么简单。
避坑指南:我曾经在动画回调里直接修改了对象的坐标,结果和LVGL内部的布局计算冲突了,导致界面闪烁。后来我改用 lv_anim_set_exec_cb 配合专门的属性设置函数,问题就解决了。记住,不要同时用多种方式修改同一个属性。
3.5 本章小结
今天我们干了三件事:
- 搞清楚了LVGL的三层架构:应用层、核心层、驱动层。
- 理解了对象树的概念:所有控件以父子关系组织,父动子随。
- 写出了第一个Hello World界面,还加了个心跳动画。
下一章我们会深入LVGL的控件系统,聊聊按钮、滑块、进度条这些常用控件的用法和坑。嗯,到时候我会分享一个我在智能手表项目里踩过的“按钮点击无响应”的坑,保证让你少走弯路。
先消化这些,动手跑一下代码。有问题随时翻前面的内容。