4. TouchGFX工程结构与代码框架解析

TouchGFX Designer 在生成代码时,并不是把界面逻辑全部塞进一个文件,而是按照 MVP(Model-View-Presenter)模式把工程拆成若干职责明确的类。理解这些类的分工与调用方向,是后续手写业务代码、接入硬件驱动、排查界面不刷新等问题的基础。本节以 TouchGFX 4.20 及以上版本生成的典型工程为对象,剖析 Model、View、Presenter、FrontendApplication 以及 FrontendHeap、Screen 之间的关系。

一、生成目录与核心文件

一个 TouchGFX 工程通常包含 TouchGFX/、Middlewares/ST/touchgfx/、Core/ 等目录。与本节直接相关的是 TouchGFX/gui/ 与 TouchGFX/generated/gui_generated/ 两处:前者存放用户可编辑的 Model、View、Presenter 派生类,后者存放 Designer 自动生成的基类与 FrontendApplication。用户代码写在派生类中,重新生成时不会被覆盖;基类则每次生成都会重写,因此不要在其中添加逻辑。

TouchGFX/
├── gui/
│   ├── include/gui/
│   │   ├── model/Model.hpp
│   │   ├── common/FrontendApplication.hpp
│   │   └── screen1_screen/Screen1View.hpp
│   │                    Screen1Presenter.hpp
│   └── src/  (对应 .cpp 实现)
└── generated/gui_generated/
    ├── include/gui_generated/
    │   ├── common/FrontendApplicationBase.hpp
    │   ├── model/ModelBase.hpp
    │   └── screen1_screen/Screen1ViewBase.hpp
    │                    Screen1PresenterBase.hpp
    └── src/  (基类实现)

二、MVP 四类核心对象的职责

Model 是数据与业务逻辑的持有者。它不引用任何 View 或控件,只暴露 setXxx()、getXxx() 之类的接口,以及 tick() 供系统周期调用。例如温度采集、通信协议解析、状态机推进都应放在 Model 中。Model 通过 ModelListener 接口向 Presenter 通知数据变化,而不是直接操作控件。

View 负责“画”和“收事件”。它持有 Designer 生成的控件成员(如 textArea1、button1),实现 setupScreen()、tearDownScreen()、handleTickEvent() 等回调,并把用户交互(按钮点击、滑动)通过 Presenter 指针转发出去。View 不应包含业务计算,也不应直接访问硬件。

Presenter 是 View 与 Model 之间的中介。它同时持有 View 与 Model 的引用,实现 ModelListener 与 ViewListener 两套接口:当 Model 数据变化时,Presenter 调用 View 的更新方法;当 View 产生交互时,Presenter 调用 Model 的业务方法。这种“双向适配”使 View 与 Model 完全解耦。

FrontendApplication 是应用入口,继承自 FrontendApplicationBase。它负责创建 Model、FrontendHeap,并在 handleTickEvent() 中依次驱动 Model 与当前 Screen 的 tick,同时处理屏幕切换请求。它相当于整个 GUI 的“总调度”,但不参与具体业务。

三、数据流向与调用链

把上述对象串起来,可以看到两条方向相反的主链路。第一条是“数据上行”:硬件或定时器改变 Model 内部状态,Model 通过 modelListener->... 通知 Presenter,Presenter 再调用 View 的 updateXxx(),最终由控件重绘。第二条是“交互下行”:用户点击按钮触发 View 的虚函数,View 调用 presenter->...,Presenter 转调 Model 的业务方法,Model 处理后再沿上行链路把结果推回界面。两条链路都经过 Presenter,View 与 Model 之间没有任何直接引用。

TouchGFX MVP 核心类关系与数据流向 FrontendApplication 创建 Model / Heap,驱动 tick Model 业务数据 / 状态机 tick() / setXxx() / getXxx() Presenter ModelListener + ViewListener 双向适配,解耦 View/Model View 控件 / setupScreen() handleTickEvent() Screen1ViewBase Designer 生成控件成员 硬件 / 定时器 / 通信 ADC、UART、RTOS 任务 通知 更新 交互 调用 继承 读写 数据上行(Model→Presenter→View) 交互下行(View→Presenter→Model) 创建/驱动

