第3章:对象模型与屏幕

真相只有一个:LVGL 里的一切,都是一棵以 screen 为根的对象树

🔍

本章导师:柯南

核心方法论:真相只有一个,源码就是证据

「上一章我们跑起了第一个界面。但你有没有停下来想过:lv_screen_active() 返回的那个『根对象』到底是什么?按钮为什么能挂在它下面?屏幕又是怎么和 display 挂钩的?这些如果靠猜,十有八九会错。别猜——去 src/coresrc/display 里找证据。真相只有一个,而且它藏在源码里。」

3.1 lv_obj:一切对象的基座

上一章你随手写的 lv_button_create()lv_label_create() 返回的都是 lv_obj_t *——按钮、标签、面板……所有控件在 LVGL 里本质都是同一个结构 lv_obj_t 的实例,只是各自挂了不同的"类描述"(lv_obj_class_t)。lv_obj_create() 是它们的共同入口:它调用 lv_obj_class_create_obj() 分配内存、初始化一个"空对象",控件再在它之上叠加自己的行为。

/* 创建对象的两条路:指定父对象,或传 NULL */
lv_obj_t * parent = lv_obj_create(lv_screen_active()); /* 普通对象 */
lv_obj_t * child  = lv_obj_create(parent);              /* 挂在 parent 下 */
lv_obj_t * scr   = lv_obj_create(NULL);                /* NULL 父 = 一块新屏 */

在 LVGL 9 里,lv_obj_create(NULL) 不是创建一个"无父的孤儿对象",而是创建一块屏幕(screen)。读 lv_obj_class.clv_obj_class_create_obj() 会看到 parent == NULL 分支直接打印一行 "creating a screen",然后把新对象登记进默认 display 的屏幕数组。也就是说:对象树里 parent 为 NULL 的 lv_obj,天然就是一块屏幕

有了对象,就有了树。屏幕是根,控件是枝叶,一个对象可以有任意多个子对象,但只有一个父对象(根对象除外)。这棵树不是概念模型——LVGL 的渲染、事件、布局全部沿着它工作:渲染时从根向下遍历,把每个对象按自己的位置与样式画出来;触摸时按命中测试找"谁被点到了"。理解"一切皆对象、对象皆成树",你就拿到了整座库的地图。

/* 一棵真实的对象树:屏幕 → 面板 → 按钮 → 标签 */
lv_obj_t * screen = lv_obj_create(NULL);            /* 根 */
lv_obj_t * panel  = lv_obj_create(screen);            /* 一级子对象 */
lv_obj_t * btn    = lv_button_create(panel);          /* 二级 */
lv_obj_t * label  = lv_label_create(btn);            /* 三级 */
lv_obj_center(panel);
术语含义
根 / screen对象树的顶层,parent 为 NULL 的 lv_obj
父对象(parent)直接上级,控制布局、裁剪与事件冒泡的范围
子对象(child)挂在某对象之下的对象,随父移动与显隐
兄弟(sibling)同一父对象下的平级对象,决定绘制与点击的先后
柯南提示

判断一个对象是不是屏幕,别猜——看它调用的是不是 lv_obj_create(NULL),或者用 lv_obj_get_parent(obj) 看返回是否 NULL。空父即根,根即屏幕,这个等式就是线索。

3.2 对象树:父子关系与生命周期

建立父子关系有两条路:一是创建时把父对象传进 lv_obj_create(parent);二是创建后再改挂,用公开 API lv_obj_set_parent(obj, new_parent) 把对象从旧父对象下解绑、挂到新父对象下。运行时动态改挂是切换界面组织的常用手法,例如把一个控件从一屏挪到另一屏。

/* 动态改挂:把 btn 从 screen 挪到 panel 下 */
lv_obj_set_parent(btn, panel);
/* 验证父子关系 */
lv_obj_t * p   = lv_obj_get_parent(btn);        /* 应为 panel */
uint32_t cnt = lv_obj_get_child_count(panel); /* 面板的子对象数 */

这里有一个值得停下来查证的点:v8 时代很多人熟悉的 lv_obj_add_child(),在 v9 里已经是内部函数了。搜一下它的声明你会发现它不在任何公开头文件里,而是在 src/core/lv_obj_private.h,返回类型是 lv_result_t;公开的"新增子对象"入口只有 lv_obj_set_parent()。这就是柯南式真相:网上大量 v8 教程的写法,在 v9 未必能直接用——拿源码核对永远比背 API 靠谱。

/* 内部函数:src/core/lv_obj_private.h(仅供库内部使用) */
lv_result_t lv_obj_add_child(lv_obj_t * parent, lv_obj_t * child);

/* 公开 API:include/lvgl/core/lv_obj_tree.h */
void lv_obj_set_parent(lv_obj_t * obj, lv_obj_t * parent);

