☰
LVGL 样式系统实战指南:lv_style 核心概念与避坑技巧
2026/9/30 10:43:00 网站建设 项目流程

在LVGL里做界面,一开始都会觉得挺省事的:控件拉出来,位置摆一摆,一个能跑的界面就出来了。但你很快就会发现,默认那套灰底蓝按钮的观感,放在自己的项目里怎么都不对劲。想把一个按钮改成圆角、渐变底、按下去有反馈、选中变绿、禁用置灰,光靠控件自带属性根本不够,所有答案都指向同一个东西——lv_style样式系统。这篇笔记就是围绕它写的,核心解决三件事:lv_style到底是什么、它跟CSS的对应关系在哪里、实际用的时候有哪些绕不开的坑。刚移植完LVGL、准备做界面定制的朋友,应该能从中省下不少自己翻源码的时间。

1. lv_style到底是什么:给控件外观定规则的那张“样式表”

1.1 从一次改按钮背景的尝试说起

我第一次在LVGL里想改按钮背景时,直觉反应是找类似lv_btn_set_bg_color这种接口,结果发现新版LVGL里这类控件专属的设置函数基本都被移除了。翻了一圈文档,看到的全是lv_style_set_bg_color、lv_obj_add_style这一套组合。当时心里直犯嘀咕:一个背景色而已,为什么搞得这么麻烦?

跑通几个Demo之后我才明白,这套设计其实很有远见。LVGL把“控件外观长什么样”这个事,从“每一个控件自己管自己”里彻底拆了出来。一个按钮看起来是什么样子,不再由按钮内部写死的逻辑决定,而是由一张样式表决定。这张样式表本身是独立的,可以复用到十个按钮上,也可以按按钮的不同交互状态分别配置。说白了,lv_style就是LVGL自己实现的那套CSS,虽然比Web的CSS简单不少,但核心思路是相通的。

1.2 从CSS对应关系理解lv_style

搞前端的朋友看到这里可能已经觉得眼熟了。确实,lv_style的设计思路跟CSS高度一致,但又比CSS更克制。我自己习惯把两边概念做个映射:

  • CSS里的class选择器,对应LVGL里的lv_style_init加lv_obj_add_style,一个样式对象可以被多个控件引用;
  • CSS里的属性,对应LVGL里的lv_style_set_*系列接口,设置颜色、圆角、边框、字体、间距等等;
  • CSS里的伪类比如:hover、:active,对应LVGL里的状态(state),按下、聚焦、选中、禁用这些交互状态;
  • CSS里的子元素、伪元素选择器,对应LVGL里的部件(part),比如滑块的指示条、旋钮,滚动条这些子部件。

这个映射弄清楚之后,lv_style的很多设计决策就不难理解了:为什么会有优先级的说法?因为CSS里也有层叠优先级。为什么变更样式需要刷新?因为CSS里改class也会触发重绘。带着这套心智模型去看官方文档,会觉得顺畅很多。

1.3 样式的三个基本坐标

lv_style实际使用时,有三个坐标决定了最终效果:作用在哪个对象上、作用于哪个部件、作用于哪个状态。对象就是具体控件,比如一个按钮;部件是控件内部的子组成部分,比如按钮的滚动条;状态是控件当前交互情况,比如被按下、被选中。三者的组合才能精确定位出一条样式规则。

理解这个三维关系是后面所有实操的基础。很多初学者写出的样式不生效,并不是属性写错了,而是没有把部件和状态选对。后面我单开了一章专门讲这个,因为这是最容易绊倒人的地方。

2. lv_style_t内部:属性值对 + 选择器 + local style

2.1 最基本的“三行代码”

一个lv_style_t样式对象,核心就是一组“属性-值”对的集合。使用流程永远是三步:初始化、设置属性、添加到某对象上。比如我要做一块蓝色圆角背景,代码是这样:

static lv_style_t style_bg; lv_style_init(&style_bg); lv_style_set_bg_color(&style_bg, lv_color_hex(0x2196f3)); lv_style_set_radius(&style_bg, 8); lv_obj_add_style(btn, &style_bg, LV_STATE_DEFAULT);

