4、控件系统迁移(上):按钮、标签、文本框的创建与属性映射

好,咱们进入控件迁移的正题了。说实话,控件是GUI的骨架,用户看到的、点到的、输入的全是控件。TouchGFX和LVGL在控件体系上思路差不多,但API风格差异挺大。我刚开始从TouchGFX切到LVGL时,最不适应的就是——怎么连个按钮都要多写好几行?

但用顺手了你会发现,LVGL的控件设计更贴近嵌入式底层,灵活度也更高。今天咱们先啃三个最基础的控件:按钮、标签、文本框。这三个搞定了,后面那些复杂的控件(列表、滑块、图表)就一通百通。

核心观点:控件迁移的本质不是“找对应API”,而是理解“属性树 + 样式系统”的差异。TouchGFX用C++对象属性直接赋值,LVGL用函数链式调用。记住这个思维转换,迁移效率翻倍。

4.1 按钮(Button)的创建与属性映射

按钮是交互的起点。TouchGFX里创建一个按钮,通常是继承Button类,然后在构造函数里设图片或文本。LVGL更直接——你创建一个对象,设置样式,绑定点击回调,完事。

先看个对比,你感受一下:

功能 TouchGFX LVGL
创建按钮 Button btn; + 构造函数初始化 lv_btn_create(parent)
设置位置 btn.setXY(10, 20) lv_obj_set_pos(btn, 10, 20)
设置大小 btn.setWidthHeight(100, 40) lv_obj_set_size(btn, 100, 40)
设置文本 需组合TextArea或使用ButtonWithLabel lv_label_set_text(lv_obj_get_child(btn, 0), "OK")
点击回调 重写clickHandler虚函数 lv_obj_add_event_cb(btn, handler, LV_EVENT_CLICKED, NULL)

我个人习惯在LVGL里这样写一个标准按钮:

/* 创建按钮 */
lv_obj_t *btn = lv_btn_create(lv_scr_act());
lv_obj_set_pos(btn, 50, 50);
lv_obj_set_size(btn, 120, 40);

/* 按钮上的标签(LVGL创建按钮时会自动附带一个标签子对象) */
lv_obj_t *label = lv_obj_get_child(btn, 0);
lv_label_set_text(label, "确认");

/* 设置点击回调 */
lv_obj_add_event_cb(btn, btn_click_cb, LV_EVENT_CLICKED, NULL);

/* 回调函数 */
static void btn_click_cb(lv_event_t *e) {
    LV_LOG_USER("按钮被点击了");
    /* 你的业务逻辑 */
}
小技巧:LVGL的按钮默认自带一个lv_label作为子对象,索引为0。如果你用lv_btn_create后直接lv_obj_get_child(btn, 0)就能拿到标签。但如果你自己额外添加了子对象,索引会变,建议用lv_label_create(btn)显式创建,更可控。

我在项目里遇到过一个问题:TouchGFX的按钮有setTouchable(true/false)来控制是否可点击,LVGL里对应的是lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE)和lv_obj_clear_flag(btn, LV_OBJ_FLAG_CLICKABLE)。嗯,这个细节容易漏,迁移时记得检查。

4.2 标签(Label)的创建与属性映射

标签就是显示文本的,TouchGFX里叫TextArea,LVGL里叫lv_label。名字不同,但干的活一样——显示静态或动态文本。

来看看属性映射表:

功能 TouchGFX (TextArea) LVGL (lv_label)
创建 TextArea ta; lv_label_create(parent)
设置文本 ta.setTypedText(TypedText(T_TEXT_ID)) lv_label_set_text(label, "Hello")
动态文本 ta.setWildcard(buffer) lv_label_set_text_fmt(label, "值: %d", val)
文本颜色 ta.setColor(colortype) lv_obj_set_style_text_color(label, lv_color_hex(0x333333), 0)
对齐方式 ta.setAlignment(Alignment::CENTER) lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP) + lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0)

LVGL的标签有个很实用的功能——长文本模式。你可以设置文本超出宽度时自动换行、滚动或省略。TouchGFX里要实现类似效果,得自己写逻辑或者用TextAreaWithWildcard配合滚动容器,挺麻烦的。

/* 创建标签 */
lv_obj_t *label = lv_label_create(lv_scr_act());
lv_obj_set_pos(label, 20, 100);
lv_obj_set_size(label, 200, 40);

/* 设置文本 */
lv_label_set_text(label, "系统运行时间: 0s");

/* 设置文本颜色和对齐 */
lv_obj_set_style_text_color(label, lv_color_hex(0x2c3e50), 0);
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);

/* 动态更新(比如每秒更新一次) */
static void update_label(lv_timer_t *timer) {
    static uint32_t seconds = 0;
    seconds++;
    lv_label_set_text_fmt(lv_obj_get_user_data(timer), "系统运行时间: %d秒", seconds);
}

/* 创建定时器更新标签 */
lv_obj_set_user_data(label, label);
lv_timer_create(update_label, 1000, NULL);
注意:LVGL的lv_label_set_text内部会动态分配内存来存储文本副本。如果你频繁更新文本(比如每秒一次),建议用lv_label_set_text_static传入静态字符串指针,避免内存碎片。我吃过这个亏——跑了一周后系统莫名卡死,查了半天发现是标签更新导致的内存碎片。

