第五章:布局系统(lv_flex, lv_grid)
欢迎回来!
在第四章:样式(lv_style)中,我们掌握了如何通过色彩、字体和圆角等特性美化部件。当界面元素具备视觉吸引力后,如何优雅地组织它们便成为新的挑战。
设想我们拥有多个精美按钮,希望实现以下布局效果:
- 横向/纵向等距排列
- 屏幕尺寸变化时自动适配
- 动态增删元素时自动调整
传统手工计算坐标的方式显然低效且难以维护,这正是布局系统的价值所在。
布局系统核心价值
LVGL布局系统受现代网页设计(CSS Flexbox/Grid)启发,通过声明式配置实现:
布局类型概览
布局类型 | 适用场景 | 典型应用 |
---|---|---|
弹性布局 | 单向流式排列 | 导航栏|设置项列表 |
网格布局 | 二维矩阵排列 | 仪表盘|相册缩略图 |
启用布局模块
在lv_conf.h
中激活配置:
/*==================* 布局模块*================*/
#define LV_USE_FLEX 1 // 启用弹性布局
#define LV_USE_GRID 1 // 启用网格布局
弹性布局(lv_flex)
1. 容器初始化
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. 排列方向控制
// 横向排列(可换行)
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. 对齐方式
// 主轴居中|交叉轴居中|轨道居中
lv_obj_set_flex_align(flex_container, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
4. 空间分配
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. 间距控制
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. 容器初始化
lv_obj_t * grid_container = lv_obj_create(screen_main);
lv_obj_set_layout(grid_container, LV_LAYOUT_GRID); // 声明网格容器
2. 网格结构定义
// 列定义:固定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. 单元格定位
// 按钮定位到(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行
布局系统工作原理
处理流程
核心机制
- 注册机制:通过
lv_obj_set_layout()
将容器注册到布局系统 - 延迟计算:在屏幕刷新周期统一处理布局计算
- 动态响应:容器尺寸变化或子元素增减时自动触发重新布局
实践
- 组合使用:在复杂界面中混合使用
Flex
和Grid
布局 - 响应式设计:结合
LV_PCT
百分比单位和媒体查询实现自适应
- 性能优化:避免深层嵌套布局,控制刷新频率
- 样式分离:将布局样式与视觉样式分离管理
结论
通过本章学习,我们掌握了:
- 弹性布局的
流式排列
与空间分配
技巧 - 网格布局的
二维矩阵
定位方法 - 布局系统的底层运作原理
- 间距控制与对齐策略
布局系统的引入使界面开发从手工计算迈向声明式配置,极大提升了开发效率和可维护性。
下一章我们将探索用户交互的核心——输入设备管理
。
下一章:输入设备(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)创建之后执行。
#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
。
// ...(接续前文代码)void setup_input_device()
{lv_indev_t * my_touchpad_indev = lv_indev_create();// 2. 设置输入设备类型为POINTERlv_indev_set_type(my_touchpad_indev, LV_INDEV_TYPE_POINTER);// ... 后续配置步骤在此添加
}
3. 实现读取回调函数
这是最关键的部分!LVGL需要通过开发者提供的"读取回调"函数定期获取实际触摸数据。
my_touchpad_read
函数需要完成:
- 读取触摸的当前状态(按压或释放)
- 若处于按压状态,读取X/Y坐标
- 填充
lv_indev_data_t
结构体传递这些信息
// 将触摸数据存储为全局变量以便硬件驱动更新
//(例如在中断服务例程或主循环轮询中更新)
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
函数关联至输入设备对象。
// ...(接续前文代码)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
设备通过直接点击
屏幕坐标交互
而KEYPAD
和ENCODER
设备则通过"焦点
"与控件交互。
想象用键盘导航网页:按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控件
的传递过程。
- 轮询/读取:LVGL运行周期性定时器(由第一章:配置(lv_conf.h)中的
LV_DEF_REFR_PERIOD
控制,通常10-50ms)。该定时器触发lv_indev_read_timer_cb
,进而调用各注册输入设备的lv_indev_read
- 读取回调:
lv_indev_read
调用开发者实现的my_touchpad_read
函数,从硬件读取原始X/Y坐标和触摸状态 - 数据处理:将原始数据填入
lv_indev_data_t
结构体并返回 - 查找目标对象:LVGL获取原始输入数据后,对指针设备会基于X/Y坐标遍历显示设备上的所有活动控件(从顶层系统层到底层),查找位于触摸点下的控件。此过程涉及坐标和可见性检查
- 状态与事件管理:确定目标控件后,LVGL更新其内部状态(如
LV_STATE_PRESSED
)并触发相关事件(如LV_EVENT_PRESSED
、LV_EVENT_CLICKED
、LV_EVENT_RELEASED
)。若按压状态移动可能触发滚动或拖拽
简化序列图如下:
LVGL内部代码解析:
调用lv_indev_create()
时,LVGL会为lv_indev_t
结构体分配内存。
该结构体保存指向读取回调函数
的指针、设备类型
、内部状态变量
及关联显示设备指针
。
核心逻辑位于src/indev/lv_indev.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()
时,会通过该指针调用具体设备的读取逻辑:
if(indev->read_cb)
{indev->read_cb(indev, &data); // 回调开发者实现的硬件读取接口
}
输入数据处理流程
-
类型分发:根据
indev->type
进入对应处理器。以触摸屏(LV_INDEV_TYPE_POINTER
)为例:indev_pointer_proc(indev, &data); // 处理坐标数据
-
状态机处理:在
indev_pointer_proc()
中:- 更新坐标
act_point
和当前活动对象act_obj
- 根据
state
字段(PRESSED/RELEASED
)分发给indev_proc_press()
或indev_proc_release()
- 更新坐标
-
事件生成:在按压处理中通过
lv_obj_send_event()
发送标准事件:lv_obj_send_event(indev_obj_act, LV_EVENT_PRESSED, indev_act);
关键数据结构
lv_indev_t
包含输入设备的完整上下文:
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_t
和 uint32_t
),但同一时间只能使用其中一个成员,以节省内存空间
20.(C语言)联合和枚举全
code:
union {lv_point_t act_point; // 用于存储指针坐标(如触摸屏位置)uint32_t last_key; // 用于存储键盘按键值
};
- 共享内存:
act_point
和last_key
共用同一块内存,修改其中一个会影响另一个的值 - 应用场景:适合在设备只能触发一种输入(如触摸或按键)时复用内存,减少资源占用。
调用
开发者需要实现三个基础操作:
创建
设备实例设置
设备类型绑定读取回调
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)