第6章:样式系统

属性、状态、部件、主题——把"好看"变成可以精确控制的工程

🎨

本章导师:达芬奇

核心方法论:艺术与工程,美是可测量、可复用的

「画家从不靠灵感碰运气调色——他先想清楚要什么颜色,再把它画上去。LVGL 的样式系统也是一样:每一个视觉细节都是一个'属性',每一段交互状态都是一次'换脸'。把艺术翻译成可复用的规则,这正是工程师的浪漫。」

6.1 样式的本质:属性与选择器

样式(style)是 LVGL 里决定控件外观的唯一途径。一个样式本质上是一组「属性—值」的集合:属性(property)用枚举常量表示,例如 LV_STYLE_BG_COLOR 背景色、LV_STYLE_RADIUS 圆角、LV_STYLE_TEXT_COLOR 文字颜色、LV_STYLE_BORDER_WIDTH 边框宽度。一百多个样式属性覆盖了背景、边框、阴影、轮廓、文字、变换等几乎每一个视觉维度——第 1 章介绍 README 特性时提到的"100+ 样式属性",指的就是这份清单。

用法分三步:先用 lv_style_init() 初始化一个 lv_style_t 变量,再用 lv_style_set_bg_color() 这类 API 逐个填属性,最后用 lv_obj_add_style(obj, &style, selector) 把样式挂到对象上。其中第三个参数 selector(选择器)最值得留意——它是「部件 + 状态」的组合,0 表示 LV_PART_MAIN | LV_STATE_DEFAULT,即"主部件、默认状态"。同一个样式可以挂给任意多个对象,改一处、全局生效。

/* 步骤一:初始化并填充一个样式 */
static lv_style_t style_btn;
lv_style_init(&style_btn);
lv_style_set_bg_color(&style_btn, lv_palette_main(LV_PALETTE_GREEN));
lv_style_set_border_color(&style_btn, lv_palette_darken(LV_PALETTE_GREEN, 3));
lv_style_set_border_width(&style_btn, 3);
lv_style_set_radius(&style_btn, 10);

/* 步骤二:挂到对象上,selector 传 0 = 主部件 + 默认状态 */
lv_obj_t * btn = lv_button_create(lv_screen_active());
lv_obj_add_style(btn, &style_btn, 0);
/* 选择器 = 部件 | 状态,可以按位组合,命中更精确的粒度 */
lv_obj_add_style(slider, &style_indicator, LV_PART_INDICATOR);                    /* 滑块填充条 */
lv_obj_add_style(slider, &style_indicator_pr, LV_PART_INDICATOR | LV_STATE_PRESSED); /* 按下时的填充条 */

/* 样式可复用:同一个 lv_style_t 挂给任意多个对象 */
lv_obj_add_style(btn_a, &style_btn, 0);
lv_obj_add_style(btn_b, &style_btn, 0);
样式属性含义对应 lv_style_set_* 函数
LV_STYLE_BG_COLOR背景颜色lv_style_set_bg_color()
LV_STYLE_RADIUS圆角半径lv_style_set_radius()
LV_STYLE_BORDER_WIDTH边框宽度lv_style_set_border_width()
LV_STYLE_TEXT_COLOR文字颜色lv_style_set_text_color()
LV_STYLE_PAD_TOP / LEFT / RIGHT / BOTTOM四个方向的单边内边距lv_style_set_pad_all() 一次设四边
达芬奇提示

样式是可复用的资产:想统一一组按钮的配色,只需维护一个 lv_style_t。改完样式后调用 lv_obj_report_style_change(&style),所有用到它的对象会自动刷新——这是"一处定义、全局同步"的关键一步。

6.2 状态:让控件"换脸"

状态(state)描述一个对象当前处于什么交互阶段。LVGL 用位掩码枚举 lv_state_t 表达:LV_STATE_DEFAULT = 0LV_STATE_CHECKED = 1 << 2(勾选)、LV_STATE_FOCUSED = 1 << 3(聚焦)、LV_STATE_PRESSED = 1 << 7(按下)、LV_STATE_DISABLED = 1 << 9(禁用)等。大多数状态由库自动维护——你按下按钮,库就给它加上 LV_STATE_PRESSED,松开再移除;但你也可以用 lv_obj_add_state() / lv_obj_remove_state() 手动加减,比如给复选框预置 LV_STATE_CHECKED

