4. TouchGFX Designer基础

说实话,刚接触TouchGFX那会儿,我也有点懵。代码自动生成了那么多文件,到底哪个该写逻辑,哪个只管显示?后来我才明白,这一切都围绕着一种叫MVP的架构模式在转。

MVP,全称Model-View-Presenter。说白了,就是把界面、数据和业务逻辑拆成三块,各干各的活。这样做的好处,我在项目里体会太深了——改个UI不用动底层逻辑,换数据源也不用重画界面。

4.1 MVP三兄弟:各司其职

咱们先看看这三个角色分别负责什么:

角色 职责 在TouchGFX中的对应
Model 数据层,负责业务逻辑和数据存取 Model类、数据源、硬件抽象层
View 视图层,只负责界面显示和用户交互 ScreenView类、控件、画布
Presenter 协调层,连接Model和View,处理事件 ScreenPresenter类

嗯,这里要注意:View不直接访问Model。所有数据请求都通过Presenter中转。这样做的好处是,哪天你想把LCD屏换成OLED,或者把数据从本地文件改成云端接口,只需要改对应的那一层就行。

核心原则:View只负责"画什么",Model只负责"存什么",Presenter负责"怎么处理"。三者之间通过接口通信,不直接依赖具体实现。

4.2 TouchGFX中的MVP骨架

当你用TouchGFX Designer新建一个Screen时,它会自动生成三个文件:

  • ScreenView.hpp / ScreenView.cpp —— View层
  • ScreenPresenter.hpp / ScreenPresenter.cpp —— Presenter层
  • Screen.hpp —— 接口定义

Model层呢?通常需要你自己建。我习惯在项目根目录下建一个model文件夹,放所有数据相关的类。

举个例子,假设我们要做一个温湿度监控界面:

// Model层:SensorDataModel.hpp
class SensorDataModel {
public:
    float getTemperature();
    float getHumidity();
    void updateSensorData();
private:
    float temperature;
    float humidity;
};
// View层:TempHumidView.hpp
class TempHumidView : public TempHumidViewBase {
public:
    virtual void setupScreen();
    virtual void tearDownScreen();
    void updateTemperature(float temp);
    void updateHumidity(float hum);
};
// Presenter层:TempHumidPresenter.hpp
class TempHumidPresenter : public TempHumidPresenterBase {
public:
    TempHumidPresenter(TempHumidView& v);
    virtual void activate();
    virtual void deactivate();
    void onButtonRefreshClicked();
private:
    SensorDataModel model;
};

我的习惯:Presenter里不写具体的业务算法,只做调度。比如"用户点了刷新按钮 → Presenter通知Model更新数据 → Model返回新数据 → Presenter把数据传给View显示"。这样每个类的职责都很纯粹,调试起来特别爽。

4.3 数据流:从按钮到屏幕的完整旅程

用户按下一个刷新按钮,数据是怎么从传感器一路跑到屏幕上的?我画了张图,你看完就明白了:

MVP架构数据流与事件传递 用户点击刷新按钮 1. 回调函数 Presenter接收事件 2. 调用接口 Model读取传感器数据 3. 返回数据 Presenter处理/格式化数据 4. 更新UI View刷新显示 说明: 数据流是单向的,从View 出发,经过Presenter中转, 最终回到View更新显示。

你看,整个流程是单向的,不会出现View直接调用Model的情况。这样做的好处是:

  • 可测试性:你可以单独测试Presenter的逻辑,不用依赖真实的UI和硬件
  • 可替换性:想换UI库?只要View的接口不变,其他代码不用动
  • 可维护性:每个类的代码量都不大,出了问题很快能定位

4.4 事件传递机制:回调与监听

TouchGFX里的事件传递,主要靠两种方式:

  1. 回调函数:按钮点击、滑动等用户交互事件
  2. 监听器模式:Model数据变化后通知Presenter

先看回调。在TouchGFX Designer里给按钮添加点击事件时,它会自动生成类似这样的代码:

// View层
void TempHumidView::setupScreen() {
    buttonRefresh.setAction(buttonRefreshCallback);
}

void TempHumidView::buttonRefreshCallback(const AbstractButton& source) {
    // 通知Presenter
    presenter->onButtonRefreshClicked();
}

再看监听器。当Model的数据发生变化时,怎么通知Presenter?我常用的做法是定义一个接口:

// ModelListener接口
class ModelListener {
public:
    virtual ~ModelListener() {}
    virtual void onSensorDataUpdated(float temp, float hum) = 0;
};

// Presenter实现这个接口
class TempHumidPresenter : public TempHumidPresenterBase, public ModelListener {
public:
    virtual void onSensorDataUpdated(float temp, float hum) override {
        view.updateTemperature(temp);
        view.updateHumidity(hum);
    }
};

我曾经踩过的坑:在Presenter的构造函数里直接调用Model的初始化方法,结果View还没准备好,导致空指针崩溃。正确的做法是在activate()方法里做初始化,这时候View已经创建完毕了。

4.5 实战中的MVP最佳实践

做了几个项目后,我总结了几条经验:

场景 推荐做法 不推荐做法
界面切换 在Presenter的deactivate()中释放资源 在View的析构函数中处理业务逻辑
数据更新 Model通过回调通知Presenter Presenter轮询Model状态
UI动画 在View层处理,Presenter只触发开始 在Presenter里控制动画帧
硬件访问 封装在Model层,通过接口暴露 在View或Presenter里直接操作寄存器

嗯,还有一点我想强调:不要为了MVP而MVP。如果你的界面只有一两个控件,逻辑也很简单,硬拆成三层反而增加复杂度。我一般会在项目有3个以上Screen时才开始用MVP,这时候它的优势就体现出来了。

我的小技巧:在TouchGFX Designer里,给每个Screen的Presenter命名时加上"Presenter"后缀,比如MainPresenter、SettingsPresenter。这样在代码里一眼就能看出哪个是Presenter,哪个是View,团队协作时特别有用。

MVP架构说白了就是"分而治之"的思想。把界面、数据和逻辑拆开,各管各的一亩三分地。刚开始可能会觉得多写了好多代码,但等你需要改需求、修bug的时候,就会感谢当初的自己了。