4、控件事件响应:Button控件与ClickEvent、ToggleButton与状态切换、TextArea与文本更新、Box与颜色变化

好,咱们进入第四章。前面几章我们把 TouchGFX 的工程结构、屏幕导航和消息循环捋了一遍。从这章开始,要真正动手写交互逻辑了。

说白了,嵌入式 GUI 的核心就两件事:用户点了什么,界面怎么变。这一章我们就围绕四个最基础的控件——Button、ToggleButton、TextArea、Box——把事件响应的完整链路走一遍。

本章核心脉络:从用户手指按下屏幕,到控件状态改变、文本刷新、颜色切换,最终形成一次完整的视觉反馈闭环。

控件事件响应知识体系 用户触摸事件 TouchGFX 事件分发机制 Button ClickEvent 回调 ToggleButton 选中/未选中状态 TextArea 文本动态更新 Box 颜色变化动画 视觉反馈:界面刷新

4.1 Button 与 ClickEvent:最基础的点击响应

Button 是 TouchGFX 里最常用的控件。你拖一个 Button 到屏幕上,双击它,在属性面板里就能看到 ClickAction 这个回调。我刚开始用的时候,以为它跟单片机的中断一样,触发了就自动跑——其实不是。

TouchGFX 的 Button 回调,本质是一个 虚函数重写。你在 Screen 或 View 里重写 buttonClicked(),框架才会调用你的逻辑。

我的习惯:每次新建 Screen 后,第一件事就是去 View 的 setupScreen() 里把回调绑定好。不然你拖再多控件也是摆设。

看一个最简单的例子。假设屏幕上有一个 Button,ID 是 btnStart,我想让用户点一下之后,串口打印一条消息。

// MyView.hpp
class MyView : public MyViewBase
{
public:
    MyView() {}
    virtual ~MyView() {}

    virtual void setupScreen();
    virtual void tearDownScreen();

    // 重写 Button 回调
    virtual void buttonClicked(Button& button);
};

// MyView.cpp
void MyView::setupScreen()
{
    // 绑定回调:告诉框架,这个 Button 的点击事件由我来处理
    btnStart.setAction(buttonClickedCallback(this, &MyView::buttonClicked));
}

void MyView::buttonClicked(Button& button)
{
    if (&button == &btnStart)
    {
        // 这里写你的业务逻辑
        // 比如:切换 LED、发送数据、更新界面
    }
}

这里有个细节要注意:buttonClickedCallback 是一个模板函数,它把成员函数指针包装成框架能识别的回调类型。你想想看,如果没有这层包装,C++ 的成员函数默认带 this 指针,框架根本没法直接调用。

我曾经踩过的坑:在回调里直接调用了 model->doSomething(),结果发现 model 指针是空的。后来才想起来,Model 的初始化是在 Screen::bind() 里完成的,如果你在构造函数里就绑回调,model 还没绑定呢。正确的做法是在 setupScreen() 里绑定。

4.2 ToggleButton:状态切换的两种姿势

ToggleButton 跟普通 Button 最大的区别是:它有 选中 和 未选中 两种状态。你点一下,它切换一次。有点像家里的开关,按一下开,再按一下关。

TouchGFX 的 ToggleButton 用两张图片来表示状态:bitmapReleased 和 bitmapPressed。但注意,它跟 Button 的 pressed/released 不一样——ToggleButton 的 pressed 状态会保持,直到你再次点击。

我一般用 ToggleButton 来做模式切换。比如一个设备有「自动模式」和「手动模式」,用 ToggleButton 就很直观。

// 在 setupScreen() 中绑定
toggleMode.setAction(toggleButtonClickedCallback(this, &MyView::toggleModeClicked));

void MyView::toggleModeClicked(const ToggleButton& button)
{
    if (&button == &toggleMode)
    {
        bool isAutoMode = button.getState();  // true = 选中, false = 未选中
        if (isAutoMode)
        {
            // 切换到自动模式
            textAreaMode.setTypedText(TypedText(T_AUTO_MODE));
        }
        else
        {
            // 切换到手动模式
            textAreaMode.setTypedText(TypedText(T_MANUAL_MODE));
        }
        // 别忘了刷新
        textAreaMode.invalidate();
    }
}

看到 getState() 了吗?这个函数返回当前状态。我个人习惯在状态切换时,同时更新界面上的文字或图标,让用户一眼就知道当前是什么模式。

关键点:ToggleButton 的 setState() 可以强制设置状态,但不会触发回调。如果你在代码里手动切换状态,记得自己调用回调,或者用 setState() 配合 invalidate() 刷新。

4.3 TextArea 与文本更新:不只是改字符串

TextArea 在 TouchGFX 里是个「看起来简单,用起来有坑」的控件。很多人以为直接 textArea.setTypedText(TypedText(T_SOME_TEXT)) 就完事了——嗯,大部分情况确实是这样。但如果你要显示动态数值呢?比如温度、计数值、时间?

这时候就要用到 Unicode::snprintf() 了。TouchGFX 的文本缓冲区是固定的,你不能像 PC 上那样随意拼接字符串。

// 假设有一个 TextArea 叫 textTemp,要显示 "25.3°C"
Unicode::UnicodeChar tempBuffer[10];

