4、LVGL对象系统:对象(lv_obj_t)的概念、对象的创建与删除、父子层级关系、对象的基本属性(位置、大小、样式)

好,咱们进入第四章。这一章可以说是LVGL的基石——对象系统。你想想看,屏幕上任何一个按钮、一个滑块、一张图片,本质上都是对象。搞懂了对象,你就搞懂了LVGL的一半。

我记得刚接触LVGL那会儿,看到满屏的lv_obj_t *指针,心里直犯嘀咕:这玩意儿到底是个啥?后来在项目里踩了几个坑,才真正明白它的设计哲学。今天我就把这些经验掰开了讲给你听。

核心认知:LVGL里的一切都是对象。屏幕是对象,按钮是对象,标签是对象,甚至你自定义的复杂组件也是对象。对象系统就是LVGL的骨架。

LVGL 对象系统核心知识结构 lv_obj_t 对象 创建与删除 父子层级关系 位置与大小 样式系统 lv_obj_create lv_obj_del lv_obj_set_parent lv_obj_set_pos lv_obj_set_size lv_obj_add_style 💡 所有UI元素都是 lv_obj_t 的实例,通过父子层级构成一棵完整的对象树 位置、大小、样式是每个对象最基础的三个属性维度

4.1 对象(lv_obj_t)到底是什么?

说白了,lv_obj_t 就是一个结构体。它里面存了什么?位置坐标、宽高尺寸、样式指针、父子兄弟链表节点、事件回调……所有UI对象共有的东西都在这里。

我习惯把它比作一个空盒子。你创建一个 lv_obj_t,就像拿到一个空盒子。然后你往里面塞样式、塞子对象、绑定事件,这个盒子就变成了按钮、标签或者滑块。LVGL里所有控件(widget)都是在 lv_obj_t 基础上扩展出来的。

我的经验:刚开始学的时候,别急着用高级控件。先拿 lv_obj_t 画几个矩形框,调调位置大小,感受一下对象系统是怎么运作的。这比直接上按钮和滑块要扎实得多。

4.2 对象的创建与删除

创建对象就一个函数:

lv_obj_t * lv_obj_create(lv_obj_t * parent);

参数 parent 是父对象。返回一个指针,指向新创建的对象。就这么简单。

举个例子:

/* 创建一个屏幕对象(通常由LVGL内部创建) */
lv_obj_t * scr = lv_scr_act();

/* 在屏幕上创建一个容器对象 */
lv_obj_t * cont = lv_obj_create(scr);

/* 在容器里创建一个标签 */
lv_obj_t * label = lv_label_create(cont);

删除对象更直接:

void lv_obj_del(lv_obj_t * obj);

调用 lv_obj_del(cont),容器和它里面的所有子对象(包括那个标签)都会被删除。这就是级联删除。

我曾经踩过的坑:在中断服务函数里调用 lv_obj_del,结果程序直接跑飞。LVGL的对象操作不是线程安全的,删除操作必须在LVGL的主循环上下文中执行。如果你需要在中断里触发删除,发一个消息或者设一个标志位,回到主循环再处理。

还有一个常用的删除方式——延迟删除:

void lv_obj_del_delayed(lv_obj_t * obj, uint32_t delay_ms);

这个函数会在指定延迟后删除对象。我在做动画过渡时经常用它——比如一个弹窗淡出后,延迟200毫秒再删掉,避免闪烁。

4.3 父子层级关系

LVGL的对象组织方式是一棵树。每个对象都有一个父对象,可以有零个或多个子对象。根节点是 lv_scr_act() 返回的当前活动屏幕。

为什么要搞父子层级?三个原因:

  • 坐标继承:子对象的位置是相对于父对象的。父对象移动,子对象跟着动。
  • 裁剪:子对象默认不能超出父对象的边界(可以设置 LV_OBJ_FLAG_OVERFLOW_VISIBLE 改变)。
  • 生命周期:父对象被删除,所有子对象自动被删除。

常用的父子操作函数:

函数 作用
lv_obj_set_parent(obj, parent) 把对象移动到另一个父对象下
lv_obj_get_parent(obj) 获取对象的父对象
lv_obj_get_child(parent, idx) 按索引获取子对象
lv_obj_get_child_cnt(parent) 获取子对象数量
lv_obj_move_foreground(obj) 把对象移到最前面(Z轴顺序)
lv_obj_move_background(obj) 把对象移到最后面

重要:Z轴顺序由子对象在父对象链表中的位置决定。后创建的子对象默认在最上面。你可以用 lv_obj_move_foreground 和 lv_obj_move_background 调整。

我记得在一个智能家居项目里,需要做一个弹窗覆盖整个屏幕。当时我直接把弹窗的父对象设为 lv_scr_act(),然后调用 lv_obj_move_foreground(popup),确保它盖住所有其他控件。如果弹窗的父对象是某个容器,它就会被容器裁剪,效果就不对了。

4.4 对象的基本属性:位置、大小、样式

每个对象都有三个最基本的属性维度。我习惯叫它们对象三要素。

4.4.1 位置(Position)

位置是相对于父对象的坐标。左上角是原点 (0,0),x 向右增加,y 向下增加。

