3、LVGL核心对象与样式系统:lv_obj基本操作、样式(lv_style_t)的创建与应用、本地样式与继承样式、状态样式

好,咱们进入第三章。这一章可以说是LVGL的「地基」——对象和样式。你想想看,屏幕上任何一个按钮、滑块、标签,本质上都是一个 lv_obj_t。而它长什么样、什么颜色、什么大小,全由样式说了算。

我个人习惯,拿到一个新项目,第一件事不是画UI,而是先把对象树搭清楚。为什么?因为对象之间的父子关系,直接决定了样式怎么继承、事件怎么传递。搞反了,后面改起来能让你怀疑人生。

3.1 lv_obj基本操作——你屏幕上的每一个像素都归它管

在LVGL里,万物皆对象。创建一个对象很简单:

lv_obj_t * btn = lv_btn_create(lv_scr_act());  // 在活动屏幕上创建一个按钮

但这里有个坑——创建对象时必须指定父对象。父对象可以是屏幕(lv_scr_act()),也可以是另一个容器。我见过新手直接把所有控件都挂在屏幕下,结果想做弹出菜单时,发现层级全乱了。

常用的对象操作就那么几个,我列个表,你一看就明白:

操作 函数 说明
创建 lv_obj_create(parent) 创建一个基础对象,作为容器或自定义控件
设置大小 lv_obj_set_size(obj, w, h) 宽高单位是像素
设置位置 lv_obj_set_pos(obj, x, y) 相对于父对象的坐标
对齐 lv_obj_align(obj, ref, LV_ALIGN_CENTER, 0, 0) 相对于参考对象对齐,省去手动算坐标
添加子对象 创建时指定parent即可 子对象会显示在父对象之上
删除 lv_obj_del(obj) 会连带删除所有子对象
我的小技巧: 调试阶段,我习惯用 lv_obj_dump_tree(lv_scr_act()) 打印整个对象树。哪个对象挂在谁下面,一目了然。这招在排查显示异常时特别管用。

3.2 样式(lv_style_t)的创建与应用——给对象穿上衣服

对象是骨架,样式就是皮肤。在LVGL 8.x里,样式用 lv_style_t 表示。你得先初始化它,再设置各种属性,最后应用到对象上。

看个完整例子:

/* 1. 定义一个样式变量(通常声明为静态或全局) */
static lv_style_t style_btn;

/* 2. 初始化样式 */
lv_style_init(&style_btn);

/* 3. 设置样式属性 */
lv_style_set_bg_color(&style_btn, lv_color_hex(0x2196F3));   // 背景蓝色
lv_style_set_bg_opa(&style_btn, LV_OPA_COVER);               // 不透明
lv_style_set_radius(&style_btn, 8);                           // 圆角8像素
lv_style_set_border_width(&style_btn, 2);                    // 边框宽度
lv_style_set_border_color(&style_btn, lv_color_hex(0x1976D2));// 边框深蓝

/* 4. 应用到对象 */
lv_obj_add_style(btn, &style_btn, 0);                         // 0 表示默认状态

这里有个关键点:样式属性不是随便设的。每个控件类型支持的样式属性不同。比如按钮支持背景、边框、阴影,但文本类控件还支持字体、文字颜色。设错了不会报错,但没效果。

我曾经踩过的坑: 样式变量必须保持生命周期有效。如果你在函数里定义了一个局部 lv_style_t,函数退出后样式就被销毁了,对象会变得「六亲不认」——显示全乱套。所以,样式变量请用 static 或动态分配。

3.3 本地样式与继承样式——谁说了算?

LVGL的样式系统有一个「就近原则」:对象自己的样式 > 父对象的样式 > 屏幕的默认样式。但这里说的「继承」不是CSS那种完全继承,而是有选择地继承。

具体规则是这样的:

  • 本地样式:直接通过 lv_obj_add_style() 加到对象上的样式。优先级最高。
  • 继承样式:父对象设置了 LV_STATE_DEFAULT 的样式,且属性是可继承的(比如文字颜色、字体),子对象如果没有覆盖,就会沿用父对象的。

