属性、状态、部件、主题——把"好看"变成可以精确控制的工程
核心方法论:艺术与工程,美是可测量、可复用的
「画家从不靠灵感碰运气调色——他先想清楚要什么颜色,再把它画上去。LVGL 的样式系统也是一样:每一个视觉细节都是一个'属性',每一段交互状态都是一次'换脸'。把艺术翻译成可复用的规则,这正是工程师的浪漫。」
样式(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),所有用到它的对象会自动刷新——这是"一处定义、全局同步"的关键一步。
状态(state)描述一个对象当前处于什么交互阶段。LVGL 用位掩码枚举 lv_state_t 表达:LV_STATE_DEFAULT = 0、LV_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_DEFAULT | 0 | 默认态,几乎所有对象的基础状态 |
LV_STATE_PRESSED | 1 << 7 | 被按下/按住时 |
LV_STATE_FOCUSED | 1 << 3 | 获得焦点时(键盘/编码器导航) |
LV_STATE_CHECKED | 1 << 2 | 勾选/打开(checkbox、switch 等) |
LV_STATE_DISABLED | 1 << 9 | 禁用,不再响应交互 |
按下、聚焦这类状态是库自动维护的,你只管在对应选择器上挂"变化后的样式"。千万不要自己写回调去改颜色——那既是在重复造轮子,又容易漏掉状态切换的边界情况。样式系统就是让你"声明结果、不写过程"的。
状态回答"什么时候变",部件(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 = 0x000000、LV_PART_INDICATOR = 0x020000、LV_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 一把梭。
除了"定义样式再挂"的标准流程,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 都会跟着变色——这是做整页配色最快的方法,也是为什么主题只需要一套字体/前景色配置。
当你一个样式都不设,界面依然有模有样——因为有一个主题(theme)在背后兜底。LVGL 源码 src/themes/ 下有三个内置主题:default、simple、mono。默认主题在创建显示器(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 回调来做清理——把主题的生命周期和显示器的生命周期绑在一起,才不会泄漏内存。
创建一个 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 表示"主部件 + 默认状态",即对所有正常情况生效。
写一个按钮:默认是蓝色背景,按住时变成红色并加宽边框到 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 0 和 LV_STATE_PRESSED。库在按下时自动加/移除 LV_STATE_PRESSED,外观随之切换,全程无需回调。
创建一根滑块,要求:指示条是紫色到粉色水平渐变、圆角 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_INDICATOR;style_knob(白底 + 阴影)挂 LV_PART_KNOB;style_indicator_pressed(shadow_width 18、spread 2)挂 LV_PART_INDICATOR | LV_STATE_PRESSED。关键点:部件与状态两维都要写对,才能精确命中"按住时的指示条"。
在默认主题的基础上做一个"子主题":克隆当前主题,让所有按钮获得绿色背景 + 深绿边框,同时保留默认主题的其余外观。参考 lv_example_style_theme.c。请说明为什么用"克隆 + apply 回调"而不是直接改默认主题,并写出关键步骤。
关键 API:lv_theme_create → lv_theme_copy → lv_theme_set_parent → lv_theme_set_apply_cb → lv_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_DELETE 时 lv_theme_delete 释放。