[LVGL] 布局系统 lv_flex, lv_grid | 输入设备 lv_indev | union

第五章:布局系统(lv_flex, lv_grid)

欢迎回来!

第四章:样式(lv_style)中,我们掌握了如何通过色彩、字体和圆角等特性美化部件。当界面元素具备视觉吸引力后,如何优雅地组织它们便成为新的挑战。

设想我们拥有多个精美按钮,希望实现以下布局效果:

  • 横向/纵向等距排列
  • 屏幕尺寸变化时自动适配
  • 动态增删元素时自动调整

传统手工计算坐标的方式显然低效且难以维护,这正是布局系统的价值所在。

布局系统核心价值

LVGL布局系统受现代网页设计(CSS Flexbox/Grid)启发,通过声明式配置实现:

布局类型概览

布局类型 适用场景 典型应用
弹性布局 单向流式排列 导航栏|设置项列表
网格布局 二维矩阵排列 仪表盘|相册缩略图

启用布局模块

lv_conf.h中激活配置:

c 复制代码
/*==================
 * 布局模块
 *================*/
#define LV_USE_FLEX 1  // 启用弹性布局
#define LV_USE_GRID 1  // 启用网格布局

弹性布局(lv_flex)

1. 容器初始化

c 复制代码
lv_obj_t * flex_container = lv_obj_create(screen_main);
lv_obj_set_size(flex_container, LV_PCT(90), LV_PCT(80)); // 相对父容器90%宽/80%高
lv_obj_set_layout(flex_container, LV_LAYOUT_FLEX);       // 声明弹性容器

2. 排列方向控制

c 复制代码
// 横向排列(可换行)
lv_obj_set_flex_flow(flex_container, LV_FLEX_FLOW_ROW_WRAP);

// 纵向排列(可换列)
lv_obj_set_flex_flow(flex_container, LV_FLEX_FLOW_COLUMN_WRAP);

3. 对齐方式

c 复制代码
// 主轴居中|交叉轴居中|轨道居中
lv_obj_set_flex_align(flex_container, 
    LV_FLEX_ALIGN_CENTER, 
    LV_FLEX_ALIGN_CENTER, 
    LV_FLEX_ALIGN_CENTER);

4. 空间分配

c 复制代码
lv_obj_t * expand_btn = lv_button_create(flex_container);
lv_obj_set_flex_grow(expand_btn, 2);  // 占据剩余空间2份

lv_obj_t * normal_btn = lv_button_create(flex_container);
lv_obj_set_flex_grow(normal_btn, 1);  // 占据剩余空间1份

5. 间距控制

c 复制代码
static lv_style_t flex_style;
lv_style_init(&flex_style);
lv_style_set_pad_column(&flex_style, 10);  // 列间距10像素
lv_style_set_pad_row(&flex_style, 15);     // 行间距15像素
lv_obj_add_style(flex_container, &flex_style, 0);

网格布局(lv_grid)

1. 容器初始化

c 复制代码
lv_obj_t * grid_container = lv_obj_create(screen_main);
lv_obj_set_layout(grid_container, LV_LAYOUT_GRID);  // 声明网格容器

2. 网格结构定义

c 复制代码
// 列定义:固定100px|弹性1份|弹性2份
static int32_t col_dsc[] = {100, LV_GRID_FR(1), LV_GRID_FR(2), LV_GRID_TEMPLATE_LAST};

// 行定义:自适应内容高度|弹性3份
static int32_t row_dsc[] = {LV_GRID_CONTENT, LV_GRID_FR(3), LV_GRID_TEMPLATE_LAST};

lv_obj_set_grid_dsc_array(grid_container, col_dsc, row_dsc);

3. 单元格定位

c 复制代码
// 按钮定位到(0,0)单元格,横向居左|纵向居顶
lv_obj_t * btn = lv_button_create(grid_container);
lv_obj_set_grid_cell(btn, 
    LV_GRID_ALIGN_START, 0, 1,   // 列:起始对齐,第0列,跨1列
    LV_GRID_ALIGN_START, 0, 1);  // 行:起始对齐,第0行,跨1行

// 标签跨两列
lv_obj_t * label = lv_label_create(grid_container);
lv_obj_set_grid_cell(label,
    LV_GRID_ALIGN_CENTER, 1, 2,  // 列:居中,第1列,跨2列
    LV_GRID_ALIGN_CENTER, 0, 1); // 行:居中,第0行,跨1行

