4、控件生命周期管理:构造函数详解、析构函数、事件处理注册、样式初始化
各位同学,欢迎来到第四章。这一章我们聊聊控件的「生老病死」。
你想想看,一个控件从创建到销毁,中间经历了什么?我刚开始用LVGL那会儿,总觉得控件嘛,创建出来就能用,用完就删掉,多简单。结果呢?内存泄漏、样式错乱、事件不响应……各种坑踩了个遍。
后来我才明白,控件的生命周期管理,是自定义控件开发中最基础也最关键的一环。今天我就把这套东西掰开揉碎了讲给你听。
4.1 构造函数:控件的「出生证明」
构造函数,说白了就是控件的初始化入口。LVGL里每个控件创建时都会调用一个构造函数,我们自定义控件也不例外。
先看一个典型的构造函数长什么样:
// 自定义按钮控件的构造函数
lv_obj_t * my_button_create(lv_obj_t * parent)
{
// 1. 调用父类构造函数
lv_obj_t * obj = lv_btn_create(parent);
// 2. 分配自定义数据
my_button_ext_t * ext = lv_obj_allocate_ext_attr(obj, sizeof(my_button_ext_t));
if(ext == NULL) return NULL;
// 3. 初始化自定义数据
ext->state = MY_BUTTON_STATE_NORMAL;
ext->press_time = 0;
ext->callback = NULL;
// 4. 调用初始化函数
my_button_init(obj);
return obj;
}
这里有几个关键点,我一个个说。
4.1.1 父类构造函数调用顺序
记住一个原则:先调父类,再调自己。这跟C++的构造顺序是一样的道理。
我在项目中遇到过一个问题:有个同事写自定义控件时,先分配了自己的扩展数据,然后才调用父类构造函数。结果父类构造函数里会重置一些内部状态,把他刚初始化好的数据全覆盖了。查了两天才找到原因。
所以,正确的顺序是:
- 调用父类的 create 函数
- 分配自己的扩展数据
- 初始化扩展数据
- 调用自定义的 init 函数
4.1.2 扩展数据结构设计
扩展数据是自定义控件的「灵魂」。我一般这样设计:
typedef struct {
uint8_t state; // 控件状态
uint16_t animation_time; // 动画时长
lv_color_t bg_color; // 背景色
lv_color_t text_color; // 文字颜色
void (*click_cb)(lv_obj_t * obj); // 点击回调
void * user_data; // 用户自定义数据
} my_button_ext_t;
嗯,这里要注意:扩展数据里不要放太大的数组。每个控件都会分配一份,控件多了内存就炸了。我见过有人把1024字节的缓冲区塞进扩展数据里,创建100个控件就是100KB,太浪费了。
4.2 析构函数:体面地「告别」
有生就有死。控件销毁时,我们需要清理它占用的资源。
LVGL里没有C++那种自动调用的析构函数,但提供了事件机制来实现类似功能。我们监听 LV_EVENT_DELETE 事件,在控件被删除前做清理工作。
static void my_button_event_handler(lv_obj_t * obj, lv_event_t event)
{
my_button_ext_t * ext = lv_obj_get_ext_attr(obj);
switch(event) {
case LV_EVENT_DELETE:
// 释放动态分配的内存
if(ext->user_data != NULL) {
lv_mem_free(ext->user_data);
ext->user_data = NULL;
}
// 停止正在运行的动画
lv_anim_del(obj, NULL);
break;
// 其他事件处理...
}
}
我曾经犯过一个错误:在析构时忘记停止动画。结果控件都删了,动画回调还在跑,访问了已经释放的内存,直接硬故障。从那以后,我养成了习惯——析构函数里第一件事就是停止所有动画和定时器。
4.3 事件处理注册:让控件「活」起来
控件没有事件处理,就是个「死」的。事件注册通常在构造函数里完成。
void my_button_init(lv_obj_t * obj)
{
// 注册事件处理函数
lv_obj_set_event_cb(obj, my_button_event_handler);
// 设置默认样式
my_button_set_default_style(obj);
}
事件处理函数的设计,我建议采用「分发模式」:
static void my_button_event_handler(lv_obj_t * obj, lv_event_t event)
{
switch(event) {
case LV_EVENT_PRESSED:
my_button_on_pressed(obj);
break;
case LV_EVENT_RELEASED:
my_button_on_released(obj);
break;
case LV_EVENT_CLICKED:
my_button_on_clicked(obj);
break;
case LV_EVENT_DRAW_POST:
my_button_on_draw_post(obj);
break;
default:
break;
}
}
这样写的好处是:每个事件的处理逻辑独立,后期维护时一目了然。你想想看,如果所有逻辑都堆在一个 switch 里,几百行代码,改起来多痛苦。
4.4 样式初始化:第一印象很重要
样式初始化,说白了就是给控件「化妆」。我一般分两步走:
4.4.1 默认样式设置
void my_button_set_default_style(lv_obj_t * obj)
{
// 获取默认样式
lv_style_t * style = lv_obj_get_style(obj);
// 设置基础样式
lv_style_set_radius(style, LV_STATE_DEFAULT, 5);
lv_style_set_bg_color(style, LV_STATE_DEFAULT, LV_COLOR_BLUE);
lv_style_set_bg_grad_color(style, LV_STATE_DEFAULT, LV_COLOR_NAVY);
lv_style_set_bg_opa(style, LV_STATE_DEFAULT, LV_OPA_COVER);
// 设置不同状态的样式
lv_style_set_bg_color(style, LV_STATE_PRESSED, LV_COLOR_DARK_BLUE);
lv_style_set_bg_color(style, LV_STATE_DISABLED, LV_COLOR_GRAY);
// 设置文字样式
lv_style_set_text_color(style, LV_STATE_DEFAULT, LV_COLOR_WHITE);
lv_style_set_text_font(style, LV_STATE_DEFAULT, &lv_font_montserrat_14);
}
4.4.2 样式初始化顺序
我总结了一个「三明治」顺序:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 清空所有样式 | 避免继承父控件的样式干扰 |
| 2 | 设置默认状态样式 | 控件最基础的显示效果 |
| 3 | 设置各状态样式 | 按下、禁用、聚焦等 |
| 4 | 设置过渡动画 | 状态切换时的平滑效果 |
为什么要先清空样式?我在项目中遇到过:自定义控件继承自父控件,父控件设置了红色背景,结果子控件怎么改都改不掉。后来发现是样式继承优先级的问题。清空一下,世界清净了。
4.5 完整的生命周期流程
最后,我把整个生命周期串起来,给你一个完整的模板:
// 1. 构造函数
lv_obj_t * my_widget_create(lv_obj_t * parent)
{
lv_obj_t * obj = lv_obj_create(parent, NULL);
my_widget_ext_t * ext = lv_obj_allocate_ext_attr(obj, sizeof(my_widget_ext_t));
// 初始化扩展数据
ext->value = 0;
ext->anim = NULL;
// 注册事件
lv_obj_set_event_cb(obj, my_widget_event_handler);
// 初始化样式
my_widget_style_init(obj);
return obj;
}
// 2. 事件处理
static void my_widget_event_handler(lv_obj_t * obj, lv_event_t event)
{
my_widget_ext_t * ext = lv_obj_get_ext_attr(obj);
switch(event) {
case LV_EVENT_DELETE:
// 析构:清理资源
if(ext->anim) {
lv_anim_del(ext->anim, NULL);
}
break;
case LV_EVENT_PRESSED:
// 按下处理
break;
case LV_EVENT_RELEASED:
// 释放处理
break;
}
}
// 3. 样式初始化
void my_widget_style_init(lv_obj_t * obj)
{
lv_style_t * style = lv_obj_get_style(obj);
lv_style_set_bg_color(style, LV_STATE_DEFAULT, LV_COLOR_WHITE);
lv_style_set_border_width(style, LV_STATE_DEFAULT, 2);
// ... 其他样式设置
}
- 构造函数:先父后子,分配扩展数据,注册事件,初始化样式
- 析构函数:监听 LV_EVENT_DELETE,停止动画,释放动态内存
- 事件处理:采用分发模式,每个事件独立函数
- 样式初始化:清空→默认→各状态→过渡动画
好了,这一章就到这里。生命周期管理这块,说白了就是「善始善终」。构造函数把控件「生」出来,析构函数让它「走」得体面,中间的事件和样式让它活得精彩。下一章我们聊聊控件的绘制流程,到时候你会看到,控件是怎么一像素一像素画出来的。