LVGL基础

LVGL介绍

LVGL是什么?

LVGL(Light and Versatile Graphics Library)- 轻便是且多功能的图形库,一个开源跨平台轻量级嵌入式图形界面库(GUI),专为资源有限的MCU/MPU设备设计(无操作系统)。它广泛应用于手表、家电、仪表、工控、物联网等场景。

最初LVGL叫做LittlevGL

官网https://lvgl.io/
英文文档LVGL 9.3 documentation

中文文档:Introduction(介绍) — LVGL 文档

LVGL软硬件所需资源(推荐内存分配)

  1. 处理器要求

    • 16、32 或 64 位微控制器或处理器
    • 推荐主频 > 16MHz
  2. 存储资源

    • Flash/ROM:

      • 最低 > 64 KB(仅包含最基本组件)

      • 推荐 > 180 KB(功能更全面)

    • RAM:

      • 静态RAM占用约 2KB(视使用的控件和功能而定)
      • 处理任务堆栈(stack):最低 > 2 KB,推荐 > 8 KB (lv_task_handler任务)
      • 动态内存池(heap):最低 > 2 KB,推荐 > 48 KB(如果用大量GUI控件)通过LV_MEM_SIZE宏在lv_conf.h中设置

显示缓冲区:最少大于水平分辨率像素数,推荐为水平分辨率 × 10像素

  1. 显示控制
  • MCU内部或外部显示控制器支持至少一个帧缓冲区
  1. 编译环境
  • 支持 C99 或更新版本的 C 编译器

LVGL主要特点

  • 轻量级:LVGL只需要少量的记忆体和运算资源,可以在低端的微控制器上运行。
  • 多功能:LVGL提供了超过40种控件,例如按钮、滑块、列表、图表等,以及多种主题、动画、字体、图像等元素,可以创建各种风格和效果的GUI。
  • 灵活:LVGL采用了面向对象的设计,可以方便地创建和管理GUI的组件和属性。LVGL还支持自动和手动布局,可以适应不同的显示器和方向。LVGL还支持事件驱动和回调函数,可以实现GUI的交互和逻辑。
  • 可移植:LVGL可以在不同的硬件和软件平台上运行,只需要提供一个显示器刷新函数和一个触摸屏读取函数。LVGL还提供了多种预配置的平台,例如STM32、ESP32、Raspberry Pi等,可以快速地开始使用LVGL。

相关资料

LVGL官方英文手册:https://docs.lvgl.io/master/index.html

韦东山LVGL中文手册:https://lvgl.100ask.net/master/index.html

韦东山 LVGL V8视频教程:https://www.bilibili.com/video/BV1Ya411r7K2

LVGL基础

对象(lv_obj_t)

阅读手册对应部分学习https://lvgl.100ask.net/9.2/overview/obj.html

LVGL采用面向对象的编程思想OOP,用户界面的基本组成部分是对象(控件),也称为Widgts。例如,一个按钮、标签、图像、列表、图表或者文本区域。

所有的对象都使用lv_obj_t指针作为句柄进行引用。之后可以使用该指针来设置或获取对象的属性。

image-20250810114707409

基础对象(lv_obj)

阅读手册对应部分学习https://lvgl.100ask.net/9.2/widgets/obj.html

重要概念

基础对象(控件)实现了屏幕上控件的基本属性,例如:

  • 坐标

  • ⽗对象

  • 基于⽗对象的后代

  • 包含样式

  • 诸如 Clickable、Scrollable等属性

在⾯向对象的思想中,基础对象是LVGL中所有其他对象都继承的基类。

基础对象的功能可以与其他控件⼀起使⽤。例如 lv_obj_set_width(slider, 100)

基础对象可以直接⽤作⼀个简单的控件:它只不过是⼀个矩形

屏幕是没有⽗对象的基础对象

LVGL的一共有三层屏幕:活动屏幕、顶层、系统层

层级关系: 活动屏幕(screen_active) < 顶层(top layer) < 系统层(system layer)

所以我们创建控件的时候就可以选择在任意创建一个控件: 最常用的是活动屏幕(screen_active)上创建控件

