3、创建第一个自定义控件:lv_obj_create扩展、注册新类、实现构造函数、基础绘制

好,咱们正式开始动手了。

前面两章聊了不少概念,什么控件树啊、消息传递啊、绘制流程啊。说实话,光听理论容易犯困。我自己学LVGL那会儿,也是看了半天文档,不如亲手写一个控件来得痛快。

这一章,我们就来创建第一个自定义控件。目标很明确:基于lv_obj扩展,注册一个新类,实现构造函数,然后做最基础的绘制。

3.1 为什么要从lv_obj扩展?

你想想看,LVGL里所有的控件,什么按钮、标签、滑块,本质上都是lv_obj的“变种”。它们继承了lv_obj的基本属性——位置、大小、父对象、样式——然后加上自己特有的行为和外观。

所以,创建自定义控件的第一步,就是先创建一个lv_obj,然后告诉LVGL:“嘿,我要在这个基础上搞点新花样。”

核心思路: 自定义控件 = 标准lv_obj + 私有数据 + 自定义绘制 + 自定义事件处理

我在项目中遇到过不少新手,一上来就想直接画一个全新的控件,连基础对象都不要了。结果折腾半天,连点击事件都收不到。嗯,这里要注意:永远从lv_obj开始扩展,这是最稳妥的路。

3.2 注册新类:告诉LVGL“我来了”

在LVGL里,每个控件类型都有一个对应的“类”。比如按钮是lv_button_class,标签是lv_label_class。我们要做的,就是注册一个自己的类。

注册类的过程,说白了就是填一张表。这张表叫lv_obj_class_t,里面记录了控件的构造函数、析构函数、事件处理函数、绘制函数等等。

看代码:

// 1. 声明一个类结构体
static lv_obj_class_t my_widget_class;

// 2. 实现构造函数
static void my_widget_constructor(const lv_obj_class_t * class_p, lv_obj_t * obj)
{
    LV_UNUSED(class_p);
    lv_obj_t * parent = lv_obj_get_parent(obj);
    /* 调用父类的构造函数 */
    lv_obj_constructor(&my_widget_class, obj);
    
    /* 初始化私有数据 */
    /* 这里先留空,后面会补 */
}

// 3. 注册类
void my_widget_init(void)
{
    lv_obj_class_t * cls = &my_widget_class;
    cls->base_class = &lv_obj_class;  // 继承自lv_obj
    cls->constructor_cb = my_widget_constructor;
    cls->destructor_cb = NULL;        // 暂时不实现析构
    cls->event_cb = NULL;             // 暂时不处理事件
    cls->width_def = LV_SIZE_CONTENT; // 默认宽度
    cls->height_def = LV_SIZE_CONTENT;// 默认高度
    cls->instance_size = sizeof(lv_obj_t); // 实例大小
    
    lv_obj_class_register(cls);
}

这段代码看起来简单,但有几个坑我得提醒你。

我曾经踩过的坑:

  • 构造函数里一定要调用父类的构造函数。我刚开始写的时候忘了这步,结果控件创建出来,位置和大小全乱套了。因为父类的初始化没执行,基础属性都是未定义状态。
  • instance_size 要设置正确。如果你的控件有私有数据,这里要改成 sizeof(my_widget_t)。我见过有人忘了改,结果私有数据的内存根本没分配,一访问就崩。
  • 注册函数只调用一次。建议放在 lv_init() 之后,或者用一个全局初始化函数统一管理。

3.3 实现构造函数:让控件“活”起来

构造函数是控件生命周期的起点。每次你调用 lv_my_widget_create(parent) 时,LVGL内部会先分配内存,然后调用构造函数。

构造函数的任务很简单:

  • 调用父类构造函数,完成基础初始化
  • 设置控件的默认样式
  • 初始化私有数据(如果有的话)
  • 做一些你想要的默认配置

来看一个更完整的例子:

typedef struct {
    int32_t value;
    lv_color_t bg_color;
} my_widget_t;