生命周期同样沿着树走。lv_obj_delete(obj) 销毁对象并连带销毁它的全部后代lv_obj_clean(obj) 只清空所有子对象、保留本体;另外还有延迟删除 lv_obj_delete_delayed()(用于动画等场景,避免正在使用的对象被立刻释放)和 lv_obj_delete_async()(安全地在下一个周期删除)。这条规则帮你养成一个好习惯:想删一棵子树,删它的根即可,不必逐个遍历子对象。

/* 生命周期三兄弟 */
lv_obj_delete(btn);                    /* 删掉 btn 及其全部后代 */
lv_obj_clean(panel);                  /* 清空 panel 的子对象,panel 仍在 */
lv_obj_delete_delayed(panel, 500);   /* 500ms 后再删 */

/* 调试神器:打印整棵对象树 */
lv_obj_dump_tree(lv_screen_active());
注意

删掉的 lv_obj_t * 指针不会再为你"变有效"。删对象前先想清楚有没有其他地方还握着这个指针(例如事件回调的 user_data),否则会踩到悬空指针——这也是 lv_obj_delete_async() 存在的理由。

3.3 display 与 screen:屏幕的世界

对象树之上还有一层容器:display(显示设备)。注意它的类型是 lv_display_t不是 lv_obj——display 不是一个对象,而是 LVGL 世界里"一块物理屏幕"的抽象。调用 lv_display_create(w, h) 时,内部会做一串动作:创建一个默认屏幕并记为 act_scr、额外创建 bottom/top/sys 三个层对象、建一个刷新 timer(lv_display_refr_timer,周期 LV_DEF_REFR_PERIOD)、并初始化默认主题。你看到的每一帧,都是这个 timer 驱动的。

/* lv_display_create 内部帮你做的事(src/display/lv_display.c) */
lv_display_t * disp = lv_display_create(320, 240);

/* 于是下面这句总能拿到一块默认屏: */
lv_obj_t * scr = lv_screen_active();

lv_screen_active() 是上一章一直在用的"根对象"入口。查源码,它的实现只有一行:return lv_display_get_screen_active(lv_display_get_default())——取"默认 display 的当前活跃屏幕"。这意味着三件事:先要有 display,才有 screen(没有 display 时 lv_obj_create(NULL) 会直接警告并返回 NULL);一个 display 同一时刻只有一个活跃 screen;切换屏幕用 lv_screen_load(scr)

/* 两屏切换:各自有独立对象树,切换不互相破坏 */
lv_obj_t * home_scr = lv_obj_create(NULL);
lv_obj_t * set_scr  = lv_obj_create(NULL);

lv_obj_set_style_bg_color(home_scr, lv_palette_main(LV_PALETTE_BLUE), 0);
lv_obj_set_style_bg_color(set_scr, lv_palette_main(LV_PALETTE_RED), 0);

lv_screen_load(home_scr);   /* 切到主屏 */
/* ... 某个按钮事件里 ... */
lv_screen_load(set_scr);    /* 切到设置屏 */

最后还你一个真相:前文说 screen 是"display 的子对象",这句话在对象树层面并不准确。screen 的 parent 字段是 NULL,它并不挂在 display 之下——display 是 lv_display_t,根本不参与对象树;真实关系是 display 用 disp->screens[] 数组持有这块屏、用 disp->act_scr 记住当前活跃屏。正因如此,屏幕切换 lv_screen_load() 只是改 display 的活跃指针,不会破坏任何父子的对象树结构。多显示器场景下还能用 lv_display_set_default() 切换"默认 display",但对新手,先牢牢抓住"一块 display + 一块活跃屏"这个模型就够用了。

柯南提示

把三个概念分清,很多困惑瞬间消失:lv_display_t设备(屏幕硬件),screen树的根对象(一个 lv_obj),display 持有一组 screen 并通过 act_scr 指向当前活跃的那个。三者是完全不同的东西,源码里各自有各自的容器。

3.4 lv_init 与对象定位

把根捋清楚之后,lv_init() 干的事就一目了然了——它是整棵世界树的"开机流程"。读 src/lv_init.c,初始化的顺序严格遵循依赖关系:先内存(lv_mem_init、draw buffer 处理器),再操作系统抽象(lv_os_init),然后 timer 核心、文件系统、布局、动画、分组,接着绘制系统(lv_draw_init 及各渲染后端),最后是样式与刷新系统(lv_obj_style_initlv_refr_init)。顺序错不得——绘制系统依赖前面的内存与 timer。

/* lv_init() 的初始化顺序(src/lv_init.c,按真实调用序排列) */
lv_mem_init();              /* 内存池 */
lv_timer_core_init();       /* 计时器核心 */
lv_fs_init();               /* 文件系统 */
lv_layout_init();           /* Flex/Grid 布局 */
lv_anim_core_init();        /* 动画核心 */
lv_draw_init();             /* 绘制系统(含 sw/opengles 等后端) */
lv_obj_style_init();        /* 样式系统 */
lv_refr_init();             /* 屏幕刷新系统 */