状态的意义在于一个朴素的事实:同一个属性,不同状态可以有不同值。给同一个对象挂两份样式——一份给默认状态、一份给按下状态,按下的瞬间 LV_STATE_PRESSED 那套生效,控件就"换脸"了。这也是 lvgl-master/examples/styles/lv_example_style_parts_states.c 展示的核心:同一个部件既保留基础外观,又叠一层"状态专属的覆盖"。交互反馈不再靠你手写回调,样式系统替你完成。

/* 默认态:蓝底;按下态:红底 + 粗边框 */
static lv_style_t style_normal;
static lv_style_t style_pressed;

lv_style_init(&style_normal);
lv_style_set_bg_color(&style_normal, lv_palette_main(LV_PALETTE_BLUE));

lv_style_init(&style_pressed);
lv_style_set_bg_color(&style_pressed, lv_palette_main(LV_PALETTE_RED));
lv_style_set_border_color(&style_pressed, lv_palette_darken(LV_PALETTE_RED, 3));
lv_style_set_border_width(&style_pressed, 6);

lv_obj_t * btn = lv_button_create(lv_screen_active());
lv_obj_add_style(btn, &style_normal, 0);                   /* 默认外观 */
lv_obj_add_style(btn, &style_pressed, LV_STATE_PRESSED);        /* 按下覆盖 */
/* 手动操控状态位:加、减、查询 */
lv_obj_add_state(cb, LV_STATE_CHECKED);          /* 加上勾选态 */
lv_obj_remove_state(cb, LV_STATE_CHECKED);       /* 移除勾选态 */
bool checked = lv_obj_has_state(cb, LV_STATE_CHECKED);  /* 查询当前是否勾选 */

/* 状态可按位或叠加:聚焦 + 按下 同时生效 */
lv_obj_add_state(btn, LV_STATE_FOCUSED | LV_STATE_PRESSED);
状态触发时机
LV_STATE_DEFAULT0默认态,几乎所有对象的基础状态
LV_STATE_PRESSED1 << 7被按下/按住时
LV_STATE_FOCUSED1 << 3获得焦点时(键盘/编码器导航)
LV_STATE_CHECKED1 << 2勾选/打开(checkbox、switch 等)
LV_STATE_DISABLED1 << 9禁用,不再响应交互
达芬奇提示

按下、聚焦这类状态是库自动维护的,你只管在对应选择器上挂"变化后的样式"。千万不要自己写回调去改颜色——那既是在重复造轮子,又容易漏掉状态切换的边界情况。样式系统就是让你"声明结果、不写过程"的。

6.3 部件(Parts):一个控件多种形态

状态回答"什么时候变",部件(part)回答"变哪个部位"。许多控件不是一整块,而是由多个部件拼成:滑块的"身体"是 LV_PART_MAIN(背景矩形)、已填充的部分是 LV_PART_INDICATOR(指示条)、可拖拽的是 LV_PART_KNOB(旋钮);进度条、开关、复选框同理。部件枚举 lv_part_t 里还有 LV_PART_SCROLLBAR(滚动条)、LV_PART_SELECTED(选中文本)、LV_PART_ITEMS(表格单元格)等,LV_PART_MAIN = 0x000000LV_PART_INDICATOR = 0x020000LV_PART_KNOB = 0x030000——它们的取值恰好与状态位错开,所以可以安全地按位或进同一个选择器。

选择器的完整形态因此是「部件 | 状态」。把两维组合起来,你能精确命中"滑块被按下时它的指示条"这种极细的颗粒度——LV_PART_INDICATOR | LV_STATE_PRESSED。这正是 lv_example_style_parts_states.c 的做法:默认给指示条一条紫到粉的渐变,按住时才叠加一圈发光阴影,松手即消失。

/* 给滑块的"指示条"部件上妆:紫色渐变圆角条 */
static lv_style_t style_indicator;
lv_style_init(&style_indicator);
lv_style_set_bg_opa(&style_indicator, LV_OPA_COVER);
lv_style_set_bg_color(&style_indicator, lv_color_hex(0x8b5cf6));
lv_style_set_bg_grad_color(&style_indicator, lv_color_hex(0xec4899));
lv_style_set_bg_grad_dir(&style_indicator, LV_GRAD_DIR_HOR);
lv_style_set_radius(&style_indicator, 10);

