LVGL学习(四)- Objects

一、简介

前面介绍的东西都比较简单,这篇文章介绍点有深度的东西。

二、什么是Objects

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

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

lv_obj_t 是 LVGL 中 所有 UI 对象的基类 ,可以理解为"万能对象"。所有控件(Label、Button、Slider...)在底层都是 lv_obj_t ,只是各自附加了不同的属性和行为。

属性

一个 lv_obj_t 内部包含这些通用属性:

属性 说明 设置函数
位置 x, y 坐标 lv_obj_set_pos()
尺寸 宽度、高度 lv_obj_set_size() / lv_obj_set_width()
样式 背景色、边框、圆角、字体等 lv_obj_set_style_*()
状态 默认、按下、选中、禁用等 lv_obj_add_state() / lv_obj_clear_state()
事件 点击、值改变、滚动等 lv_obj_add_event_cb()
父子关系 每个对象有且只有一个父对象 创建时指定
标志 可点击、可滚动、隐藏等 lv_obj_add_flag()
布局 Flex、Grid lv_obj_set_flex_flow() / lv_obj_set_layout()

特殊属性

不同的对象,有特定的属性,例如滑块对象有最大值、最小值等等。后续用到特地给对象的时候再详细说。

cpp 复制代码
lv_obj_t (基类)
    ├── 位置、尺寸、样式、事件、父子关系...(基类属性)
    │
    └── 各种控件继承 lv_obj_t,再附加自己的属性:
        ├── Label   : lv_obj_t + text(文本内容)
        ├── Button  : lv_obj_t + 按下效果样式
        ├── Slider  : lv_obj_t + value、range、拖拽逻辑
        ├── Image   : lv_obj_t + src(图片源)
        └── ...

父子关系

一个父对象可以被视为其子对象的容器。每个对象都都必须会有且仅有一个父对象(屏幕除外),但一个父对象可以有任意数量的子对象

cpp 复制代码
lv_obj_t * screen = lv_screen_active();           // 屏幕(根)
lv_obj_t * cont   = lv_obj_create(screen);        // 容器,父亲是 screen
lv_obj_t * label  = lv_label_create(cont);        // Label,父亲是 cont
lv_obj_t * btn    = lv_button_create(cont);       // Button,父亲是 cont
lv_obj_t * btn_lbl = lv_label_create(btn);        // 按钮里的文字,父亲是 btn

screen
  └── cont
       ├── label
       └── btn
            └── btn_lbl

父子关系带来的特性

|-------|-----------------------------|
| 特性 | 说明 |
| 继承位置 | 子对象坐标相对于父对象,移动父对象子对象跟着动 |
| 裁剪 | 子对象超出父对象边界默认被裁剪 |
| 继承透明度 | 父对象半透明,子对象也跟着半透明 |
| 事件冒泡 | 子对象的事件可以冒泡到父对象(可选) |
| 生命周期 | 删除父对象时,所有子对象自动删除 |

lv_obj_t

来看一下结构体lv_obj_t。

cpp 复制代码
typedef struct _lv_obj_t lv_obj_t;
cpp 复制代码
struct _lv_obj_t {
    const lv_obj_class_t * class_p;    // 类信息(决定这是 Label/Button/Slider...)
    lv_obj_t * parent;                  // 父对象指针
    lv_obj_spec_attr_t * spec_attr;     // 特殊属性(事件回调、子对象列表等,懒加载)
    lv_obj_style_t * styles;            // 样式列表
    void * user_data;                   // 用户自定义数据
    lv_area_t coords;                   // 坐标(x1, y1, x2, y2)
    lv_obj_flag_t flags;                // 标志位(可点击、隐藏、可滚动...)
    uint16_t state;                     // 状态(默认、按下、选中、禁用...)
    // 各种 1bit 标志:
    uint16_t layout_inv : 1;            // 布局需要重新计算
    uint16_t style_cnt  : 6;            // 样式数量
    uint16_t h_layout   : 1;            // 高度由布局决定
    uint16_t w_layout   : 1;            // 宽度由布局决定
    uint16_t is_deleting : 1;           // 正在删除中(防止重复删除)
    uint16_t rendered : 1;              // 是否已渲染过至少一次
    // ...
};

