4. LVGL核心对象模型

各位同学,欢迎来到第四章。前面几章我们把LVGL的控件、事件、基础绘图都过了一遍。现在你手里的控件,就像一堆乐高积木——能搭起来,但看起来灰扑扑的。这一章我们要聊的,就是给这些积木上色、抛光、做造型。

样式系统,说白了就是LVGL的“皮肤引擎”。它决定了你的按钮是圆角还是直角,文字是红色还是蓝色,背景是渐变还是纯色。我刚开始用LVGL时,觉得样式不就是改改颜色嘛,结果一上手才发现,这里面的门道比我想象的多得多。

4.1 样式属性详解:你能改什么?

LVGL的样式属性,我习惯把它们分成几大类。这样记起来方便,用起来也顺手。

类别 常用属性 说明
背景 bg_color, bg_opa, bg_grad_color, bg_grad_dir 背景色、透明度、渐变色、渐变方向
边框 border_color, border_width, border_side, border_opa 边框颜色、宽度、显示哪几条边、透明度
文字 text_color, text_font, text_letter_space, text_line_space 文字颜色、字体、字间距、行间距
图像 img_recolor, img_recolor_opa, img_opa 图像重新着色、着色透明度、图像透明度
轮廓 outline_color, outline_width, outline_pad, outline_opa 轮廓颜色、宽度、内边距、透明度(常用于焦点指示)
阴影 shadow_color, shadow_width, shadow_ofs_x, shadow_ofs_y, shadow_opa 阴影颜色、宽度、偏移量、透明度
圆角 radius 圆角半径,设为LV_RADIUS_CIRCLE就是圆形
内边距 pad_top, pad_bottom, pad_left, pad_right, pad_row, pad_column 内边距,控制内容与边界的距离
变换 transform_angle, transform_zoom, transform_pivot_x, transform_pivot_y 旋转角度、缩放、旋转/缩放的中心点

这里我特别想提一下 transform_zoom。我在做智能手表项目时,需要做一个“点击放大”的动画效果。一开始我用的是 lv_obj_set_size 来改变大小,结果发现文字和图像都变形了。后来换成 transform_zoom,整个控件等比缩放,效果完美。你想想看,这个属性其实是“视觉缩放”,不改变控件的实际坐标和布局,只是画出来的时候放大或缩小。

核心要点:样式属性只影响“绘制”,不影响“布局”。你改了 radius,控件还是原来的矩形区域,只是画出来变圆了。这个区别很重要,后面讲事件区域时会用到。

4.2 样式状态(State):按钮的“喜怒哀乐”

一个按钮,按下去和没按下去,看起来应该不一样吧?这就是样式状态要做的事。

LVGL定义了这些状态:

  • LV_STATE_DEFAULT(0x0000):默认状态,啥也没发生
  • LV_STATE_CHECKED(0x0001):选中状态,比如开关打开
  • LV_STATE_FOCUSED(0x0002):焦点状态,键盘或遥控器选中时
  • LV_STATE_EDITED(0x0004):编辑状态,比如输入框正在输入
  • LV_STATE_HOVERED(0x0008):悬停状态,鼠标悬停时
  • LV_STATE_PRESSED(0x0010):按下状态,手指或鼠标按下时
  • LV_STATE_SCROLLED(0x0020):滚动状态,正在滚动时
  • LV_STATE_DISABLED(0x0040):禁用状态,不可交互

这些状态可以组合使用。比如一个按钮既被按下又被选中,状态值就是 LV_STATE_PRESSED | LV_STATE_CHECKED。

设置不同状态的样式,代码是这样的:

/* 创建一个按钮 */
lv_obj_t *btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 120, 50);

/* 创建样式对象 */
static lv_style_t style_btn_def;
static lv_style_t style_btn_pr;
lv_style_init(&style_btn_def);
lv_style_init(&style_btn_pr);

/* 默认状态:蓝色背景,白色文字 */
lv_style_set_bg_color(&style_btn_def, lv_color_hex(0x2196F3));
lv_style_set_text_color(&style_btn_def, lv_color_white());
lv_style_set_radius(&style_btn_def, 8);

/* 按下状态:深蓝色背景,文字稍微缩小 */
lv_style_set_bg_color(&style_btn_pr, lv_color_hex(0x1565C0));
lv_style_set_transform_zoom(&style_btn_pr, 256);  /* 256 = 100%,192 = 75% */
lv_style_set_text_color(&style_btn_pr, lv_color_white());

/* 添加样式到不同状态 */
lv_obj_add_style(btn, &style_btn_def, LV_STATE_DEFAULT);
lv_obj_add_style(btn, &style_btn_pr, LV_STATE_PRESSED);

小技巧:如果你想让按钮在按下时“弹”一下,可以设置 transform_zoom 为 230(约90%),再配合一个过渡动画,手感会非常好。我在做车载中控项目时,这个效果让客户非常满意。

4.3 样式层叠与继承:谁说了算?

这是样式系统里最容易让人迷糊的地方。我当年也踩过坑,所以咱们好好捋一捋。

一个控件最终长什么样,是由多个样式“叠加”决定的。LVGL的样式优先级规则是这样的:

  1. 本地样式 > 外部样式:直接加在控件上的样式,优先级最高
  2. 具体状态 > 通用状态:LV_STATE_PRESSED 的样式,覆盖 LV_STATE_DEFAULT 的同属性
  3. 后添加的 > 先添加的:同一个状态,后添加的样式覆盖先添加的
  4. 子控件继承父控件:如果子控件没有设置某个属性,会从父控件继承