哪些属性可以继承?我直接给你答案:

可继承属性 不可继承属性
文字颜色 (text_color) 背景颜色、背景透明度
字体 (text_font) 边框、轮廓、阴影
文字对齐 (text_align) 大小、位置、圆角
透明度 (opa) 内边距 (pad 系列)

举个例子:你给一个容器设置了红色文字,容器里的所有标签默认都会是红色,除非你单独给某个标签设置了蓝色文字。这就是继承的威力——一次设置,全局生效。

实战建议: 我通常把主题色、默认字体、默认文字颜色放在屏幕级别的样式里。这样整个应用风格统一,改起来也快——改一处,全盘变。

3.4 状态样式——按钮按下去是什么样?

一个按钮,平时是一种颜色,按下去变深,松开又恢复。这就是状态样式干的事。

LVGL预定义了几种状态:

  • LV_STATE_DEFAULT (0):默认状态
  • LV_STATE_PRESSED (1):按下状态
  • LV_STATE_FOCUSED (2):聚焦状态(键盘/遥控器操作时)
  • LV_STATE_DISABLED (4):禁用状态
  • LV_STATE_CHECKED (8):选中状态(复选框、开关等)

用法很简单,就是在 lv_obj_add_style() 的第三个参数指定状态:

/* 默认样式 */
lv_obj_add_style(btn, &style_btn_default, LV_STATE_DEFAULT);

/* 按下样式 */
lv_obj_add_style(btn, &style_btn_pressed, LV_STATE_PRESSED);

/* 禁用样式 */
lv_obj_add_style(btn, &style_btn_disabled, LV_STATE_DISABLED);

当按钮被按下时,LVGL自动应用 LV_STATE_PRESSED 的样式。你不需要写任何事件处理代码——框架帮你做了。

但注意:状态样式是叠加的,不是替换的。什么意思?如果你在按下样式里只设置了背景颜色,那么边框、圆角等属性会沿用默认样式。只有你显式设置的属性才会覆盖。

我的习惯: 每个控件至少定义两种状态——默认和按下。禁用状态看需求。聚焦状态在触摸屏上可以忽略,但如果是按键式设备,一定要做,否则用户不知道焦点在哪。

3.5 知识体系总览

说了这么多,咱们用一张图把整个样式系统的脉络理清楚。这张图我画了好几次才满意,你仔细看看:

LVGL 对象与样式系统结构图 lv_obj_t 对象 创建 / 删除 / 移动 / 对齐 lv_style_t 样式 初始化 / 设置属性 / 应用 样式分类 本地样式 继承样式 状态样式 预定义状态 DEFAULT / PRESSED FOCUSED / DISABLED CHECKED 样式优先级规则 本地样式 > 父对象继承样式 > 屏幕默认样式 状态样式叠加:只覆盖显式设置的属性,未设置属性沿用默认状态

这张图把整个体系串起来了。你从对象出发,找到样式,再根据场景选择本地、继承还是状态样式。记住这个流程,写代码时思路会清晰很多。

3.6 实战避坑指南

最后,我把自己这些年踩过的坑总结一下,你遇到了能少走弯路:

  • 样式不生效? 先检查 lv_style_init() 有没有调用。我至少有三次熬夜调试,最后发现是忘了初始化。
  • 颜色不对? 确认你用的是 lv_color_hex() 还是 lv_color_hex32()。前者是16位色深,后者是32位。混用会导致颜色偏差。
  • 继承没起作用? 检查父对象是否设置了 LV_STATE_DEFAULT 的样式。只有默认状态的样式才会被子对象继承。
  • 状态切换卡顿? 不要在状态样式里设置太多属性。LVGL每次状态切换都会重新计算样式,属性越多越慢。
我曾经犯过的错: 在一个产品里,我给每个按钮都单独创建了样式,结果内存爆了。后来改成共用样式 + 状态样式,内存直接降了60%。记住:样式是资源,能复用就别新建。

嗯,这一章的内容就到这。对象和样式是LVGL的根基,你花时间把它吃透,后面学控件、学动画都会轻松很多。别急,慢慢来,写几个demo试试,手感就有了。