本笔记以项目中的 obj/myexample.c 系列函数作为核心代码示例进行讲解

配置好跨平台的项目链接:跨平台 LVGL 项目模板

1. LVGL 的对象与盒子模型

在 LVGL 中,所有图形元素(部件/Widgets)都是基于对象的。这种面向对象的思想,使得图形界面的构建模块化且易于管理。

核心理念

  • 父子结构: 每个被创建的对象都有一个父对象。屏幕本身也是一个对象,通常作为最顶层的父对象。子对象的位置是相对于父对象的,并且其可见范围不会超出父对象的边界。
    • 获取当前活动的屏幕: lv_scr_act()
    • 创建一个新的对象: lv_obj_t *lv_obj_create(lv_obj_t *parent);
  • 继承与派生: 所有不同类型的部件(如按钮、标签)都继承自一个基础的 lv_obj_t 结构体,并在此之上进行功能的扩展与具体化。

代码示例:创建对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void demo_create_basic_objects(void) {
// 获取当前屏幕作为父对象
lv_obj_t* parent_screen = lv_scr_act();

// 在屏幕上创建一个面板 (panel) 对象
lv_obj_t* panel = lv_obj_create(parent_screen);
// 为了能看见效果,我们给面板设置一个大小并居中
lv_obj_set_size(panel, 200, 150);
lv_obj_center(panel);

// 在面板 (panel) 上创建一个标签 (label) 对象
lv_obj_t* label = lv_label_create(panel);
// 为了能看见效果,我们给标签设置一点文字
lv_label_set_text(label, "A Label");
}

在屏幕中央显示一个灰色的面板,面板左上角有一个标签

常用接口 (API)

以下是一些用于操作对象基本属性的常用函数:

  • 尺寸设置与获取:
    • lv_obj_set_width(obj, new_width);
    • lv_obj_set_height(obj, new_height);
    • lv_obj_set_size(obj, width, height);
    • lv_obj_get_width(obj);
    • lv_obj_get_height(obj);
  • 位置设置与获取:
    • lv_obj_set_x(obj, new_x);
    • lv_obj_set_y(obj, new_y);
    • lv_obj_set_pos(obj, x, y);
    • lv_obj_get_x(obj);
    • lv_obj_get_y(obj);
  • 对齐:
    • lv_obj_align(obj, LV_ALIGN_..., x_offset, y_offset);
    • lv_obj_align_to(obj, base_obj, LV_ALIGN_..., x_offset, y_offset);

代码示例:设置位置和尺寸

1
2
3
4
5
6
7
8
9
10
11
void demo_set_position_and_size(void) {
lv_obj_t* parent_screen = lv_scr_act();
lv_obj_t* panel = lv_obj_create(parent_screen);
lv_obj_set_size(panel, 200, 150);
lv_obj_center(panel);
lv_obj_t* label = lv_label_create(panel);
lv_label_set_text(label, "Hello, World!");

// 将标签在面板顶部居中对齐,并向下偏移20像素
lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 20);
}

在屏幕中央显示一个面板,面板顶部中央有一个标签

对象的盒子模型

LVGL 遵循类似 CSS 的 border-box 模型,这对于理解对象的布局与样式至关重要。一个对象的“盒子”由内到外包含以下几个部分:

  • 内容 (Content): 盒子实际容纳其子对象的区域。
  • 填充 (Padding): 内容区域与边框之间的空间。
  • 边框 (Border): 包围在填充区外围的线条,拥有厚度、颜色等属性。
  • 轮廓 (Outline): 绘制在边框之外的线条,用于突出元素,但不占据实际的布局空间。LVGL 中没有外边距 (margin) 的概念,轮廓起到了类似的作用。
  • 边界 (Bounding Box): 指的是由对象的宽度和高度定义的整个区域。

2. 样式 (Style)

样式用于定义对象的外观。LVGL 的样式系统类似网页三大件中的 CSS,具有灵活、高效的特点。

样式的组成:局部与状态

样式可以被应用于对象的特定部分 (Part) 和在特定的状态 (State) 下。

  • 常见局部: LV_PART_MAIN (主体), LV_PART_SCROLLBAR (滚动条), LV_PART_INDICATOR (指示器), LV_PART_KNOB (旋钮) 等。
  • 常见状态: LV_STATE_DEFAULT (默认), LV_STATE_PRESSED (按下), LV_STATE_FOCUSED (聚焦), LV_STATE_DISABLED (禁用) 等。