1
2
3
4
// 可以在不同层上创建对象(控件)
lv_obj_create(lv_screen_active()); //最常用的就是这个
lv_obj_create(lv_layer_top());
lv_obj_create(lv_layer_sys());

三者最终都指向基础对象lv_obj_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
lv_obj_t * lv_screen_active(void)
{
return lv_display_get_screen_active(lv_display_get_default());
}

lv_obj_t * lv_layer_top(void)
{
return lv_display_get_layer_top(lv_display_get_default());
}

lv_obj_t * lv_layer_sys(void)
{
return lv_display_get_layer_sys(lv_display_get_default());
}

基础对象的大小(控件的size)

阅读手册对应部分学习

  • 设置大小(Set Size)
1
2
3
4
5
lv_obj_set_width(obj, new_width);  //设置⾼度:

lv_obj_set_height(obj, new_height); //设置高度

lv_obj_set_size(obj, new_width, new_height); //同时设置宽度、⾼度:
  • 获取大小(Get Size)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取实际宽度
lv_obj_get_width(obj);

// 获取实际高度
lv_obj_get_height(obj);

// 获取实际可用的宽度
lv_obj_get_content_width(obj);

// 获取实际可用的高度
lv_obj_get_content_height(obj);

// 获取实际可用的宽高区域
lv_obj_get_content_coords(obj);

基础对象的位置(position)

阅读手册对应部分学习

LVGL屏幕的原点:

我们常⻅的坐标系是笛卡尔坐标系,或者叫 直⻆坐标系坐标原点在左下⻆,⽔平向右为x轴正⽅向,竖直向上位y轴正⽅向)。这种坐标系我们很熟悉,因为我们学习数学的时候⼀般就是使⽤这种坐标系。

image-20250810162916892

而LVGL屏幕的坐标系和我们熟悉的坐标系不⼀样,LVGL的坐标系我们⼀般称之为LCD坐标系,他的原点位置和直⻆坐标系的不⼀样(坐标原点在左上⻆⽔平向右为 x 轴正⽅向,竖直向下为y轴正⽅向)。

image-20250810163049950

image-20250810163135672

其实我们只需要记住⼀点: LVGL的原点位置在左上⻆


设置位置

1
2
3
4
5
lv_obj_set_x(obj, new_x); //设置x轴⽅向的坐标位置

lv_obj_set_y(obj, new_y); //设置y轴⽅向的坐标位置

lv_obj_set_pos(obj, new_x, new_y); //同时设置x、y坐标位置

设置对齐:

  • 使对象对齐到其父对象
1
2
3
lv_obj_set_align(obj, LV_ALIGN);// 参照父对象对齐

lv_obj_align(obj, LV_ALIGN_, x, y); // 参照父对象改变对齐方式并设置新的坐标

LV_ALIGN对齐方式取值如下:

  • LV_ALIGN_TOP_LEFT 顶部左对齐
  • LV_ALIGN_TOP_MID 顶部居中对齐
  • LV_ALIGN_TOP_RIGHT 顶部右对齐
  • LV_ALIGN_BOTTOM_LEFT 底部左对齐
  • LV_ALIGN_BOTTOM_MID 底部居中对齐
  • LV_ALIGN_BOTTOM_RIGHT 底部右对齐
  • LV_ALIGN_LEFT_MID 左侧垂直居中对齐
  • LV_ALIGN_RIGHT_MID 右侧垂直居中对齐
  • LV_ALIGN_CENTER 水平和垂直居中对齐
  • 对象对齐到任意参考对象
1
lv_obj_align_to(obj_to_align, obj_referece, LV_ALIGN, x, y); // 参照另一个对象(无父子关系)对齐后设置坐标位置

