尽管 LVGL 中的对象具有许多内置功能,但有时您可能需要一些特殊的东西。例如,在对象上绘制独特的内容或改变其行为。在这篇文章中,我将向您展示如何为 LVGL 中的对象添加额外的功能。您还将了解 LVGL 的底层工作原理。
继承#
也许您已经了解 LVGL 中对象类型之间的继承关系。如果不了解也不必担心——您现在就可以学习它。
虽然 C 语言不像 C++ 那样支持类和继承,但通过一些技巧和限制,可以实现类似的功能。在 LVGL 中,继承意味着派生对象知道其祖先的所有特性。
例如,按钮继承自容器。容器具有布局功能,因此按钮也具有此功能。容器继承自基础对象。基础对象知道所有基本的东西,如位置和大小。因此容器和按钮也具有位置和大小属性。

因为基础对象是每个对象类型的祖先(有时通过其他对象类型间接继承),所以每个对象类型都具有大小、位置和类似的基本属性。
扩展数据#
在 LVGL 中,每个对象类型都有一个数据结构(struct),用于保存对象的数据。这被称为扩展数据。
例如,对于按钮,此结构存储当前状态(按下、释放等)以及每个状态的样式。
为了实现继承,祖先应该以相同的方式查看对象,而不管是否从它派生了其他内容。因此容器不应该关心它到底是一个容器还是一个按钮——布局功能应该能够工作。为了实现这一点,扩展数据的声明如下:
/*祖先的扩展数据示例 */
typedef struct {
int value1;
int value2;
} ancestor_t;
/*派生对象的扩展数据示例*/
typedef struct {
ancestor_t ancestor; /*首先包含祖先的扩展数据*/
char * text; /*添加新数据*/
} derived_t;c关键是首先添加祖先的扩展数据。这样,如果祖先将扩展数据读取为 ancestor_t,它将看到正确的类型;如果派生对象读取它,它将看到祖先 + 派生数据。
向对象类型添加自定义扩展数据#
让我们看一个简单但真实的例子。在按钮的扩展数据中存储一个整数温度值:
/*基于按钮的自定义扩展数据*/
typedef struct {
lv_btn_ext_t btn; /*祖先按钮结构*/
int temp; /*新的自定义数据*/
}temp_btn_ext_t;
/**
* 单击按钮时调用
* @param btn 指向按钮的指针
* @return LV_RES_OK 因为按钮在此处未被删除
*/
lv_res_t btn_click(lv_obj_t * btn)
{
/*获取扩展数据*/
temp_btn_ext_t * ext = lv_obj_get_ext_attr(btn);
/*对温度值执行某些操作*/
printf("Button temp: %d\n", ext->temp);
return LV_RES_OK;
}
void test(void)
{
lv_obj_t * btn = lv_btn_create(lv_scr_act(), NULL);
lv_btn_set_action(btn, LV_BTN_ACTION_CLICK, btn_click); /*分配一个动作函数*/
lv_obj_allocate_ext_attr(btn, sizeof(temp_btn_ext_t)); /*重新分配扩展数据*/
/*使用自定义扩展数据*/
temp_btn_ext_t * ext = lv_obj_get_ext_attr(btn);
ext->temp = 42;
}c就是这样。现在您知道如何向对象添加任何自定义数据了。
自定义函数#
在 LVGL 中,每个对象都有两个特殊函数:
- 设计函数定义对象的外观
- 信号函数定义对象的行为
设计函数#
当需要重新绘制对象时,库会自动调用对象的设计函数。绘制矩形的简单设计函数如下所示:
/**
* 示例设计函数
* @param obj 指向要绘制的对象的指针
* @param mask 仅此区域将被重新绘制
* @param mode 见下文
* @return 如果 mode == LV_DESIGN_COVER_CHK:
* 如果对象完全覆盖遮罩区域,则为 true
* 否则:
* 始终为 true
*/
bool example_design(lv_obj_t * obj,
const lv_area_t * mask,
lv_design_mode_t mode)
{
if(mode == LV_DESIGN_COVER_CHK) {
/* 决定对象的坐标 (obj->coords)
* 是否完全覆盖遮罩 */
if(cover) return true;
else return false;
}
else if(mode == LV_DESIGN_DRAW_MAIN) {
/* 绘制对象。例如绘制一个矩形。 */
lv_style_t * style = lv_obj_get_style(obj);
lv_draw_rect(&obj->coords, mask_p, style, lv_obj_get_opa_scale(obj));
}
else if(mode == LV_DESIGN_DRAW_POST) {
/* 在绘制完此对象的所有子对象后,
* 您可以在此处进行进一步的绘制。
* 例如,lv_page 的滚动条在此处绘制以保持在顶部*/
}
return true;
}c这可能看起来有点复杂。但通常您不必编写一个全新的设计函数,只需扩展现有的设计函数即可。
让我们继续前面的"按钮 + 温度"示例,并使用自定义设计函数在按钮上显示存储的温度数据。为简单起见,文本未垂直对齐。