四、关键接口与代码骨架

理解职责划分后,最实用的做法是记住几个必须实现的接口。Model 侧需要继承 ModelBase 并实现 tick();Presenter 侧需要继承 Screen1PresenterBase,同时实现 ModelListener 与 ViewListener;View 侧需要继承 Screen1ViewBase,并实现 setupScreen()、tearDownScreen()、handleTickEvent() 等回调。下面给出一个最小骨架,展示温度值从 Model 推到界面的过程。

// Model.hpp
class Model : public ModelBase {
public:
    void setTemperature(int16_t t) {
        temperature = t;
        if (modelListener) modelListener->temperatureUpdated(t);
    }
    int16_t getTemperature() const { return temperature; }
    void tick() override { /* 周期采集或状态推进 */ }
private:
    int16_t temperature = 0;
};

// Screen1Presenter.hpp
class Screen1Presenter : public Screen1PresenterBase {
public:
    void temperatureUpdated(int16_t t) override {
        view.updateTemperature(t);   // 上行:Model → View
    }
    void onStartButtonClicked() override {
        model->startSampling();      // 下行:View → Model
    }
};

// Screen1View.hpp
class Screen1View : public Screen1ViewBase {
public:
    void updateTemperature(int16_t t) {
        Unicode::snprintf(textAreaTempBuffer, 8, "%d", t);
        textAreaTemp.invalidate();
    }
    void handleTickEvent() override {
        Screen1ViewBase::handleTickEvent();
    }
};

注意 modelListener 与 view 指针由基类在 bind() 阶段注入,用户不需要手动 new。当 Designer 中新增一个 Screen 时,会同步生成对应的 ScreenXView、ScreenXPresenter 以及 ScreenXViewBase,并在 FrontendHeap 中注册切换入口。

五、常见误区与排查要点

第一,把业务逻辑写进 View。这样会导致界面与数据强耦合,一旦换屏或复用逻辑就要复制代码。正确做法是 View 只做显示与转发。第二,在 Model 中直接调用控件。Model 不应包含任何 touchgfx:: 控件类型,否则编译依赖和可测试性都会变差。第三,忘记调用 invalidate()。TouchGFX 采用脏矩形重绘,更新了缓冲区但未 invalidate,界面不会刷新,这是新手最常见的“数据变了但屏幕不动”的原因。第四,在 setupScreen() 中做耗时操作。该函数在屏幕切换时同步执行,阻塞过久会造成切换卡顿,应把耗时初始化放到 Model 的 tick 或独立任务中。

判断某个函数该放在哪一层,可以用一句话检验:它是否直接操作控件?是则放 View;它是否涉及业务规则或硬件数据?是则放 Model;它是否只是把两者对接起来?是则放 Presenter。

六、FrontendApplication 的调度细节

FrontendApplication::handleTickEvent() 每帧被调用一次,内部先执行 Model::tick(),再执行当前 Screen 的 handleTickEvent(),最后处理 pendingScreenTransitionCallback。这意味着 Model 的状态更新先于界面刷新,同一帧内 Presenter 推给 View 的数据能在本帧绘制中体现。若在 Model 的 tick 中做了阻塞式等待,会直接拖慢整个 GUI 帧率,因此 Model 的 tick 应保持轻量,重活交给 RTOS 任务并通过消息或标志位回传。

掌握以上结构后,后续无论是添加自定义控件、接入 FreeRTOS 任务,还是实现多屏数据共享,都能快速定位代码应落在哪一层。建议在首次阅读生成工程时,沿着 FrontendApplication → Model → Presenter → View 的顺序把每个类的头文件通读一遍,再对照 Designer 中的控件名找到对应成员,这样对 MVP 的数据流向会形成稳定的直觉。