嗯,这里要注意“继承”这个概念。不是所有属性都能继承。能继承的主要是文字相关的属性:text_color、text_font、text_letter_space、text_line_space。背景、边框、阴影这些,子控件不会继承。

举个例子:

/* 父容器样式 */
static lv_style_t style_parent;
lv_style_init(&style_parent);
lv_style_set_text_color(&style_parent, lv_color_hex(0xFF0000));  /* 红色文字 */
lv_style_set_bg_color(&style_parent, lv_color_hex(0x333333));    /* 深灰背景 */

lv_obj_t *cont = lv_obj_create(lv_scr_act());
lv_obj_add_style(cont, &style_parent, LV_STATE_DEFAULT);

/* 子标签,没有设置文字颜色 */
lv_obj_t *label = lv_label_create(cont);
lv_label_set_text(label, "我会是红色吗?");

/* 答案是:会!因为文字颜色可以继承 */

为什么会这样?因为LVGL在绘制子控件时,如果发现它没有设置 text_color,就会沿着父控件链往上找,直到找到为止。这个机制在写复杂界面时非常有用——你只需要在顶层容器设置一次字体和颜色,所有子控件都跟着变。

我曾经踩过的坑:有一次我在父容器设置了 bg_color,以为子按钮也会继承这个背景色。结果按钮还是默认的灰色。后来才想起来,背景色不是可继承属性。所以,如果你想让所有子控件都有同样的背景,要么在每个子控件上单独设置,要么用 lv_obj_set_style_bg_color 直接设置到子控件上。

4.4 本地样式与外部样式:两种用法,各有千秋

LVGL里,样式可以“贴”在控件上,也可以独立存在。这就引出了本地样式和外部样式的概念。

本地样式:直接用 lv_obj_set_style_xxx() 系列函数设置。比如:

lv_obj_set_style_bg_color(btn, lv_color_hex(0xFF5722), LV_STATE_DEFAULT);
lv_obj_set_style_radius(btn, 10, LV_STATE_DEFAULT);

这种方式的优点是简单直接,适合快速原型。缺点也很明显——如果你有10个按钮都要同样的样式,你得写10遍。

外部样式:先定义一个 lv_style_t 对象,设置好属性,然后通过 lv_obj_add_style() 加到控件上。就像前面例子里的 style_btn_def 那样。

外部样式的优势在于复用。我一般这样组织代码:

/* 定义一组全局样式 */
static lv_style_t style_btn_primary;
static lv_style_t style_btn_secondary;
static lv_style_t style_label_title;

void my_style_init(void) {
    lv_style_init(&style_btn_primary);
    lv_style_set_bg_color(&style_btn_primary, lv_palette_main(LV_PALETTE_BLUE));
    lv_style_set_text_color(&style_btn_primary, lv_color_white());
    lv_style_set_radius(&style_btn_primary, 6);

    lv_style_init(&style_btn_secondary);
    lv_style_set_bg_color(&style_btn_secondary, lv_palette_main(LV_PALETTE_GREY));
    lv_style_set_text_color(&style_btn_secondary, lv_color_white());
    lv_style_set_radius(&style_btn_secondary, 6);

    lv_style_init(&style_label_title);
    lv_style_set_text_color(&style_label_title, lv_color_hex(0x333333));
    lv_style_set_text_font(&style_label_title, &lv_font_montserrat_20);
}

/* 使用时 */
lv_obj_add_style(btn_ok, &style_btn_primary, LV_STATE_DEFAULT);
lv_obj_add_style(btn_cancel, &style_btn_secondary, LV_STATE_DEFAULT);

你看,这样写,样式定义集中在一处,修改起来也方便。我个人的习惯是:所有重复使用的样式,都做成外部样式;只有某个控件独有的样式,才用本地样式。

最佳实践:在项目初期就建立一个 styles.c 文件,把所有全局样式放在里面。这样后期换主题、改配色,只需要改这一个文件。我在做智能家居中控屏项目时,客户中途要换品牌色,我花了10分钟就改完了——就是因为样式都集中管理了。

4.5 本章知识体系图

下面这张图,把样式系统的核心脉络画出来了。你可以把它当作一个“样式速查地图”。

LVGL 样式系统核心结构 样式系统 样式属性 背景 / 边框 / 文字 阴影 / 圆角 / 内边距 变换(旋转/缩放) 样式状态 DEFAULT / PRESSED FOCUSED / CHECKED DISABLED / HOVERED 层叠与继承 本地 > 外部(优先级) 具体状态 > 通用状态 子控件继承父控件(文字属性) 本地 vs 外部样式 本地:lv_obj_set_style_xxx() 外部:lv_style_t + lv_obj_add_style() 最终效果 = 属性 × 状态 × 层叠规则

这张图把本章的四个核心知识点串起来了。你从中心出发,往四个方向走,就能看到每个分支下的具体内容。最下面那句“最终效果 = 属性 × 状态 × 层叠规则”,是我自己总结的公式。你写代码时,脑子里过一遍这个公式,基本不会出错。

好了,这一章的内容就到这里。样式系统是LVGL里最灵活、也最需要练习的部分。我建议你打开编辑器,把今天讲的代码都敲一遍,然后试着改改颜色、加加阴影、调调状态。只有亲手试过,才能真正理解“层叠”和“继承”是怎么回事。