LVGL库入门教程02-基本控件与交互

LVGL 本质上是一个 GUI 库,它包含大量的控件(widget),即按钮、标签、滑块、菜单栏这种具有一定人机交互特征的组合图形。LVGL 在设计时,采用了一定面向对象编程的设计思路,有效降低了代码编写的难度。

LVGL 和大多数 GUI 库的工作方式都是类似的,其代码编写的基础思路为:

  • 创建 GUI 根窗体对象
  • 在窗体上绘制各种控件
  • 为控件编写响应函数函数
  • 在主事件循环中等待用户触发事件响应

如果之前有 GUI 库的使用经验的话,应该可以比较容易明白 LVGL 代码的编写思路。

标签

标签(label)应该是 GUI 最简单也是最基础的控件之一。标签的作用就是显示一小段说明文字。接下来通过介绍标签来介绍 LVGL 控件的创建、布局与设置属性。

标签的创建

通过以下函数可以创建一个标签:

lv_obj_t* lv_label_create(lv_obj_t* parent);

lv_obj_t 是 LVGL 所有控件的通用类型,包括根窗体在内的所有控件都使用该结构描述。

参数 parent 指定了标签需要被放在哪一个父容器中。由于一个较大的项目内会存在许多控件,因此往往需要将一个较大的窗口划分为若干结构,每一个结构放入用途相似的的控件,使用户更易熟悉如何操作。例如,一个文本编辑器窗口可能会按功能分为顶层菜单栏、侧边导航栏、底部状态栏以及中间的编辑区,每个区域的控件都可以安排在各栏内统一调整。

最基本的父容器就是整个显示屏窗口对象,可以使用 lv_scr_act() 函数获取当前的窗口对象。操作系统上的窗口可以设置一些属性,例如窗口大小、标题文字、图标等,不过嵌入式屏幕往往是固定的,因此窗口对象一般只作控件的父容器使用。

使用以下代码就可以在当前窗口中创建一个标签了:

lv_obj_t* label01 = lv_label_create(lv_scr_act());

创建得到的标签没有任何可显示的内容,可以调用 lv_label_set_text() 为标签添加上文字:

lv_label_set_text(label01, "Hello, world!");

这样就可以在屏幕中显示一些文本了。LVGL 支持直接显示 Unicode 文字,只要在源文件使用 UTF-8 编码即可。如果要显示变量的值,LVGL 也提供了 lv_label_set_text_fmt() 函数,可以直接格式化文本。

接下来编译工程并下载,就可以看到显示的效果了:

-

标签的布局

以上创建的标签默认放在屏幕的左上角,并且如果创建多个标签等控件,它们都会被重叠放置在左上角。如果需要将控件安排到合适的位置,就需要安排它们的布局。一般情况下,可以用以下函数重新调整一个控件的布局:

void lv_obj_align(lv_obj_t* obj, lv_align_t align, lv_coord_t x_ofs, lv_coord_t y_ofs);

align 指定了控件的对齐方式,可以检查枚举类型 lv_align_t 来获取支持的对齐方式。x_ofsy_ofs 是对齐后的额外偏移量,正值表示额外向右下偏移。

LVGL 包含了许多枚举类型,如果不知道该如何传值,可以查看头文件包含的枚举值。

和大多数 GUI 库一样,屏幕的左上角为坐标原点 (0, 0) ,往右为 x 轴正向,往下为 y 轴正向,坐标的单位为像素或分辨率。

例如,如果额外给以上标签添加对齐:

lv_obj_align(label01, LV_ALIGN_CENTER, 0, -30);

那么它就会出现在屏幕中间向上 30 像素的位置:

-

如果要创建更灵活的布局,可以使用 lv_obj_create() 创建一个基本对象。这种直接创建的基本对象一般用作框架,然后通过嵌套框架的形式组织对齐,例如:

/* outer widget align */
lv_obj_t* cont_top = lv_obj_create(lv_scr_act());
lv_obj_t* cont_bottom = lv_obj_create(lv_scr_act());
lv_obj_align(cont_top, LV_ALIGN_TOP_LEFT, 0, 0);
lv_obj_align(cont_bottom, LV_ALIGN_BOTTOM_RIGHT, 0, 0);
/* inner widget align */
lv_obj_t* label_top = lv_label_create(cont_top);
lv_label_set_text(label_top, "At Top Left");
lv_obj_align(label_top, LV_ALIGN_CENTER, 0, 0);
lv_obj_t* label_bottom = lv_label_create(cont_bottom);
lv_label_set_text(label_bottom, "At Bottom Right");
lv_obj_align(label_bottom, LV_ALIGN_CENTER, 0, 0);