lv_obj_t 只是个前向声明的别名,让你在代码里用 lv_obj_t * 而不需要知道内部细节。真正的结构体内容在 _lv_obj_t 里,定义在 lv_obj_private.h (注意文件名带private)。

为什么要这样设计

|--------|-----------------------------------------------------|
| 设计目标 | 实现方式 |
| 信息隐藏 | 公开头文件只暴露 typedef struct _lv_obj_t lv_obj_t; ,看不到字段 |
| ABI 稳定 | 用户代码只持有指针,不依赖结构体大小,LVGL 升级时字段变动不影响用户代码 |
| 封装 | 强制用户通过 API 函数( lv_obj_set_pos() 等)操作对象,不能直接读写字段 |

关键字段说明

|-------------|--------------------------|-------------------------------------------|---------------------------------------------|
| 字段 | 类型 | 作用 | 对应 API |
| class_p | const lv_obj_class_t * | 类信息,决定这是哪种控件。Label 和 Button 的区别就在这里 | 内部使用 |
| parent | lv_obj_t * | 父对象,形成对象树 | lv_obj_get_parent() |
| spec_attr | lv_obj_spec_attr_t * | 特殊属性(按需分配),包含事件回调、子对象链表、滚动信息等 | lv_obj_allocate_spec_attr() |
| styles | lv_obj_style_t * | 样式数组 | lv_obj_set_style_*() |
| coords | lv_area_t | 坐标区域 (x1,y1,x2,y2),由它推算出位置和尺寸 | lv_obj_get_pos() / lv_obj_get_width() |
| flags | lv_obj_flag_t | 标志位(可点击、可滚动、隐藏等) | lv_obj_add_flag() / lv_obj_clear_flag() |
| state | uint16_t | 当前状态(DEFAULT/PRESSED/CHECKED/DISABLED...) | lv_obj_add_state() |

三、实例

醒醒、醒醒。是不是看概念久了,都快睡着了。那我们来个实例看看。先看代码:

cpp 复制代码
/* ============================================================
 *  父子关系演示 Demo
 *
 *  展示 LVGL 对象树的两个核心特性:
 *    1. 子对象坐标相对于父对象,移动父对象子对象跟着动
 *    2. 删除父对象时,所有子对象自动被删除
 * ============================================================ */

/* "移动父容器"按钮回调:让父容器水平移动,子对象跟着动 */
static void move_parent_cb(lv_event_t * e)
{
    lv_obj_t * parent = lv_event_get_user_data(e);
    int32_t x = lv_obj_get_x(parent);
    x = (x + 30) % 300;
    lv_obj_set_x(parent, x);
}

/* "删除父容器"按钮回调:删除父容器,所有子对象自动消失 */
static void del_parent_cb(lv_event_t * e)
{
    lv_obj_t * parent = lv_event_get_user_data(e);
    lv_obj_del(parent);
}

