目录

一、LVGL 介绍

LVGL(Light and Versatile Graphics Library)是一个免费、开源的嵌入式图形库,专门用于在资源受限的微控制器(MCU)和微处理器(MPU)上创建现代、美观的用户界面(GUI)。

官网:https://lvgl.io

二、源码获取

LVGL 是一个开源项目,因此获取 LVGL 的源码非常简单,可以在 LVGL 的 GitHub 源码仓库获取源码,地址:GitHub - lvgl/lvgl: LVGL is a free, full-featured embedded UI library for devices from small MCUs to 3D-capable MPUs, enhanced by LVGL Pro, a professional editor and tooling. · GitHub

三、启动流程与任务管理

LVGL启动流程图

LVGL 提供了丰富的功能,包括界面元素的创建、布局和渲染等。LVGL 的定时器处理程序是其内部的一个重要组件,负责管理并调度各种任务,以确保界面的流畅运行和响应性能。可以说 LVGL 是由定时器处理程序驱动起来的,因此,理解了 LVGL 的定时器处理程序我们就可以把握住 LVGL 的内部运作机制。

LVGL 的定时器处理程序是一个内置的任务系统,允许开发者注册函数,使其可以定期被调用,它帮助管理时间敏感的操作(毫秒级别)和周期性的活动。

要注意一点,LVGL 中的定时器是非抢占式的。也就是说一旦一个定时器任务开始执行,它会运行直到函数退出,期间不会被其他定时器任务中断。所以我们要确保注册的定时器任务不会运行太长时间,否则会影响整个系统的响应性。

任务管理的理解

假设一位职场妈妈需要同时解决 2 个问题:给小孩喂饭、回复工作信息,场景如图所示:

任务管理示意图

这个妈妈的状态就像是一个典型的 C 语言轮询程序:

void main()
{
    while (1)
    {
        喂一口饭();
        回一个信息();
    }
}

在 main 函数中是一个 while 循环,里面依次调用 2 个函数,这两个函数相互之间有影响:如果喂一口饭太花时间,就会导致迟迟无法回一个信息;如果回一个信息太花时间,就会导致迟迟无法喂下一口饭。使用轮询模式编写程序看起来很简单,但是要求 while 循环里调用到的函数要执行得非常快,否则会影响到其他函数的执行。

LVGL 的任务管理器本质上和上述的情况差不多,只不过 LVGL 的任务管理器是时间观念非常强,也就是说假如这位妈妈的时间观念非常强,她会这样处理:

void main()
{
    制定两个时间规划:每隔 1 分钟喂一口饭;每隔 1 分钟回复一次信息
    while (1)
    {
        看时钟判断:对比上次喂饭的时间,到了该喂饭的时间了
        喂一口饭();
        记下这次喂饭的时间
        看时钟判断:对比上次回信息的时间,到了该回信息的时间了
        回一个信息();
        记下这次回信息的时间
    }
}

这样这位妈妈会根据时间办事:先看一下时钟,如果现在的时间和上一次操作的时间比较,到了该进行处理的时间,就进行处理,然后记录这次处理的时间,下次再进行对比。

四、初步理解 LVGL 对象

1. 对象概念

在 LVGL 中,用户界面的基本组成部分是对象(控件),也称为 Widgets。例如,一个按钮、标签、图像、列表、图表或者文本区域。

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

2. 基础对象

基础对象(控件)实现了屏幕上控件的基本属性。

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

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

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

3. 对象的大小

设置大小

设置宽度:lv_obj_set_width(obj, new_width);
设置高度:lv_obj_set_height(obj, new_height);
同时设置宽度、高度:lv_obj_set_size(obj, new_width, new_height);

获取大小

获取实际宽度: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);

4. 对象的位置

LVGL 屏幕的坐标系和我们熟悉的坐标系不一样,LVGL 的坐标系是我们一般称之为 "LCD 坐标系",它的原点位置和直角坐标系的不一样(坐标原点在左上角,水平向右为 x 轴正方向,竖直向下为 y 轴正方向)。其实我们只需要记住一点:LVGL 的原点位置在左上角。

LVGL坐标系示意图

设置位置

设置 x 轴方向的坐标位置:lv_obj_set_x(obj, new_x);
设置 y 轴方向的坐标位置:lv_obj_set_y(obj, new_y);
同时设置 x、y 坐标位置:lv_obj_set_pos(obj, new_x, new_y);

设置对齐

参照父对象对齐:lv_obj_set_align(obj, LV_ALIGN_...);
参照父对象对齐后再设置坐标位置:lv_obj_align(obj, LV_ALIGN_..., x, y);
参照另一个对象(无父子关系)对齐后设置坐标位置:lv_obj_align_to(obj_to_align, obj_reference, LV_ALIGN_..., x, y)

对齐类型:

对齐类型示意图

获取位置

获取 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 键让一个选项或者一个图片获得焦点的时候,这个元素就会多了一个轮廓框围绕。
  • 边框(border):边框有大小和颜色等属性(相当于盒子的厚度和它的颜色)。
  • 填充(padding):对象两侧与其子对象之间的空间(盒子的填充物)。
  • 内容(content):如果边界框按边框宽度和填充的大小缩小,则显示其大小的内容区域(盒子实际装东西的区域)。

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

盒子模型示意图

六、对象的样式