这里先将外层的框架在屏幕上对齐,然后再在框内创建标签,让标签在框架内对齐。效果为:

!

通过这种嵌套的对齐方式,可以先让一些基础控件在框架内对齐,然后再让框架之间相对对齐。这种对齐方式更灵活,而且方便日后调整各个控件的相对位置。

LVGL 的所有控件都是以这种相对位置的形式组织的。官方文档提供了一张图片,可以很清楚地描述所有的相对对齐方式:

alt

由于居中对齐经常用到,可以直接使用 lv_obj_center(*obj*) 函数设置无偏移的居中对齐。

默认的基本控件是有样式的,并且注意到它们长宽都是固定的,如果包含的控件过长,它还会提供一个滚动条。如果需要调整控件的尺寸,可以使用函数,lv_obj_set_width()lv_obj_set_height() 分别调整长宽,或使用 lv_obj_set_size() 一并调整:

lv_obj_t* cont = lv_obj_create(lv_scr_act());
lv_obj_t* label = lv_label_create(cont);
lv_label_set_text(label, "Helllllo, world!");
lv_obj_set_size(cont, 160, 50);
lv_obj_center(cont);
lv_obj_center(label);
!

所有的控件都具有宽度和高度基本属性,因此这几个函数对任意的控件都有效。

标签的长模式和颜色调整

框架包含的控件过长会提供一个滚动条,确保包含的内容都可见。标签在创建时,它的宽度会适应包含文本的宽度。如果给一个标签重新调整尺寸,使得它的宽度小于文本的宽度,那么它包含的文本就会自动折叠:

lv_obj_t* label01 = lv_label_create(lv_scr_act());
lv_label_set_text(label01, "A very loooooooooooooooong text");
lv_obj_set_width(label01, 100);
!

如果文本确实过长,超过了标签的长宽极限,那么可以使用函数

void lv_label_set_long_mode(lv_obj_t * obj, lv_label_long_mode_t long_mode);

给标签设置一个长模式。标签一共有 5 种长模式,每种模式的表现形式如下:

枚举值 说明
LV_LABEL_LONG_WRAP 将过宽的文本换行,以多行的方式显示所有文本
LV_LABEL_LONG_DOT 将过长的文本隐藏并以省略号代替
LV_LABEL_LONG_SCROLL 将文本来回滚动显示
LV_LABEL_LONG_SCROLL_CIRCULAR 将文本循环滚动显示
LV_LABEL_LONG_CLIP 去除过长部分的文本

如果文本显示时有多行,那么可以使用

void lv_obj_set_style_text_align(lv_obj_t* obj, lv_text_align_t value, lv_style_selector_t selector);

将文本垂直对齐。第三个参数 selector 是设置样式用的,这里可以暂时不用理会。

以下动图展示了三种长模式:显示省略号、换行并居中对齐,以及循环滚动:

!

需要注意的是,除了滚动以外的其它模式如果没有明确高度,都会在文本过长时优先尝试调整标签高度。

滚动是一种特殊的动画,在后续介绍到动画时还可以创建更丰富的动画效果,可以自行调整文本的滚动行为。


标签的文本可以改变颜色。LVGL 里,调整颜色是通过特殊格式的文本作用的。为了改变颜色,首先需要启用这一模式:

lv_label_set_recolor(label01, true);

重新调整颜色的文本格式为:

#RRGGBB text#

这样 text 对应的文本就会显示为 #RRGGBB 对应的色值。如果屏幕使用的是 16bit 的颜色也不要紧,LVGL 会自动转换颜色。

例如:

lv_label_set_text(label01, "#0000ff Re-color# #ff00ff text# #ff0000 of a# label.");

显示效果为:

alt

按钮

按钮(button)也是一个比较基础的控件。按钮除了可以显示一些提示文字外,还可以点击并获取响应。接下来通过介绍按钮来介绍为控件绑定事件的一般方式。

按钮的创建和事件绑定

按钮的创建和布局方式都与标签类似:

lv_obj_t* btn01 = lv_btn_create(lv_scr_act());
lv_obj_align(btn01, LV_ALIGN_CENTER, 0, -40);

