3、TouchGFX核心架构:MVP架构模式、Model-View-Presenter详解、消息传递机制
做嵌入式GUI开发这么多年,我见过太多人一上来就撸代码,结果项目到后期改一个按钮位置要改三天。说白了,没有好的架构,UI项目就是一团乱麻。TouchGFX之所以能成为工业级方案,核心就在于它严格遵循了MVP架构模式。
今天我们就把它拆开揉碎了讲清楚。你搞懂了MVP,就等于拿到了TouchGFX的钥匙。
3.1 为什么是MVP?不是MVC?
很多人问我:为什么TouchGFX不用经典的MVC(Model-View-Controller)?
嗯,这个问题我刚开始接触时也困惑过。后来在做一个医疗设备界面时,我踩了个大坑——MVC里View和Controller耦合太紧,换一套屏幕尺寸,Controller几乎要重写。TouchGFX选MVP,说白了就是为了解决这个问题。
| 对比项 | MVC | MVP |
|---|---|---|
| View与逻辑的耦合 | Controller直接操作View | Presenter通过接口操作View |
| 可测试性 | 较差,View难以Mock | 强,Presenter可独立测试 |
| View更换成本 | 高,Controller需同步修改 | 低,只需实现新View接口 |
| TouchGFX采用 | 否 | 是 |
你看,MVP把View和Presenter之间用接口隔开了。View只负责显示,Presenter只负责逻辑。各干各的,互不干扰。
3.2 三剑客:Model、View、Presenter
我们一个一个说。
3.2.1 Model(模型)
Model就是数据本身。它不关心数据怎么显示,也不关心用户点了哪里。它只做一件事:管理数据状态。
我在项目中习惯把Model设计成单例,因为整个应用共享同一份数据状态。比如温控器的当前温度、设备开关状态,这些都放在Model里。
// Model示例:温度数据模型
class TemperatureModel {
public:
static TemperatureModel& getInstance() {
static TemperatureModel instance;
return instance;
}
void setCurrentTemp(float temp) {
currentTemp = temp;
// 通知所有监听者
notifyListeners();
}
float getCurrentTemp() const {
return currentTemp;
}
private:
float currentTemp = 25.0f;
};
注意看,Model里没有一行UI代码。它就是个纯粹的数据容器。
3.2.2 View(视图)
View就是你在屏幕上看到的一切。按钮、文本框、图片,都是View。在TouchGFX里,每个屏幕都是一个View类。
View的职责很单一:
- 把Presenter给的数据画出来
- 把用户的操作告诉Presenter
它不自己做任何决策。用户点了按钮,View不会自己去处理业务逻辑,而是调用Presenter的方法。
核心原则:View只做"显示"和"转发",不做"决策"。
3.2.3 Presenter(主持人)
Presenter才是真正的"大脑"。它从Model拿数据,加工后推给View显示。它接收View传来的用户事件,决定要不要修改Model。
我曾经在一个项目中犯过错误——把数据校验逻辑写在了View里。结果后来要改校验规则,得改好几个View文件。改成Presenter统一处理后,改一个地方就行。
// Presenter示例
class TemperaturePresenter {
public:
void onButtonUpClicked() {
float newTemp = model.getCurrentTemp() + 0.5f;
if (newTemp <= MAX_TEMP) {
model.setCurrentTemp(newTemp);
view.updateTemperatureDisplay(newTemp);
}
}
void onButtonDownClicked() {
float newTemp = model.getCurrentTemp() - 0.5f;
if (newTemp >= MIN_TEMP) {
model.setCurrentTemp(newTemp);
view.updateTemperatureDisplay(newTemp);
}
}
private:
TemperatureModel& model = TemperatureModel::getInstance();
TemperatureView& view;
};
3.3 消息传递机制:数据怎么流动?
搞清楚了三个角色,接下来就是它们之间怎么通信。我画了一张图,你看完就明白了。
数据流动其实就两条路:
- 用户操作路径:View → Presenter → Model
- 数据更新路径:Model → Presenter → View
单向流动,不绕圈子。这就是MVP的精髓。
3.4 TouchGFX中的MVP实现细节
在TouchGFX里,每个屏幕对应三个文件:
| 文件 | 对应角色 | 典型命名 |
|---|---|---|
| ScreenView.hpp / .cpp | View | MainScreenView |
| ScreenPresenter.hpp / .cpp | Presenter | MainScreenPresenter |
| Model.hpp / .cpp | Model | Model(全局单例) |
你打开TouchGFX Designer生成的代码,会发现View里有一个Presenter指针,Presenter里有一个View引用。它们互相持有对方的引用,但通过接口解耦。
我的习惯:在Presenter的构造函数里就把View传进去,用引用而不是指针。引用不会为空,省去判空代码,更干净。
3.5 避坑指南:我踩过的三个坑
讲完理论,说点实际的。这些坑我当年都踩过,你注意避开。
坑一:在View里直接操作Model
我曾经图省事,在View里直接调了Model的set方法。结果后来要加日志记录,得改View和Presenter两个地方。记住:View永远不要直接碰Model,必须经过Presenter。
坑二:Presenter里塞了太多View操作
Presenter应该调用View的接口方法,而不是直接操作UI控件。比如调用view.showError("温度超限"),而不是view.errorText.setVisible(true)。前者View可以自由决定怎么显示,后者就把View的实现细节暴露了。
坑三:Model里放了UI状态
Model只放业务数据。按钮是否选中、列表滚动位置这些UI状态,应该放在View里。我见过有人把界面主题色存在Model里,结果换主题时要重启应用才能生效。
3.6 消息传递的两种模式
TouchGFX里消息传递主要有两种方式:
方式一:直接调用
这是最常用的方式。View调用Presenter的方法,Presenter调用Model的方法。简单直接,适合大多数场景。
// View中
void MainScreenView::buttonClicked() {
presenter.onButtonClicked();
}
// Presenter中
void MainScreenPresenter::onButtonClicked() {
model.incrementCounter();
view.updateCounter(model.getCounter());
}
方式二:回调/监听器
当Model的数据需要主动推送给多个Presenter时,用监听器模式。比如一个温度传感器定时更新,所有关心温度的屏幕都能收到通知。
// Model中注册监听
class TemperatureListener {
public:
virtual void onTemperatureChanged(float newTemp) = 0;
};
class Model {
void registerListener(TemperatureListener* listener) {
listeners.push_back(listener);
}
void notifyListeners() {
for (auto* l : listeners) {
l->onTemperatureChanged(currentTemp);
}
}
};
我个人更推荐直接调用,因为代码流程清晰,调试时一眼就能看出数据怎么走的。监听器用多了,代码会变得像蜘蛛网一样,你追一个数据流要跳好几个文件。
3.7 小结
MVP架构说白了就是"各司其职"四个字。Model管数据,View管显示,Presenter管逻辑。TouchGFX把这个模式贯彻得很彻底,你只要遵循这个规则,项目越大,优势越明显。
下次写代码前,先问问自己:这行代码该放在哪一层?养成这个习惯,你的代码会越来越干净。