2. TouchGFX基础回顾
在进入复杂HMI项目开发之前,有必要把TouchGFX最核心的架构约定重新梳理一遍。TouchGFX并不是“在屏幕上画控件的库”这么简单,它提供了一套以MVP(Model-View-Presenter)为骨架的应用框架,把界面渲染、业务数据和交互逻辑拆成三层,并通过事件驱动机制把它们串起来。理解这套骨架,决定了后续多屏切换、数据绑定、动画协同和性能优化能否顺利展开。
一、MVP架构的整体分工
TouchGFX的MVP架构可以理解为“界面与逻辑解耦”的落地方式。三个角色各司其职:
- Model(模型):负责业务数据与状态,例如温度值、电机转速、报警标志、通信缓冲区。它不关心界面长什么样,只提供数据读写接口,并在数据变化时通知Presenter。
- View(视图):负责“长什么样”和“怎么画”。它由TouchGFX Designer生成的控件树组成,包含Box、TextArea、Image、FlexButton、自定义控件等。View只做渲染和把用户操作(点击、滑动)上报给Presenter,不直接改业务数据。
- Presenter(呈现器):是View与Model之间的协调者。它接收View上报的交互事件,调用Model更新数据,再把Model的变化翻译成View可执行的显示指令,例如“把温度文本改成26.5℃”“切换到报警页面”。
这种划分带来的直接好处是:界面改版时不必动业务逻辑,业务逻辑变化时也不必重写界面。对于复杂HMI,尤其是多页面、多状态、多数据源的场景,MVP能显著降低耦合度。
二、事件驱动机制:从触摸到响应
TouchGFX是典型的事件驱动框架。系统启动后进入主循环,不断执行“采集事件—分发事件—处理事件—重绘”的流程。触摸、按键、定时器、模型回调都会产生事件,最终由框架分发到对应的Presenter或View。
以一次按钮点击为例,典型链路如下:
- 触摸屏产生坐标,TouchGFX的触摸控制器识别为一次点击。
- 点击落在某个FlexButton上,View中的按钮回调被触发。
- View通过
presenter->buttonClicked()把事件交给Presenter。 - Presenter调用Model的接口修改数据,例如
model->setLedState(true)。 - Model更新后通知Presenter,Presenter调用View的更新函数,例如
view.setLedOn()。 - View标记需要重绘,TouchGFX在下一帧把变化区域刷新到屏幕。
这里的关键是:View不直接改Model,Model也不直接操作View,所有跨层动作都经过Presenter。这样事件流是单向、可追踪的,排障时能快速定位是交互没上报、数据没更新,还是界面没刷新。
三、基本工程结构
一个标准的TouchGFX工程通常包含以下目录与文件:
| 结构 | 作用 | 典型内容 |
|---|---|---|
| TouchGFX/ | 框架与生成代码 | generated、gui、touchgfx 配置 |
| gui/ | 界面资源与代码 | Screen、View、Presenter、自定义控件 |
| Core/ | MCU底层与业务 | main.c、外设驱动、Model实现 |
| Middlewares/ | 中间件 | FreeRTOS、FatFS、通信协议栈 |
| Application/ | 应用层 | Model、业务逻辑、数据管理 |
其中,gui/下的每个Screen通常对应一组View与Presenter。TouchGFX Designer负责生成View的控件树和基础绑定代码,开发者在此基础上补充Presenter逻辑和Model接口。需要注意的是,Designer生成的代码在重新生成时可能被覆盖,因此自定义逻辑应尽量放在用户代码区或独立的Presenter/Model文件中。
四、一个最小案例:LED开关界面
假设界面只有一个按钮和一个状态文本,点击按钮切换LED状态。MVP分工如下:
// Model
class LedModel {
public:
void setLedState(bool on) { state = on; notify(); }
bool getLedState() const { return state; }
private:
bool state = false;
};
// Presenter
void LedPresenter::buttonClicked() {
bool next = !model->getLedState();
model->setLedState(next);
view.setLedText(next ? "LED ON" : "LED OFF");
}
// View
void LedView::setLedText(const char* text) {
ledText.setTypedText(TypedText(text));
ledText.invalidate();
}
这个例子虽小,却完整体现了“View上报—Presenter决策—Model更新—View刷新”的闭环。复杂项目只是把这条链路扩展成多页面、多数据源、多事件类型。
五、常见误区与复盘
- 误区一:在View里直接改Model。这会让界面逻辑和业务逻辑纠缠,后期换屏或换数据源时改动量巨大。
- 误区二:把大量业务判断塞进Presenter。Presenter应保持“协调者”角色,复杂算法和状态机应下沉到Model或独立服务。
- 误区三:忽略事件来源追踪。当界面无响应时,应先确认事件是否到达View,再确认Presenter是否处理,最后确认View是否invalidate。
- 误区四:在Designer生成文件中写自定义逻辑。重新生成后代码可能丢失,应使用用户代码区或独立文件。
把这张图记在脑子里,后续无论是做多语言切换、动态图表、报警弹窗还是页面跳转,都可以先问自己三个问题:这个动作属于View、Presenter还是Model?事件从哪来、到哪去?数据变化后谁负责刷新界面?回答清楚,复杂HMI的架构就不会乱。