但是注意,创建得到的按钮只是一个简单的形状。为了给它添加说明文本,需要在其中创建一个标签:

lv_obj_t* label01 = lv_label_create(btn01);
lv_label_set_text(label01, "Button");
lv_obj_center(label01);

显示的效果为:

alt

按钮不同于框架,按钮会自动调整宽高来适应其包含的标签大小。

创建的按钮已经默认具有点击动画,不过还无法对点击作出回应。接下来需要给按钮添加回调函数。可以使用以下函数为按钮绑定回调函数:

lv_obj_add_event_cb(lv_obj_t* obj, lv_event_cb_t event_cb, lv_event_code_t filter, void* user_data);

任意可交互控件都可以使用该函数添加回调函数。这里不用管该函数的返回值。event_cb 是事件的回调函数,filter 决定按钮会对哪些事件作出响应,可以在 user_data 传入一些自定义的数据。

检查类型 lv_event_cb_t 的定义就可以明白如何编写回调函数。回调函数有且仅有一个 lv_event_t 类型的参数。该类型是一个比较复杂的结构类型,目前只需要明白它包括的结构成员包括自定义数据 user_data 即可。

例如,以下创建了一个简单的回调函数:

static void button_clicked_cb(lv_event_t* e) {
    static uint8_t count = 0;
    count++;
    lv_label_set_text_fmt((lv_obj_t*)e->user_data, "Clicked: %d", count);
}

这里通过自定义参数来修改外部标签的文本。那么在绑定时,就需要这样传入参数:

lv_obj_add_event_cb(btn01, button_simple_cb, LV_EVENT_CLICKED, label01);
alt

这里让按钮只对点击事件产生响应。如果要让按钮对多个事件响应的话,需要先让按钮对所有事件 LV_EVENT_ALL 产生响应的话,然后在回调函数内进一步判断事件类型:

lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
    /* ... event handler ... */
}

这就像在中断函数内判断中断源一样。

不过以上回调还可以使用另一种不传入用户参数的形式完成。首先,通过

lv_obj_t* lv_event_get_target(lv_event_t* e);

可以获取产生事件的控件,然后通过

lv_obj_t* lv_obj_get_child(const lv_obj_t* obj, int32_t id);

获取该控件的子控件。在创建控件时,需要传入父容器控件,创建时父容器也会通过 id 记录包含的子控件,创建最早的控件 id 就是 0 ,第二早的 id 是 1 ,最晚的 id 还可以表示为 -1 等。这样就可以在事件回调函数内获取被点击按钮的标签控件对象了。

控件的通用行为

LVGL 中,可以通过

void lv_obj_add_flag(lv_obj_t* obj, lv_obj_flag_t f);

为控件设置一些通用的标志,来改变控件的行为。

例如,以上按钮都是普遍的按钮,它们通过点击来触发响应。但是还有一部分按钮,像控制键是通过点击来切换启用/关闭状态的。那么此时就可以给按钮添加一个这样的标志:

lv_obj_t* btn02 = lv_btn_create(lv_scr_act());
lv_obj_add_flag(btn02, LV_OBJ_FLAG_CHECKABLE);

这样创建的按钮可以对 LV_EVENT_VALUE_CHANGED 这个特殊的事件响应,而普通的按钮不行。不仅如此,切换之后的部分样式也会发生改变:

alt

可以给一个控件添加多个标志,只需要使用按位或运算符 | 连接起来即可。还可以清除一个控件的标志。例如,如果给一个框架清除可滚动的标志,那么当它包含长文本时就不再可以滚动显示全部内容:

lv_obj_t* cont = lv_obj_create(lv_scr_act());
lv_obj_t* label = lv_label_create(cont);
lv_obj_clear_flag(cont, LV_OBJ_FLAG_SCROLLABLE);
lv_label_set_text(label, "A label contains very long text");
lv_obj_set_size(cont, 160, 50);

效果为:

alt

标志是一个很重要的内容,通过为控件加上各种标志,可以自定义更多抽象的控件类型。例如,具有 LV_OBJ_FLAG_CLICKABLE 标志的控件可以响应点击事件,这种响应不仅包括回调函数,还关系着点击时的动画效果。LVGL 一共提供了 27 个独立的标志,其中有 8 个可供用户自定义。可以检查 lv_obj_flag_t 枚举定义来查看包含的所有标志位。

开关

开关的创建

