3. LVGL 对象系统:从 lv_obj 到你的第一个屏幕

LVGL 里的一切,都是对象。你看到的按钮、标签、滑块、图表……底层全是 lv_obj_t 这个结构体。我刚开始接触 LVGL 时,总觉得“万物皆对象”是句空话,直到自己动手写了一个复杂界面,才真正体会到这套设计有多巧妙。

说白了,LVGL 的对象系统就是一棵树。每个控件都是一个节点,节点之间通过父子关系连接,构成完整的界面层级。你想想看,一个屏幕上有多个页面,每个页面里有多个控件,控件里可能还有子控件——这不就是树吗?

3.1 lv_obj_t:一切控件的根

lv_obj_t 是所有 LVGL 控件的基类。无论你创建按钮、标签还是图表,底层都先分配一个 lv_obj_t,再往上叠加特定控件的行为和样式。

核心要点:lv_obj_t 本身就是一个“空容器”,它不显示任何内容,但提供了所有控件共有的基础设施——位置、大小、父子关系、样式、事件处理。

创建一个基础对象很简单:

lv_obj_t * obj = lv_obj_create(lv_scr_act());  // 在活动屏幕上创建对象

这个 lv_obj_create 函数做了什么?我拆开源码看过,它主要干了三件事:

  1. 分配内存——从 LVGL 的内存池里拿一块空间
  2. 初始化属性——把大小、位置、样式等字段设为默认值
  3. 挂入对象树——把新对象添加到父对象的子列表里

嗯,这里要注意:lv_obj_create 的父对象参数不能传 NULL。如果你传 NULL,LVGL 会直接断言报错。我早期踩过这个坑,调试了半天才发现是父对象没传对。

3.2 对象的父子关系与树形结构

LVGL 的界面是一棵树。根节点是 lv_scr_act()(当前活动屏幕),下面挂载各种容器和控件。每个对象都可以有多个子对象,但只能有一个父对象。

为什么要用树形结构?两个原因:

  • 事件传递——点击事件从父对象向下传播,子对象可以拦截或处理
  • 样式继承——子对象默认继承父对象的某些样式属性,减少重复代码

我画了一张图,帮你理解这个结构:

LVGL 对象树形结构 lv_scr_act() lv_cont(容器A) lv_cont(容器B) lv_btn(按钮) lv_label(标签) lv_btn(按钮) lv_slider(滑块) lv_label(按钮文字) lv_img(图标) 说明 每个对象只有一个父对象 但可以有多个子对象

操作父子关系的 API 很直观:

// 获取/设置父对象
lv_obj_t * parent = lv_obj_get_parent(child);
lv_obj_set_parent(child, new_parent);

// 遍历子对象
uint32_t child_cnt = lv_obj_get_child_cnt(parent);
for(uint32_t i = 0; i < child_cnt; i++) {
    lv_obj_t * child = lv_obj_get_child(parent, i);
    // 处理子对象
}

// 判断是否子对象
bool is_child = lv_obj_is_child_of(child, parent);

注意:不要在一个对象的回调函数里删除它自己或它的父对象。LVGL 的事件处理是同步的,删除正在处理事件的对象会导致野指针崩溃。我遇到过这种问题,后来改用 lv_event_send 延迟删除才解决。

3.3 对象的属性:大小、位置与样式

每个 lv_obj_t 都有一组核心属性。我习惯把它们分成三类:

属性类别 常用 API 说明
位置 lv_obj_set_pos(obj, x, y) 相对于父对象左上角的坐标
大小 lv_obj_set_size(obj, w, h) 宽度和高度,单位像素
对齐 lv_obj_align(obj, LV_ALIGN_CENTER, 0, 0) 相对于父对象的对齐方式
样式 lv_obj_add_style(obj, &style, selector) 样式通过选择器叠加,支持多状态

3.3.1 位置与大小

位置和大小是对象最基础的属性。LVGL 支持绝对定位和相对定位:

// 绝对定位:直接设置坐标
lv_obj_set_pos(btn, 50, 100);