lv_style_init这一步看着像废话,但一定不能省。它会把lv_style_t内部清空并标记为未使用状态。如果不初始化就直接lv_style_set_*,往一个充满随机垃圾值的结构体里写数据,轻则样式错乱,重则直接死机。这是新手常踩的第一个坑,我后面会再具体说。

设置属性的时候,名字非常直观:背景相关的有bg_color、bg_opa、bg_grad_color;边框相关有border_width、border_color;文字相关有text_color、text_font;间距有pad_all、pad_row、pad_column。每一个set函数都对应一个样式属性,参数要么是颜色、要么是整型、要么是指针,类型各不相同,但函数名基本能让你一眼猜到用途。

2.2 属性分类与常用属性速查

lv_style的属性数量很多,但记忆上可以分门别类。我自己在项目里用得最多的就是下面几个大类:

分类代表属性作用
位置尺寸width, height, min_width, min_height, max_width, max_height控制控件大小与最小最大限制
内边距pad_all, pad_top, pad_bottom, pad_left, pad_right, pad_row, pad_column控制内容与边界的距离
背景bg_color, bg_opa, bg_grad_color, bg_grad_dir, bg_grad_stop背景颜色、透明度、渐变
边框border_color, border_width, border_opa, border_side, border_post边框样式与显示哪几条边
圆角radius圆角半径,值越大越圆
轮廓outline_color, outline_width, outline_opa, outline_pad外描边,常用来做焦点提示
阴影shadow_color, shadow_opa, shadow_width, shadow_ofs_x, shadow_ofs_y, shadow_spread外阴影,立体感
文字text_color, text_opa, text_font, text_letter_space, text_line_space, text_align, text_decor字号、字体、字间距、对齐方式
线条line_color, line_width, line_opa, line_dash_width, line_dash_gap线条控件样式
图片img_opa, img_recolor, img_recolor_opa图片透明度和重新着色
变换transform_angle, transform_zoom, transform_width, transform_height, transform_pivot_x, transform_pivot_y控件旋转、缩放、变形
过渡transition_property, transition_duration, transition_delay, transition_path_cb属性变化动画
透明度opa整个控件的透明度
布局layout, flex_main_place, flex_cross_place, grid_row_align等子控件的排列方式

我并不建议一次性把所有属性背下来,真正需要用的时候查手册就行。但有两类属性值得先记住,因为它们在多控件布局里特别常用:一类是pad_*系列,控制的是子控件相对本控件的内边距;另一类是flex_*、grid_*系列,控制的是子控件在父容器里的排布方式。这两类一旦用熟,复杂界面的开发效率会明显提升。

2.3 local style:那些lv_obj_set_style_xxx背后是什么

除了建一个lv_style_t再添加,LVGL还提供了很多lv_obj_set_style_*接口。比如:

lv_obj_set_style_bg_color(btn, lv_color_hex(0xff0000), LV_STATE_DEFAULT); lv_obj_set_style_radius(btn, 4, LV_STATE_DEFAULT);

这个系列其实就是“局部样式”(local style)。它不需要你先lv_style_init一个对象,而是直接在控件内部维护一个隐藏的样式表,把属性写进去。这种写法在只改一两个控件的时候非常方便,不需要单独建变量,代码也短。但从实现角度看,它也是在内部的样式链表头部插入一条样式记录,只不过生命周期由控件自己管理。

我个人的做法是:如果某个样式要复用到多个控件,就建独立的lv_style_t加进去;如果只是临时给某一个控件微调,就用lv_obj_set_style_*。需要强调的是,local style的优先级高于后添加的外部样式,所以当一个控件没按你预想的效果显示时,记得先检查它身上有没有残留的local style正在抢样式。

3. parts与states:按照“部位”和“状态”区分样式

3.1 LV_PART_*:控件的各个零件

一个复杂控件并不是一个整体,而是由多个“零件”拼成的,这在LVGL里称为part。每一个part都可以被单独赋予样式。常用part如下:

部件宏说明典型应用
LV_PART_MAIN主体部分几乎所有控件都有
LV_PART_INDICATOR指示条滑块的进度条、进度条的填充部分
LV_PART_KNOB旋钮滑块的滑块头、旋钮控件
LV_PART_ITEMS条目列表的每一行、按钮矩阵的每个按钮
LV_PART_SCROLLBAR滚动条内容超出后的滚动条
LV_PART_SELECTED选中区域文本被选中的高亮背景
LV_PART_CURSOR光标文本输入框的光标
LV_PART_TEXTAREA_PLACEHOLDER占位文本输入框的灰色提示文字

看一个滑块例子最直观。同一个滑块,轨道背景是LV_PART_MAIN,进度部分(已走过的部分)是LV_PART_INDICATOR,拖动的圆头是LV_PART_KNOB。你想让三块颜色不同,就得分别给三个part设置背景色:

lv_obj_t *slider = lv_slider_create(lv_scr_act()); lv_obj_set_style_bg_color(slider, lv_color_hex(0xeeeeee), LV_PART_MAIN); lv_obj_set_style_bg_color(slider, lv_color_hex(0x2196f3), LV_PART_INDICATOR); lv_obj_set_style_bg_color(slider, lv_color_white(), LV_PART_KNOB); lv_obj_set_style_pad_all(slider, 8, LV_PART_KNOB);

这里的LV_PART_MAIN其实就是0,所以很多例子里直接传0也能跑。但为了可读性和避免遗漏,还是建议显式写清楚。

3.2 LV_STATE_*:交互状态切换

状态跟部件是两个维度。部件是空间上的部位,状态则是时间上的交互情况。常用状态有这些:

状态宏说明什么时候触发
LV_STATE_DEFAULT默认状态没有任何其他状态时
LV_STATE_CHECKED选中开启勾选标志后点击切换
LV_STATE_FOCUSED聚焦支持方向键导航时获得焦点
LV_STATE_EDITED编辑中文本编辑进入编辑状态
LV_STATE_HOVERED悬停鼠标悬停在控件上方
LV_STATE_PRESSED按下手指或鼠标按下期间
LV_STATE_SCROLLED滚动中正在滚动时
LV_STATE_DISABLED禁用通过lv_obj_add_state手动设置

状态的最大价值在于:同一个控件,在不同状态下可以呈现完全不同的外观,不需要你在业务代码里反复改属性。比如让按钮可勾选后,只需加上LV_OBJ_FLAG_CHECKABLE标志,系统就会在你每次点击时自动切换LV_STATE_CHECKED状态,你在样式上为这个状态配好颜色,选中效果就自动出来了。

3.3 优先级规则:冲突了到底听谁的

多个样式加在同一个对象上,属性冲突是必然的。LVGL的裁决规则可以简化成三句话:

  • 谁的选择器更具体,谁说了算;
  • 选择器一样具体时,后添加的样式优先;
  • local style永远排在最高优先级。

特殊状态和特殊部件都属于“更具体的选择器”。比如同一个按钮,默认状态背景是蓝色,按下状态背景是深蓝色,这两条规则一个用在LV_STATE_DEFAULT,一个用在LV_STATE_PRESSED,正常不会打架,因为状态不重叠。但如果两个LV_STATE_DEFAULT样式都设置了bg_color,那就只能听最后add_style进去的那一条了。

这个“后添加的优先”规则,我见过太多人栽跟头。写代码时习惯是把默认样式和按下样式分成两个lv_style_t,先加默认、再加按下,这是对的。但如果顺序反过来,默认样式反而会把按下样式里没覆盖的属性抢走,表现出来就是“按下没反应”或者“按下颜色不对”。后面踩坑章节会专门展开。

3.4 组合selector:部位加状态

部件和状态可以组合使用,这是定制控件外观的关键能力。比如把滑块旋钮的按下状态做成红色:

lv_obj_set_style_bg_color(slider_knob, lv_color_hex(0xff1744), LV_PART_KNOB | LV_STATE_PRESSED);

组合selector的写法就是按位或。什么时候需要组合?就是你想对“某个部件的某个状态”单独做样式的时候。比如进度条的指示条在禁用时变灰,就是LV_PART_INDICATOR | LV_STATE_DISABLED。理解了这两个维度,你会发现LVGL控件外观的可控程度非常高,几乎每个细节都用样式暴露了出来。

4. 实战:给按钮写一套完整的交互皮肤

4.1 先规划再写代码