以上创建的通过点击来切换启用/关闭状态的按钮可以使用开关(switch)代替。创建开关和创建其它控件类似:

lv_obj_t* sw = lv_switch_create(lv_scr_act());

开关的效果如下,通过单击可以切换开关状态:

alt

开关具有标志 LV_OBJ_FLAG_CHECKABLE ,因此可以响应事件 LV_EVENT_VALUE_CHANGED

开关的状态

一个控件可以具有多种标志,标志就是控件的抽象接口,决定了控件具有哪些行为。控件还具有多种不同的状态,在每种状态下,它的样式都是不一样的。可以通过

void lv_obj_add_state(lv_obj_t* obj, lv_state_t state);

给一个控件设置不同的状态来切换样式。例如,如果给开关设置状态 LV_STATE_CHECKED ,它会表现出打开的状态。不同状态下控件接收的响应也不一样,例如如果给开关加上 LV_STATE_DISABLED 的状态,点击时它就无法接收任何响应,连样式也不会再切换了。

可以在响应函数内通过 lv_obj_has_state(obj, state) 来判断一个控件处于什么状态,从而决定执行什么样的代码。这种方式更贴合控件的行为。

每个控件都有 9 种独立的状态,还有 4 种状态可以由用户自由定义,这些状态都被放在头文件 lv_obj.h 中。可以使用按位与运算符 | 给一个控件添加多个状态。例如,可以给一个开关设置为既开启又只读 LV_STATE_CHECKED | LV_STATE_DISABLED ,那么它的样式就会表现为:

alt

状态是在标志之上的概念,在不同的状态下控件可能具有不同的标志。

基本交互控件

下拉列表

下拉列表(drop-down list)也是一个非常简单的控件。下拉列表在点击后会出现一些选项,点击选择后就可以触发一些事件。

可以通过 lv_dropdown_set_options() 为下拉列表创建列表项:

lv_obj_t* drop01 = lv_dropdown_create(lv_scr_act());
lv_dropdown_set_options(drop01, "STM32F1\n"
                                "STM32F4\n"
                                "STM32H7\n"
                                "STM8");

LVGL 会自动拆分多行本文的每一行并分别创建一个列表项。下拉列表默认的行为是展示第一个列表项,并通过用户选择来切换展示的列表项:

alt

下拉列表在选择列表项时会触发 LV_EVENT_VALUE_CHANGED 事件,可以通过

uint16_t lv_dropdown_get_selected(const lv_obj_t* obj);
void lv_dropdown_get_selected_str(const lv_obj_t* obj, char* buf, uint32_t buf_size);

来获取当前选中列表项索引或文本,如果要获取文本的话需要自行准备一个文本缓冲区。

下拉列表可以通过

void lv_dropdown_set_text(lv_obj_t* obj, const char* txt)

给它设置一个固定的文本,这样的下拉列表可以充当下拉菜单使用。

下拉列表还可以通过

void lv_dropdown_set_dir(lv_obj_t* obj, lv_dir_t dir);
void lv_dropdown_set_symbol(lv_obj_t* obj, const void* symbol);

修改列表项出现的位置和下拉列表右侧的符号,由此可以组合出上拉列表、左拉列表等。

滚动列表

滚动列表(roller)和下拉列表类似,不过它是通过滚动来切换选择的列表项的。

滚动列表的创建、事件响应和获取选中值的方式都和下拉列表类似。以下是滚动列表的创建方式:

lv_obj_t* roller01 = lv_roller_create(lv_scr_act());
lv_roller_set_options(roller01,
                      "Monday\nTuesday\nWednesday\n"
                      "Thursday\nFriday\nSaturday\nSunday",
                      LV_ROLLER_MODE_INFINITE);
alt

在设置列表项时滚动列表多了一个参数,代表滚动到底后需要停止还是循环往复。滚动列表非常适合用于列表项稍微有些多,没有足够的空间展示所有列表项的情况。因此,滚动列表还可以使用函数

void lv_roller_set_visible_row_count(lv_obj_t *obj, uint8_t row_cnt);

设置可见的列表项个数。如果设置为偶数,那么会有两个列表项只显示一半,就像动图中展示的一样。

参考资料/延伸阅读

https://docs.lvgl.io/master/widgets/index.html

LVGL 官方文档——控件。在此可以查看更多文中没有提到的控件类型和使用细节,并查看官方编写的示例代码。

京ICP备2021034974号
contact me by hello@frozencandles.fun