// 相对定位:基于父对象对齐
lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0);          // 居中
lv_obj_align(btn, LV_ALIGN_TOP_LEFT, 10, 10);      // 左上角偏移

// 设置大小
lv_obj_set_size(btn, 120, 40);

// 获取当前大小
lv_coord_t w = lv_obj_get_width(btn);
lv_coord_t h = lv_obj_get_height(btn);

我个人习惯用 lv_obj_align 而不是手动算坐标。为什么?因为屏幕尺寸变化时,对齐会自动适应,手动算坐标很容易跑偏。我在一个项目里用绝对定位做了个键盘布局,换了个分辨率稍有不同的屏幕,所有按键位置全乱了——从那以后,能用对齐的地方我绝不用硬编码坐标。

3.3.2 样式系统

样式是 LVGL 最强大的特性之一。每个对象可以叠加多个样式,样式之间通过选择器决定优先级。

样式的核心概念:

  • 样式属性——比如背景色、边框宽度、圆角半径、文字颜色等
  • 样式状态——默认、按下、选中、禁用等,不同状态可以有不同的样式
  • 样式层级——局部样式 > 全局样式 > 默认样式

看个例子:

// 1. 创建一个样式
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);
lv_style_set_border_width(&style_btn, 2);

// 2. 创建按钮并应用样式
lv_obj_t * btn = lv_btn_create(lv_scr_act());
lv_obj_add_style(btn, &style_btn, 0);  // 0 表示默认状态

// 3. 按下状态单独设置
static lv_style_t style_btn_pressed;
lv_style_init(&style_btn_pressed);
lv_style_set_bg_color(&style_btn_pressed, lv_color_hex(0x1565C0));
lv_obj_add_style(btn, &style_btn_pressed, LV_STATE_PRESSED);

小技巧:样式对象(lv_style_t)最好声明为 static,避免每次创建对象时重复初始化。我在一个仪表盘项目里,把按钮、标签、滑块的样式都定义成全局静态变量,代码量减少了 40%,而且修改样式只需改一处。

3.4 实战:创建一个带样式的按钮

把上面学到的知识串起来,写一个完整的例子:

void my_app_create(void) {
    // 1. 创建父容器
    lv_obj_t * cont = lv_obj_create(lv_scr_act());
    lv_obj_set_size(cont, 200, 150);
    lv_obj_align(cont, LV_ALIGN_CENTER, 0, 0);

    // 2. 定义按钮样式
    static lv_style_t style_btn;
    lv_style_init(&style_btn);
    lv_style_set_bg_color(&style_btn, lv_palette_main(LV_PALETTE_BLUE));
    lv_style_set_radius(&style_btn, 6);
    lv_style_set_shadow_width(&style_btn, 8);

    // 3. 创建按钮(子对象挂到容器上)
    lv_obj_t * btn = lv_btn_create(cont);
    lv_obj_set_size(btn, 100, 40);
    lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0);
    lv_obj_add_style(btn, &style_btn, 0);

    // 4. 创建标签(子对象挂到按钮上)
    lv_obj_t * label = lv_label_create(btn);
    lv_label_set_text(label, "Click Me");
    lv_obj_center(label);
}

这个例子展示了完整的对象树:lv_scr_act() → cont → btn → label。每个对象都通过父子关系连接,样式从父对象继承,事件从父对象向下传递。

3.5 避坑指南

最后分享几个我踩过的坑:

  • 不要重复初始化样式——lv_style_init 只能调用一次,重复调用会导致内存泄漏。我早期在循环里初始化样式,跑了一晚上内存就爆了。
  • 样式变量生命周期——样式对象必须保持有效,不能是局部变量。如果样式在函数返回后被销毁,对象会引用野指针,界面直接花屏。
  • 父子关系不能成环——LVGL 不会检测循环引用,如果你不小心把父对象设成了子对象,递归遍历时会死循环。我建议在 lv_obj_set_parent 之前先检查一下。