static void my_widget_constructor(const lv_obj_class_t * class_p, lv_obj_t * obj)
{
    LV_UNUSED(class_p);
    
    /* 先调用父类构造函数 */
    lv_obj_constructor(&my_widget_class, obj);
    
    /* 获取私有数据指针 */
    my_widget_t * priv = (my_widget_t *)obj;
    
    /* 初始化私有数据 */
    priv->value = 0;
    priv->bg_color = lv_color_hex(0x2196F3);
    
    /* 设置一些默认样式 */
    lv_obj_set_size(obj, 100, 50);
    lv_obj_set_style_border_width(obj, 2, 0);
    lv_obj_set_style_border_color(obj, lv_color_hex(0x333333), 0);
}

我个人习惯在构造函数里只做“必须的”初始化。那些可以在运行时动态修改的属性,比如文字内容、图标等,我倾向于提供专门的 setter 函数。这样构造函数保持轻量,调用也快。

3.4 基础绘制:让控件“看得见”

控件创建出来了,构造函数也跑了,但屏幕上啥也没有。为什么?因为没有实现绘制函数。

LVGL的绘制流程是这样的:当控件需要重绘时,LVGL会调用类的 draw_cb 回调。在这个回调里,你可以用LVGL提供的绘制API,想画什么就画什么。

先来个最简单的:画一个带圆角的矩形,中间显示一个数字。

static void my_widget_draw(lv_event_t * e)
{
    lv_obj_t * obj = lv_event_get_target(e);
    lv_draw_ctx_t * draw_ctx = lv_event_get_draw_ctx(e);
    my_widget_t * priv = (my_widget_t *)obj;
    
    /* 获取控件的坐标区域 */
    lv_area_t coords;
    lv_obj_get_coords(obj, &coords);
    
    /* 绘制背景矩形 */
    lv_draw_rect_dsc_t rect_dsc;
    lv_draw_rect_dsc_init(&rect_dsc);
    rect_dsc.bg_color = priv->bg_color;
    rect_dsc.radius = 8;
    rect_dsc.border_width = 2;
    rect_dsc.border_color = lv_color_hex(0x333333);
    
    lv_draw_rect(draw_ctx, &rect_dsc, &coords);
    
    /* 绘制文字 */
    lv_draw_label_dsc_t label_dsc;
    lv_draw_label_dsc_init(&label_dsc);
    label_dsc.color = lv_color_hex(0xFFFFFF);
    label_dsc.font = &lv_font_montserrat_20;
    
    char buf[16];
    lv_snprintf(buf, sizeof(buf), "%d", priv->value);
    
    lv_area_t label_area;
    label_area.x1 = coords.x1 + 10;
    label_area.y1 = coords.y1 + 10;
    label_area.x2 = coords.x2 - 10;
    label_area.y2 = coords.y2 - 10;
    
    lv_draw_label(draw_ctx, &label_dsc, &label_area, buf, LV_TEXT_ALIGN_CENTER);
}

// 在注册类时,加上绘制回调
cls->draw_cb = my_widget_draw;

小技巧:

绘制回调里不要做复杂的计算。LVGL的绘制是逐帧执行的,如果绘制函数太慢,整个UI都会卡顿。我一般把需要计算的数据提前算好,存到私有数据里,绘制时直接拿来用。

3.5 完整的控件代码

把上面几块拼起来,就是一个完整的自定义控件了。我习惯把声明和实现分开,这样结构清晰。

my_widget.h

#ifndef MY_WIDGET_H
#define MY_WIDGET_H

#ifdef __cplusplus
extern "C" {
#endif

#include "lvgl.h"

/* 创建控件的函数 */
lv_obj_t * lv_my_widget_create(lv_obj_t * parent);

/* 初始化控件类(注册) */
void lv_my_widget_init(void);

/* 设置控件的值 */
void lv_my_widget_set_value(lv_obj_t * obj, int32_t value);

#ifdef __cplusplus
}
#endif

#endif

my_widget.c

#include "my_widget.h"

/* 私有数据结构 */
typedef struct {
    int32_t value;
    lv_color_t bg_color;
} my_widget_t;

/* 类声明 */
static lv_obj_class_t my_widget_class;

