草庐IT

LVGL库入门教程03-布局方式

frozencandles 2023-03-28 原文

LVGL布局方式

LVGL的布局

上一节介绍了如何在 LVGL 中创建控件。如果在创建控件时不给控件安排布局,那么控件默认会被放在父容器的左上角。

可以使用 lv_obj_set_pos(obj, x, y) 调整一个控件的位置(或者使用类似的函数单独调整一个方向的坐标),将它放在相对父容器左上角的合适位置。不过这种布局方式非常死板,因为绝对坐标一旦设定就不能自动调整;而且当控件数量较多时,也很难确定合适的坐标值。

上一节介绍过,可以使用 lv_obj_align(obj, align, x_ofs, y_ofs) 设置一个控件相对父容器的对齐,并用以下图片展示所有的对齐方式:

从图片中可以看到,控件之间不仅可以内对齐,也可以外对齐。如果两个控件间没有包含关系也不要紧,可以使用 lv_obj_align_to(obj, base, align, x_ofs, y_ofs); 设置两个控件的相对对齐方式。

这种对齐的方式对于控件不多的情况下来说是足够了,但是有些时候需要对很多并列的控件布局(例如,一个计算机界面的所有按钮)。这个时候常规的对齐方式就难以满足需求了。

因此,LVGL 提供了两种更复杂的布局方式:

  • flex(弹性盒子)
  • grid(网格)

这两种布局和 CSS3 新增的 flex 布局和 grid 布局比较相似,如果熟悉 CSS 的话对它们应该不会陌生。

flex布局

flex 是一个实验性质的布局,首先需要确定已经在 lv_conf.h 大约 588 行的位置启用了 flex 布局:

/*A layout similar to Flexbox in CSS.*/
#define LV_USE_FLEX 1

后续介绍的 grid 布局也是如此。

创建flex布局

如果不添加任何布局方式,那么所有的控件都会堆放在左上角。flex 布局可以将一些控件按行或列均匀布局,并且可以自动调整它们的间距。

可以给一个容器设置一个 flex-flow 属性,这样容器就可以使用 flex 布局方式:

lv_obj_t* cont = lv_obj_create(lv_scr_act());
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_ROW);

对于设置了 flex 布局的容器,在其中创建的元素都会在一个坐标轴上均匀排布。例如,以下使用 for 循环创建多个控件:

lv_obj_set_size(cont, 300, 75);
for (uint8_t i = 0; i < 9; i++) {
    lv_obj_t* btn = lv_btn_create(cont);
    lv_obj_t* label = lv_label_create(btn);
    lv_label_set_text_fmt(label, "%d", i + 1);
}

效果为:

尽管没有设置按钮的位置,但是每一个按钮都会在水平位置上均匀排布。如果要让排布时不超过父容器的最大宽度,可以使用 LV_FLEX_FLOW_ROW_WRAP 折行。

也可以使用按列的方式排布控件。可以通过 lv_flex_flow_t 枚举类型检查更多的 flex 布局形式。

flex布局的对齐

以上 flex 布局中,各控件的尺寸和间距都是固定的,并且第一个控件依然会出现在左上角。如果

可以使用

void lv_obj_set_flex_align(lv_obj_t * obj, 
                           lv_flex_align_t main_place, 
                           lv_flex_align_t cross_place,
                           lv_flex_align_t track_place);

设置 flex 布局的对齐方式。该函数一次性会设置三个方面的对齐:

  • main_place :设置行或列的对齐
  • cross_place :设置控件在一行或一列内的对齐(当控件高度或宽度不一致时就可以看出效果)
  • track_place :flex-flow 方向上的对齐

如果接触过 CSS 的话,可以明白这些对齐方式实际上就是 CSS 里的 justify-contentalign-itemsalign-content

例如,以下调用

lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);

创建的每个控件之间在水平方向上均匀对齐、行内上下居中对齐,并作为一个整体上下居中对齐,效果为:

又如,以下调用:

lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);

创建的每个控件之间在水平方向上两端对齐、行内顶端对齐,并作为一个整体顶端对齐,效果为:

flex 布局还可以通过

void lv_obj_set_flex_grow(lv_obj_t *obj, uint8_t grow);

动态调整各个控件的相对宽度,实现更灵活的布局规则。例如,以下代码在一个 flex-flow 框架内创建了 4 个按钮,并将第二个按钮的相对宽度设置为其它按钮的两倍:

