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()触发重绘
嗯,到这里,你的第一个自定义控件就跑起来了。虽然它还很简陋,但骨架已经搭好了。下一章,我们会给它加上事件处理,让它能响应用户的点击和触摸。
到时候你会发现,控件“活”起来的感觉,真的很爽。