4、LVGL对象系统:对象(lv_obj_t)的概念、对象的创建与删除、父子层级关系、对象的基本属性(位置、大小、样式)
好,咱们进入第四章。这一章可以说是LVGL的基石——对象系统。你想想看,屏幕上任何一个按钮、一个滑块、一张图片,本质上都是对象。搞懂了对象,你就搞懂了LVGL的一半。
我记得刚接触LVGL那会儿,看到满屏的lv_obj_t *指针,心里直犯嘀咕:这玩意儿到底是个啥?后来在项目里踩了几个坑,才真正明白它的设计哲学。今天我就把这些经验掰开了讲给你听。
核心认知:LVGL里的一切都是对象。屏幕是对象,按钮是对象,标签是对象,甚至你自定义的复杂组件也是对象。对象系统就是LVGL的骨架。
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的根基。创建和删除是生命周期管理,父子层级是组织结构,位置大小和样式是视觉表现。这三块搞明白了,后面学控件、学布局、学动画都会轻松很多。
嗯,这一章就到这里。内容不少,但都是硬核干货。你可以在自己的开发板上把上面的例子跑一遍,改改参数看看效果。亲手试过的东西,印象才深。