lv_obj_t * slider = lv_slider_create(lv_screen_active());
lv_obj_add_style(slider, &style_indicator, LV_PART_INDICATOR);
/* 按下时指示条叠加发光阴影:部件 × 状态 的组合选择器 */
static lv_style_t style_indicator_pressed;
lv_style_init(&style_indicator_pressed);
lv_style_set_shadow_color(&style_indicator_pressed, lv_color_hex(0xec4899));
lv_style_set_shadow_width(&style_indicator_pressed, 18);
lv_style_set_shadow_spread(&style_indicator_pressed, 2);

lv_obj_add_style(slider, &style_indicator_pressed,
                 LV_PART_INDICATOR | LV_STATE_PRESSED);

/* 旋钮部件单独上妆:白底 + 阴影 */
static lv_style_t style_knob;
lv_style_init(&style_knob);
lv_style_set_bg_color(&style_knob, lv_color_hex(0xffffff));
lv_obj_add_style(slider, &style_knob, LV_PART_KNOB);
部件含义典型控件
LV_PART_MAIN主体/背景矩形所有控件
LV_PART_INDICATOR已填充/选中部分slider、bar、switch、checkbox 勾选块
LV_PART_KNOB可拖拽的旋钮slider、arc
LV_PART_SCROLLBAR滚动条所有可滚动对象
LV_PART_SELECTED被选中的文本label、textarea
达芬奇提示

LV_OPA_COVER 是"完全不透明"的别名,值等于 255(源码里常写作 255 * 100 / 100)。想要半透明就写 128,完全透明用 LV_OPA_TRANSP。先想清楚要哪个部件变,再决定用哪个选择器——别一上来就 LV_PART_MAIN 一把梭。

6.4 本地样式:lv_obj_set_style_* 与优先级

除了"定义样式再挂"的标准流程,LVGL 还提供一条更直接的捷径:lv_obj_set_style_xxx(obj, value, selector),把属性直接设为对象的本地样式(local style),连 lv_style_t 都不用定义。常用的几个:lv_obj_set_style_bg_color()lv_obj_set_style_radius()lv_obj_set_style_text_color()lv_obj_set_style_pad_all()。注意最后一个参数依然是选择器——传 0 表示主部件默认态,传 LV_STATE_PRESSED 就变成"仅按下时生效的本地样式"。

本地样式值得单独讲,是因为它优先级最高:无论共享样式、主题样式怎么排,本地样式总是最后覆盖。源码 lv_example_style_local.c 用一组精确的实验证明了这一点——三个按钮共享同一份靛蓝样式,其中一个用 lv_obj_set_style_bg_color(..., 0) 加了本地背景色,它永远是粉色;另一个用 LV_STATE_PRESSED 加的是"按下态"的本地样式,平时不受影响、按住才变绿。而三个按钮的共享边框始终保留。

/* 本地样式:不定义 lv_style_t,直接设属性 */
lv_obj_t * card = lv_obj_create(lv_screen_active());
lv_obj_set_style_bg_color(card, lv_color_hex(0xec4899), 0);   /* 0 = 主部件默认态 */
lv_obj_set_style_radius(card, 12, 0);
lv_obj_set_style_shadow_width(card, 10, LV_STATE_PRESSED);    /* 仅按下时出现阴影 */
lv_obj_set_style_pad_all(card, 16, 0);
/* 共享样式 + 本地覆盖:优先级对比(参考 lv_example_style_local.c) */
static lv_style_t style_shared;
lv_style_init(&style_shared);
lv_style_set_bg_color(&style_shared, lv_color_hex(0x6366f1));
lv_style_set_border_color(&style_shared, lv_color_hex(0x312e81));
lv_style_set_border_width(&style_shared, 3);

lv_obj_t * btn_a = lv_button_create(lv_screen_active());  /* 靛蓝 */
lv_obj_add_style(btn_a, &style_shared, 0);

