4、控件事件响应:Button控件与ClickEvent、ToggleButton与状态切换、TextArea与文本更新、Box与颜色变化
好,咱们进入第四章。前面几章我们把 TouchGFX 的工程结构、屏幕导航和消息循环捋了一遍。从这章开始,要真正动手写交互逻辑了。
说白了,嵌入式 GUI 的核心就两件事:用户点了什么,界面怎么变。这一章我们就围绕四个最基础的控件——Button、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 就会轻松很多。