3、LVGL基础对象与屏幕管理:从零搭建你的界面骨架

各位同学,欢迎来到第三章。这一章我们聊聊LVGL里最基础、也最核心的东西——对象和屏幕管理。

说实话,我刚开始接触LVGL时,觉得它跟其他GUI框架不太一样。它把每个可见元素都抽象成一个“对象”,屏幕本身也是一个特殊对象。这种设计思路,说白了就是“万物皆对象”。你想想看,一个按钮是对象,一个标签是对象,甚至整个屏幕也是对象。这种统一性让代码变得非常规整。

3.1 lv_obj基础对象:一切UI的起点

在LVGL里,lv_obj_t是所有可视化组件的基类。不管是按钮、标签、滑块,还是进度条,它们本质上都是lv_obj_t的“子类”。

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

lv_obj_t * obj = lv_obj_create(lv_scr_act());  // 在当前活动屏幕上创建对象
lv_obj_set_size(obj, 100, 50);                  // 设置宽高
lv_obj_set_pos(obj, 20, 30);                    // 设置位置

嗯,这里要注意:lv_obj_create()的第一个参数是父对象。如果不指定父对象,它默认会挂在当前活动屏幕上。我个人习惯是显式指定父对象,这样代码可读性更好。

小技巧: 如果你只是想创建一个“容器”来装其他控件,直接用 lv_obj_create() 就行。它本身不显示任何内容,但可以设置背景色、边框等样式。

3.2 屏幕创建与切换:多页面应用的基础

屏幕在LVGL里也是对象,只不过它是顶层容器。每个屏幕都是一个独立的“画布”,你可以自由切换。

创建新屏幕:

lv_obj_t * screen1 = lv_obj_create(NULL);  // 创建新屏幕,父对象为NULL
lv_obj_set_style_bg_color(screen1, lv_color_hex(0x2196F3), 0);  // 设置背景色

lv_obj_t * screen2 = lv_obj_create(NULL);
lv_obj_set_style_bg_color(screen2, lv_color_hex(0x4CAF50), 0);

切换屏幕:

lv_scr_load(screen1);  // 切换到screen1
// 或者带动画切换
lv_scr_load_anim(screen2, LV_SCR_LOAD_ANIM_MOVE_LEFT, 500, 0, false);

我在项目中遇到过一个问题:频繁切换屏幕时,如果不注意内存管理,很容易造成内存泄漏。我的建议是:

  • 对于固定数量的屏幕,在初始化时一次性创建好
  • 对于动态创建的屏幕,切换后记得用 lv_obj_del() 删除不再使用的屏幕
  • 使用 lv_scr_load_anim() 时,动画回调里可以做资源清理
避坑指南: 我曾经在切换屏幕时忘记删除旧屏幕,结果跑了几个小时内存就爆了。后来我养成了一个习惯:每次切换屏幕前,先检查旧屏幕是否还需要保留。不需要的话,果断删除。

3.3 父子层级关系:UI的树形结构

LVGL的UI结构是一棵树。每个对象可以有多个子对象,但只能有一个父对象。这种层级关系决定了:

  • 子对象的位置是相对于父对象的
  • 子对象的可见性受父对象影响(父对象隐藏,子对象也隐藏)
  • 事件会从子对象向上冒泡到父对象

举个例子:

lv_obj_t * parent = lv_obj_create(lv_scr_act());
lv_obj_set_size(parent, 200, 150);

lv_obj_t * child1 = lv_obj_create(parent);  // child1的父对象是parent
lv_obj_set_pos(child1, 10, 10);             // 相对于parent的位置

lv_obj_t * child2 = lv_obj_create(parent);
lv_obj_set_pos(child2, 10, 60);

你想想看,这种树形结构有什么好处?

第一,方便批量操作。比如你想隐藏一组控件,只需要隐藏它们的父对象就行。第二,坐标计算更直观。子对象的位置是相对于父对象的,不用关心全局坐标。

我记得有一次做仪表盘界面,需要把多个仪表放在一个面板里。我创建了一个面板对象作为父对象,然后把所有仪表作为子对象挂上去。后来客户要求整体移动面板位置,我只需要改父对象的坐标,所有仪表自动跟着移动。省了不少事。

3.4 坐标系统:理解LVGL的定位机制

LVGL的坐标系统其实很简单,但有几个关键点需要掌握:

概念 说明
原点 屏幕左上角为 (0, 0)
X轴 向右为正方向
Y轴 向下为正方向
相对坐标 子对象的位置相对于父对象
绝对坐标 对象相对于屏幕左上角的位置

获取坐标的API:

lv_obj_t * obj = /* 某个对象 */;

// 获取相对坐标(相对于父对象)
lv_coord_t x = obj->coords.x1;  // 左边界
lv_coord_t y = obj->coords.y1;  // 上边界

// 获取绝对坐标(相对于屏幕)
lv_coord_t abs_x = lv_obj_get_x(obj);   // 注意:这个返回的是相对坐标
lv_coord_t abs_y = lv_obj_get_y(obj);

// 真正获取绝对坐标需要自己计算
lv_coord_t screen_x = obj->coords.x1;
lv_coord_t screen_y = obj->coords.y1;
核心要点: 在LVGL里,lv_obj_get_x()lv_obj_get_y() 返回的是相对于父对象的坐标。如果你需要绝对坐标,直接读取 obj->coords 结构体成员。

为什么会这样设计?说白了,就是为了让UI布局更灵活。你移动父对象时,所有子对象自动跟着移动,不需要重新计算每个子对象的坐标。

我刚开始用LVGL时,总搞混相对坐标和绝对坐标。有一次调试一个嵌套很深的UI,子对象的位置怎么调都不对。后来才发现,我一直在用绝对坐标去设置子对象的位置,结果子对象跑到了屏幕的某个角落。嗯,从那以后,我每次设置坐标都会先确认父对象是谁。

3.5 实战建议:屏幕管理的最佳实践

基于我多年的嵌入式UI开发经验,给你几个屏幕管理的建议:

  1. 屏幕数量控制: 对于资源受限的MCU,建议屏幕数量不超过5个。如果需要更多页面,考虑使用滚动容器或标签页控件。
  2. 屏幕切换策略: 如果切换频繁,使用 lv_scr_load_anim() 带动画切换,用户体验更好。但要注意动画时长不要超过300ms,否则会感觉卡顿。
  3. 内存管理: 每个屏幕都会占用内存。不用的屏幕及时删除,或者用 lv_obj_set_hidden() 隐藏而不是删除。
  4. 父子关系设计: 尽量保持层级不超过3层。太深的层级会影响渲染性能,也增加调试难度。
个人习惯: 我通常会在项目开始时先画一个UI层级图,明确每个屏幕的父子关系。这样写代码时思路清晰,不容易出错。

好了,这一章的内容就到这里。下一章我们会深入讲解LVGL的样式系统,看看如何让我们的界面变得更好看。记住,基础对象和屏幕管理是LVGL的骨架,骨架搭好了,后面加肌肉和皮肤就轻松多了。