理论归理论,真正动手才是检验理解的唯一标准。下面我完整演示一遍:做一个按钮,四种状态下的外观都不同。

我的需求是这样:

状态外观需求
默认蓝色渐变背景、白色文字、圆角8、边框
按下背景变浅、按钮略微缩小、整体半透明
选中背景变绿、边框变深绿
禁用背景变灰、文字变淡

基于需求,我先规划出三张样式表:默认样式、按下样式、选中样式。禁用样式我打算用local style在事件回调里动态设置,这样可以顺便演示两种写法的混用。

4.2 完整代码实现

static lv_style_t style_btn_main; static lv_style_t style_btn_pressed; static lv_style_t style_btn_checked; void btn_style_demo(void) { /* 默认样式:蓝渐变底、白字、圆角8、1px边框 */ lv_style_init(&style_btn_main); lv_style_set_bg_color(&style_btn_main, lv_color_hex(0x2196f3)); lv_style_set_bg_grad_color(&style_btn_main, lv_color_hex(0x0d47a1)); lv_style_set_bg_grad_dir(&style_btn_main, LV_GRAD_DIR_VER); lv_style_set_radius(&style_btn_main, 8); lv_style_set_border_width(&style_btn_main, 1); lv_style_set_border_color(&style_btn_main, lv_color_hex(0x1565c0)); lv_style_set_text_color(&style_btn_main, lv_color_white()); lv_style_set_pad_all(&style_btn_main, 12); /* 按下样式:背景更亮、整体缩小两个像素 */ lv_style_init(&style_btn_pressed); lv_style_set_bg_color(&style_btn_pressed, lv_color_hex(0x42a5f5)); lv_style_set_transform_width(&style_btn_pressed, -2); lv_style_set_transform_height(&style_btn_pressed, -2); lv_style_set_opa(&style_btn_pressed, LV_OPA_80); /* 选中样式:绿色 */ lv_style_init(&style_btn_checked); lv_style_set_bg_color(&style_btn_checked, lv_color_hex(0x43a047)); lv_style_set_bg_grad_color(&style_btn_checked, lv_color_hex(0x1b5e20)); lv_style_set_bg_grad_dir(&style_btn_checked, LV_GRAD_DIR_VER); lv_style_set_border_color(&style_btn_checked, lv_color_hex(0x2e7d32)); /* 创建按钮 */ lv_obj_t *btn = lv_btn_create(lv_scr_act()); lv_obj_set_pos(btn, 50, 50); lv_obj_add_flag(btn, LV_OBJ_FLAG_CHECKABLE); lv_obj_add_style(btn, &style_btn_main, LV_STATE_DEFAULT); lv_obj_add_style(btn, &style_btn_pressed, LV_STATE_PRESSED); lv_obj_add_style(btn, &style_btn_checked, LV_STATE_CHECKED); /* 按钮上的文字 */ lv_obj_t *label = lv_label_create(btn); lv_label_set_text(label, "Click me"); lv_obj_center(label); }

这里有个细节值得说:按下状态我用了transform_width和transform_height的负值,它跟直接改width、height不同,不会改变控件在布局中占用的原始尺寸,只是在绘制层面做一个收缩,视觉效果上就是“被按进去了”。这种反馈在一些工控触屏项目里很实用,但注意它不会触发父容器重新布局,所以不会引起旁边的控件跳动。

4.3 事件回调里动态调整禁用样式

继续这个例子,给按钮加一个“禁用”入口。假设业务场景是:某个条件不满足时按钮不可点。最直接的做法是给它加LV_STATE_DISABLED,然后用local style动态设置禁用外观:

static void btn_disable(lv_obj_t *btn) { lv_obj_add_state(btn, LV_STATE_DISABLED); lv_obj_set_style_bg_color(btn, lv_color_hex(0xcccccc), LV_STATE_DISABLED); lv_obj_set_style_text_color(btn, lv_color_hex(0x999999), LV_STATE_DISABLED); }

注意LV_STATE_DISABLED不会自动阻止所有点击事件,如果你要求完全禁用点击逻辑,还需要在事件处理里判断状态。另外,禁用后控件默认会损失部分绘制性能之外的交互响应,比如焦点和点击的视觉反馈,但事件回调依然会收到,这个要根据项目实际需求处理。