除了上述的对齐选项外,还可以使用以下选项将对象对齐到参考对象外部:

  • LV_ALIGN_OUT_TOP_LEFT 外部顶部左对齐
  • LV_ALIGN_OUT_TOP_MID 外部顶部居中对齐
  • LV_ALIGN_OUT_TOP_RIGHT 外部顶部右对齐
  • LV_ALIGN_OUT_BOTTOM_LEFT 外部底部左对齐
  • LV_ALIGN_OUT_BOTTOM_MID 外部底部居中对齐
  • LV_ALIGN_OUT_BOTTOM_RIGHT 外部底部右对齐
  • LV_ALIGN_OUT_LEFT_TOP 外部左侧顶部对齐
  • LV_ALIGN_OUT_LEFT_MID 外部左侧垂直居中对齐
  • LV_ALIGN_OUT_LEFT_BOTTOM 外部左侧底部对齐
  • LV_ALIGN_OUT_RIGHT_TOP 外部右侧顶部对齐
  • LV_ALIGN_OUT_RIGHT_MID 外部右侧垂直居中对齐
  • LV_ALIGN_OUT_RIGHT_BOTTOM 外部右侧底部对齐

对应所有对齐效果如下:

image-20250810164445237

获取位置

1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取x轴坐标位置(在父对象之内)
lv_obj_get_x(obj);
lv_obj_get_x2(obj);

// 获取y轴坐标位置(在父对象之内)
lv_obj_get_y(obj);
lv_obj_get_y2(obj);

// 获取x轴坐标位置(对齐之后)
lv_obj_get_x_aligned(obj);

// 获取y轴坐标位置(对齐之后)
lv_obj_get_y_aligned(obj);

基础对象的盒子模型

阅读手册对应部分学习

LVGL遵循CSS的border-box模型,对象的盒子由以下部分构成:

  • 边界(bounding): 元素的宽度/高度范围或区域(整个盒子)。
  • 轮廓(outline): 盒子之间的距离,确认什么是轮廓(outline)。它是绘制于元素(盒子)周围的一条线,它不占据空间,位于边框边缘的外侧,可起到突出元素(盒子)的作用。在浏览器里,当鼠标点击或使用Tab键让一个选项或者一个图片获得焦点的时候,这个元素就会多了一个轮廓框围绕。轮廓(outline)。
  • 边框(border): 边框有大小和颜色等属性(相当于盒子的厚度和它的颜色)。
  • 填充(padding): 对象两侧与其子对象之间的空间(盒子的填充物)。
  • 内容(content): 如果边界框按边框宽度和填充的大小缩小,则显示其大小的内容区域(盒子实际该东西的区域)。

LVGL的盒子模型是我们理解对象(部件)的组成、修改对象的样式、实现对象的布局、处理对象排列等的关键。

image-20250811141451980

盒子模型通俗理解:

  • 这些属性我们可以把它转移到我们日常生活中的盒子(箱子)上来理解,日常生活中所见的盒子也就是能装东西的一种箱子,也具有这些属性,所以叫它盒子模型。

  • content 就是盒子里装的东西,它有高度(height)和宽度(width),可以是图片,可以是文字或者小盒子嵌套。在现实中,内容不能大于盒子,内容大于盒子就会撑爆盒子,在 LVGL 中盒子不会变化,会产生滚动条,我们可以滚动查看超出盒子的内容,盒子也可以是有弹性的,内容太大就会撑大盒子(自动调整大小),但是不会损害盒子。

  • padding 即是填充,就好像我们为了保证盒子里的东西不损坏,填充了一些东西,比如泡沫或者塑料薄膜,填充物有大小,有软有硬。

  • border 就是再外一层的属性,因为边框有大小和颜色的属性,相当于盒子的厚度和它的颜色或者材质

image-20250811141833520

基础对象的样式(style)

阅读手册对应部分学习Styles(风格样式) — LVGL 文档

初始化样式

样式存储在 lv_style_t 变量中。样式变量应该是静态、全局或动态分配的,也就是它们不能是函数中的局部变量,因为当函数结束时它们会被销毁。样式初始化示例:

1
2
static lv_style_t style_obj;
lv_style_init(&style_obj);

设置样式属性

初始化好一个样式之后,就可以设置它的样式属性了。接口函数格式如下:

1
lv_style_set_<property_name>(&style, <value>);

示例:

1
2
3
4
lv_style_set_bg_color(&style_obj, lv_color_hex(0x000000));  // 设置背景色
lv_style_set_bg_opa(&style_obj, LV_OPA_50);
lv_style_set_.... // 设置背景透明度

添加(应用)样式到对象

