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 消息传递机制:数据怎么流动?

搞清楚了三个角色,接下来就是它们之间怎么通信。我画了一张图,你看完就明白了。

TouchGFX MVP 消息传递机制 Model 数据状态管理 Presenter 业务逻辑处理 View 界面显示 读取数据 更新数据 推送数据 用户事件 数据流向说明 用户操作View → View通知Presenter → Presenter更新Model Model变化 → Presenter读取 → Presenter推送新数据到View显示 典型交互流程 ① 用户点击按钮 ② View调用Presenter ③ Presenter处理逻辑 ④ 更新View显示

数据流动其实就两条路:

  • 用户操作路径: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把这个模式贯彻得很彻底,你只要遵循这个规则,项目越大,优势越明显。

下次写代码前,先问问自己:这行代码该放在哪一层?养成这个习惯,你的代码会越来越干净。