/* 构造函数 */
static void my_widget_constructor(const lv_obj_class_t * class_p, lv_obj_t * obj)
{
    LV_UNUSED(class_p);
    lv_obj_constructor(&my_widget_class, obj);
    
    my_widget_t * priv = (my_widget_t *)obj;
    priv->value = 0;
    priv->bg_color = lv_color_hex(0x2196F3);
    
    lv_obj_set_size(obj, 100, 50);
}

/* 绘制函数 */
static void my_widget_draw(lv_event_t * e)
{
    lv_obj_t * obj = lv_event_get_target(e);
    lv_draw_ctx_t * draw_ctx = lv_event_get_draw_ctx(e);
    my_widget_t * priv = (my_widget_t *)obj;
    
    lv_area_t coords;
    lv_obj_get_coords(obj, &coords);
    
    /* 绘制背景 */
    lv_draw_rect_dsc_t rect_dsc;
    lv_draw_rect_dsc_init(&rect_dsc);
    rect_dsc.bg_color = priv->bg_color;
    rect_dsc.radius = 8;
    lv_draw_rect(draw_ctx, &rect_dsc, &coords);
    
    /* 绘制数值 */
    lv_draw_label_dsc_t label_dsc;
    lv_draw_label_dsc_init(&label_dsc);
    label_dsc.color = lv_color_hex(0xFFFFFF);
    label_dsc.font = &lv_font_montserrat_20;
    
    char buf[16];
    lv_snprintf(buf, sizeof(buf), "%d", priv->value);
    
    lv_area_t label_area;
    label_area.x1 = coords.x1 + 5;
    label_area.y1 = coords.y1 + 5;
    label_area.x2 = coords.x2 - 5;
    label_area.y2 = coords.y2 - 5;
    
    lv_draw_label(draw_ctx, &label_dsc, &label_area, buf, LV_TEXT_ALIGN_CENTER);
}

/* 注册类 */
void lv_my_widget_init(void)
{
    lv_obj_class_t * cls = &my_widget_class;
    cls->base_class = &lv_obj_class;
    cls->constructor_cb = my_widget_constructor;
    cls->destructor_cb = NULL;
    cls->event_cb = NULL;
    cls->draw_cb = my_widget_draw;
    cls->width_def = LV_SIZE_CONTENT;
    cls->height_def = LV_SIZE_CONTENT;
    cls->instance_size = sizeof(my_widget_t);
    
    lv_obj_class_register(cls);
}

/* 创建控件 */
lv_obj_t * lv_my_widget_create(lv_obj_t * parent)
{
    lv_obj_t * obj = lv_obj_class_create_obj(&my_widget_class, parent);
    lv_obj_class_init_obj(obj);
    return obj;
}

/* 设置值 */
void lv_my_widget_set_value(lv_obj_t * obj, int32_t value)
{
    my_widget_t * priv = (my_widget_t *)obj;
    priv->value = value;
    lv_obj_invalidate(obj);  // 触发重绘
}

3.6 使用这个控件

在 main 函数里,先调用 lv_my_widget_init() 注册类,然后就能像使用标准控件一样创建它了。

int main(void)
{
    lv_init();
    
    /* 注册自定义控件类 */
    lv_my_widget_init();
    
    /* 创建屏幕 */
    lv_obj_t * scr = lv_scr_act();
    
    /* 创建自定义控件 */
    lv_obj_t * widget = lv_my_widget_create(scr);
    lv_obj_align(widget, LV_ALIGN_CENTER, 0, 0);
    
    /* 设置值 */
    lv_my_widget_set_value(widget, 42);
    
    while(1) {
        lv_task_handler();
        lv_delay_ms(5);
    }
}

总结一下这一章的核心:

  • 自定义控件从 lv_obj 扩展,继承基础能力
  • 注册新类需要填写 lv_obj_class_t 结构体
  • 构造函数里一定要调用父类构造函数
  • 绘制函数用 LVGL 的绘制 API,想画什么画什么
  • 修改私有数据后,调用 lv_obj_invalidate() 触发重绘

嗯,到这里,你的第一个自定义控件就跑起来了。虽然它还很简陋,但骨架已经搭好了。下一章,我们会给它加上事件处理,让它能响应用户的点击和触摸。

到时候你会发现,控件“活”起来的感觉,真的很爽。