初始化并设置好样式后,可以将它添加到对象上。接口函数如下:

1
lv_obj_add_style(obj, &style, <selector>);
  • obj:要添加到的对象
  • style:指向样式变量的指针
  • <selector>:要应用样式的部分和状态的 OR-ed 值(不能是互斥,否则就是清除标志,没法合并)

示例:

1
2
lv_obj_add_style(obj, &style_obj, 0);                  // 默认(常用)
lv_obj_add_style(obj, &style_obj, LV_STATE_PRESSED); // 在对象被按下时应用样式

获取样式属性

可以获取属性的最终值(考虑级联、继承、本地样式和转换)。接口函数格式如下:

1
lv_obj_get_style_<property_name>(obj, <part>);

函数使用对象的当前状态,如果没有更好的候选对象,则返回默认值,例如:

1
lv_color_t color = lv_obj_get_style_bg_color(obj, LV_PART_MAIN);

删除样式

  • 删除对象的所有样式:

    1
    lv_obj_remove_style_all(obj);
  • 删除对象的特定样式:

    1
    lv_obj_remove_style(obj, &style_obj, selector);

    1.只有当 selectorlv_obj_add_style 中使用的 selector 匹配时,此函数才会删除 style

    2.如果 style 为空,则会根据给出的 selector 检查并删除所有匹配的样式。

    3.如果 selectorLV_STATE_ANYLV_PART_ANY,就会删除具有任何状态或部分的样式。

    如下代码与 lv_obj_remove_style_all 效果一致:

1
lv_obj_remove_style(obj, NULL, LV_STATE_ANY | LV_PART_ANY );

查看样式属性

所有可用的样式属性可在文档或代码中获取:

文档位置和代码位置可能会随版本更新而变化,这里方法仅供参考,不需要死记硬背函数接口名。

背景属性和我们前⾯学习的盒⼦模型关系很⼤,背景属性主要有⼀下这些:

  • 背景 (Background)
  • 边界 (Border)
  • 轮廓 (Outline)
  • 阴影 (Shadow)
  • 填充 (Padding)
  • 宽度和⾼度变换
  • X和Y变换

样式的状态和部分

状态

对象可以处于以下状态的组合:

  • LV_STATE_DEFAULT (0x0000) 正常,释放状态
  • LV_STATE_CHECKED (0x0001) 切换或检查状态
  • LV_STATE_FOCUSED (0x0002) 通过键盘或编码器聚焦,或通过触摸板/鼠标点击
  • LV_STATE_FOCUS_KEY (0x0004) 通过键盘或编码器聚焦,但不通过触摸板/鼠标聚焦
  • LV_STATE_EDITED (0x0008) 由编码器编辑
  • LV_STATE_HOVERED (0x0010) 鼠标悬停(现在不支持)
  • LV_STATE_PRESSED (0x0020) 被按下
  • LV_STATE_SCROLLED (0x0040) 正在滚动
  • LV_STATE_DISABLED (0x0080) 禁用状态
  • LV_STATE_USER_1 (0x1000) 自定义状态
  • LV_STATE_USER_2 (0x2000) 自定义状态
  • LV_STATE_USER_3 (0x4000) 自定义状态
  • LV_STATE_USER_4 (0x8000) 自定义状态

这些状态可能会随着 LVGL 的更新而不断增加,推荐查阅最新文档获取最新资料。

部分

对象可以有多个部分(parts),每个部分也可以有自己的样式。LVGL 中存在以下预定义部分:

  • LV_PART_MAIN 类似矩形的背景
  • LV_PART_SCROLLBAR 滚动条
  • LV_PART_INDICATOR 指标,例如用于滑块、条、开关或复选框的勾选框
  • LV_PART_KNOB 像手柄一样可以抓取调整值
  • LV_PART_SELECTED 表示当前选择的选项或部分
  • LV_PART_ITEMS 如果小部件具有多个相似元素(例如表格单元格)
  • LV_PART_TICKS 刻度上的刻度,例如对于图表或仪表
  • LV_PART_CURSOR 标记一个特定的地方,例如文本区域或图表的光标
  • LV_PART_CUSTOM_FIRST 可以从这里添加自定义部件

