LVGL学习笔记

243 阅读8分钟

lvgl学习第一期 基本概念, 基本流程 , 绑定回调函数, 焦点控制, 渲染和输入设备解耦

lvgl的基本概念:

1.对象(Object / Widget)
LVGL 的界面由“对象”组成,比如按钮、标签、滑块、进度条等。每个对象都有:

-   位置和大小
-   样式(颜色、字体、边框等)
-   事件回调(点击、滑动、触摸)
  • 2.屏幕(Screen)

    • LVGL 有多个“屏”,类似多页面,可以用 lv_scr_load() 切换屏幕。
  • 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。