3、LVGL对象与样式:从基础到实战
各位同学,欢迎来到第三章。这一章我们聊聊LVGL里最核心的两个概念——对象和样式。说白了,你写LVGL界面,就是在摆弄各种对象,然后给它们穿上不同的样式外衣。我刚开始接触LVGL时,觉得对象不就是个矩形框吗?后来才发现,这个“框”的学问可大了。
3.1 LVGL对象(lv_obj)基础
LVGL里的一切都是对象。按钮是对象,标签是对象,滑块也是对象。它们都继承自一个基类——lv_obj_t。你可以把lv_obj_t想象成一张白纸,所有控件都是在这张白纸上画出来的。
创建一个对象很简单:
lv_obj_t * btn = lv_btn_create(lv_scr_act()); // 在当前屏幕上创建按钮
lv_obj_t * label = lv_label_create(btn); // 在按钮上创建标签
这里有个关键点:父子关系。每个对象都有父对象,父对象移动,子对象跟着移动;父对象被删除,子对象也一起消失。我在项目中遇到过一个问题——创建了上百个对象,但忘记设置正确的父子关系,结果删除父对象时内存泄漏了。嗯,从那以后我每次创建对象都会检查父对象是谁。
核心API速查:
lv_obj_create(parent)— 创建通用对象lv_obj_del(obj)— 删除对象lv_obj_clean(obj)— 清空对象的所有子对象lv_obj_get_parent(obj)— 获取父对象lv_obj_get_child(obj, idx)— 获取第idx个子对象
3.2 样式(Style)系统
样式就是对象的“衣服”。LVGL的样式系统非常灵活,它采用级联的方式——你可以给对象本身设置样式,也可以给对象的某个状态(比如按下、禁用)设置样式,还可以给对象的类(比如所有按钮)设置样式。
我个人习惯把样式分成三类:
- 本地样式:只影响单个对象
- 局部样式:影响某个对象及其子对象
- 全局样式:影响所有同类对象
来看一个实际例子:
/* 创建一个样式 */
static lv_style_t style_btn;
lv_style_init(&style_btn);
lv_style_set_bg_color(&style_btn, lv_color_hex(0x2196F3));
lv_style_set_radius(&style_btn, 8);
lv_style_set_pad_all(&style_btn, 10);
/* 应用到按钮 */
lv_obj_add_style(btn, &style_btn, LV_STATE_DEFAULT);
你想想看,如果每个按钮都要单独设置颜色和圆角,那代码得多冗余?所以样式系统的价值就在这里——一次定义,到处复用。
我的小技巧:样式变量最好声明为static,放在函数外部。这样不会占用栈空间,而且可以跨函数共享。我曾经把样式声明在函数内部,结果每次调用都要重新初始化,浪费了不少CPU周期。
3.3 样式的状态与过渡
一个对象可以有多个状态:默认、按下、聚焦、禁用等等。每个状态都可以有独立的样式。比如按钮按下时颜色变深:
lv_style_set_bg_color(&style_btn_pr, lv_color_hex(0x1565C0));
lv_obj_add_style(btn, &style_btn_pr, LV_STATE_PRESSED);
更酷的是,LVGL支持样式过渡动画。从默认状态切换到按下状态时,颜色可以平滑变化:
lv_style_set_transition(&style_btn, &trans_dsc);
/* trans_dsc 需要先配置好过渡时间和路径 */
我记得有一次做智能家居面板,客户要求按钮按下时有柔和的颜色渐变效果。用LVGL的过渡动画,几行代码就搞定了。要是自己写定时器做渐变,那得折腾半天。
3.4 对象的布局与对齐
布局,说白了就是决定对象放在哪里。LVGL提供了两种方式:
- 手动布局:用
lv_obj_set_pos()、lv_obj_set_size()直接指定坐标和尺寸 - 自动布局:使用Flex或Grid布局,让系统自动排列子对象
手动布局适合简单界面,但一旦屏幕尺寸变化,你就得重新计算坐标。我建议在复杂界面中使用自动布局。
3.4.1 对齐(Alignment)
对齐是最常用的功能。你想让一个标签居中显示?一行代码:
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
这里的LV_ALIGN_CENTER是相对于父对象居中。还有LV_ALIGN_TOP_LEFT、LV_ALIGN_BOTTOM_RIGHT等等。后面的两个参数是偏移量,单位是像素。
常用对齐方式:
| 宏定义 | 说明 |
|---|---|
| LV_ALIGN_CENTER | 居中 |
| LV_ALIGN_TOP_MID | 顶部居中 |
| LV_ALIGN_BOTTOM_LEFT | 左下角 |
| LV_ALIGN_OUT_RIGHT_MID | 父对象右侧居中(外部) |
3.4.2 Flex布局
Flex布局适合一行或一列排列子对象。比如做一个水平排列的按钮组:
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(container,
LV_FLEX_ALIGN_SPACE_EVENLY, /* 主轴对齐 */
LV_FLEX_ALIGN_CENTER, /* 交叉轴对齐 */
LV_FLEX_ALIGN_CENTER); /* 换行对齐 */
这样,你往容器里添加按钮,它们会自动均匀分布。屏幕旋转或尺寸变化时,布局会自动调整。嗯,这里要注意:Flex布局的子对象不要手动设置位置,否则会冲突。
3.4.3 Grid布局
Grid布局适合表格形式的排列。比如做一个2行3列的计算器按键:
lv_obj_set_grid_dsc_array(container, col_dsc, row_dsc);
/* col_dsc 和 row_dsc 是描述列宽和行高的数组 */
lv_obj_set_grid_cell(btn1, LV_GRID_ALIGN_STRETCH, 0, 1,
LV_GRID_ALIGN_STRETCH, 0, 1);
Grid布局的威力在于,你可以精确控制每个单元格的跨行跨列。我在做一个仪表盘项目时,用Grid布局实现了复杂的仪表盘排列,代码量比手动布局少了至少一半。
避坑指南:我曾经在Flex布局里嵌套Grid布局,结果子对象的尺寸计算出了问题。原因是父对象的Flex属性影响了子对象的Grid单元格尺寸计算。解决方案是:给子对象设置固定尺寸,或者用lv_obj_set_flex_grow()控制弹性比例。
3.5 实战:组合使用对象与样式
光说不练假把式。我们做一个带图标的卡片按钮:
/* 1. 创建卡片容器 */
lv_obj_t * card = lv_obj_create(lv_scr_act());
lv_obj_set_size(card, 200, 120);
lv_obj_align(card, LV_ALIGN_CENTER, 0, -50);
/* 2. 设置卡片样式 */
static lv_style_t style_card;
lv_style_init(&style_card);
lv_style_set_bg_color(&style_card, lv_color_hex(0xFFFFFF));
lv_style_set_shadow_width(&style_card, 10);
lv_style_set_radius(&style_card, 12);
lv_obj_add_style(card, &style_card, LV_STATE_DEFAULT);
/* 3. 添加图标(用LVGL内置符号) */
lv_obj_t * icon = lv_label_create(card);
lv_label_set_text(icon, LV_SYMBOL_SETTINGS);
lv_obj_align(icon, LV_ALIGN_TOP_MID, 0, 10);
/* 4. 添加文字标签 */
lv_obj_t * label = lv_label_create(card);
lv_label_set_text(label, "设置");
lv_obj_align(label, LV_ALIGN_BOTTOM_MID, 0, -10);
这个例子展示了对象创建、样式设置、对齐操作的综合运用。你想想看,如果不用样式系统,每个卡片都要单独设置背景色、阴影、圆角,那得多麻烦?
3.6 性能优化小贴士
最后分享几个我在实际项目中总结的经验:
- 样式复用:尽量复用样式对象,不要每个控件都创建新样式。我见过有人给100个按钮创建了100个样式,内存直接爆了。
- 减少样式层级:样式级联查找是有开销的。能用本地样式解决的问题,就不要用全局样式。
- 布局缓存:如果布局不会频繁变化,可以调用
lv_obj_update_layout()强制刷新一次,之后LVGL会缓存布局结果。 - 避免频繁对齐:在循环里反复调用
lv_obj_align()会导致性能下降。最好在布局完成后统一对齐一次。
好了,这一章的内容就到这里。对象和样式是LVGL的基石,掌握了它们,你就能搭建出任何想要的界面。下一章我们聊聊事件处理——怎么让界面“活”起来。