/* 设置位置 */
lv_obj_set_pos(obj, 50, 100);   /* x=50, y=100 */

/* 单独设置x或y */
lv_obj_set_x(obj, 50);
lv_obj_set_y(obj, 100);

/* 获取位置 */
lv_coord_t x = lv_obj_get_x(obj);
lv_coord_t y = lv_obj_get_y(obj);

这里有个细节:lv_obj_get_x 返回的是对象左上角相对于父对象左上角的偏移。如果你想获取对象在屏幕上的绝对坐标,可以用 lv_obj_get_x_aligned 或者自己递归累加父对象的坐标。

4.4.2 大小(Size)

大小就是宽和高,单位是像素。

/* 设置大小 */
lv_obj_set_size(obj, 200, 80);   /* 宽200,高80 */

/* 获取大小 */
lv_coord_t w = lv_obj_get_width(obj);
lv_coord_t h = lv_obj_get_height(obj);

/* 获取内容区域大小(不含边框、内边距) */
lv_coord_t cw = lv_obj_get_content_width(obj);
lv_coord_t ch = lv_obj_get_content_height(obj);

我常用的技巧:用 lv_obj_set_size(obj, LV_PCT(50), LV_PCT(100)) 设置百分比大小。这样对象会随父对象大小自动缩放,做自适应布局特别方便。但要注意,父对象必须先有确定的大小,否则百分比计算会出问题。

4.4.3 样式(Style)

样式决定了对象长什么样——背景颜色、边框粗细、圆角半径、文字颜色、透明度等等。LVGL的样式系统非常强大,但也很复杂。这里我们先讲最基础的使用方式。

每个对象可以关联多个样式。样式有优先级,后添加的样式会覆盖先添加的样式中的相同属性。

/* 创建一个样式 */
lv_style_t style;
lv_style_init(&style);
lv_style_set_bg_color(&style, lv_color_hex(0x2196F3));
lv_style_set_radius(&style, 8);
lv_style_set_border_width(&style, 2);
lv_style_set_border_color(&style, lv_color_hex(0x1976D2));

/* 将样式添加到对象 */
lv_obj_add_style(obj, &style, 0);

第三个参数 selector 是样式选择器,0 表示应用到对象的所有状态(默认态、按下态、禁用态等)。如果你想给按钮的按下态单独设置样式:

lv_style_t style_pr;
lv_style_init(&style_pr);
lv_style_set_bg_color(&style_pr, lv_color_hex(0x1565C0));

lv_obj_add_style(btn, &style_pr, LV_STATE_PRESSED);

这样按钮按下去时背景颜色会变深,松开后恢复。

注意:lv_style_t 变量需要保持有效,不能是局部变量在函数返回后就被销毁。我一般把它们声明为 static 或者全局变量。曾经有个同事把样式定义在函数里,函数退出后样式数据被覆盖,界面显示全乱了——排查了半天才发现是这个问题。

4.5 综合示例:创建一个带样式的卡片

把今天讲的东西串起来,写一个完整的例子:

/* 创建卡片容器 */
lv_obj_t * card = lv_obj_create(lv_scr_act());
lv_obj_set_size(card, 300, 200);
lv_obj_set_pos(card, 50, 50);

/* 卡片样式 */
static lv_style_t style_card;
lv_style_init(&style_card);
lv_style_set_bg_color(&style_card, lv_color_hex(0xFFFFFF));
lv_style_set_radius(&style_card, 12);
lv_style_set_shadow_width(&style_card, 10);
lv_style_set_shadow_color(&style_card, lv_color_hex(0x888888));
lv_style_set_border_width(&style_card, 1);
lv_style_set_border_color(&style_card, lv_color_hex(0xE0E0E0));
lv_obj_add_style(card, &style_card, 0);

/* 卡片标题 */
lv_obj_t * title = lv_label_create(card);
lv_label_set_text(title, "设备状态");
lv_obj_set_pos(title, 20, 20);
static lv_style_t style_title;
lv_style_init(&style_title);
lv_style_set_text_color(&style_title, lv_color_hex(0x333333));
lv_style_set_text_font(&style_title, &lv_font_montserrat_16);
lv_obj_add_style(title, &style_title, 0);

/* 卡片内容 */
lv_obj_t * content = lv_label_create(card);
lv_label_set_text(content, "温度: 26°C\n湿度: 65%\n状态: 正常");
lv_obj_set_pos(content, 20, 60);
static lv_style_t style_content;
lv_style_init(&style_content);
lv_style_set_text_color(&style_content, lv_color_hex(0x666666));
lv_obj_add_style(content, &style_content, 0);

这个例子展示了:创建对象、设置父子关系、设置位置大小、添加样式。一个完整的卡片组件就出来了。

总结一下:对象系统是LVGL的根基。创建和删除是生命周期管理,父子层级是组织结构,位置大小和样式是视觉表现。这三块搞明白了,后面学控件、学布局、学动画都会轻松很多。

嗯,这一章就到这里。内容不少,但都是硬核干货。你可以在自己的开发板上把上面的例子跑一遍,改改参数看看效果。亲手试过的东西,印象才深。