这些部分也会随着 LVGL 的更新增加,建议查阅最新版文档。

案例

例如一个滑杆(Slider)包含三个部分:

  • 背景(main)
  • 指标(indicator)
  • 旋钮(knob)

这意味着滑块的这三个部分都可以拥有自己的样式。

⽰例体验: http://lvgl.100ask.net/9.1/widgets/slider.html#slider-with-custom-style

image-20250811145044244

本地样式

  • 除了“普通”样式外,对象还可以存储本地样式(私有样式)。
  • 本地样式与普通样式类似,但它不能在其他对象之间共享。如果使用本地样式,将自动分配局部样式,并在删除对象时释放。本地样式对于向对象添加本地自定义很有用。
  • 本地样式的接口函数是这样的格式:lv_obj_set_style_<property_name>(obj, ...);

示例:

1
2
3
lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffff), 0);  // 设置背景色
lv_obj_set_style_bg_opa(obj, LV_OPA_50, 0); // 设置背景透明度
lv_style_set_style_.....
  • 删除本地样式的时候我们只需调用类似一个样式:
1
lv_obj_remove_local_style_prop(obj, LV_STYLE_..., selector);

LV_STYLE_… 的取值请看:lvgl/src/misc/lv_style.h 中的 lv_style_prop_t

样式继承

  • 某些属性(通常与文本相关)可以从父对象的样式继承。

  • 只有没有在对象设置样式属性的时候,才会继承。在这种情况下,如果这个属性是可继承的,那么这个属性的值会在父类中检索,直到为一个对象为该属性指定了一个值。父类将使用自己的状态来确定该值。因此,如果按钮按下,且文本颜色来自此处,则将使用按下的文本颜色。

过渡特效

默认情况下,当一个对象改变状态(例如它被按下)时,新状态的属性会立即设置。但是,通过过渡,可以在状态更改时播放过渡效果。例如,按下按钮,其背景颜色可以在 300 毫秒内动画显示为按下的颜色。

demo体验:

这部分内容将在后面的课程再展开讨论。

样式主题

主题是风格的集合。如果存在活动主题,LVGL将其应用于每个创建的部件(对象)。这将为UI提供一个默认外观,然后可以通过添加更多样式对其进行修改。

demo体验:

基础对象事件

阅读手册对应部分学习:https://lvgl.100ask.net/9.1/overview/event.html

什么是事件?

当发生用户可能感兴趣的事情时,LVGL中会触发事件并作出相应的处理(反馈),例如当一个对象:

  • 被点击
  • 滚动
  • 数值改变
  • 重绘

……

基于过程与事件驱动的交互:

image-20250811145906012

使用事件

  1. 添加事件:

    1
    lv_obj_add_event_cb(obj, event_cb, event_code, user_data);
  2. 发送事件:

    1
    lv_obj_send_event(obj, event_code, param);
  3. 删除事件:

    1
    2
    lv_obj_remove_event_cb(obj, event_cb);
    lv_obj_remove_event_dsc(obj, event_dsc); // event_dsc 是 lv_obj_add_event_cb 返回的指针

事件类型 (event_code)

  • 输入设备事件 (Input device events)
  • 绘图事件 (Drawing events)
  • 其他事件 (Special events)
  • 特殊事件 (Other events)
  • 自定义事件 (Custom events)

更全面的信息请查询:

事件回调函数的lv_event_t参数

事件回调函数只有一个参数,函数原型如下:

1
static void my_event_cb(lv_event_t * e);

这个参数包含了很多信息,其中我们常用的有:

  • 获取触发的事件代码:

    1
    lv_event_code_t code = lv_event_get_code(e);
  • 获取触发事件的对象:

    1
    lv_obj_t * target = lv_event_get_target(e);
  • 获取最初触发事件的对象(事件冒泡):

    1
    lv_obj_t * target = lv_event_get_current_target(e);
  • 获取事件传递的用户数据:

    • lv_event_get_user_data(e):获取使用 lv_obj_add_event_cb 函数传递的用户数据
    • lv_event_get_param(e):获取使用 lv_obj_send_event 函数传递的用户数据