void MyView::updateTemperature(float temp)
{
    // 格式化:保留一位小数
    Unicode::snprintf(tempBuffer, 10, "%d.%d°C", (int)temp, (int)(temp * 10) % 10);
    textTemp.setWildcard(tempBuffer);
    textTemp.invalidate();
}

这里有个概念叫 Wildcard(通配符)。你在 TouchGFX Designer 里编辑文本时,可以用 <> 占位符,比如 温度:<temp>°C。然后在代码里用 setWildcard() 替换它。

我建议:所有动态文本的缓冲区都定义成全局或成员变量,不要每次在函数里临时创建。因为 TouchGFX 的文本更新是异步的,临时缓冲区可能被覆盖。

另外,TextArea 的 invalidate() 一定要调。它告诉框架:这块区域需要重绘。你不调,界面就不会刷新。我刚开始做的时候经常忘记,盯着屏幕看了半天,心想「我明明改了文本啊,怎么没反应?」——其实就是少了一句 invalidate()。

4.4 Box 与颜色变化:最简单的视觉反馈

Box 是什么?就是一个纯色矩形。没有边框,没有文字,没有图片。但它有一个特别有用的功能:颜色可以动态改变。

你想想看,当用户点击按钮时,如果只是文字变了,视觉反馈其实不够明显。如果背景颜色也跟着闪一下,用户体验会好很多。Box 就是干这个的。

// 假设有一个 Box 叫 boxIndicator,初始颜色是灰色
void MyView::buttonClicked(Button& button)
{
    if (&button == &btnAlarm)
    {
        // 切换 Box 颜色:灰色 -> 红色
        if (boxIndicator.getColor() == Color::getColorFrom24BitRGB(0x80, 0x80, 0x80))
        {
            boxIndicator.setColor(Color::getColorFrom24BitRGB(0xFF, 0x00, 0x00));
        }
        else
        {
            boxIndicator.setColor(Color::getColorFrom24BitRGB(0x80, 0x80, 0x80));
        }
        boxIndicator.invalidate();
    }
}

Color::getColorFrom24BitRGB() 是 TouchGFX 的颜色转换函数。它把 RGB 三个 8 位值拼成一个 24 位颜色值。注意,TouchGFX 内部用的是 16 位色(RGB565),但 API 层面你传 24 位值就行,它会自动转换。

我曾经遇到的问题:在 setupScreen() 里设置 Box 颜色,结果发现颜色不对。后来查文档才知道,Box 的默认颜色是在 TouchGFX Designer 里设置的,如果你在代码里改,一定要在 setupScreen() 里改,不要在构造函数里改——那时候控件还没初始化完成。

Box 还有一个妙用:做 进度条背景。你可以用两个 Box 叠加,一个做背景,一个做填充,通过改变填充 Box 的宽度来实现进度效果。这个我们后面章节会详细讲。

4.5 综合示例:一个带反馈的开关按钮

把上面四个控件串起来,做一个完整的例子。假设我们要实现一个「报警开关」:

  • ToggleButton 控制报警的开启/关闭
  • TextArea 显示当前状态文字
  • Box 用颜色表示状态(绿色=关闭,红色=开启)
  • 普通 Button 用来复位
void MyView::setupScreen()
{
    // 绑定 ToggleButton
    toggleAlarm.setAction(toggleButtonClickedCallback(this, &MyView::toggleAlarmClicked));
    // 绑定普通 Button
    btnReset.setAction(buttonClickedCallback(this, &MyView::btnResetClicked));

    // 初始状态:报警关闭
    updateAlarmUI(false);
}

void MyView::toggleAlarmClicked(const ToggleButton& button)
{
    bool isAlarmOn = button.getState();
    updateAlarmUI(isAlarmOn);
}

void MyView::btnResetClicked(Button& button)
{
    // 复位:强制关闭报警
    toggleAlarm.forceState(false);   // 强制设置状态,不触发回调
    updateAlarmUI(false);
}

void MyView::updateAlarmUI(bool isOn)
{
    if (isOn)
    {
        textStatus.setTypedText(TypedText(T_ALARM_ON));
        boxIndicator.setColor(Color::getColorFrom24BitRGB(0xFF, 0x00, 0x00)); // 红色
    }
    else
    {
        textStatus.setTypedText(TypedText(T_ALARM_OFF));
        boxIndicator.setColor(Color::getColorFrom24BitRGB(0x00, 0xFF, 0x00)); // 绿色
    }
    // 刷新所有控件
    textStatus.invalidate();
    boxIndicator.invalidate();
}

这个例子虽然简单,但涵盖了事件响应、状态切换、文本更新、颜色变化四个核心点。你可以在 TouchGFX Designer 里搭好界面,然后把这段代码贴进去试试。

总结一下本章的关键点:

  • Button 的回调用 buttonClickedCallback 绑定,在 setupScreen() 里做
  • ToggleButton 用 getState() 获取状态,forceState() 强制设置
  • TextArea 动态文本用 setWildcard() + Unicode::snprintf()
  • Box 颜色用 setColor() + Color::getColorFrom24BitRGB()
  • 所有控件修改后都要调 invalidate(),否则界面不刷新

嗯,这一章的内容就到这里。代码不多,但都是实际项目里每天都要写的东西。你把这四个控件的响应逻辑练熟了,后面学 List、Scroll、Custom Widget 就会轻松很多。