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_ofs
和 y_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 的所有控件都是以这种相对位置的形式组织的。官方文档提供了一张图片,可以很清楚地描述所有的相对对齐方式:
由于居中对齐经常用到,可以直接使用 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.");
显示效果为:
按钮
按钮(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);
显示的效果为:
按钮不同于框架,按钮会自动调整宽高来适应其包含的标签大小。
创建的按钮已经默认具有点击动画,不过还无法对点击作出回应。接下来需要给按钮添加回调函数。可以使用以下函数为按钮绑定回调函数:
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);
这里让按钮只对点击事件产生响应。如果要让按钮对多个事件响应的话,需要先让按钮对所有事件 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
这个特殊的事件响应,而普通的按钮不行。不仅如此,切换之后的部分样式也会发生改变:
可以给一个控件添加多个标志,只需要使用按位或运算符 |
连接起来即可。还可以清除一个控件的标志。例如,如果给一个框架清除可滚动的标志,那么当它包含长文本时就不再可以滚动显示全部内容:
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);
效果为:
标志是一个很重要的内容,通过为控件加上各种标志,可以自定义更多抽象的控件类型。例如,具有 LV_OBJ_FLAG_CLICKABLE
标志的控件可以响应点击事件,这种响应不仅包括回调函数,还关系着点击时的动画效果。LVGL 一共提供了 27 个独立的标志,其中有 8 个可供用户自定义。可以检查 lv_obj_flag_t
枚举定义来查看包含的所有标志位。
开关
开关的创建
以上创建的通过点击来切换启用/关闭状态的按钮可以使用开关(switch)代替。创建开关和创建其它控件类似:
lv_obj_t* sw = lv_switch_create(lv_scr_act());
开关的效果如下,通过单击可以切换开关状态:
开关具有标志 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
,那么它的样式就会表现为:
状态是在标志之上的概念,在不同的状态下控件可能具有不同的标志。
基本交互控件
下拉列表
下拉列表(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 会自动拆分多行本文的每一行并分别创建一个列表项。下拉列表默认的行为是展示第一个列表项,并通过用户选择来切换展示的列表项:
下拉列表在选择列表项时会触发 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);
在设置列表项时滚动列表多了一个参数,代表滚动到底后需要停止还是循环往复。滚动列表非常适合用于列表项稍微有些多,没有足够的空间展示所有列表项的情况。因此,滚动列表还可以使用函数
void lv_roller_set_visible_row_count(lv_obj_t *obj, uint8_t row_cnt);
设置可见的列表项个数。如果设置为偶数,那么会有两个列表项只显示一半,就像动图中展示的一样。
参考资料/延伸阅读
https://docs.lvgl.io/master/widgets/index.html
LVGL 官方文档——控件。在此可以查看更多文中没有提到的控件类型和使用细节,并查看官方编写的示例代码。