一、简介
前面介绍的东西都比较简单,这篇文章介绍点有深度的东西。
二、什么是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() |
特殊属性
不同的对象,有特定的属性,例如滑块对象有最大值、最小值等等。后续用到特地给对象的时候再详细说。
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(图片源) └── ...父子关系
一个父对象可以被视为其子对象的容器。每个对象都都必须会有且仅有一个父对象(屏幕除外),但一个父对象可以有任意数量的子对象。
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。
typedef struct _lv_obj_t lv_obj_t;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 |
|
| 类信息,决定这是哪种控件。Label 和 Button 的区别就在这里 | 内部使用 |
|
| 父对象,形成对象树 |
|
|
| 特殊属性(按需分配),包含事件回调、子对象链表、滚动信息等 |
|
|
| 样式数组 |
|
|
| 坐标区域 (x1,y1,x2,y2),由它推算出位置和尺寸 |
|
|
| 标志位(可点击、可滚动、隐藏等) |
|
|
| 当前状态(DEFAULT/PRESSED/CHECKED/DISABLED...) |
|
三、实例
醒醒、醒醒。是不是看概念久了,都快睡着了。那我们来个实例看看。先看代码:
/* ============================================================ * 父子关系演示 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全都移动了。
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也都跟着被删掉了。
static void del_parent_cb(lv_event_t * e) { lv_obj_t * parent = lv_event_get_user_data(e); lv_obj_del(parent); }