/*在此处存储原始按钮设计函数*/
static lv_design_func_t old_btn_design;
bool new_btn_design(lv_obj_t * obj, const lv_area_t * mask, lv_design_mode_t mode)
{
if(mode == LV_DESIGN_COVER_CHK) {
/*使用原始函数进行覆盖检查*/
return old_btn_design(obj, mask, mode);
} else if(mode == LV_DESIGN_DRAW_MAIN) {
/*首先绘制原始按钮*/
old_btn_design(obj, mask, mode);
/*绘制标签*/
temp_btn_ext_t * ext = lv_obj_get_ext_attr(obj);
char buf[32];
sprintf(buf, "Temp\n%d", ext->temp);
lv_draw_label(&obj->coords, mask, &lv_style_btn_rel, lv_obj_get_opa_scale(obj), buf, LV_TXT_FLAG_CENTER, NULL);
} else if(mode == LV_DESIGN_DRAW_POST) {
/*使用原始函数进行后续绘制*/
old_btn_design(obj, mask, mode);
}
return true;
}
void test_btn(void)
{
lv_obj_t * btn = lv_btn_create(lv_scr_act(), NULL);
old_btn_design = lv_obj_get_design_func(btn);
lv_obj_set_design_func(btn, new_btn_design);
}c动画示例#
现在使用一些额外的扩展数据和新的设计函数创建一个自定义动画。该动画将在按钮释放时将其外观更改为按下样式并返回。

/*在此处存储原始按钮设计函数*/
static lv_design_func_t old_btn_design;
/**
* 由动画调用
* @param btn 指向要动画的按钮的指针
* @param value 动画的当前值
*/
static void btn_anim_func(void * btn, int32_t value)
{
/* 保存新值并刷新对象。
* `anim_state` 将在设计函数中使用*/
temp_btn_ext_t * ext = lv_obj_get_ext_attr(btn);
ext->anim_state = value;
lv_obj_invalidate(btn);
}
/**
* 单击按钮时调用
* @param btn 指向按钮的指针
* @return LV_RES_OK 因为按钮在此处未被删除
*/
lv_res_t btn_click(lv_obj_t * btn)
{
/*启动播放返回动画 */
lv_anim_t a;
a.var = btn;
a.start = 0;
a.end = 255;
a.fp = (lv_anim_fp_t)btn_anim_func;
a.path = lv_anim_path_linear;
a.end_cb = NULL;
a.act_time = 0;
a.time = 200;
a.playback = 1;
a.playback_pause = 100;
a.repeat = 0;
a.repeat_pause = 0;
lv_anim_create(&a);
return LV_RES_OK;
}
bool new_btn_design(lv_obj_t * obj, const lv_area_t * mask, lv_design_mode_t mode)
{
if(mode == LV_DESIGN_COVER_CHK) {
/*使用原始函数进行覆盖检查*/
return old_btn_design(obj, mask, mode);
} else if(mode == LV_DESIGN_DRAW_MAIN) {
/*使用正常释放样式绘制矩形*/
lv_draw_rect(&obj->coords, mask,
lv_btn_get_style(obj, LV_BTN_STYLE_REL), lv_obj_get_opa_scale(obj));
/* 在前一个矩形上方绘制另一个具有按下状态的矩形,但
* 根据 `anim_state` 设置其不透明度*/
temp_btn_ext_t * ext = lv_obj_get_ext_attr(obj);
lv_style_t style;
lv_style_copy(&style, lv_btn_get_style(obj, LV_BTN_STYLE_PR));
style.body.opa = ext->anim_state;
lv_draw_rect(&obj->coords, mask, &style, lv_obj_get_opa_scale(obj));
} else if(mode == LV_DESIGN_DRAW_POST) {
/*使用原始函数进行后续绘制*/
old_btn_design(obj, mask, mode);
}
return true;
}
void test_btn(void)
{
lv_obj_t * btn = lv_btn_create(lv_scr_act(), NULL);
lv_btn_set_action(btn, LV_BTN_ACTION_CLICK, btn_click);
lv_obj_allocate_ext_attr(btn, sizeof(temp_btn_ext_t)); /*重新分配扩展数据*/
/*初始化自定义扩展数据*/
temp_btn_ext_t * ext = lv_obj_get_ext_attr(btn);
ext->anim_state = 0;
/*保存旧设计函数并设置新函数 */
old_btn_design = lv_obj_get_design_func(btn);
lv_obj_set_design_func(btn, new_btn_design);
}c信号函数#
信号函数负责对象的行为。当对象发生某些事情时,库会自动向对象发送信号以通知它。信号在信号函数中处理,对象可以根据事件执行某些操作。
例如,当释放对象时,会发送 LV_SIGNAL_RELEASED。在这种情况下,按钮会调用用户定义的释放动作。
另一个信号是 LV_SIGNAL_CORD_CHG。例如,页面对象使用它来限制可滚动区域的滚动。