for (uint8_t i = 0; i < 4; i++) {
    lv_obj_t* btn = lv_btn_create(cont);
    lv_obj_t* label = lv_label_create(btn);
    lv_label_set_text_fmt(label, "%d", i);
    if (i == 1)
        lv_obj_set_flex_grow(btn, 2);
    else
        lv_obj_set_flex_grow(btn, 1);
}

效果为:

以下利用相对宽度创建了一个更复杂的类似数字输入键盘的布局规则:

lv_obj_t* cont = lv_obj_create(lv_scr_act());
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_size(cont, 160, 180);
lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_style_base_dir(cont, LV_BASE_DIR_RTL, 0);
for (int8_t i = 9; i >= 0; i--) {
    lv_obj_t* btn = lv_btn_create(cont);
    lv_obj_t* label = lv_label_create(btn);
    lv_label_set_text_fmt(label, "%d", i);
}
lv_obj_t* btn = lv_btn_create(cont);
lv_obj_set_flex_grow(btn, 2);
lv_obj_t* label = lv_label_create(btn);
lv_label_set_text(label, "OK");

效果为:

这里使用 lv_obj_set_style_base_dir() 函数设置从右向左的书写方式,因此滚动条才会出现在左侧。后续介绍样式时还会介绍更多类似函数。

一般情况下 flex-grow 和带 wrap 的 flex-flow 是冲突的,也就是说所有设置了 flex-grow 的控件都会在同一行布局,但它们的宽度可能变得很窄。因此,以上的各个数字按钮相对宽度并不一致。

使用这种布局创建键盘非常别扭,不过好在 LVGL 提供了另一种形式的布局:grid 。

grid布局

创建grid布局

grid 布局是一种网格形式的布局,可以按行或列来对齐控件。

为了创建网格布局,首先要给出格子的长度和宽度。一般来说,可以通过两个数组分别描述网格每一行的宽度和每一列的宽度:

static lv_coord_t col_size[] = { 60, 60, 90, LV_GRID_TEMPLATE_LAST };
static lv_coord_t row_size[] = { 40, 40, 30, LV_GRID_TEMPLATE_LAST };

每一个数组都需要以 LV_GRID_TEMPLATE_LAST 结尾。然后就可以通过

void lv_obj_set_grid_dsc_array(lv_obj_t *obj, const lv_coord_t col_dsc[], const lv_coord_t row_dsc[])

函数为一个容器设置网格划分。

注意,创建的数组一定要声明为 static 或全局变量,因为这部分数据在后续渲染时才会被用上。

划分好了网格以后,接下来就可以使用以下函数:

void lv_obj_set_grid_cell(lv_obj_t * obj, 
    lv_grid_align_t x_align, uint8_t col_pos, uint8_t col_span,
    lv_grid_align_t y_align, uint8_t row_pos, uint8_t row_span);

将每一个控件摆放在合适的网格位置。align 指定每一个放置在网格上的控件相对格线的对齐;pos 指定控件放置在哪个格子里,最左上角的格子位置为 (0, 0) ;有的控件可能占据不止一个格子的位置,那么就需要使用 span 来跨越多格。

例如,以下代码:

for (uint8_t i = 0; i < 9; i++) {
    uint8_t col = i % 3;
    uint8_t row = i / 3;
    lv_obj_t* btn = lv_btn_create(cont);
    lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, col, 1,
                              LV_GRID_ALIGN_STRETCH, row, 1);
    lv_obj_t* label = lv_label_create(btn);
    lv_label_set_text_fmt(label, "r%d c%d", row, col);
    lv_obj_center(label);
}

得到的网格为:

这里使用 LV_GRID_ALIGN_STRETCH 让网格内的控件尺寸伸展至网格大小,使网格布局的特点更加明显。

grid布局的对齐

使用网格布局时,每个格子内的控件在创建时都可以在网格内对齐。除此之外,还可以设置网格自身的对齐方式:

void lv_obj_set_grid_align(lv_obj_t * obj, lv_grid_align_t column_align, lv_grid_align_t row_align);

网格在横向和竖向对齐摆放时,对齐方式都类似于 flex ,因此可以认为 grid 是一种二维的 flex 布局。

例如,如果略微修改以上代码,添加如下语句:

lv_obj_set_grid_align(cont, LV_GRID_ALIGN_SPACE_BETWEEN, LV_GRID_ALIGN_END);
for (uint8_t i = 0; i < 9; i++) {
    /* ... */
    lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_START, col, 1,
                              LV_GRID_ALIGN_START, row, 1);
    /* ... */
}

这里去除了控件尺寸的伸展,使网格的对齐特点更明显:


网格也可以使用相对大小,具体做法是利用 LV_GRID_FR(x) 宏计算相对宽度。例如,以下定义了一个这样的宽度数组:

static lv_coord_t col_pos[] = { LV_GRID_FR(1), 60, LV_GRID_FR(2), LV_GRID_TEMPLATE_LAST };

那么第二列的宽度是绝对宽度 60 ,剩余的宽度被划分为 3 份:第一列占一份,第三列占 2 份。这种形式创建的网格可以适应容器的尺寸大小:

组合控件

复选框

复选框(ckeckbox)是一种类似开关,但是带有标签的控件。可以使用以下代码创建复选框并设置标签文本:

lv_obj_t* check = lv_checkbox_create(cont);
lv_checkbox_set_text(check, "Use DMA");

一般用复选框并列表示一些“是/否”的选项,因此多个并列的复选项很适合使用 flex 布局表现。复选框可以通过状态 LV_STATE_CHECKED 检查是否被勾选。

LVGL 中没有提供单选按钮(radio button)这一控件,不过可以使用复选框表示单选按钮。单选按钮在同一时间内只有且必须有一个选择框被选中。首先创建一个框架并使用列模式的 flex 布局:

lv_obj_t* cont = lv_obj_create(lv_scr_act());
lv_obj_set_size(cont, 140, 200);
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER);

然后可以在其中创建一些复选框:

#define CHECKBOX_ITEMS 4
char* checkbox_labels[CHECKBOX_ITEMS] = {
    "Use parity bits", "Use stop bit", "Auto send", "Debug mode" };
for (uint8_t i = 0; i < CHECKBOX_ITEMS; i++) {
    lv_obj_t* check = lv_checkbox_create(cont);
    lv_checkbox_set_text(check, checkbox_labels[i]);
}

为了实现单选按钮的效果,需要在点击事件中清除上一个被选中的选择框。这里介绍一个技巧如何获取事件控件的父容器。如果一个控件被设置了冒泡事件标志 LV_OBJ_FLAG_EVENT_BUBBLE ,那么该控件被点击时,事件将会由它的父容器触发(如果父容器也设置了这一标志位,那么事件还会继续向上冒泡)。

可以通过

lv_obj_t* lv_event_get_current_target(lv_event_t* e);

获取最终触发真正送出事件的控件(也就是冒泡后的父控件),而之前介绍的 lv_event_get_target() 函数则获取的是最先触发事件的控件(也就是子控件)。这样通过设置合适的冒泡层数,就可以同时获取控件与它的父容器了。

了解了这一特性后,就可以编写合适的代码了。首先定义一个全局变量 checked_index 记录单选按钮组此刻选中的按钮索引号,并作为用户数据传给回调函数中:

static uint8_t checked_index = 0;
/* ... */
lv_obj_add_event_cb(cont, radio_checked_cb, LV_EVENT_CLICKED, &checked_index);
for (uint8_t i = 0; i < CHECKBOX_ITEMS; i++) {
    /* ... */
    lv_obj_add_flag(check, LV_OBJ_FLAG_EVENT_BUBBLE);
}

由于事件最终由父容器触发,因此要给父容器提供回调函数。然后,在回调函数中通过父容器与索引值取消上一个被点击的选择框选择,选择点击的选择框并更新索引值:

static void radio_checked_cb(lv_event_t* e) {
    uint8_t* post_checked_index = lv_event_get_user_data(e);
    lv_obj_t* target = lv_event_get_target(e);
    lv_obj_t* parent = lv_event_get_current_target(e);
    if (target == parent) 
        return;
    lv_obj_clear_state(lv_obj_get_child(parent, *post_checked_index), LV_STATE_CHECKED);
    lv_obj_add_state(target, LV_STATE_CHECKED);
    *post_checked_index = lv_obj_get_index(target);
}

由于父容器也拥有点击事件,因此首先要判断事件是否是由选择框触发的。这种事件处理方式非常简洁高效,而且无需定义额外的辅助数组。

这样就可以使用复选框代替单选按钮了,并且这样的回调函数是可以复用的,如果有另一组单选按钮也可以使用类似的方式提供响应:

列表

LVGL 的列表(list)表现形式更像大多数界面提供的标题栏菜单。这里先介绍列表仅仅是因为它比较简单。列表的核心函数只有 3 个:

lv_obj_t *lv_list_create(lv_obj_t *parent);
lv_obj_t *lv_list_add_text(lv_obj_t *list, const char *txt);
lv_obj_t *lv_list_add_btn(lv_obj_t *list, const void *icon, const char *txt);

以下应用这三个函数创建一个列表:

lv_obj_t* list = lv_list_create(lv_scr_act());
lv_list_add_text(list, "group1");
for (uint8_t i = 0; i < 2; i++)
    lv_list_add_btn(list, NULL, "item");
lv_list_add_text(list, "group2");
for (uint8_t i = 0; i < 3; i++)
    lv_list_add_btn(list, NULL, "item");

效果为:

默认创建的列表尺寸较大,可以手动调整尺寸大小。

列表中的按钮和一般创建的按钮没有区别,可以给返回值提供回调函数。按钮在创建时还可以指定按钮的图标,图标的本质就是 Unicode 中的特殊符号,在 lvgl/src/font/lv_symbol_def.h 中可以查看提供的特殊符号。

首发于:http://frozencandles.fun/archives/342

参考资料/延伸阅读

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

官方文档——布局部分。

有关LVGL库入门教程03-布局方式的更多相关文章

  1. ruby - 如何以所有可能的方式将字符串拆分为长度最多为 3 的连续子字符串? - 2

    我试图获取一个长度在1到10之间的字符串,并输出将字符串分解为大小为1、2或3的连续子字符串的所有可能方式。例如:输入:123456将整数分割成单个字符,然后继续查找组合。该代码将返回以下所有数组。[1,2,3,4,5,6][12,3,4,5,6][1,23,4,5,6][1,2,34,5,6][1,2,3,45,6][1,2,3,4,56][12,34,5,6][12,3,45,6][12,3,4,56][1,23,45,6][1,2,34,56][1,23,4,56][12,34,56][123,4,5,6][1,234,5,6][1,2,345,6][1,2,3,456][123

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby-on-rails - 正确的 Rails 2.1 做事方式 - 2

    question的一些答案关于redirect_to让我想到了其他一些问题。基本上,我正在使用Rails2.1编写博客应用程序。我一直在尝试自己完成大部分工作(因为我对Rails有所了解),但在需要时会引用Internet上的教程和引用资料。我设法让一个简单的博客正常运行,然后我尝试添加评论。靠我自己,我设法让它进入了可以从script/console添加评论的阶段,但我无法让表单正常工作。我遵循的其中一个教程建议在帖子Controller中创建一个“评论”操作,以添加评论。我的问题是:这是“标准”方式吗?我的另一个问题的答案之一似乎暗示应该有一个CommentsController参

  4. ruby - nanoc 和多种布局 - 2

    是否可以为特定(或所有)项目使用多个布局?例如,我有几个项目,我想对其应用两种不同的布局。一个是绿色的,一个是蓝色的(但是)。我想将它们编译到我的输出目录中的两个不同文件夹中(例如v1和v2)。我一直在玩弄规则和编译block,但我不知道这是怎么回事。因为,每个项目在编译过程中只编译一次,我不能告诉nanoc第一次用layout1编译,第二次用layout2编译。我试过这样的东西,但它导致输出文件损坏。compile'*'doifitem.binary?#don’tfilterbinaryitemselsefilter:erblayout'layout1'layout'layout2'

  5. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  6. postman接口测试工具-基础使用教程 - 2

    1.postman介绍Postman一款非常流行的API调试工具。其实,开发人员用的更多。因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。2.下载安装官网地址:https://www.postman.com/下载完成后双击安装吧,安装过程极其简单,无需任何操作3.使用教程这里以百度为例,工具使用简单,填写URL地址即可发送请求,在下方查看响应结果和响应状态码常用方法都有支持请求方法:getpostputdeleteGet、Post、Put与Delete的作用get:请求方法一般是用于数据查询,

  7. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  8. 在VMware16虚拟机安装Ubuntu详细教程 - 2

    在VMware16.2.4安装Ubuntu一、安装VMware1.打开VMwareWorkstationPro官网,点击即可进入。2.进入后向下滑动找到Workstation16ProforWindows,点击立即下载。3.下载完成,文件大小615MB,如下图:4.鼠标右击,以管理员身份运行。5.点击下一步6.勾选条款,点击下一步7.先勾选,再点击下一步8.去掉勾选,点击下一步9.点击下一步10.点击安装11.点击许可证12.在百度上搜索VM16许可证,复制填入,然后点击输入即可,亲测有效。13.点击完成14.重启系统,点击是15.双击VMwareWorkstationPro图标,进入虚拟机主

  9. 微信小程序开发入门与实战(Behaviors使用) - 2

    @作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors    1、什么是behaviors    2、behaviors的工作方式    3、创建behavior    4、导入并使用behavior    5、behavior中所有可用的节点    6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors    1、什么是behaviorsbehaviors是小程序中,用于实现

  10. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

随机推荐