static void create_parent_child_demo(void)
{
    /* 获取当前活动屏幕对象,后续所有控件都创建在它上面 */
    lv_obj_t * screen = lv_screen_active();

    /* 设置屏幕的背景色为浅蓝灰色(调亮等级4),看起来比纯白柔和 */
    lv_obj_set_style_bg_color(screen, lv_palette_lighten(LV_PALETTE_BLUE_GREY, 4), 0);

    /* ============================================================
     * 创建父容器(一个空盒子,用来装子对象)
     * 参数 screen 表示父容器的父亲是屏幕
     * ============================================================ */
    lv_obj_t * parent = lv_obj_create(screen);

    /* 设置父容器的尺寸:宽 300 像素,高 200 像素 */
    lv_obj_set_size(parent, 300, 200);
    /* 设置父容器的位置:相对于屏幕左上角 (100, 50) */
    lv_obj_set_pos(parent, 100, 50);
    /* 设置父容器的背景色为浅蓝色 */
    lv_obj_set_style_bg_color(parent, lv_palette_lighten(LV_PALETTE_BLUE, 4), 0);
    /* 设置父容器的边框颜色为蓝色 */
    lv_obj_set_style_border_color(parent, lv_palette_main(LV_PALETTE_BLUE), 0);
    /* 设置父容器的边框宽度为 2 像素 */
    lv_obj_set_style_border_width(parent, 2, 0);

    /* ============================================================
     * 创建父容器顶部的标题标签
     * 参数 parent 表示标签的父亲是父容器
     * ============================================================ */
    lv_obj_t * title = lv_label_create(parent);

    /* 设置标题文本内容 */
    lv_label_set_text(title, "I am PARENT");
    /* 设置标题文字颜色为蓝色 */
    lv_obj_set_style_text_color(title, lv_palette_main(LV_PALETTE_BLUE), 0);
    /* 让标题对齐到父容器的顶部居中位置,偏移 (0, 5) */
    lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 5);

    /* ============================================================
     * 创建子对象 1(普通对象,不是按钮)
     * 用不同背景色区分,移动时视觉效果明显
     * ============================================================ */
    lv_obj_t * child1 = lv_obj_create(parent);              /* 父亲是 parent */
    lv_obj_set_pos(child1, 20, 40);                         /* 相对父容器 (20, 40) */
    lv_obj_set_size(child1, 80, 40);                         /* 尺寸 80x40 */
    lv_obj_set_style_bg_color(child1, lv_palette_main(LV_PALETTE_GREEN), 0);  /* 绿色背景 */
    lv_obj_set_style_border_width(child1, 0, 0);            /* 无边框 */
    lv_obj_set_style_pad_all(child1, 0, 0);                 /* 无内边距 */

    /* 在子对象 1 内部创建一个标签(父亲是 child1) */
    lv_obj_t * lbl1 = lv_label_create(child1);
    lv_label_set_text(lbl1, "Child 1");
    lv_obj_set_style_text_color(lbl1, lv_color_white(), 0);  /* 白色文字 */
    lv_obj_center(lbl1);                                     /* 居中 */

    /* ============================================================
     * 创建子对象 2(结构同上,用不同颜色)
     * ============================================================ */
    lv_obj_t * child2 = lv_obj_create(parent);              /* 父亲是 parent */
    lv_obj_set_pos(child2, 110, 40);                        /* 相对父容器 (110, 40) */
    lv_obj_set_size(child2, 80, 40);                         /* 尺寸 80x40 */
    lv_obj_set_style_bg_color(child2, lv_palette_main(LV_PALETTE_ORANGE), 0); /* 橙色背景 */
    lv_obj_set_style_border_width(child2, 0, 0);            /* 无边框 */
    lv_obj_set_style_pad_all(child2, 0, 0);                 /* 无内边距 */

    lv_obj_t * lbl2 = lv_label_create(child2);              /* 子对象 2 内创建标签 */
    lv_label_set_text(lbl2, "Child 2");                     /* 设置文字 */
    lv_obj_set_style_text_color(lbl2, lv_color_white(), 0);  /* 白色文字 */
    lv_obj_center(lbl2);                                     /* 居中 */

    /* ============================================================
     * 操作按钮区(直接创建在屏幕上,不在父容器内)
     * ============================================================ */

    /* 创建"移动父容器"按钮,父亲是 screen */
    lv_obj_t * move_btn = lv_button_create(screen);
    lv_obj_set_pos(move_btn, 100, 290);                      /* 相对屏幕 (100, 290) */
    lv_obj_set_size(move_btn, 120, 40);                       /* 尺寸 120x40 */
    lv_obj_t * move_lbl = lv_label_create(move_btn);          /* 按钮内创建标签 */
    lv_label_set_text(move_lbl, "Move Parent");               /* 设置文字 */
    lv_obj_center(move_lbl);                                  /* 居中 */
    /* 绑定点击事件,传入 parent 作为 user_data,回调中通过它移动父容器 */
    lv_obj_add_event_cb(move_btn, move_parent_cb, LV_EVENT_CLICKED, parent);

    /* 创建"删除父容器"按钮,结构同上 */
    lv_obj_t * del_btn = lv_button_create(screen);           /* 父亲是 screen */
    lv_obj_set_pos(del_btn, 240, 290);                       /* 相对屏幕 (240, 290) */
    lv_obj_set_size(del_btn, 120, 40);                        /* 尺寸 120x40 */
    lv_obj_t * del_lbl = lv_label_create(del_btn);            /* 按钮内创建标签 */
    lv_label_set_text(del_lbl, "Del Parent");                 /* 设置文字 */
    lv_obj_center(del_lbl);                                   /* 居中 */
    /* 绑定点击事件,传入 parent 作为 user_data,回调中通过它删除父容器 */
    lv_obj_add_event_cb(del_btn, del_parent_cb, LV_EVENT_CLICKED, parent);
}