样式特点

  • 复用与级联: 一个样式可以被多个对象使用。一个对象也可以添加多个样式,后添加的样式优先级更高。
  • 继承: 如果对象没有指定某个属性(如文本颜色),它会从父对象继承。
  • 本地样式: 对象可以拥有高优先级的“本地样式”,用于进行个别的、独特的样式设置。
  • 过渡效果: 样式支持过渡动画,可以在状态改变时产生平滑的视觉效果。

使用样式三部曲

  1. 初始化,向系统申请一个空间用来存样式
  2. 设置样式
  3. 将样式添加到对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void demo_create_and_apply_style(void) {
lv_obj_t* screen = lv_scr_act();

// 1. 初始化样式
// 样式变量必须是 static 或全局的,以防函数退出后被销毁
static lv_style_t style_btn_green;
lv_style_init(&style_btn_green);

// 2. 设置样式属性
// 设置背景颜色为绿色
lv_style_set_bg_color(&style_btn_green, lv_palette_main(LV_PALETTE_GREEN));
// 设置圆角半径为 10
lv_style_set_radius(&style_btn_green, 10);
// 创建一个按钮对象
lv_obj_t* btn = lv_btn_create(screen);
lv_obj_center(btn); // 居中显示

// 3. 将样式添加到对象
lv_obj_add_style(btn, &style_btn_green, LV_STATE_DEFAULT);

// 给按钮添加一个文本,方便观察
lv_obj_t* label = lv_label_create(btn);
lv_label_set_text(label, "Styled Button");
lv_obj_center(label);
}

带有样式的按钮

样式应用举例:状态过渡动画

通过为不同状态设置不同的样式,并定义过渡描述,可以实现平滑的动画效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义一个过渡描述,指定哪些属性(props)参与动画
static const lv_style_prop_t props[] = {LV_STYLE_BG_COLOR, 0};
static lv_style_transition_dsc_t trans;
lv_style_transition_dsc_init(&trans, props, lv_anim_path_linear, 300, 0, NULL);

// 默认状态的样式
static lv_style_t style_def;
lv_style_init(&style_def);
lv_style_set_bg_color(&style_def, lv_palette_main(LV_PALETTE_BLUE));
lv_style_set_transition(&style_def, &trans); // 添加过渡描述

// 按下状态的样式
static lv_style_t style_pr;
lv_style_init(&style_pr);
lv_style_set_bg_color(&style_pr, lv_palette_main(LV_PALETTE_RED));

// 将样式应用到对象
lv_obj_t * obj = lv_obj_create(lv_scr_act());
lv_obj_add_style(obj, &style_def, 0); // 应用默认样式
lv_obj_add_style(obj, &style_pr, LV_STATE_PRESSED); // 应用按下状态的样式

3. 事件 (Event) 处理机制

当对象的状态发生改变时(如被点击、数值变化等),LVGL 会触发相应的事件。我们可以通过为对象添加回调函数来响应这些事件。

事件特点

  • 一个回调函数可以处理多种不同的事件。
  • 一个对象可以根据不同的事件,绑定不同的回调函数。
  • 对象的事件可以传递给其父对象,这一机制被称为事件冒泡

事件种类

事件被分为几大类,每一类都包含具体的事件代码(宏定义在 lv_event.h 中):

  • 输入设备事件: 如 LV_EVENT_PRESSED (按下), LV_EVENT_CLICKED (单击), LV_EVENT_LONG_PRESSED (长按)。
  • 绘图事件
  • 其他特殊事件: 如 LV_EVENT_VALUE_CHANGED (数值改变)。
  • 自定义事件