信号函数始终首先调用其祖先的信号函数。这样,对象类型可以继承其祖先的行为。例如,如果按钮的大小发生了变化,则需要由容器(祖先)重新计算布局。
那么让我们看看发生了什么:
- 您调用
lv_obj_set_size(btn, width, height) lv_obj_set_size向按钮发送LV_SIGNAL_CORD_CHG- 在按钮的信号函数中,调用其祖先(容器)的信号函数
- 在容器的信号函数中,重新计算布局——即根据新大小重新定位子对象
- 最后,按钮处理信号;但是,它对
LV_SIGNAL_CORD_CHG无需执行任何操作
因此,通过从容器继承按钮,按钮可以使用容器的所有功能。
使用自定义信号函数限制位置#
我将向您展示一个如何创建信号函数的示例,该函数:
- 不让按钮被拖出其父对象
- 仅启用水平拖动
- 在释放时将按钮动画到中间
没有新信号函数时的行为如下:

但是使用新的信号函数:

static lv_signal_func_t old_btn_signal; /*存储旧信号函数*/
static lv_obj_t * info_label; /*演示用标签*/
/**
* 新的信号函数
* @param btn 指向按钮对象的指针
* @param sign 来自 `lv_signal_t` 枚举的信号类型
* @param param 指向信号特定变量的指针
* @return LV_RES_OK: 对象在函数中未被删除;LV_RES_INV: 对象被删除
*/
static lv_res_t new_btn_signal(lv_obj_t * btn, lv_signal_t sign, void * param)
{
lv_res_t res;
/* 必须:包含祖先信号函数 */
res = old_btn_signal(btn, sign, param);
if(res != LV_RES_OK) return res;
/*如果坐标已更改,则限制水平移动*/
if(sign == LV_SIGNAL_CORD_CHG) {
lv_coord_t x = lv_obj_get_x(btn);
lv_coord_t w = lv_obj_get_width(btn);
lv_obj_t * parent = lv_obj_get_parent(btn);
lv_coord_t parent_w = lv_obj_get_width(parent);
if(x < 0) x = 0;
else if(x + w > parent_w) x = parent_w - w;
/*垂直对齐到中间并使用受限的 x 坐标*/
lv_obj_align(btn, parent, LV_ALIGN_IN_LEFT_MID, x, 0);
}
/*如果旋钮被释放... */
else if(sign == LV_SIGNAL_RELEASED || sign == LV_SIGNAL_PRESS_LOST) {
lv_coord_t x = lv_obj_get_x(btn);
lv_coord_t w = lv_obj_get_width(btn);
lv_obj_t * parent = lv_obj_get_parent(btn);
lv_coord_t parent_w = lv_obj_get_width(parent);
/* 如果拖动按钮,则不会调用单击动作。
* 因此手动调用它*/
lv_action_t click_action = lv_btn_get_action(btn, LV_BTN_ACTION_CLICK);
if(click_action) click_action(btn);
/*将旋钮动画回到中间*/
lv_anim_t a;
a.var = btn;
a.start = x;
a.end = parent_w / 2 - w / 2;
a.fp = (lv_anim_fp_t)lv_obj_set_x;
a.path = lv_anim_path_linear;
a.end_cb = NULL;
a.act_time = 0;
a.time = 200;
a.playback = 0;
a.playback_pause = 0;
a.repeat = 0;
a.repeat_pause = 0;
lv_anim_create(&a);
}
return res;
}
/* 释放按钮时调用 */
static lv_res_t btn_click(lv_obj_t* btn)
{
lv_coord_t x = lv_obj_get_x(btn);
lv_obj_t * parent = lv_obj_get_parent(btn);
lv_coord_t parent_w = lv_obj_get_width(parent);
/*确定一侧并更新信息标签*/
if(x < parent_w / 2) lv_label_set_text(info_label, "Left");
else lv_label_set_text(info_label, "Right");
lv_obj_align(info_label, parent, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
return LV_RES_OK;
}
void test_knob(void)
{
/*创建一个父对象(灰色矩形)*/
lv_obj_t * parent = lv_obj_create(lv_scr_act(), NULL);
lv_obj_set_size(parent, 200, 50);
lv_obj_set_style(parent, &lv_style_pretty);
/*创建一个方形矩形按钮*/
lv_obj_t * btn = lv_btn_create(parent, NULL);
old_btn_signal = lv_obj_get_signal_func(btn); /*保存旧信号函数*/
lv_obj_set_signal_func(btn, new_btn_signal); /*设置新信号函数*/
lv_btn_set_action(btn, LV_BTN_ACTION_CLICK, btn_click); /*设置动作*/
lv_obj_set_drag(btn, true); /*启用拖动*/
lv_obj_set_size(btn, 40, 40); /*设置大小*/
lv_obj_align(btn, parent, LV_ALIGN_CENTER, 0, 0); /*对齐到父对象的中心*/
lv_obj_set_protect(btn, LV_PROTECT_PRESS_LOST); /*防止在快速拖动时丢失按压*/
/*为演示创建信息标签*/
info_label = lv_label_create(lv_scr_act(), NULL);
lv_label_set_text(info_label, "Drag the knob");
lv_obj_align(info_label, parent, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
}c总结#
您刚刚学习了 LVGL 最重要的三个内部机制:
- 扩展数据
- 设计函数
- 信号函数
使用它们根据您的需求自定义现有对象!