4.4 怎么验证当前生效的样式

样式写完之后,怎么确认它真的生效了?除了眼睛看,LVGL还提供了获取生效样式的接口,比如:

lv_color_t c = lv_obj_get_style_bg_color(btn, LV_PART_MAIN); const lv_font_t *font = lv_obj_get_style_text_font(label, LV_PART_MAIN);

lv_obj_get_style_*返回的不是某个样式表里的原始值,而是经过优先级计算、状态匹配、继承合并之后的最终生效值。所以当你怀疑某个属性到底有没有生效时,直接调用对应的getter,打印出来看一眼,比反复改代码去猜要高效得多。我在PC模拟器上调试时,会专门用一个调试小页面,把几个关键控件的生效值实时显示出来,排查效率提升很多。

5. 样式不生效?这6个坑我基本都踩过

5.1 忘了移除默认主题样式

LVGL在多数移植例程里都启用了默认主题。你新建的按钮,默认已经套了一层主题样式,比如背景色、圆角、阴影这些。如果你自己又加了一套样式,而主题样式优先级在某些情况下并不低,就会出现“我明明设置了背景色,怎么显示的还是蓝色/灰色”的诡异现象。

我一般在新控件加入自定义样式之前,先调用lv_obj_remove_style_all(obj)清掉所有已有样式,再添加自己的。这个函数会移除该对象上所有局部样式和已添加的外部样式,相当于从一张白纸开始画。注意它不会影响子对象已经继承的来自父对象的字体等属性,但会直接影响这个对象的外观,所以要在add_style之前调用。

5.2 add_style顺序:后来者居上

前面提过,同selector的两个样式,后添加的优先级更高。这属于“看一眼文档就不会错,但实际总会忘”的坑。我遇到过这样的代码:

lv_obj_add_style(btn, &style_btn_pressed, LV_STATE_PRESSED); lv_obj_add_style(btn, &style_btn_main, LV_STATE_DEFAULT);

顺序反了之后,按下状态时,两个样式都带bg_color,按照规则,后添加的style_btn_main的蓝色背景把style_btn_pressed的浅色背景覆盖了,按下反馈完全消失。解决思路也很简单:保持“默认样式先加,局部特殊样式后加”的书写顺序,同时在命名变量时把状态写清楚,避免自己看混。

5.3 只给PRESSED样式设了属性,但对象进不了PRESSED状态

还有一次我折腾了很久:给按钮设置了LV_STATE_PRESSED下的文本颜色,结果按下时文字颜色一点没变。排查半天才发现,文字颜色的样式我加在了按钮上,而显示文字的是一个label子对象。按钮被按下时只有按钮本体会进入LV_STATE_PRESSED,子label并不会自动进入这个状态。

这个问题的本质是:状态是对象层面的属性,不会自动传递给子对象。想让label也跟着变化,要么给label也添加LV_STATE_PRESSED样式,要么用事件回调手动给label加状态,要么干脆把按钮样式里的text_color设置为继承属性并直接用按钮的样式控制文字。总之,不要假设父对象状态会“染”到子对象上。

5.4 字体和文字颜色:继承带来的迷惑

LVGL里有一类属性是可继承的:text_font、text_color、text_opa、text_letter_space、text_line_space、text_align等等。这些属性在父对象上设置了,子对象如果没有显式设置,就会沿用父对象的值。

好处是:在屏幕上统一设置一次字体和文字颜色,所有label都跟着变,非常方便。坏处是:如果你在某层父容器上设置了text_color,而子控件里的文字颜色迟迟不对,你可能根本想不到问题出在父对象身上。排查时如果发现一个label的文字颜色怎么改都不生效,先往它的父对象链路上找一找,看看是否有某个父容器把继承压在头上了。要注意,背景色、边框这些默认都是不可继承的,所以不要用“父对象设背景,子控件自动变”的思维去套。

5.5 渐变四件套:少一个都不行