布局系统工作原理

处理流程

核心机制

  1. 注册机制 :通过lv_obj_set_layout()将容器注册到布局系统
  2. 延迟计算 :在屏幕刷新周期统一处理布局计算
  3. 动态响应:容器尺寸变化或子元素增减时自动触发重新布局

实践

  1. 组合使用 :在复杂界面中混合使用FlexGrid布局
  2. 响应式设计 :结合LV_PCT百分比单位和媒体查询实现自适应
  3. 性能优化 :避免深层嵌套布局,控制刷新频率
  4. 样式分离:将布局样式与视觉样式分离管理

结论

通过本章学习,我们掌握了:

  • 弹性布局的流式排列空间分配技巧
  • 网格布局的二维矩阵定位方法
  • 布局系统的底层运作原理
  • 间距控制与对齐策略

布局系统的引入使界面开发从手工计算迈向声明式配置,极大提升了开发效率和可维护性。

下一章我们将探索用户交互的核心------输入设备管理

下一章:输入设备(lv_indev)


github: https://github.com/lvy010/Cpp-Lib-test/tree/main/LVGL/indev

第六章:输入设备(lv_indev)

第五章:布局(lv_flex, lv_grid)中,我们已成为屏幕布局的大师,能够确保界面元素美观且自适应。但若用户无法真正触摸交互这些精心设计的界面,再惊艳的UI又有何用?

如何才能让那个完美居中、蓝色圆角按钮真正被点击?

这正是**输入设备(lv_indev)**大显身手之时~

假设我们的设备拥有物理触摸屏、鼠标、键盘甚至旋转编码器,LVGL需要通过某种方式理解用户通过这些物理输入设备的操作,并将这些动作转化为屏幕上控件(如按钮、滑块或文本输入框)能够理解和响应的指令

lv_indev模块就像用户交互的通用翻译器。

它接收来自硬件的原始信息(例如"手指触摸了X,Y坐标"或"Enter键被按下"),并将其转化为GUI可理解的语义事件。

这使得LVGL应用能够响应多样化的物理输入,而无需为每个交互编写复杂的硬件专用代码。

本章目标是理解如何连接常见输入设备------触摸屏 (属于"指针"类设备),让LVGL按钮能够响应点击操作

什么是lv_indev

lv_indev(LVGL输入设备缩写,代码中以lv_indev_t结构体表示)是表征单个输入硬件设备的对象。

它充当微控制器原始输入数据与LVGL事件系统之间的桥梁。