lv_obj_t * btn_b = lv_button_create(lv_screen_active());  /* 本地粉色覆盖背景 */
lv_obj_set_style_bg_color(btn_b, lv_color_hex(0xec4899), 0);
lv_obj_add_style(btn_b, &style_shared, 0);             /* 边框仍来自共享样式 */
便捷 API作用
lv_obj_set_style_bg_color(obj, c, sel)背景色
lv_obj_set_style_radius(obj, v, sel)圆角半径
lv_obj_set_style_text_color(obj, c, sel)文字颜色
lv_obj_set_style_border_width(obj, v, sel)边框宽度
lv_obj_set_style_pad_all(obj, v, sel)四周内边距
lv_obj_set_style_shadow_width(obj, v, sel)阴影宽度
达芬奇提示

文字颜色、文字字体这类属性会继承给子对象,而背景色不会。把文字颜色设在父容器上,所有后代的 lv_label 都会跟着变色——这是做整页配色最快的方法,也是为什么主题只需要一套字体/前景色配置。

6.5 主题:默认主题与主题定制

当你一个样式都不设,界面依然有模有样——因为有一个主题(theme)在背后兜底。LVGL 源码 src/themes/ 下有三个内置主题:defaultsimplemono。默认主题在创建显示器(lv_display_create)时自动初始化并挂到屏幕上:src/display/lv_display.c 里正是 lv_theme_default_init(disp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), LV_THEME_DEFAULT_DARK, LV_FONT_DEFAULT)——主色是蓝色调、强调色红色调、深浅模式与默认字体由配置宏决定。

主题的本质,是"把一批样式批量应用到整棵对象树":它会自动给按钮、滑块、label 等各类控件套上默认外观,而你手动加的样式优先级永远在它之上。要定制时有两条路:一是重新初始化默认主题(换主色、切深浅色);二是克隆当前主题并注册 apply 回调lv_theme_set_apply_cb()),在对象创建时追加自定义样式。lvgl-master/examples/styles/lv_example_style_theme.c 是后一种的范本:克隆 → 设父主题 → 注册回调 → lv_display_set_theme() 挂回去。

/* 方案一:重新初始化默认主题(换主色、切到浅色模式) */
lv_theme_default_deinit();
lv_theme_default_init(lv_display_get_default(),
                      lv_palette_main(LV_PALETTE_TEAL),   /* 主色:青绿 */
                      lv_palette_main(LV_PALETTE_RED),    /* 强调色 */
                      false,                            /* 浅色模式 */
                      LV_FONT_DEFAULT);
lv_obj_report_style_change(NULL);   /* 通知所有对象按新主题重绘 */
/* 方案二:克隆当前主题 + 自定义 apply 回调(参考 lv_example_style_theme.c) */
static lv_style_t style_btn;
lv_style_init(&style_btn);
lv_style_set_bg_color(&style_btn, lv_palette_main(LV_PALETTE_GREEN));
lv_style_set_border_color(&style_btn, lv_palette_darken(LV_PALETTE_GREEN, 3));
lv_style_set_border_width(&style_btn, 3);

static void my_apply_cb(lv_theme_t * th, lv_obj_t * obj)
{
    if(lv_obj_check_type(obj, &lv_button_class)) {
        lv_obj_add_style(obj, &style_btn, 0);   /* 每个按钮都加绿色样式 */
    }
}

void set_my_theme(void)
{
    lv_theme_t * base = lv_theme_get_from_obj(lv_screen_active());
    lv_theme_t * th_new = lv_theme_create();
    lv_theme_copy(th_new, base);
    lv_theme_set_parent(th_new, base);
    lv_theme_set_apply_cb(th_new, my_apply_cb);
    lv_display_set_theme(lv_display_get_default(), th_new);
}
主题 API作用
lv_theme_default_init()初始化默认主题,返回主题句柄
lv_theme_default_get() / is_inited()获取 / 查询默认主题
lv_theme_get_from_obj()取某对象所在显示器的主题
lv_theme_set_apply_cb()注册主题应用回调(对象创建时被调用)
lv_display_set_theme()把主题挂到指定显示器
lv_theme_copy() / lv_theme_set_parent()克隆主题 / 设置父主题,支持链式继承
达芬奇提示