4.3 文本框(Text Area)的创建与属性映射

文本框是用户输入的入口。TouchGFX里叫TextAreaWithWildcard(其实更接近显示),真正的输入框是Keyboard配合TextArea。LVGL直接提供了lv_textarea,自带光标、选中、密码模式,省事不少。

功能 TouchGFX LVGL
创建 组合TextArea + Keyboard lv_textarea_create(parent)
设置占位文本 需手动实现 lv_textarea_set_placeholder_text(ta, "请输入...")
获取输入内容 通过回调读取缓冲区 const char *text = lv_textarea_get_text(ta)
密码模式 需自定义 lv_textarea_set_password_mode(ta, true)
最大字符数 通过缓冲区大小限制 lv_textarea_set_max_length(ta, 20)
光标控制 无原生光标 lv_textarea_set_cursor_pos(ta, 0)

写个实际例子,创建一个带键盘输入的文本框:

/* 创建文本框 */
lv_obj_t *ta = lv_textarea_create(lv_scr_act());
lv_obj_set_pos(ta, 30, 200);
lv_obj_set_size(ta, 260, 40);

/* 设置属性 */
lv_textarea_set_placeholder_text(ta, "点击输入用户名...");
lv_textarea_set_max_length(ta, 16);
lv_textarea_set_one_line(ta, true);

/* 设置样式:边框、圆角、背景色 */
lv_obj_set_style_border_width(ta, 1, 0);
lv_obj_set_style_border_color(ta, lv_color_hex(0xcccccc), 0);
lv_obj_set_style_radius(ta, 4, 0);
lv_obj_set_style_bg_color(ta, lv_color_hex(0xffffff), 0);

/* 创建键盘(LVGL内置键盘) */
lv_obj_t *kb = lv_keyboard_create(lv_scr_act());
lv_keyboard_set_textarea(kb, ta);  /* 关联文本框 */

你看,LVGL的文本框和键盘是解耦的,但通过lv_keyboard_set_textarea一键关联。TouchGFX里你得自己处理键盘按键映射、焦点切换、输入法状态……说实话,LVGL这套省心太多了。

经验之谈:如果你做的是密码输入框,记得调用lv_textarea_set_password_mode(ta, true),并且把密码显示字符改成你想要的,比如lv_textarea_set_password_bullet(ta, "*")。默认是圆点,有些客户要求用星号,这个接口很贴心。

4.4 控件属性映射总览

为了让你迁移时能快速查阅,我把三个控件的核心属性映射整理成了一张图。这张SVG图展示了从TouchGFX到LVGL的思维转换路径:

控件属性映射总览图 控件属性映射总览:TouchGFX → LVGL 按钮 (Button) TouchGFX: Button类 LVGL: lv_btn_create() • setXY → lv_obj_set_pos • setWidthHeight → lv_obj_set_size • clickHandler → LV_EVENT_CLICKED • setTouchable → LV_OBJ_FLAG_CLICKABLE 标签 (Label) TouchGFX: TextArea LVGL: lv_label_create() • setTypedText → lv_label_set_text • setWildcard → lv_label_set_text_fmt • setColor → lv_obj_set_style_text_color • setAlignment → LV_TEXT_ALIGN_* 文本框 (TextArea) TouchGFX: 组合实现 LVGL: lv_textarea_create() • 占位文本 → lv_textarea_set_placeholder • 获取内容 → lv_textarea_get_text • 密码模式 → lv_textarea_set_password_mode • 最大长度 → lv_textarea_set_max_length 迁移核心思维转换 TouchGFX: 面向对象继承 + 属性直接赋值(btn.setXY(x, y)) LVGL: 函数式创建 + 链式调用(lv_obj_set_pos(btn, x, y)) 迁移路径:理解API差异 → 掌握属性映射 → 复用业务逻辑 理解API差异 掌握属性映射 复用业务逻辑

4.5 迁移避坑指南

最后,我把自己踩过的坑总结一下,你迁移时直接对照检查:

  • 父子关系别搞反:TouchGFX里控件添加到父容器是隐式的(通过Container::add),LVGL里lv_xxx_create(parent)的第一个参数就是父对象。如果你传NULL或lv_scr_act(),控件会添加到当前活动屏幕。我刚开始写的时候,经常忘记传父对象,结果控件显示不出来。
  • 样式是全局的:LVGL的样式默认会继承。如果你给屏幕设了一个字体,所有子控件默认都用这个字体。TouchGFX里每个控件独立设置样式,迁移时记得检查样式冲突。
  • 回调参数不同:TouchGFX的回调是void handler(const AbstractButton& btn),LVGL是void handler(lv_event_t *e)。你需要通过lv_event_get_target(e)拿到触发事件的控件对象。
  • 内存管理:LVGL的控件创建会动态分配内存,删除用lv_obj_del(obj)。如果你频繁创建删除控件(比如动态菜单),记得调用lv_mem_monitor检查内存碎片。
曾经踩过的坑:我在迁移一个登录界面时,TouchGFX里文本框和键盘是分开的,我直接照搬了结构——在LVGL里创建了文本框,又单独创建了一个自定义键盘。结果发现LVGL自带的lv_keyboard已经处理了所有按键映射和焦点切换。白写了200行代码。所以迁移前,先翻翻LVGL的控件文档,看看有没有现成的。