LVGL基础

LVGL基础
THEDILVGL介绍
LVGL是什么?
LVGL(Light and Versatile Graphics Library)- 轻便是且多功能的图形库,一个开源
、跨平台
的轻量级
嵌入式图形界面库(GUI),专为资源有限的MCU/MPU设备设计(无操作系统)。它广泛应用于手表、家电、仪表、工控、物联网等场景。
最初LVGL叫做LittlevGL
官网:https://lvgl.io/
英文文档:LVGL 9.3 documentation
中文文档:Introduction(介绍) — LVGL 文档
LVGL软硬件所需资源(推荐内存分配)
处理器要求
- 16、32 或 64 位微控制器或处理器
- 推荐主频 > 16MHz
存储资源
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像素
- 显示控制
- MCU内部或外部显示控制器支持至少一个帧缓冲区
- 编译环境
- 支持 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)
LVGL采用面向对象
的编程思想OOP,用户界面的基本组成部分是对象
(控件),也称为Widgts。例如,一个按钮、标签、图像、列表、图表或者文本区域。
所有的对象都使用lv_obj_t
指针作为句柄进行引用。之后可以使用该指针来设置或获取对象的属性。
基础对象(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 | // 可以在不同层上创建对象(控件) |
三者最终都指向基础对象lv_obj_t
1 | lv_obj_t * lv_screen_active(void) |
基础对象的大小(控件的size)
阅读手册对应部分学习:
- 设置大小(Set Size)
1 | lv_obj_set_width(obj, new_width); //设置⾼度: |
- 获取大小(Get Size)
1 | // 获取实际宽度 |
基础对象的位置(position)
阅读手册对应部分学习:
LVGL屏幕的原点
:
我们常⻅的坐标系是笛卡尔坐标系
,或者叫 直⻆坐标系
坐标原点在左下⻆,⽔平向右为x轴正⽅向,竖直向上位y轴正⽅向)。这种坐标系我们很熟悉,因为我们学习数学的时候⼀般就是使⽤这种坐标系。
而LVGL屏幕的坐标系和我们熟悉的坐标系不⼀样,LVGL的坐标系我们⼀般称之为LCD坐标系
,他的原点位置和直⻆坐标系的不⼀样(坐标原点在左上⻆
,⽔平向右
为 x 轴正⽅向,竖直向下
为y轴正⽅向)。
其实我们只需要记住⼀点: LVGL的原点位置在左上⻆
设置位置
:
1 | lv_obj_set_x(obj, new_x); //设置x轴⽅向的坐标位置 |
设置对齐
:
- 使对象对齐到其父对象
1 | lv_obj_set_align(obj, LV_ALIGN);// 参照父对象对齐 |
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
外部右侧底部对齐
对应所有对齐效果如下:
获取位置
:
1 | // 获取x轴坐标位置(在父对象之内) |
基础对象的盒子模型
阅读手册对应部分学习:
LVGL遵循CSS的border-box模型,对象的盒子
由以下部分构成:
- 边界(bounding): 元素的宽度/高度范围或区域(整个盒子)。
- 轮廓(outline): 盒子之间的距离,确认什么是轮廓(outline)。它是绘制于元素(盒子)周围的一条线,它不占据空间,位于边框边缘的外侧,可起到突出元素(盒子)的作用。在浏览器里,当鼠标点击或使用Tab键让一个选项或者一个图片获得焦点的时候,这个元素就会多了一个轮廓框围绕。轮廓(outline)。
- 边框(border): 边框有大小和颜色等属性(相当于盒子的厚度和它的颜色)。
- 填充(padding): 对象两侧与其子对象之间的空间(盒子的填充物)。
- 内容(content): 如果边界框按边框宽度和填充的大小缩小,则显示其大小的内容区域(盒子实际该东西的区域)。
LVGL的盒子模型是我们理解对象(部件)的组成、修改对象的样式、实现对象的布局、处理对象排列等的关键。
盒子模型通俗理解:
这些属性我们可以把它转移到我们日常生活中的盒子(箱子)上来理解,日常生活中所见的盒子也就是能装东西的一种箱子,也具有这些属性,所以叫它盒子模型。
content 就是盒子里装的东西,它有高度(height)和宽度(width),可以是图片,可以是文字或者小盒子嵌套。在现实中,内容不能大于盒子,内容大于盒子就会撑爆盒子,在 LVGL 中盒子不会变化,会产生滚动条,我们可以滚动查看超出盒子的内容,盒子也可以是有弹性的,内容太大就会撑大盒子(自动调整大小),但是不会损害盒子。
padding 即是填充,就好像我们为了保证盒子里的东西不损坏,填充了一些东西,比如泡沫或者塑料薄膜,填充物有大小,有软有硬。
border 就是再外一层的属性,因为边框有大小和颜色的属性,相当于盒子的厚度和它的颜色或者材质
基础对象的样式(style)
阅读手册对应部分学习:Styles(风格样式) — LVGL 文档
初始化样式
样式存储在 lv_style_t
变量中。样式变量应该是静态、全局或动态分配的,也就是它们不能是函数中的局部变量,因为当函数结束时它们会被销毁。样式初始化示例:
1 | static lv_style_t style_obj; |
设置样式属性
初始化好一个样式之后,就可以设置它的样式属性了。接口函数格式如下:
1 | lv_style_set_<property_name>(&style, <value>); |
示例:
1 | lv_style_set_bg_color(&style_obj, lv_color_hex(0x000000)); // 设置背景色 |
添加(应用)样式到对象
初始化并设置好样式后,可以将它添加到对象上。接口函数如下:
1 | lv_obj_add_style(obj, &style, <selector>); |
obj
:要添加到的对象style
:指向样式变量的指针<selector>
:要应用样式的部分和状态的 OR-ed 值(不能是互斥,否则就是清除标志,没法合并)
示例:
1 | lv_obj_add_style(obj, &style_obj, 0); // 默认(常用) |
获取样式属性
可以获取属性的最终值(考虑级联、继承、本地样式和转换)。接口函数格式如下:
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.只有当
selector
与lv_obj_add_style
中使用的selector
匹配时,此函数才会删除style
。2.如果
style
为空,则会根据给出的selector
检查并删除所有匹配的样式。3.如果
selector
是LV_STATE_ANY
或LV_PART_ANY
,就会删除具有任何状态或部分的样式。如下代码与
lv_obj_remove_style_all
效果一致:
1 | lv_obj_remove_style(obj, NULL, LV_STATE_ANY | LV_PART_ANY ); |
查看样式属性
所有可用的样式属性可在文档或代码中获取:
- 英文原版文档:https://docs.lvgl.io/9.1/overview/style-props.html
- 中文翻译文档:http://lvgl.100ask.net/9.1/overview/style-props.html
- 代码位置:
- 普通样式:
lvgl/src/misc/lv_style_gen.h
- 本地样式:
lvgl/src/core/lv_obj_style_gen.h
- 普通样式:
文档位置和代码位置可能会随版本更新而变化,这里方法仅供参考,不需要死记硬背函数接口名。
背景属性和我们前⾯学习的盒⼦模型关系很⼤,背景属性主要有⼀下这些:
- 背景 (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
本地样式
- 除了“普通”样式外,对象还可以存储本地样式(私有样式)。
- 本地样式与普通样式类似,但它不能在其他对象之间共享。如果使用本地样式,将自动分配局部样式,并在删除对象时释放。本地样式对于向对象添加本地自定义很有用。
- 本地样式的接口函数是这样的格式:
lv_obj_set_style_<property_name>(obj, ...);
示例:
1 | lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffff), 0); // 设置背景色 |
- 删除本地样式的时候我们只需调用类似一个样式:
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体验:
- https://docs.lvgl.io/9.1/overview/style.html#transition
- http://lvgl.100ask.net/9.1/overview/style.html#transition
这部分内容将在后面的课程再展开讨论。
样式主题
主题是风格的集合。如果存在活动主题,LVGL将其应用于每个创建的部件(对象)。这将为UI提供一个默认外观,然后可以通过添加更多样式对其进行修改。
demo体验:
- https://docs.lvgl.io/9.1/overview/style.html#extending-the-current-theme
- http://lvgl.100ask.net/9.1/overview/style.html#extending-the-current-theme
基础对象事件
什么是事件?
当发生用户可能感兴趣的事情时,LVGL中会触发事件
并作出相应的处理(反馈),例如当一个对象:
- 被点击
- 滚动
- 数值改变
- 重绘
……
基于过程与事件驱动的交互:
使用事件
添加事件:
1
lv_obj_add_event_cb(obj, event_cb, event_code, user_data);
发送事件:
1
lv_obj_send_event(obj, event_code, param);
删除事件:
1
2lv_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)
更全面的信息请查询:
- 源码:
lvgl/src/misc/lv_event.h
(lv_event_code_t) - 开发文档:
事件回调函数的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
2lv_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
2lv_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 *
类型的指针
准备与重置定时器
1
2lv_timer_ready(timer); // 使计时器在下次调用 lv_timer_handler() 时运行
lv_timer_reset(timer); // 重置计时器的周期,在定义的周期过去后再次被调用设置定时器的参数
1
2
3lv_timer_set_cb(timer, new_cb); // 设置新的回调函数
lv_timer_set_period(timer, new_period); // 设置新的时间周期
lv_timer_set_user_data(timer, new_user_data); // 设置新的用户数据设置重复次数
使用lv_timer_set_repeat_count(timer, count)
接口让指定的定时器指定重复的次数。- 当定时器调用了定义的次数后,会自动被删除。将计数设置为 -1 表示无限重复。
启用和禁用定时器
1
lv_timer_enable(en); // 启用或禁用定时器
暂停和恢复
1
2lv_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
。
- 对于这些情况,可以使用