渐变背景在ui里很常见,但LVGL的渐变必须同时设置三个属性:bg_color、bg_grad_color、bg_grad_dir。bg_color是渐变的起点颜色,bg_grad_color是终点颜色,bg_grad_dir决定方向是垂直、水平还是对角。只设起点和方向,不设终点,渐变是不会出现的;只设两个颜色但不设方向,同样不显示,因为你根本没告诉它往哪个方向过渡。

另外还有bg_grad_stop,它是一个0到255的值,表示渐变过渡的“断点”。默认值通常够用,但如果你发现渐变过渡出现了一个明显的色彩跳变,往往就是这个断点被误改了。还有一点容易忽略:渐变只在bg_opa大于0时可见。如果你把背景透明度设成0,颜色配得再好看也白搭。

5.6 性能与内存:样式也不是随便玩的

最后说一个嵌入式项目必须关心的点。每初始化一个lv_style_t,它内部就要为属性值分配相应存储;每给一个对象添加一条样式,链表中就多一个条目。少量控件看不出问题,但如果你在一个页面上创建了几百个控件,并且给每个控件都单独用local style设置了一堆属性,内存开销就会变成一个不容忽视的问题。

我常用的优化思路是:凡是多个控件长得一样的,就用同一个静态样式对象,而不是每个控件各自set一遍。在LVGL 9里还可以用LV_STYLE_CONST定义只读样式,直接把样式数据放到只读存储段,进一步省RAM。另一个优化方向是优先考虑用“继承”和“默认主题”来铺底,只对确实要特化的控件添加样式,而不是每个控件都从零开始喂满所有属性。

6. LVGL 8.x 和 9.x 的样式API差异备忘

6.1 8.x的习惯用法

LVGL 8是目前嵌入式社区里资料最多、案例最丰富的版本。它的样式API设计相对稳定,大家常见到的是这套写法:

static lv_style_t style; lv_style_init(&style); lv_style_set_bg_color(&style, lv_color_hex(0x2196f3)); lv_style_set_radius(&style, 6); lv_obj_add_style(obj, &style, LV_STATE_DEFAULT);

8.x里,lv_obj_set_style_*这个局部样式系列也已经很成熟,适合快速改单点属性。如果你的项目基于8.x,目前绝大多数问题都能在社区里找到现成答案,上手成本比较低。

6.2 9.x的关键变化

LVGL 9在样式这块最主要的改动,是把样式数据的管理方式往“大量使用常量样式”方向推进。最明显的体现是LV_STYLE_CONST,允许你定义编译期只读的样式对象,这对RAM受限的单片机影响很大。颜色宏也做了一次清洗,很多8.x里容易混淆的颜色写法在9里被规范成lv_color_hex、lv_color_white这类函数式调用。属性本身的枚举和分组更细致了,但lv_style_set_*以及lv_obj_add_style这两个核心API的写法基本保持兼容,所以把8.x的样式代码迁到9.x,多数情况只需要小改。

如果你是刚起步,我建议直接学9.x,除非你有必须停留在8.x的旧项目依赖。9.x在样式内部实现上更干净,调试信息也更完善,长期看维护成本更低。

6.3 调试效率:模拟器永远是第一选择

因为样式的视觉效果和运行环境强相关,我强烈建议在PC模拟器上先调样式,再搬到板子上。PC模拟器里改代码重新编译只要几秒,按键和鼠标事件都很方便模拟PRESSED、HOVERED这些状态,排查优先级问题时直观得多。板子上每次烧录、断电、看效果,时间成本高出太多了。等PC上调得差不多,再交叉编译到目标平台,通常只剩一些色彩深度不同造成的微调。

最后再分享一点个人经验

这篇笔记写到这里,核心内容基本都覆盖了。如果让我用一句话总结对lv_style的使用感受,那就是:不要把它当“控件属性设置器”来用,而是当成一套独立的“外观规则系统”来设计。我现在写界面,习惯先把界面里会出现的视觉风格抽成一个个样式对象,统一放到一个单独的文件里,就像前端的公共样式表一样。不同的按钮、卡片、弹窗,都从这套公共样式里复用或微调,整个项目的UI一致性会好非常多,后期换肤也更方便。这个习惯让我在几个实际项目里省了大量重复调参的时间,也推荐你试试。下一篇笔记,我大概率会写主题定制和动画过渡相关的内容,到时候再见。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询