事件与对象之间的关系

  • 一个事件回调函数可给多个对象使用

    • 我们创建了一个事件处理函数之后是可以给不同的对象使用的。
  • 一个对象可以使用多个事件回调函数

    • 我们创建的对象可以绑定多个事件,比如一个事件是处理点击类型的事件,一个事件处理按下类型的事件等等。
  • 其他

    • 如果传入的用户数据不一样,一个对象可以绑定同一个事件回调函数多次,事件将按照添加的顺序调用。例如:
    1
    2
    lv_obj_add_event_cb(obj, my_clicked_event_cb, LV_EVENT_CLICKED, &num1);
    lv_obj_add_event_cb(obj, my_clicked_event_cb, LV_EVENT_CLICKED, &num2);

事件冒泡

如果对象启用了 lv_obj_add_flag(obj, LV_OBJ_FLAG_EVENT_BUBBLE),该对象的所有事件将会发送到该对象的父级。如果父级也启用了 LV_OBJ_FLAG_EVENT_BUBBLE,那么事件继续发送到他的父级,依此类推。

  • lv_event_get_target(e):获取触发事件的当前对象。
  • lv_event_get_current_target(e):获取事件冒泡的父对象。

LVGL定时器

阅读手册对应部分学习: https://lvgl.100ask.net/9.1/overview/timer.html

LVGL的定时器是按照指定周期(单位:ms)执行的函数。LVGL有一个内置的计时器系统。我们可以注册一个函数,让它定期被调用,这个函数称为定时器处理任务。这些定时器任务在 lv_task_handler() 中进行处理和调用,需要每隔 x 毫秒调用一次。

定时器是非抢占式的,也就是说一个定时器不能中断另一个定时器。因此,我们可以在定时器回调函数中调用任何与LVGL相关或无关的函数。

使用定时器

  1. 创建定时器:

    1
    2
    lv_timer_t * timer = lv_timer_create(timer_cb, period_ms, user_data);
    //通过 lv_timer_create 返回的定时器句柄 timer变量,可以修改定时器的参数。
    • timer_cb:定时器回调函数原型:void (*lv_timer_cb_t)(lv_timer_t *)

    • period_ms:定时器执行周期,以毫秒(ms)为单位

    • user_data:用户数据,void * 类型的指针

  2. 准备与重置定时器

    1
    2
    lv_timer_ready(timer);   // 使计时器在下次调用 lv_timer_handler() 时运行
    lv_timer_reset(timer); // 重置计时器的周期,在定义的周期过去后再次被调用
  3. 设置定时器的参数

    1
    2
    3
    lv_timer_set_cb(timer, new_cb);                 // 设置新的回调函数
    lv_timer_set_period(timer, new_period); // 设置新的时间周期
    lv_timer_set_user_data(timer, new_user_data); // 设置新的用户数据
  4. 设置重复次数
    使用 lv_timer_set_repeat_count(timer, count) 接口让指定的定时器指定重复的次数。

    • 当定时器调用了定义的次数后,会自动被删除。将计数设置为 -1 表示无限重复。
  5. 启用和禁用定时器

    1
    lv_timer_enable(en);  // 启用或禁用定时器
  6. 暂停和恢复

    1
    2
    lv_timer_pause(timer);   // 暂停指定的定时器
    lv_timer_resume(timer); // 恢复指定的定时器

其他用法

  • 可以通过 lv_timer_create_basic() 在不指定任何参数的情况下创建一个新定时器。
  • 可以使用 lv_timer_get_idle 函数获取 lv_task_handler() 函数的空闲百分比时间。
    • 注意:它并不测量整个系统的空闲时间,仅测量 lv_task_handler() 的空闲时间。
    • 如果在操作系统中使用 lvgl 定时器调用 lv_task_handler(),这可能会产生误导,因为它实际上不能测量操作系统在空闲线程中的消耗时间。
  • 在某些情况下,不能立即执行某个操作。例如,如果还有其他东西在使用该对象,或者不希望阻塞执行。
    • 对于这些情况,可以使用 lv_async_call(my_function, data_p) 在下一次调用 lv_task_handler() 时调用 my_function