在 LVGL 中,styles 用于设置对象的外观:

  • 样式是一个 lv_style_t 变量,它可以保存边框宽度、文本颜色等属性。
  • 将样式(变量)分配给对象就可以改变其外观。在赋值过程中,可以指定目标部分和目标状态。
  • 一个样式可以给多个对象使用(正常样式)。
  • 样式可以级联,也就是可以将多个样式分配给一个对象。所以,我们不用将所有属性都在一个样式中指定,可以通过多个样式组合的形式指定。LVGL 会优先使用我们定义的样式,如果没有就会使用默认值。
  • 后来添加的样式具有更高的优先级。也就是说如果在两种样式中指定了同一个属性,则将使用最后添加的样式。
  • 如果对象中未指定某些属性(例如文本颜色),就会从父级继承。
  • 上面说的是 "正常" 样式,对象还有本地样式,它比 "正常" 样式具有更高的优先级。
  • 可以定义有过渡效果的样式。
  • 默认有一个样式主题,我们也可以自己定义样式主题,作为默认的样式主题使用。

使用

初始化样式

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

static lv_style_t style_obj;
lv_style_init(&style_obj);
设置样式属性

当我们初始化好一个样式之后就可以设置它的样式属性了,接口函数是这样的格式:

lv_style_set_<property_name>(&style, <value>);
lv_style_set_bg_color(&style_obj, lv_color_hex(0x000000)); // 设置背景色
lv_style_set_bg_opa(&style_obj, LV_OPA_50); // 设置背景透明度
lv_style_set_....
添加(应用)样式到对象

当我们初始化并且设置好一个样式之后就可以将它添加到对象上面了,接口函数只有一个:

lv_obj_add_style(obj, &style, <selector>)

参数 "obj" 就是要添加到的对象,"style" 是指向样式变量的指针,selector 是应添加样式的部分和状态的 OR-ed 值(不能是互斥,否则就是清除标志,没法合并)。示例:

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

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

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

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

lv_color_t color = lv_obj_get_style_bg_color(obj, LV_PART_MAIN)
删除样式

删除对象的所有样式:

lv_obj_remove_style_all(obj);

删除对象的特定样式:

lv_obj_remove_style(obj, &style_obj, selector);

只有当 selector 与 lv_obj_add_style 中使用的 selector 匹配时,此函数才会删除 style。

如果 style 是空,那么会根据给出的 selector 检查并删除所有匹配的样式。

如果 selector 是 LV_STATE_ANYLV_PART_ANY 就会删除具有任何状态或部分的样式。下面这个效果和 lv_obj_remove_style_all 的效果是一样的:

lv_obj_remove_style(obj, NULL, LV_STATE_ANY | LV_PART_ANY);
样式的状态和部分

状态:

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) 自定义状态

部分:

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 可以从这里添加自定义部件。
本地样式

除了 "普通" 样式外,对象还可以存储本地样式(私有样式)。

本地样式与普通样式类似,但是它不能在其他对象之间共享。如果使用本地样式,将自动分配局部样式,并在删除对象时释放。本地样式对于向对象添加本地自定义很有用。

本地样式的接口函数是这样的格式:lv_obj_set_style_<property_name>(obj, value, selector);

lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffff), 0); // 设置背景色
lv_obj_set_style_bg_opa(obj, LV_OPA_50, 0); // 设置背景透明度

删除本地样式

删除本地样式的时候我们只删除某⼀个样式:

lv_obj_remove_local_style_prop(obj, LV_STYLE_..., selector);
样式继承

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

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

七、对象的事件

在 LVGL 中,事件是对象响应用户交互或系统状态变化的重要机制。事件允许我们在特定动作发生时执行自定义代码。

事件处理示意图

1. 事件类型

LVGL 提供了多种事件类型,常见的事件包括:

  • LV_EVENT_PRESSED:对象被按下
  • LV_EVENT_RELEASED:对象被释放
  • LV_EVENT_CLICKED:对象被点击
  • LV_EVENT_VALUE_CHANGED:对象的值发生变化
  • LV_EVENT_FOCUSED:对象获得焦点
  • LV_EVENT_DEFOCUSED:对象失去焦点
  • LV_EVENT_DELETE:对象被删除
  • LV_EVENT_DRAW_MAIN:对象主部分绘制
  • LV_EVENT_DRAW_PART_BEGIN:对象部分绘制开始
  • LV_EVENT_DRAW_PART_END:对象部分绘制结束

2. 使用事件

添加事件
lv_obj_add_event_cb(obj, event_cb, event_code, user_data);

参数说明:

  • obj:要添加事件的对象
  • event_cb:事件回调函数
  • event_code:事件类型(如 LV_EVENT_CLICKED)
  • user_data:用户自定义数据,会传递给回调函数
发送事件
lv_obj_send_event(obj, event_code, param);
删除事件
lv_obj_remove_event_cb(obj, event_cb);
lv_obj_remove_event_dsc(obj, event_dsc); // event_dsc 是 lv_obj_add_event_cb 返回的指针

3. 事件回调函数

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

获取触发的事件代码:
lv_event_code_t code = lv_event_get_code(e);
获取触发事件的对象:
lv_obj_t * target = lv_event_get_target(e);
获取最初触发事件的对象(事件冒泡):
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 函数传递的用户数据
事件与对象之间的关系

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

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

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

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

其他

如果传入的用户数据不一样,一个对象可以绑定同一个事件回调函数多次,事件将按照添加的顺序调用。例如:

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 的定时器就是会按照指定周期(单位:毫秒 ms)执行的函数。

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

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

1. 使用定时器

1. 创建定时器:

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. 准备与重置定时器

  • lv_timer_ready(timer):使计时器在下一次调用 lv_timer_handler() 时运行。
  • lv_timer_reset(timer):重置计时器的周期。在定义的毫秒周期过去后,它将再次被调用。

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. 启用和禁用定时器

使用 lv_timer_enable(en) 来启用或禁用定时器。

6. 暂停和恢复

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
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