试用 LVGL Pro,一套完整的工具包,助您高效构建、测试、分享和交付 UI!
LVGL
教程

使用自定义功能扩展 LVGL 对象

学习如何使用扩展数据、自定义设计函数和信号处理器为 LVGL 对象添加自定义数据、设计函数和行为。

加博尔·基什-瓦莫希加博尔·基什-瓦莫希15 分钟阅读

尽管 LVGL 中的对象具有许多内置功能,但有时您可能需要一些特殊的东西。例如,在对象上绘制独特的内容或改变其行为。在这篇文章中,我将向您展示如何为 LVGL 中的对象添加额外的功能。您还将了解 LVGL 的底层工作原理。

继承#

也许您已经了解 LVGL 中对象类型之间的继承关系。如果不了解也不必担心——您现在就可以学习它。

虽然 C 语言不像 C++ 那样支持类和继承,但通过一些技巧和限制,可以实现类似的功能。在 LVGL 中,继承意味着派生对象知道其祖先的所有特性。

例如,按钮继承自容器。容器具有布局功能,因此按钮也具有此功能。容器继承自基础对象。基础对象知道所有基本的东西,如位置和大小。因此容器和按钮也具有位置和大小属性。

显示基础对象、容器和按钮层次结构的继承关系图
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

这可能看起来有点复杂。但通常您不必编写一个全新的设计函数,只需扩展现有的设计函数即可。

让我们继续前面的"按钮 + 温度"示例,并使用自定义设计函数在按钮上显示存储的温度数据。为简单起见,文本未垂直对齐。

显示自定义温度文本的按钮
扩展设计函数为 LVGL 按钮添加自定义文本
/*在此处存储原始按钮设计函数*/
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

动画示例#

现在使用一些额外的扩展数据和新的设计函数创建一个自定义动画。该动画将在按钮释放时将其外观更改为按下样式并返回。

在状态之间转换的动画按钮
使用 LVGL 实现的动画按钮
/*在此处存储原始按钮设计函数*/
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。例如,页面对象使用它来限制可滚动区域的滚动。

滚动限制在边界内的页面对象
页面上的滚动受到限制

信号函数始终首先调用其祖先的信号函数。这样,对象类型可以继承其祖先的行为。例如,如果按钮的大小发生了变化,则需要由容器(祖先)重新计算布局。

那么让我们看看发生了什么:

  1. 您调用 lv_obj_set_size(btn, width, height)
  2. lv_obj_set_size 向按钮发送 LV_SIGNAL_CORD_CHG
  3. 在按钮的信号函数中,调用其祖先(容器)的信号函数
  4. 在容器的信号函数中,重新计算布局——即根据新大小重新定位子对象
  5. 最后,按钮处理信号;但是,它对 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 最重要的三个内部机制:

  • 扩展数据
  • 设计函数
  • 信号函数

使用它们根据您的需求自定义现有对象!

关于作者

加博尔·基什-瓦莫希
加博尔·基什-瓦莫希

LVGL 创始人

LVGL 的创始人和首席开发者,这是一个被全球数百万设备使用的开源嵌入式图形库。

认识博客背后的作者们

了解那些分享 LVGL 知识的优秀作者们

查看作者

订阅我们的通讯 不错过任何关于 LVGL 的新闻。我们每月最多发送 2 封邮件。

LVGL

LVGL 是最受欢迎的免费开源嵌入式图形库,支持任何 MCU、MPU 和显示类型,助您构建精美的用户界面。

我们还提供 UI 设计、实现和咨询等服务。

© 2026 LVGL。保留所有权利。
YouTubeGitHubLinkedIn