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 本章小结

今天我们干了三件事:

  1. 搞清楚了LVGL的三层架构:应用层、核心层、驱动层。
  2. 理解了对象树的概念:所有控件以父子关系组织,父动子随。
  3. 写出了第一个Hello World界面,还加了个心跳动画。

下一章我们会深入LVGL的控件系统,聊聊按钮、滑块、进度条这些常用控件的用法和坑。嗯,到时候我会分享一个我在智能手表项目里踩过的“按钮点击无响应”的坑,保证让你少走弯路。

先消化这些,动手跑一下代码。有问题随时翻前面的内容。