lv_indev_t对象管理的关键要素:

  • 设备类型:属于触摸屏、键盘还是编码器?
  • 数据读取 :需要开发者提供的特殊函数("读取回调")来获取硬件当前状态
  • 当前状态:按压/释放状态、指向位置或激活的按键
  • 关联显示 :该输入设备控制的显示设备(回忆第二章:显示设备(lv_display)

输入设备类型(LV_INDEV_TYPE_

LVGL支持多种输入设备类别:

类型 描述 典型硬件
LV_INDEV_TYPE_POINTER 能够指向屏幕具体坐标并触发按压/释放动作的设备 触摸屏、鼠标、轨迹球
LV_INDEV_TYPE_KEYPAD 提供按键输入的设备,常用于导航和文本输入 物理键盘、数字键盘
LV_INDEV_TYPE_ENCODER 带有增量旋转(左/右)和可选按压按钮的旋转设备 旋钮编码器、滚轮
LV_INDEV_TYPE_BUTTON 映射到屏幕坐标的物理按键(如设备外壳上的实体按钮) 前面板按键

本章将以LV_INDEV_TYPE_POINTER类型的触摸屏为例进行说明。

连接第一个输入设备(触摸屏)

让我们配置基础触摸屏功能,使LVGL能够检测按钮的触摸操作。

1. 创建输入设备对象

首先需要通过创建lv_indev_t对象告知LVGL存在输入设备。

该操作必须在显示设备(lv_display)创建之后执行。

c 复制代码
#include "lvgl.h" // 始终包含主LVGL头文件

// 在应用初始化函数中(如main.c或app_init())
void setup_input_device() 
{
    // 确保显示设备已初始化(例如调用第二章的setup_display())
    // lv_init();
    // setup_display(); // 需在输入设备设置前调用!

    // 1. 创建输入设备对象
    lv_indev_t * my_touchpad_indev = lv_indev_create();

    // ... 后续配置步骤在此添加
}
  • lv_indev_create():创建lv_indev_t对象,默认关联到首个创建的显示设备
  • lv_indev_t * my_touchpad_indev:该变量持有输入设备的操作句柄

2. 设置设备类型

告知LVGL输入设备类型,触摸屏属于LV_INDEV_TYPE_POINTER

c 复制代码
// ...(接续前文代码)

void setup_input_device() 
{
    lv_indev_t * my_touchpad_indev = lv_indev_create();

    // 2. 设置输入设备类型为POINTER
    lv_indev_set_type(my_touchpad_indev, LV_INDEV_TYPE_POINTER);

    // ... 后续配置步骤在此添加
}

3. 实现读取回调函数

这是最关键的部分!LVGL需要通过开发者提供的"读取回调"函数定期获取实际触摸数据。

my_touchpad_read函数需要完成:

  • 读取触摸的当前状态(按压或释放)
  • 若处于按压状态,读取X/Y坐标
  • 填充lv_indev_data_t结构体传递这些信息
c 复制代码
// 将触摸数据存储为全局变量以便硬件驱动更新
//(例如在中断服务例程或主循环轮询中更新)
static int32_t touch_x = 0;
static int32_t touch_y = 0;
static bool touch_pressed = false; // 触摸屏激活时为true

// *** 重要:需替换为实际硬件读取函数!***
// 以下仅为演示概念占位符
// 实际嵌入式系统中应读取触摸控制器IC数据
// 示例:*x = get_actual_touch_x(); *y = get_actual_touch_y(); *is_pressed = is_touch_down();

void read_touchscreen_hardware(int32_t *x, int32_t *y, bool *is_pressed) 
{
    // 演示用模拟输入(如PC模拟器中的鼠标)
    // 实际应用中应从触摸传感器获取真实数据:
    *x = touch_x;
    *y = touch_y;
    *is_pressed = touch_pressed;
}

// ****************************************************************************

// 3. 自定义读取回调函数
void my_touchpad_read(lv_indev_t * indev, lv_indev_data_t * data) {
    // 从实际触摸硬件读取当前状态
    read_touchscreen_hardware(&touch_x, &touch_y, &touch_pressed);

    if (touch_pressed) {
        data->state = LV_INDEV_STATE_PRESSED; // 告知LVGL按压状态
        data->point.x = touch_x;              // 设置X坐标
        data->point.y = touch_y;              // 设置Y坐标
    } else {
        data->state = LV_INDEV_STATE_RELEASED; // 告知LVGL释放状态
    }
}
  • lv_indev_t * indev:触发回调的输入设备对象指针
  • lv_indev_data_t * data:必须填充当前输入数据的结构体
  • LV_INDEV_STATE_PRESSED / LV_INDEV_STATE_RELEASED:指针设备的两种基本状态
  • data->point.x, data->point.y:按压状态时的坐标位置

4. 连接读取回调

最后将my_touchpad_read函数关联至输入设备对象。

c 复制代码
// ...(接续前文代码)

void setup_input_device() 
{
    lv_indev_t * my_touchpad_indev = lv_indev_create();
    lv_indev_set_type(my_touchpad_indev, LV_INDEV_TYPE_POINTER);

    // 4. 关联读取回调函数
    lv_indev_set_read_cb(my_touchpad_indev, my_touchpad_read);
}

现在调用setup_input_device()后,LVGL将周期调用my_touchpad_read获取触摸状态,并据此判断控件(如第三章:控件(lv_obj)中的按钮)是否被点击

若结合第四章:样式(lv_style)中的LV_STATE_PRESSED样式,我们甚至能看到按钮在触摸时的颜色变化

理解控件组(适用于键盘/编码器)

POINTER设备通过直接点击屏幕坐标交互

KEYPADENCODER设备则通过"焦点"与控件交互。

想象用键盘导航网页:按Tab键在按钮间切换焦点,Enter键点击焦点按钮。

LVGL使用**控件组(lv_group_t)**实现此机制。

  • 创建组lv_group_t * g = lv_group_create();
  • 添加控件至组lv_group_add_obj(g, my_button);(对所有需导航的交互控件执行此操作)
  • 为输入设备分配组lv_indev_set_group(my_keypad_indev, g);

my_keypad_read回调报告LV_KEY_NEXT时,焦点将自动在g组的控件间切换

(Qt的话,有信号和槽机制)

[Qt] 信号和槽(1) | 本质 | 使用 | 自定义

[Qt] 信号和槽(2) | 多对多 | disconnect | 结合lambda | sum

输入设备工作原理

让我们观察触摸事件从硬件LVGL控件的传递过程。

  1. 轮询/读取 :LVGL运行周期性定时器(由第一章:配置(lv_conf.h)中的LV_DEF_REFR_PERIOD控制,通常10-50ms)。该定时器触发lv_indev_read_timer_cb,进而调用各注册输入设备的lv_indev_read
  2. 读取回调lv_indev_read调用开发者实现的my_touchpad_read函数,从硬件读取原始X/Y坐标和触摸状态
  3. 数据处理 :将原始数据填入lv_indev_data_t结构体并返回
  4. 查找目标对象:LVGL获取原始输入数据后,对指针设备会基于X/Y坐标遍历显示设备上的所有活动控件(从顶层系统层到底层),查找位于触摸点下的控件。此过程涉及坐标和可见性检查
  5. 状态与事件管理 :确定目标控件后,LVGL更新其内部状态(如LV_STATE_PRESSED)并触发相关事件(如LV_EVENT_PRESSEDLV_EVENT_CLICKEDLV_EVENT_RELEASED)。若按压状态移动可能触发滚动或拖拽

简化序列图如下:

LVGL内部代码解析:

调用lv_indev_create()时,LVGL会为lv_indev_t结构体分配内存。

该结构体保存指向读取回调函数的指针、设备类型内部状态变量关联显示设备指针

核心逻辑位于src/indev/lv_indev.c,以下是简化代码片段:

c 复制代码
// 摘自lv_indev.c(简化版)
lv_indev_t * lv_indev_create(void)
{
    // 为输入设备对象分配内存
    lv_indev_t * indev = lv_ll_ins_head(indev_ll_head);
    // ... 初始化默认值 ...
    // 创建周期性调用读取函数的定时器
    indev->read_timer = lv_timer_create(lv_indev_read_timer_cb, LV_DEF_REFR_PERIOD, indev);
    // ...
    return indev;
}

void lv_indev_set_read_cb(lv_indev_t * indev, lv_indev_read_cb_t read_cb)
{
    // 存储开发者提供的读取回调函数指针
    indev->read_cb = read_cb;
}

void lv_indev_read(lv_indev_t * indev)
{
    lv_indev_data_t data;
    // 调用开发者实现的读取回调
    if(indev->read_cb) 
    {
        indev->read_cb(indev, &data);
    }
    
    // ... 根据indev->type处理data ...
    if(indev->type == LV_INDEV_TYPE_POINTER) 
    {
        indev_pointer_proc(indev, &data); // 处理指针数据
    }
    // ... 其他类型处理(键盘、编码器、按钮)...
}

static void indev_pointer_proc(lv_indev_t * i, lv_indev_data_t * data) 
{
    // ... 从data->point更新内部'act_point' ...
    // ... 通过pointer_search_obj()查找指针下对象 ...
    // ... 更新内部状态(如i->pointer.act_obj, i->state)...
    if (i->state == LV_INDEV_STATE_PRESSED) {
        indev_proc_press(i); // 处理按压逻辑
    } else {
        indev_proc_release(i); // 处理释放逻辑
    }
}

static void indev_proc_press(lv_indev_t * indev) 
{
    // ... 检测新对象、长按、滚动的逻辑 ...
    // 若启用,向活动对象(indev_obj_act)发送LV_EVENT_PRESSED事件
    // 示例:
    // lv_obj_send_event(indev_obj_act, LV_EVENT_PRESSED, indev_act);
}

// lv_indev.h中完整的lv_indev_t定义
// 包含输入设备状态和配置的所有相关数据
typedef struct _lv_indev_t 
{
    // ... 其他成员 ...
    lv_indev_type_t type;            /**< 输入设备类型(POINTER, KEYPAD, ENCODER, BUTTON) */
    lv_indev_read_cb_t read_cb;      /**< 输入设备数据读取函数 */
    lv_indev_state_t state;          /**< 当前状态(PRESSED或RELEASED) */
    struct _lv_display_t * disp;     /**< 关联的显示设备 */
    lv_timer_t * read_timer;         /**< 周期性调用read_cb的定时器 */
    lv_group_t * group;              /**< 针对KEYPAD/ENCODER:交互的控件组 */
    // ... 指针、键盘等内部状态变量 ...
    // 例如:lv_point_t pointer.act_point; 当前坐标
    // 例如:uint32_t keypad.last_key; 最后按下的键
    // ... 更多手势、长按、滚动相关参数 ...
} lv_indev_t;

这种内部结构和处理流程确保了LVGL能够高效处理多种输入源,将底层硬件细节与GUI逻辑解耦。

代码功能

lv_indev_create()函数是LVGL输入设备系统的核心接口,用于创建并初始化一个输入设备实例。

函数返回lv_indev_t结构体指针,该结构体存储输入设备的全部运行时数据。

内存分配与初始化

lv_ll_ins_head(indev_ll_head)通过链表管理器为输入设备分配内存,同时将新设备插入全局链表头部。

返回的lv_indev_t指针包含以下关键字段:

  • read_timer:通过lv_timer_create()创建定时器,周期性地调用lv_indev_read_timer_cb触发输入事件处理
  • type:初始化为LV_INDEV_TYPE_NONE,需通过lv_indev_set_type()显式设置
  • read_cb:初始化为NULL,需通过lv_indev_set_read_cb()绑定具体设备的读取函数
回调机制实现

lv_indev_set_read_cb()将开发者实现的设备读取函数指针存入indev->read_cb。当定时器触发lv_indev_read()时,会通过该指针调用具体设备的读取逻辑:

c 复制代码
if(indev->read_cb) 
{
    indev->read_cb(indev, &data); // 回调开发者实现的硬件读取接口
}
输入数据处理流程
  1. 类型分发 :根据indev->type进入对应处理器。以触摸屏(LV_INDEV_TYPE_POINTER)为例:

    c 复制代码
    indev_pointer_proc(indev, &data); // 处理坐标数据
  2. 状态机处理 :在indev_pointer_proc()中:

    • 更新坐标act_point和当前活动对象act_obj
    • 根据state字段(PRESSED/RELEASED)分发给indev_proc_press()indev_proc_release()
  3. 事件生成 :在按压处理中通过lv_obj_send_event()发送标准事件:

    c 复制代码
    lv_obj_send_event(indev_obj_act, LV_EVENT_PRESSED, indev_act);
关键数据结构

lv_indev_t包含输入设备的完整上下文:

c 复制代码
typedef struct _lv_indev_t 
{
    lv_indev_type_t type;        // 设备类型标识
    lv_indev_read_cb_t read_cb;  // 设备级读取回调
    lv_indev_state_t state;      // PRESSED/RELEASED状态
    struct _lv_display_t * disp; // 绑定到特定显示器
    lv_timer_t * read_timer;     // 输入轮询定时器
    
    union 
    {
        lv_point_t act_point;   // 指针设备当前坐标
        uint32_t last_key;      // 键盘设备最后按键
    };
    // ...其他手势/滚动参数...
} lv_indev_t;
⭕union

union 是一种特殊的 C 语言结构,允许同一块内存存储不同的数据类型(如 lv_point_tuint32_t),但同一时间只能使用其中一个成员,以节省内存空间

20.(C语言)联合和枚举全

code:

c 复制代码
union {
    lv_point_t act_point;  // 用于存储指针坐标(如触摸屏位置)
    uint32_t last_key;     // 用于存储键盘按键值
};
  • 共享内存act_pointlast_key 共用同一块内存,修改其中一个会影响另一个的值
  • 应用场景:适合在设备只能触发一种输入(如触摸或按键)时复用内存,减少资源占用。

调用

开发者需要实现三个基础操作:

  1. 创建设备实例
  2. 设置设备类型
  3. 绑定读取回调
c 复制代码
lv_indev_t * touchpad = lv_indev_create();
lv_indev_set_type(touchpad, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(touchpad, my_touchpad_read);

总结

至此我们已成功为LVGL应用连接输入设备!本章要点包括:

  • lv_indev是处理用户输入的核心模块
  • LVGL支持多种输入设备类型
  • 如何创建lv_indev_t对象、设置类型并提供硬件数据读取回调
  • lv_indev如何将原始输入转化为控件交互
  • "控件组"对键盘和编码器导航的重要性

配置完输入设备后,我们精心设计的样式化控件已具备完整交互能力!下一步将深入探索控件如何响应这些交互事件。

下一章:事件系统(lv_event)