3. TouchGFX 架构解析

在嵌入式 GUI 开发中,界面逻辑与业务逻辑一旦耦合,代码就会迅速变得难以维护。TouchGFX 采用 MVP(Model-View-Presenter)模式来组织应用,把“数据从哪来”“界面怎么画”“用户操作如何响应”三件事拆开,使界面刷新与硬件、RTOS、业务状态解耦。理解 MVP 的职责边界与数据流,是后续使用 TouchGFX Designer、编写 Screen 类与处理交互事件的基础。

一、MVP 三层的核心职责

TouchGFX 的 MVP 并不是一个抽象口号,而是直接映射到代码结构中的三类对象。每个 Screen 通常由一对 View 与 Presenter 组成,Model 则是全局单例,供所有 Presenter 访问。

  • Model:应用的数据与业务状态中心。它保存温度、电量、模式、列表数据等,负责与底层驱动、RTOS 队列、传感器或通信模块交互。Model 不关心任何控件,也不直接调用 invalidate()。
  • View:负责“长什么样”和“怎么画”。它持有 TextArea、Button、Box 等控件,处理底层绘制与触摸区域,但不决定业务规则。View 通过 presenter->… 把用户操作上报。
  • Presenter:连接 Model 与 View 的协调者。它从 Model 取数据,转换成 View 能显示的格式,调用 View 的更新接口;同时接收 View 上报的事件,决定调用 Model 的哪个业务方法。

一句话记忆:Model 管“数据与规则”,View 管“像素与触摸”,Presenter 管“翻译与调度”。

二、数据流与调用方向

TouchGFX 的数据流是双向的,但方向被严格约束:View 不直接读写 Model,Model 也不直接操作 View。所有跨层通信都经过 Presenter。典型流程如下:

  1. 用户点击“开始测量”按钮,View 的 handleClickEvent 或绑定回调触发。
  2. View 调用 presenter->startMeasure(),把意图交给 Presenter。
  3. Presenter 调用 model->startMeasure(),由 Model 启动采集或发送命令。
  4. Model 更新内部状态,例如 measureState = RUNNING。
  5. Presenter 在 tick() 或回调中读取 Model 状态,调用 view.setMeasureState(...)。
  6. View 更新控件文本、颜色或进度条,并调用 invalidate() 请求重绘。

这个链条中,Presenter 是唯一同时持有 Model 与 View 引用的角色。它不直接绘制像素,也不直接访问硬件寄存器,从而保证界面代码可以在模拟器中独立运行。

TouchGFX MVP 架构与数据流 Model 业务数据与状态 传感器 / 通信 / RTOS 不持有任何控件 不调用 invalidate() View 控件与绘制 TextArea / Button / Box 触摸事件入口 不访问硬件寄存器 Presenter 协调 Model 与 View 读取状态 → 更新界面 接收事件 → 调用业务 tick() 中轮询刷新 读取数据 / 调用业务 更新控件 / 请求重绘 用户操作上报 约束:View 不直接读写 Model;Model 不直接操作 View;跨层通信必须经过 Presenter

三、代码结构中的 MVP 映射

在 TouchGFX Designer 生成的项目中,每个 Screen 通常包含 Screen1View 与 Screen1Presenter,而 Model 是全局单例。下面是一个简化的温度显示场景:

// Model.hpp
class Model {
public:
    void setTemperature(float t) { temperature = t; }
    float getTemperature() const { return temperature; }
private:
    float temperature = 0.0f;
};

// Screen1Presenter.hpp
class Screen1Presenter : public touchgfx::Presenter {
public:
    void onTemperatureUpdated(float t) {
        view.setTemperatureText(t);
    }
    void onStartClicked() {
        model->startMeasure();
    }
private:
    Screen1View& view;
};

// Screen1View.hpp
class Screen1View : public touchgfx::View<Screen1Presenter> {
public:
    void setTemperatureText(float t) {
        Unicode::snprintf(tempBuffer, TEMP_SIZE, "%.1f C", t);
        tempText.invalidate();
    }
    void buttonCallback() {
        presenter->onStartClicked();
    }
};

可以看到,View 只做字符串格式化与 invalidate(),Presenter 只做转发与转换,Model 只保存数据。三者各司其职,界面代码可以在 PC 模拟器中运行,而 Model 可以替换为真实硬件驱动。

四、典型场景:温度采集与界面刷新

假设系统每 500 ms 采集一次温度,并在界面上显示。数据流如下:

步骤 触发者 动作 涉及对象
1 定时器 / RTOS 任务 读取 ADC,得到 26.5°C Model
2 Model 更新内部 temperature 字段 Model
3 Presenter::tick() 读取 model->getTemperature() Presenter → Model
4 Presenter 调用 view.setTemperatureText(26.5) Presenter → View
5 View 更新 TextArea 并 invalidate() View
6 TouchGFX 引擎 重绘脏区域,显示 26.5 C 框架

如果用户点击“切换单位”按钮,流程则反向:View 捕获点击 → 调用 presenter->toggleUnit() → Presenter 修改 Model 中的单位标志 → 下一次 tick() 时 Presenter 读取新标志并更新 View 文本。整个过程中,View 从未直接访问 Model。

五、常见误区与复盘

误区一:在 View 中直接调用 Model。这会让界面代码依赖硬件或全局状态,模拟器无法独立运行,单元测试也难以编写。正确做法是让 View 只调用 Presenter。

误区二:在 Model 中调用 invalidate()。Model 不应知道任何控件或绘制细节。刷新应由 Presenter 在合适的时机驱动 View 完成。

误区三:把所有逻辑都塞进 Presenter。Presenter 应保持“薄”,只做数据转换与事件转发。复杂业务规则应下沉到 Model,避免 Presenter 膨胀成新的“上帝类”。

复盘建议:检查每个 Screen 的 View 类,确认其中没有出现 model-> 调用;检查 Model 类,确认其中没有出现 invalidate() 或控件类型。只要这两条守住,MVP 的边界就基本清晰。

掌握 MVP 的职责划分与数据流后,后续在 TouchGFX Designer 中拖拽控件、绑定交互、编写 tick() 刷新逻辑时,就能清楚地知道每一行代码应该放在哪一层,从而构建出可维护、可测试、可移植的触摸交互系统。