lvgl学习第一期 基本概念, 基本流程 , 绑定回调函数, 焦点控制, 渲染和输入设备解耦
lvgl的基本概念:
1.对象(Object / Widget)
LVGL 的界面由“对象”组成,比如按钮、标签、滑块、进度条等。每个对象都有:
- 位置和大小
- 样式(颜色、字体、边框等)
- 事件回调(点击、滑动、触摸)
-
2.屏幕(Screen)
- LVGL 有多个“屏”,类似多页面,可以用
lv_scr_load()切换屏幕。
- LVGL 有多个“屏”,类似多页面,可以用
-
3.事件回调
- 每个控件都可以绑定事件函数:
lv_obj_event_cb(obj,event_cb,LV_EVENT_XXXX,user_data);
- 每个控件都可以绑定事件函数:
-
4.布局
- 默认是绝对定位,也可以用 LVGL 提供的布局功能(Flex / Grid)。
基本流程:
初始化lvgl
创建控件;
刷新/循环;
绑定控件回调函数
lv_obj_add_event_cb(obj,event_cb,LV_EVENT_XXXX,user_data);
obj : 控件名
event_cb: 用户绑定回调函数 ,在回调函数中 可以处理任务 沟通ui和程序
LV_EVENT_XXXX: 触发事件 ,例如 LV_EVENT_CLICKED
user_data: 用户数据 ,绑定控件时候 可以传入一个数据 (指针 结构体 数字地址) 回调中可以取用
对于user_data参数的讲解
user_data 本质上是一个 void* 指针
-
可以指向:
- 普通变量(int、float…)
- 指针(数组、对象、控件等)
- 函数指针
- 结构体 / 自定义数据
-
用途:
- 修改外部变量
- 调用不同逻辑函数
- 保存控件状态 / 标识
- 在回调里做灵活处理
这样 user_data就具有了非常高的灵活性
示例如下:
- 传递数据 修改变量:
int counter = 0;
void btn_event_cb(lv_event_t * e){
int * p = (int *)lv_event_get_user_data(e); // 拿到变量指针
(*p)++; // 修改变量
printf("counter = %d\n", *p);
}
void create_button(){
lv_obj_t * btn = lv_btn_create(lv_scr_act());
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, &counter);
}
// - 点击按钮 → 回调里通过 `user_data` 增加 `counter`
- 传入函数 执行逻辑
void my_task1(void) { printf("任务1执行\n"); }
void my_task2(void) { printf("任务2执行\n"); }
void btn_event_cb(lv_event_t * e){
void (*func)(void) = lv_event_get_user_data(e); // 拿到函数指针
func(); // 执行
}
void create_buttons(){
lv_obj_t * btn1 = lv_btn_create(lv_scr_act());
lv_obj_add_event_cb(btn1, btn_event_cb, LV_EVENT_CLICKED, my_task1);
lv_obj_t * btn2 = lv_btn_create(lv_scr_act());
lv_obj_add_event_cb(btn2, btn_event_cb, LV_EVENT_CLICKED, my_task2);
}
// 点击不同按钮 执行不同函数
// 高灵活性 可读性更好 不用再cb函数中执行逻辑 只需要从绑定函数中读取used_data指针
- 传入结构体
- 需要多个参数时候 定义结构体 打包传入
typedef struct {
int id;
int * counter;
} BtnData;
void btn_event_cb(lv_event_t * e){
BtnData * data = lv_event_get_user_data(e);
(*data->counter)++;
printf("按钮 %d 点击,counter=%d\n", data->id, *data->counter);
}
void create_button(){
static int counter = 0;
static BtnData data = {1, &counter};
lv_obj_t * btn = lv_btn_create(lv_scr_act());
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, &data);
}
焦点控制(focus)和Group机制
什么是焦点控制
在lvgl中, 每个可交互控件(按钮 滑块 选项等)都可以获得 focus(焦点)
- 焦点控件 : 当前正在被聚焦的控件 可以被操作
- 当使用 键盘 编码器 按键时 焦点控件可以接收事件
- 没有触摸屏时 焦点时必要的 可以实现控制功能
Group(焦点组)
一个Group是lvgl管理一组控件的结构
功能
- 管理控件聚焦顺序
- 控制焦点切换
- 将输入设备和被管理控件绑定到一起 实现控制和切换
*Group内每次只有一个控件会被聚焦 类似人眼聚焦东西 一次一个
实现
创建 Group
lv_group_t * my_group = lv_group_create();
将控件添加到Group
lv_group_add_obj(my_group, btn1);
lv_group_add_obj(my_group, btn2);
lv_group_add_obj(my_group, slider1);
将输入设备添加到代码块
lv_indev_set_group(indev_encoder,my_group);
-经过以上 编码器就可以控制Group中的控件焦点了 -如上 通过编码器切换焦点 通过按下 触发焦点事件
焦点状态控制
LVGL 中控件焦点有几个常用函数:
-
可点击控件才能获得焦点
lv_obj_add_flag(obj, LV_OBJ_FLAG_CLICKABLE); -
手动设置控件获得焦点(通常由 Group 自动管理)
lv_obj_add_state(obj, LV_STATE_FOCUSED); -
直接把 Group 内焦点切换到某个控件
lv_group_focus_obj(obj);
lvgl焦点控制的Api
// 设置焦点到某个对象
lv_group_focus_obj(obj);
// 移动焦点到下一个对象
lv_group_focus_next(group);
// 移动焦点到上一个对象
lv_group_focus_prev(group);
- 数值调用:
lv_event_send(obj, LV_EVENT_KEY, &key);// 给对象发送键盘/编码器事件
-- 或者直接调用对象的 setter,比如 lv_slider_set_value()。
多屏幕焦点控制
具体方式
- 不同屏幕控件 添加到同一个Group中
- 不同屏幕控件 添加到不同的多个Group中 (推荐)
方式一
#include "lvgl.h"
lv_group_t * g; // 一个 group
void example_group_one(void)
{
// 创建屏幕1
lv_obj_t * scr1 = lv_obj_create(NULL);
lv_obj_t * btn1 = lv_btn_create(scr1);
lv_obj_center(btn1);
lv_obj_t * label1 = lv_label_create(btn1);
lv_label_set_text(label1, "Button 1");
// 创建屏幕2
lv_obj_t * scr2 = lv_obj_create(NULL);
lv_obj_t * btn2 = lv_btn_create(scr2);
lv_obj_center(btn2);
lv_obj_t * label2 = lv_label_create(btn2);
lv_label_set_text(label2, "Button 2");
// 创建一个 group
g = lv_group_create();
// 将不同屏幕的控件加入同一个 group
lv_group_add_obj(g, btn1);
lv_group_add_obj(g, btn2);
// 设置编码器/键盘输入设备绑定到这个 group
lv_indev_t * indev = lv_indev_get_next(NULL);
lv_indev_set_group(indev, g);
// 显示 scr1
lv_scr_load(scr1);
}
- 缺点 当切换屏幕后,group 里仍然有另一个屏幕的控件,逻辑容易混乱。
方式二
#include "lvgl.h"
lv_group_t * g1;
lv_group_t * g2;
void example_group_multiple(void)
{
// 创建屏幕1
lv_obj_t * scr1 = lv_obj_create(NULL);
lv_obj_t * btn1 = lv_btn_create(scr1);
lv_obj_center(btn1);
lv_obj_t * label1 = lv_label_create(btn1);
lv_label_set_text(label1, "Button 1");
// 创建屏幕2
lv_obj_t * scr2 = lv_obj_create(NULL);
lv_obj_t * btn2 = lv_btn_create(scr2);
lv_obj_center(btn2);
lv_obj_t * label2 = lv_label_create(btn2);
lv_label_set_text(label2, "Button 2");
// 每个屏幕一个 group
g1 = lv_group_create();
g2 = lv_group_create();
lv_group_add_obj(g1, btn1);
lv_group_add_obj(g2, btn2);
// 获取输入设备
lv_indev_t * indev = lv_indev_get_next(NULL);
// 默认绑定到 scr1 的 group
lv_indev_set_group(indev, g1);
lv_scr_load(scr1);
// 假设你要切换到 scr2 时,改 group
// lv_indev_set_group(indev, g2);
// lv_scr_load(scr2);
}
- 优点:切换屏幕时切换 group,逻辑更清晰,不会误操作不在当前屏幕的控件。
image控件的旋转 缩放
在 LVGL 里,lv_img 控件是否支持旋转、缩放,取决于 配置文件 lv_conf.h 中的几个开关。
默认情况下有些是关闭的,需要你手动打开。
你需要在 lv_conf.h 中增加或修改以下定义:
/* 使能图像变换(旋转、缩放) */
#define LV_USE_IMG_TRANSFORM 1
/* 如果你的版本没有这个宏,则要确认启用了图像缓存和旋转支持 */
#define LV_IMG_CACHE_DEF_SIZE 1 /* 保证至少有一个缓存 */
然后代码中就可以使用了
lv_obj_t *img = lv_img_create(lv_scr_act());
lv_img_set_src(img, &some_image);
/* 旋转角度,单位为 0.1°,例如 450 = 45.0° */
lv_img_set_angle(img, 450);
/* 设置缩放比例,256 = 1.0倍 */
lv_img_set_zoom(img, 256);
注意
-
旋转和缩放属于 软件处理,会占用 MCU 运算时间,如果 MCU 性能有限、屏幕较大,速度会比较慢。
-
如果你发现旋转后花屏,需要确认
LV_COLOR_DEPTH与图片色深一致(常见 16 位 RGB565)
重点 渲染任务和输入设备的解耦
为解决软件渲染刷新图片造成的mcu占用 需要把渲染任务和输入设备的识别放在不同的层次中
方式一 中间变量+定时任务刷新
思路:
- 编码器事件里只更新一个“旋转角度目标值”。
- 图片旋转的刷新交给
lv_timer定时器,周期性读取目标值再更新 UI。
示例
static int32_t encoder_angle = 0; // 全局变量保存角度目标值
static lv_obj_t * img;
// 编码器事件回调(只记录角度,不刷新UI)
void encoder_event_cb(lv_event_t * e) {
int32_t diff = lv_event_get_param(e); // 获取旋转差值
encoder_angle += diff * 5; // 每步5度
if(encoder_angle > 360) encoder_angle -= 360;
if(encoder_angle < 0) encoder_angle += 360;
}
// 定时器回调(真正刷新UI)
void image_update_timer(lv_timer_t * timer) {
lv_img_set_angle(img, encoder_angle * 10); // lv_img用0.1度单位
}
void ui_init(void) {
img = lv_img_create(lv_scr_act());
lv_img_set_src(img, &my_image);
// 绑定编码器输入
lv_group_t * g = lv_group_create();
lv_group_add_obj(g, img);
lv_indev_set_group(lv_get_indev(LV_INDEV_TYPE_ENCODER), g);
lv_obj_add_event_cb(img, encoder_event_cb, LV_EVENT_KEY, NULL);
// 创建定时器,周期刷新UI
lv_timer_create(image_update_timer, 50, NULL); // 50ms刷新一次
}
- 优点 简单 输入不丢失 旋转刷新节奏可控
方法二 队列缓冲
- 转速快 可以在事件回调中 将旋转指令丢进一个队列 然后由定时器来一口气处理
- 对于旋转这种 不追求显示连贯 只要保留最新值就可以了 不许哟啊每次都刷新
方法三 多核心分线程 (多核专用)
-
core0 跑 LVGL 任务(UI 刷新)
-
core1 专门采集编码器,处理消抖,把最终角度写到共享变量
再由 core0 定时读变量刷新 UI。