4. LVGL对象模型

在LVGL中,界面上的每一个按钮、标签、滑块、图表,本质上都是一个对象(lv_obj_t)。LVGL不是“控件集合”,而是一棵由对象构成的树。理解对象模型,是理解LVGL布局、事件、样式和内存管理的前提。很多初学者写出的界面“位置乱跑、样式串味、删除后死机”,根源往往不在控件本身,而在于没有真正掌握对象树与父子关系。

一、对象树与父子关系:LVGL界面的骨架

LVGL采用树形结构组织对象。每个对象都有一个父对象,父对象可以拥有多个子对象。屏幕(screen)是整棵树的根节点,通常由 lv_scr_act() 获取当前活动屏幕。所有可见控件最终都挂在这棵树上。

父子关系带来三个直接结果:

  • 坐标相对性:子对象的坐标默认相对于父对象的内容区域,而不是屏幕绝对坐标。
  • 可见性继承:父对象隐藏或删除时,其所有子对象随之隐藏或删除。
  • 样式与事件传递:样式属性可沿树向下继承,事件也可从子对象冒泡到父对象。

创建对象时指定父对象,是建立父子关系最常用的方式:

lv_obj_t *panel = lv_obj_create(lv_scr_act());   // 父对象是当前屏幕
lv_obj_t *btn   = lv_btn_create(panel);          // 父对象是 panel
lv_obj_t *label = lv_label_create(btn);          // 父对象是 btn
lv_label_set_text(label, "启动");

这段代码形成的层级是:screen → panel → btn → label。label 的坐标相对于 btn,btn 的坐标相对于 panel。移动 panel,整个面板及其内部按钮、文字一起移动,这正是对象树带来的“整体联动”能力。

LVGL 对象树与坐标/样式继承示意 screen(根) lv_scr_act() panel(容器) 坐标相对 screen btn(按钮) 坐标相对 panel chart(图表) 坐标相对 panel label(文本) 坐标相对 btn 继承方向 样式 ↓ 向下继承 事件 ↑ 向上冒泡 删除父对象 panel 删除后 btn/chart/label 全删

二、坐标系统:相对坐标与对齐

LVGL的坐标系统以父对象内容区左上角为原点,x 向右为正,y 向下为正。设置位置常用 lv_obj_set_pos() 或 lv_obj_align()。其中对齐函数更符合工业面板的布局习惯,例如把按钮固定在面板右下角:

lv_obj_align(btn, LV_ALIGN_BOTTOM_RIGHT, -20, -20);
lv_obj_align_to(label, btn, LV_ALIGN_OUT_TOP_MID, 0, -8);

需要特别注意:lv_obj_set_pos() 设置的是相对父对象的坐标;如果父对象本身被移动,子对象的绝对屏幕位置会随之改变。工业面板中常把一组控件放进容器,再整体移动容器,就是利用了这一特性。

当控件位置“看起来不对”时,先检查它的父对象是谁,再检查父对象的 padding、border 和布局模式。很多偏移问题来自父容器内容区被内边距压缩,而不是控件坐标写错。

三、样式继承:属性沿树向下传递

LVGL样式系统支持继承。部分文本类属性(如文字颜色、字体、透明度等)会从父对象继承到子对象。若子对象没有显式设置某属性,就会沿对象树向上查找最近的有效值。这使我们可以给面板统一设置字体和文字颜色,内部标签自动生效。

static lv_style_t panel_style;
lv_style_init(&panel_style);
lv_style_set_text_color(&panel_style, lv_color_hex(0x1F3B57));
lv_style_set_text_font(&panel_style, &lv_font_montserrat_16);

lv_obj_add_style(panel, &panel_style, 0);   // panel 及其子对象文本继承

但要注意:并非所有属性都可继承。背景色、边框、阴影等通常只作用于设置样式的对象本身。若希望子对象也有背景,需要单独添加样式或使用可继承属性。样式优先级由“本地样式 > 状态样式 > 继承样式”决定,调试样式冲突时应先看对象自身是否覆盖了继承值。

四、对象生命周期管理:创建、删除与内存

LVGL对象由内部内存池管理。使用 lv_obj_create() 创建对象会分配内存;使用 lv_obj_del() 删除对象会释放其自身及所有子对象。删除父对象时,子对象会被递归删除,因此不要保留已删除子对象的指针,否则会形成悬空指针。

操作 函数 对子对象的影响 使用建议
创建对象 lv_obj_create(parent) 无 必须指定有效父对象
删除对象 lv_obj_del(obj) 递归删除全部子对象 删除后指针立即失效
延迟删除 lv_obj_del_async(obj) 下一周期安全删除 事件回调中删除自身时使用
隐藏对象 lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN) 子对象一并隐藏,不释放内存 频繁切换显示时优先隐藏
清理子对象 lv_obj_clean(parent) 删除全部子对象,保留父对象 重建列表、刷新页面时常用
在事件回调中直接 lv_obj_del() 删除触发事件的对象,可能导致回调返回后访问已释放内存。应使用 lv_obj_del_async(),让LVGL在安全时机完成删除。

五、工业面板场景:用对象树组织一个电机控制卡

假设要做一个电机控制卡:外层是卡片容器,内部有标题标签、状态指示灯、启动按钮和转速滑块。合理做法是:

  1. 创建 card 容器,设置固定尺寸、圆角和边框样式。
  2. 在 card 下创建 title 标签,使用 lv_obj_align() 对齐顶部。
  3. 创建 led 对象表示运行状态,放在标题右侧。
  4. 创建 btn 和 slider,分别对齐底部左右。
  5. 为 card 设置统一字体样式,子标签自动继承。
  6. 页面切换时调用 lv_obj_del(card),整张卡片连同内部控件一次性释放。

这样组织后,卡片移动、隐藏、删除都以 card 为操作单位,代码清晰,内存也不会泄漏。反之,如果所有控件都直接挂在 screen 上,删除和布局都会变得零散而难以维护。

六、常见误区与复盘

  • 误区一:父对象已删除,仍继续使用子对象指针。删除父对象会递归删除子对象,任何保存的子对象指针都会失效。
  • 误区二:把绝对坐标当相对坐标用。子对象坐标相对父对象,父对象移动后绝对位置会变化。
  • 误区三:认为所有样式都会继承。只有部分文本类属性可继承,背景、边框等需单独设置。
  • 误区四:频繁创建删除对象。工业面板中频繁刷新应优先使用隐藏/显示或更新内容,减少内存碎片。
  • 误区五:在回调里直接删除对象。应使用异步删除,避免回调返回后访问已释放内存。
掌握对象模型的关键一句话:先想清楚父子关系,再谈坐标、样式和删除。对象树决定了LVGL界面的组织方式,也决定了内存释放的边界。把每个控件挂到正确的父对象上,界面结构、样式继承和生命周期管理都会变得可控。