2. 嵌入式UI基础

做嵌入式 GUI 这几年,我接触过不少图形框架。但 LVGL 的对象模型,说实话,是我见过最直观的之一。它不像某些框架那样绕来绕去,而是把「万物皆对象」的理念贯彻得很彻底。今天我们就来拆解一下这个模型的核心。

2.1 对象树与父子关系

LVGL 里的所有界面元素,都是对象。按钮是对象,标签是对象,滑块也是对象。这些对象不是孤立的,它们组成了一棵树。

这棵树怎么理解?你想想看,一个典型的手机界面:最外层是屏幕(Screen),屏幕上有面板(Panel),面板里有按钮(Button),按钮上还有标签(Label)。这就是一个典型的父子关系链。

核心概念:每个 LVGL 对象都有一个父对象,也可以有零个或多个子对象。父对象移动,子对象跟着移动;父对象隐藏,子对象也一起隐藏。这种层级关系,就是对象树。

我在项目里遇到过一个问题:一个复杂的仪表盘界面,有几十个控件。如果把它们全部平铺在屏幕上,管理起来非常痛苦。后来我把它们按功能分组,每个组放到一个容器(Container)里,再把这些容器挂到屏幕上。这样一来,显示/隐藏一组控件只需要操作父容器,代码量直接减半。

LVGL 对象树结构示例 Screen (根对象) Container (导航栏) Container (主区域) Container (状态栏) Button (返回) Button (菜单) Label (标题) Slider (音量) Label (时间) 对象树特性 • 根节点:每个屏幕(Screen)是一棵独立对象树的根 • 父子关系:子对象的位置、可见性、生命周期受父对象控制 • 兄弟关系:同一父对象下的子对象按创建顺序决定 Z 轴层级

2.2 lv_obj_t 结构体

所有 LVGL 对象的底层,都是一个 lv_obj_t 结构体。这个结构体里装了什么?说白了,就是描述一个对象「是什么、在哪、长什么样」的全部信息。

我习惯把它分成几个核心部分来看:

字段类别 包含内容 说明
父子关系 parent, child_ll 指向父对象的指针,以及子对象链表
位置与大小 coords 实际占用的矩形区域(x1, y1, x2, y2)
样式 style_list 样式链表,支持多样式叠加
标志位 flags 控制可见、可点击、可拖动等行为
事件 event_list 绑定到该对象的事件回调链表
用户数据 user_data 留给开发者自由使用的指针

我的小技巧:调试时,我经常在 user_data 里存一个字符串标识,比如 "btn_confirm"。这样在事件回调里打印对象信息时,一眼就能看出是哪个控件在触发,比靠地址猜方便多了。

2.3 基本属性:大小与位置

每个对象都有两个核心几何属性:位置和大小。LVGL 处理这两者的方式,有一些值得注意的细节。

位置:对象的坐标是相对于父对象的。也就是说,如果你把一个按钮放到一个面板里,按钮的 (0,0) 是面板的左上角,不是屏幕的左上角。这一点和 CSS 的 position: relative 很像。

大小:LVGL 支持三种尺寸模式:

  • 固定尺寸:用 lv_obj_set_width() 直接指定像素值
  • 内容自适应:用 LV_SIZE_CONTENT,让对象根据子对象自动撑开
  • 百分比:用 lv_pct(50),相对于父对象尺寸的百分比
// 创建一个按钮,设置位置和大小
lv_obj_t *btn = lv_btn_create(lv_scr_act());  // 创建在活动屏幕上
lv_obj_set_pos(btn, 50, 100);                  // 距离屏幕左上角 (50, 100)
lv_obj_set_size(btn, 120, 40);                 // 固定宽 120px,高 40px

// 创建一个标签,宽度自适应内容
lv_obj_t *label = lv_label_create(btn);
lv_label_set_text(label, "Click Me");
lv_obj_set_size(label, LV_SIZE_CONTENT, LV_SIZE_CONTENT);  // 自动适配文字大小

我曾经踩过的坑:用 LV_SIZE_CONTENT 时,如果子对象本身也是自适应尺寸,而且没有内容,父对象会缩到 0。比如一个空标签放在自适应容器里,容器就看不见了。解决办法是给容器设一个最小尺寸,或者确保子对象有实际内容。

2.4 基本属性:样式

样式是 LVGL 里最灵活的部分。它不像传统嵌入式 GUI 那样,每个控件写死颜色和字体。LVGL 的样式系统,说白了就是一套「属性叠加层」。

每个样式(lv_style_t)可以包含多个属性:背景色、边框、圆角、文字颜色、透明度等等。你可以创建一个样式,然后把它应用到任意多个对象上。改一个样式,所有引用它的对象都会跟着变。

样式的优先级规则是这样的:

  1. 对象本地样式(优先级最高)
  2. 对象所在屏幕的样式
  3. 主题默认样式(优先级最低)

如果同一个属性在多个层级都有定义,本地样式会覆盖上层样式。这个机制让我在项目里省了不少事——全局统一风格用主题,个别控件微调用本地样式。

// 创建一个样式
static lv_style_t style_btn;
lv_style_init(&style_btn);
lv_style_set_bg_color(&style_btn, lv_color_hex(0x2196F3));  // 蓝色背景
lv_style_set_radius(&style_btn, 8);                          // 圆角 8px
lv_style_set_text_color(&style_btn, lv_color_hex(0xFFFFFF)); // 白色文字

// 应用到按钮
lv_obj_add_style(btn, &style_btn, 0);  // 0 表示应用到默认状态

关键理解:样式不是绑定到对象的,而是对象引用样式。同一个样式可以被多个对象共享。修改样式后,调用 lv_obj_refresh_style() 就能让所有引用它的对象立即更新。

2.5 对象生命周期管理

创建对象很简单,lv_btn_create(parent) 就完了。但删除对象时,有个细节要注意:删除父对象时,所有子对象会被自动递归删除。你不需要手动去删每个子对象。

反过来,如果你只想删一个子对象,用 lv_obj_del(child) 就行。父对象不受影响。

// 创建一组对象
lv_obj_t *parent = lv_obj_create(lv_scr_act());
lv_obj_t *child1 = lv_btn_create(parent);
lv_obj_t *child2 = lv_label_create(parent);

// 删除父对象,child1 和 child2 也会被自动删除
lv_obj_del(parent);  // 一删全删,干净利落

嗯,这里要注意一点:如果你在事件回调里删除了一个对象,要确保后续代码不再使用这个对象的指针。我习惯在删除后立即把指针置 NULL,防止野指针问题。

对象模型这块,说白了就是理解「树」和「属性」两个维度。树决定了对象怎么组织,属性决定了对象长什么样。把这两个维度理清了,LVGL 的界面开发就成功了一半。