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的坐标系统以父对象内容区左上角为原点,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() 设置的是相对父对象的坐标;如果父对象本身被移动,子对象的绝对屏幕位置会随之改变。工业面板中常把一组控件放进容器,再整体移动容器,就是利用了这一特性。
三、样式继承:属性沿树向下传递
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在安全时机完成删除。五、工业面板场景:用对象树组织一个电机控制卡
假设要做一个电机控制卡:外层是卡片容器,内部有标题标签、状态指示灯、启动按钮和转速滑块。合理做法是:
- 创建 card 容器,设置固定尺寸、圆角和边框样式。
- 在 card 下创建 title 标签,使用
lv_obj_align()对齐顶部。 - 创建 led 对象表示运行状态,放在标题右侧。
- 创建 btn 和 slider,分别对齐底部左右。
- 为 card 设置统一字体样式,子标签自动继承。
- 页面切换时调用
lv_obj_del(card),整张卡片连同内部控件一次性释放。
这样组织后,卡片移动、隐藏、删除都以 card 为操作单位,代码清晰,内存也不会泄漏。反之,如果所有控件都直接挂在 screen 上,删除和布局都会变得零散而难以维护。
六、常见误区与复盘
- 误区一:父对象已删除,仍继续使用子对象指针。删除父对象会递归删除子对象,任何保存的子对象指针都会失效。
- 误区二:把绝对坐标当相对坐标用。子对象坐标相对父对象,父对象移动后绝对位置会变化。
- 误区三:认为所有样式都会继承。只有部分文本类属性可继承,背景、边框等需单独设置。
- 误区四:频繁创建删除对象。工业面板中频繁刷新应优先使用隐藏/显示或更新内容,减少内存碎片。
- 误区五:在回调里直接删除对象。应使用异步删除,避免回调返回后访问已释放内存。