事件处理流程

  1. 编写事件回调函数: 定义一个符合 lv_event_cb_t 原型的函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 事件回调函数
    static void button_event_handler(lv_event_t *e) {
    // 获取事件代码
    lv_event_code_t code = lv_event_get_code(e);
    // 获取通过 lv_obj_add_event_cb 传递的用户数据
    lv_obj_t *label = (lv_obj_t *)lv_event_get_user_data(e);

    // 判断是否是点击事件
    if (code == LV_EVENT_CLICKED) {
    // 修改标签的文本
    lv_label_set_text(label, "Hello, World!");
    }
    }
  2. 为对象添加事件: 使用 lv_obj_add_event_cb 函数将回调函数与特定的事件绑定。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    void demo_add_event_handler(void) {
    lv_obj_t* panel = lv_obj_create(lv_scr_act());
    lv_obj_set_size(panel, 200, 150);
    lv_obj_center(panel);

    // 创建一个标签用于显示结果
    lv_obj_t* info_label = lv_label_create(panel);
    lv_label_set_text(info_label, "Please click the button");
    lv_obj_align(info_label, LV_ALIGN_TOP_MID, 0, 10);

    // 创建一个按钮
    lv_obj_t* btn = lv_btn_create(panel);
    lv_obj_align(btn, LV_ALIGN_CENTER, 0, 20);

    lv_obj_t* btn_label = lv_label_create(btn);
    lv_label_set_text(btn_label, "Click Me");
    lv_obj_center(btn_label);

    // 关键步骤:为按钮添加事件回调
    // 将 info_label 作为用户数据传递给回调函数
    lv_obj_add_event_cb(btn, my_event_handler, LV_EVENT_CLICKED, info_label);
    }

Peek 2025-09-16 19-22

事件冒泡

当一个子对象的事件被触发后,如果开启了事件冒泡,该事件会像气泡一样“向上”传递给它的父对象,父对象也可以对这个事件进行响应。

  • 如何开启: 使用 lv_obj_add_flag(obj, LV_OBJ_FLAG_EVENT_BUBBLE); 为子对象添加标志。
  • 如何判断: 在回调函数中,可以通过 lv_event_get_target() 获取最初触发事件的对象,通过 lv_event_get_current_target() 获取当前正在处理事件的对象。如果两者不一致,则说明事件是冒泡上来的。

代码示例
下面的代码创建了一个按钮(子)和一个面板(父)。点击按钮后,上方的标签文本会先显示事件在按钮上触发,短暂延迟后会显示事件冒泡到了父面板上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 事件冒泡的回调函数
static void bubbling_event_cb(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t* label_to_update = (lv_obj_t*)lv_event_get_user_data(e);

// 获取当前正在处理事件的对象
lv_obj_t* current_target = lv_event_get_current_target(e);
// 获取最初触发事件的对象
lv_obj_t* original_target = lv_event_get_target(e);

if (code == LV_EVENT_CLICKED) {
if (lv_obj_check_type(current_target, &lv_btn_class)) {
printf("事件由按钮触发 (Child).\n");
} else if (lv_obj_check_type(current_target, &lv_obj_class)) {
if (current_target != original_target) {
printf("事件冒泡到面板 (Parent).\n");
lv_label_set_text(label_to_update, "Event bubbled to: Panel (Parent)");
}
}
}
}

// 事件冒泡的演示函数
void demo_event_bubbling(void) {
// 1. 创建父对象 (面板)
lv_obj_t* parent_panel = lv_obj_create(lv_scr_act());
lv_obj_set_size(parent_panel, 250, 150);
lv_obj_center(parent_panel);
// 父对象必须是可点击的,才能接收到冒泡的点击事件
lv_obj_add_flag(parent_panel, LV_OBJ_FLAG_CLICKABLE);

// 创建一个标签用于显示事件处理流程
lv_obj_t* info_label = lv_label_create(lv_scr_act());
lv_label_set_text(info_label, "Click the button below");
lv_obj_align(info_label, LV_ALIGN_TOP_MID, 0, 150);

// 2. 创建子对象 (按钮)
lv_obj_t* child_btn = lv_btn_create(parent_panel);
lv_obj_set_size(child_btn, 120, 50);
lv_obj_align(child_btn, LV_ALIGN_CENTER, 0, 20);

lv_obj_t* btn_label = lv_label_create(child_btn);
lv_label_set_text(btn_label, "Click Me!");
lv_obj_center(btn_label);

// 3. (关键步骤) 为子对象开启事件冒泡标志
lv_obj_add_flag(child_btn, LV_OBJ_FLAG_EVENT_BUBBLE);

// 4. 为父对象和子对象都绑定同一个事件回调
// 这样我们才能在同一个函数里观察到事件的传递过程
lv_obj_add_event_cb(child_btn, bubbling_event_cb, LV_EVENT_CLICKED,
info_label);
lv_obj_add_event_cb(parent_panel, bubbling_event_cb, LV_EVENT_CLICKED,
info_label);
}

Peek 2025-09-17 08-47