LVGL 嵌入式图形库入门指南 目录一、LVGL 介绍二、源码获取三、启动流程与任务管理四、初步理解 LVGL 对象五、对象的盒子模型六、对象的样式七、对象的事件八、定时器入门一、LVGL 介绍LVGLLight 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 中的定时器是非抢占式的。也就是说一旦一个定时器任务开始执行它会运行直到函数退出期间不会被其他定时器任务中断。所以我们要确保注册的定时器任务不会运行太长时间否则会影响整个系统的响应性。任务管理的理解假设一位职场妈妈需要同时解决 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 的原点位置在左上角。设置位置设置 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_ANY或LV_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_CLICKEDuser_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。