int main(void)
{
    /* 初始化 LVGL 核心库 */
    lv_init();
    
    /* ---- 创建显示设备 ---- */
    /* lv_sdl_window_create() 内部会调用 SDL_Init 初始化 SDL,
     * 创建窗口,注册刷新定时器,并设置 lv_tick */
    lv_display_t * display = lv_sdl_window_create(500, 500);
    if(display == NULL) {
        return 1;
    }

    /* ---- 创建输入设备 ---- */
    /* 鼠标是必须的,否则无法与窗口交互 */
    lv_sdl_mouse_create();

    lv_log_register_print_cb(log_cb); 

    create_parent_child_demo();

    while(1) {
        lv_timer_handler();
        lv_delay_ms(5);
    }

    return 0;
}

可以看到点击"Move Parent"后,回调中执行了设置parent的位置,但是在parent下挂载的其他几个obj全都移动了

cpp 复制代码
static void move_parent_cb(lv_event_t * e)
{
    lv_obj_t * parent = lv_event_get_user_data(e);
    int32_t x = lv_obj_get_x(parent);
    x = (x + 30) % 300;
    lv_obj_set_x(parent, x);
}

点击"Del Parent"后,只是删除parent,但是挂载在parent下的多个obj也都跟着被删掉了

cpp 复制代码
static void del_parent_cb(lv_event_t * e)
{
    lv_obj_t * parent = lv_event_get_user_data(e);
    lv_obj_del(parent);
}
相关推荐
慕容白 MU1 天前
开源一个基于LVGL的图层管理框架(LMF)
开源·lvgl·图层管理框架
t_guest1 天前
LVGL学习(二)- 定时器
lvgl
t_guest1 天前
LVGL学习(三)- 日志
入门·lvgl
浮江雾3 天前
Flutter第四节------核心概念学习笔记:从基础语法到异步编程
linux·服务器·开发语言·笔记·flutter·入门·基础
浮江雾4 天前
Flutter第三节----Dart中的数据类型
android·开发语言·学习·flutter·入门·函数
浮江雾6 天前
Flutter第二节----Dart中的常量与变量
开发语言·前端·javascript·flutter·入门
雨翼轻尘1 个月前
03_HTML进阶标签与CSS入门
前端·css·html·入门·进阶标签
zhbi982 个月前
LVGL8.3标签Label高级应用
ui·lvgl
在坚持一下我可没意见2 个月前
Python 修仙修炼录 08:字典秘境,参悟键值玄机
开发语言·笔记·python·入门·字典