主题句柄要自己管理:显示器被删除时,记得用 lv_theme_delete() 释放。官方示例就是给显示器注册一个 LV_EVENT_DELETE 回调来做清理——把主题的生命周期和显示器的生命周期绑在一起,才不会泄漏内存。

章末练习

练习 1:做一张本地样式卡片 入门

创建一个 lv_obj 作为卡片,用 lv_obj_set_style_* 系列 API 直接设置:背景色 0xec4899、圆角 12、四周内边距 16。写出完整代码,并说明最后一个参数传 0 的含义。

提示

参考 6.4 的本地样式写法:lv_obj_create 后连续调用三个 lv_obj_set_style_*;参数 0 = LV_PART_MAIN | LV_STATE_DEFAULT

参考答案

lv_obj_t * card = lv_obj_create(lv_screen_active()); lv_obj_set_style_bg_color(card, lv_color_hex(0xec4899), 0); lv_obj_set_style_radius(card, 12, 0); lv_obj_set_style_pad_all(card, 16, 0);。第三个参数是选择器,0 表示"主部件 + 默认状态",即对所有正常情况生效。

练习 2:按下变色的按钮 进阶

写一个按钮:默认是蓝色背景,按住时变成红色并加宽边框到 6px,松开还原。要求:不用任何回调函数,只用样式系统完成。写出完整代码。

提示

参考 6.2:定义两份样式(正常态 + 按下态),分别用 lv_obj_add_style(btn, ..., 0)lv_obj_add_style(btn, ..., LV_STATE_PRESSED) 挂上去。

参考答案

正常态样式设 lv_palette_main(LV_PALETTE_BLUE) 背景;按下态样式设 lv_palette_main(LV_PALETTE_RED) 背景、lv_style_set_border_width(..., 6) 与深红边框。创建按钮后把两份样式分别挂到 selector 0LV_STATE_PRESSED。库在按下时自动加/移除 LV_STATE_PRESSED,外观随之切换,全程无需回调。

练习 3:给滑块部件上妆 进阶

创建一根滑块,要求:指示条是紫色到粉色水平渐变、圆角 10;旋钮是白色圆形并带一圈淡淡的阴影;按住指示条时它再叠加一层发光阴影。参考 lvgl-master/examples/styles/lv_example_style_parts_states.c。请写出需要用到的选择器组合。

提示

三个部件/状态组合:指示条用 LV_PART_INDICATOR,旋钮用 LV_PART_KNOB,按下发光用 LV_PART_INDICATOR | LV_STATE_PRESSED。渐变用 lv_style_set_bg_grad_color + lv_style_set_bg_grad_dir

参考答案

三份样式三个选择器:style_indicator(渐变 + 圆角)挂 LV_PART_INDICATORstyle_knob(白底 + 阴影)挂 LV_PART_KNOBstyle_indicator_pressed(shadow_width 18、spread 2)挂 LV_PART_INDICATOR | LV_STATE_PRESSED。关键点:部件与状态两维都要写对,才能精确命中"按住时的指示条"。

练习 4:定制一个自己的主题 挑战

在默认主题的基础上做一个"子主题":克隆当前主题,让所有按钮获得绿色背景 + 深绿边框,同时保留默认主题的其余外观。参考 lv_example_style_theme.c。请说明为什么用"克隆 + apply 回调"而不是直接改默认主题,并写出关键步骤。

提示

关键 API:lv_theme_createlv_theme_copylv_theme_set_parentlv_theme_set_apply_cblv_display_set_theme。apply 回调里用 lv_obj_check_type(obj, &lv_button_class) 判断类型。

参考答案

直接改默认主题会让所有显示器的外观跟着变,而"克隆 + 父主题"只影响当前显示器,且父主题(基础外观)+ 子主题(覆盖样式)形成链式继承,可按需叠加。步骤:(1) lv_theme_get_from_obj 拿到当前主题作 base;(2) lv_theme_create + lv_theme_copy(th_new, base) + lv_theme_set_parent(th_new, base);(3) apply 回调里对 lv_button_class 对象 lv_obj_add_style 绿色样式;(4) lv_display_set_theme(lv_display_get_default(), th_new)。最后记得在显示器 LV_EVENT_DELETElv_theme_delete 释放。