初始化完成后,就该给对象定位置了。最省事的是 lv_obj_center(obj)——把对象相对父对象居中(父的大小变化后要重新调用);更精细的控制用 lv_obj_align(obj, align, x_ofs, y_ofs) 对齐到父对象的任意位置,或用 lv_obj_set_pos(obj, x, y) 直接给像素坐标。记住一个关键事实:所有定位都是相对父对象坐标系——这正是对象树的用武之地:改一个父对象的位置,整棵子树一起移动。

/* 三种定位方式 */
lv_obj_center(btn);                          /* 相对父居中 */
lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 10); /* 顶部居中,下移 10px */
lv_obj_set_pos(panel, 20, 20);             /* 相对父的 (20, 20) */
初始化步骤作用
lv_mem_init()建立内存池,后续所有对象分配都依赖它
lv_timer_core_init()计时器系统,驱动刷新、动画与延迟任务
lv_draw_init()注册绘制单元与渲染后端(sw 等)
lv_refr_init()屏幕刷新系统,配合 display 的刷新 timer 工作
柯南提示

想确认自己没漏掉哪一步初始化,就在 lv_init() 之后立刻建一个 display、再 lv_obj_create(NULL) 建屏——如果它们能正常返回非 NULL,说明地基已经打牢。空对象树跑起来 = 你的初始化链路正确。

章末练习

练习 1:亲手建一棵对象树 入门

lv_screen_active() 上创建 panel → button → label 三层结构,然后用 lv_obj_get_parent()lv_obj_get_child_count() 验证父子关系:label 的父对象是不是 btnbtn 的父对象是不是 panel?每层各有多少子对象?

提示

创建时传哪个父对象,它就挂在哪;对照 3.2 的验证代码,逐层打印 lv_obj_get_parentlv_obj_get_child_count 的结果。

参考答案

是的,逐层关系成立:label 的父是 btnbtn 的父是 panelpanel 的父是 screen。子对象数:lv_obj_get_child_count(btn) 为 1(label),lv_obj_get_child_count(panel) 为 1(btn),lv_obj_get_child_count(screen) 为 1(panel)——注意屏幕自己可能还带 scrollbar 等内部对象,别被干扰。

练习 2:用 dump_tree 侦查对象树 进阶

在跑起一个简单 UI 后,调用 lv_obj_dump_tree(lv_screen_active()) 打印对象树。观察打印结果,解释为什么会出现 screen 之外的 bottom / top / sys 三层。

提示

回到 3.3 的 lv_display_create() 内部动作;再想想这三层的名字与"层叠次序"的关系。

参考答案

这三层是 lv_display_create() 在创建 display 时用 lv_obj_create(NULL) 一并创建的特殊屏幕对象(bottom/top/sys layer),供系统内部使用:top layer 常放提示框等悬浮物,sys layer 放系统级组件。它们与用户自己的 screen 同级,都挂在 display 的屏幕体系下,所以 dump 时能看到。

练习 3:两屏切换 进阶

创建两个 screen,各放一个颜色背景和一个标签,用一个按钮在两者之间来回 lv_screen_load() 切换;切换后用 lv_display_get_screen_active(disp) 确认当前活跃屏确实变了。

提示

参考 3.3 的切换代码;lv_display_get_screen_active 需要你持有 lv_display_t * displv_display_createlv_display_get_default() 返回)。

参考答案

两屏都是 lv_obj_create(NULL) 建屏,各自布局独立;按钮事件里 lv_screen_load() 切换后,用 lv_display_get_screen_active(disp) 与切换前比较返回值即可验证。由于活跃屏只是 display 记录的一个指针,来回切换不会破坏任何一屏的对象树。

练习 4:三处真相,逐一取证 挑战

结合源码回答三个"真相":(1) 为什么 lv_obj_add_child() 在 v9 里是 private,而公开的是 lv_obj_set_parent()?(2) 为什么说 screen 不是 display 的"子对象"?(3) lv_screen_active() 实际上做了什么?

提示

分别查 src/core/lv_obj_private.hsrc/core/lv_obj_class.c 的 NULL-parent 分支(disp->screens[])、src/display/lv_display.clv_screen_active() 的定义。

参考答案

(1) v9 把"改变父关系"收敛为唯一公开入口 lv_obj_set_parent(),内部的挂接动作(含旧的 add/remove child)被收进 private 头文件,返回 lv_result_t,仅供库内使用——所以公开 API 更简单、外部不该直接操作子列表;(2) screen 的 parent 字段是 NULL,它由 display 通过 disp->screens[] 数组持有,display 本身是 lv_display_t 而非 lv_obj、不参与对象树,因此 screen 与 display 不是父子关系,而是"设备持有根对象"的关系;(3) lv_screen_active() 的实现是 lv_display_get_screen_active(lv_display_get_default()),即"默认 display 的当前活跃屏幕"。