2. TouchGFX基础回顾
做嵌入式GUI开发,说白了就是在有限的资源下,把界面切换这件事做到流畅、不崩溃。我见过太多项目,前期图省事,所有页面都堆在一个Screen里,结果后期加需求时,代码改得想哭。
这一章,我们来聊聊TouchGFX里最核心的导航体系。你搞懂了Screen和Container的区别,学会了屏幕栈怎么玩,后面那些花哨的页面切换、路由设计,都是水到渠成的事。
Screen与Container:两个最基础的概念
很多新手会问:Screen和Container到底有啥区别?我刚开始做TouchGFX时也迷糊过。简单说——Screen是页面,Container是组件。
| 特性 | Screen | Container |
|---|---|---|
| 生命周期 | 有完整的setup/teardown | 随父Screen创建销毁 |
| 内存占用 | 独立分配,切换时可能释放 | 嵌入在Screen内 |
| 导航能力 | 可以push/pop到屏幕栈 | 不能独立导航 |
| 典型用途 | 主页面、设置页、弹窗 | 导航栏、列表项、状态指示器 |
你想想看,一个Screen就像一页PPT,翻过去就没了。而Container是PPT里的一个图表,它跟着页面走。我在一个智能家居项目里,把顶部的状态栏做成了Container,这样每个Screen都能复用,改一次全改,省了不少事。
核心原则:Screen负责页面级切换,Container负责界面内复用。别把Container当Screen用,也别把Screen当Container用。
屏幕栈(Screen Stack)管理
屏幕栈,是TouchGFX导航的基石。它的工作方式,跟函数调用栈一模一样。
你从主页进入设置页,主页被压入栈底,设置页在栈顶。再从设置页进入WiFi配置页,设置页被压住,WiFi页在栈顶。按返回键时,WiFi页弹出销毁,设置页重新显示。
我画了一张图,帮你理解这个流程:
嗯,这里要注意:栈顶的Screen是唯一活跃的。栈里的其他Screen虽然还在内存里,但不会处理任何事件。我见过有人把大量数据放在Screen的成员变量里,结果栈深了以后内存爆了——这就是没搞清楚Screen的生命周期。
避坑指南:我曾经在一个医疗设备项目里,连续push了5个Screen,每个Screen都加载了高清图片。结果在低端MCU上直接内存溢出。后来我改用Container嵌套+懒加载,才把问题解决。记住:屏幕栈不是无限深的,RAM有多少,你自己心里要有数。
导航栏与页面切换
导航栏,是用户跟你的界面交互最频繁的地方。TouchGFX里实现导航栏,我推荐用Container来做。
为什么?因为导航栏通常在每个页面都存在,但内容不同。用Container,你可以把导航栏的骨架写好,然后通过虚函数让每个Screen定制自己的按钮行为。
来看一段代码,这是我常用的导航栏基类:
// NavigationBar.hpp
class NavigationBar : public Container {
public:
NavigationBar();
virtual ~NavigationBar() {}
// 子类实现这个方法,配置自己的按钮
virtual void setupNavigation() = 0;
// 通用的返回按钮
void setBackButtonCallback(GenericCallback<>& callback) {
backButton.setAction(callback);
}
protected:
Button backButton;
TextArea titleText;
};
// MainScreenNavigationBar.cpp
class MainScreenNavigationBar : public NavigationBar {
public:
MainScreenNavigationBar() {
titleText.setTypedText(TypedText(T_MAIN_TITLE));
// 设置返回按钮不可见(主页不需要返回)
backButton.setVisible(false);
}
void setupNavigation() override {
// 主页特有的导航配置
}
};
这样做的好处很明显:导航栏的布局、样式、动画都在一个地方维护。哪个Screen需要导航栏,直接组合进去就行。
自定义导航控制器
TouchGFX自带的ScreenManager能满足基本需求,但复杂项目里,我建议你自己写一个导航控制器。
为什么?因为默认的导航器不支持页面间传参、不支持拦截返回事件、不支持动画定制。你想想看,一个电商App,从商品列表到详情页,总得传个商品ID吧?默认的导航器做不到。
我分享一个我常用的导航控制器模式:
class NavigationController {
public:
// 单例
static NavigationController& getInstance() {
static NavigationController instance;
return instance;
}
// 带参数的页面跳转
template<typename T>
void pushScreenWithData(uint16_t screenId, T* data) {
// 保存数据到参数栈
paramStack.push_back(reinterpret_cast<void*>(data));
// 执行跳转
pushScreen(screenId);
}
// 返回并带回数据
template<typename T>
void popScreenWithResult(T* result) {
// 把结果传给上一个Screen
if (!screenStack.empty()) {
auto prevScreen = screenStack[screenStack.size() - 2];
prevScreen->onResult(result);
}
popScreen();
}
// 拦截返回键
bool onBackPressed() {
if (screenStack.empty()) return false;
auto currentScreen = screenStack.back();
// 让当前Screen决定是否允许返回
return currentScreen->onBackPressed();
}
private:
std::vector<Screen*> screenStack;
std::vector<void*> paramStack;
};
个人经验:我习惯把导航控制器做成单例,这样任何地方都能调用。但要注意线程安全——TouchGFX是单线程的,所以不用担心竞态。另外,参数栈我用vector来存void指针,用的时候强转回来。虽然有点C风格,但在嵌入式环境里,这比用any或variant省资源得多。
复杂页面路由设计
当你的项目有几十个页面时,路由设计就变得至关重要。我见过最乱的代码,页面跳转逻辑散落在各个按钮的回调里,想理清调用关系,得把整个代码库翻一遍。
我的做法是:用路由表统一管理。
路由表本质上是一个映射:页面ID → 创建函数。这样跳转时只需要传一个ID,具体创建哪个Screen,由路由表决定。
// RouteTable.hpp
using ScreenCreator = std::function<Screen*()>;
class RouteTable {
public:
static RouteTable& getInstance() {
static RouteTable instance;
return instance;
}
void registerRoute(uint16_t screenId, ScreenCreator creator) {
routes[screenId] = creator;
}
Screen* createScreen(uint16_t screenId) {
auto it = routes.find(screenId);
if (it != routes.end()) {
return it->second();
}
// 找不到就返回一个错误页
return new ErrorScreen();
}
private:
std::map<uint16_t, ScreenCreator> routes;
};
// 在main.cpp里注册路由
void setupRoutes() {
RouteTable::getInstance().registerRoute(SCREEN_MAIN, []() { return new MainScreen(); });
RouteTable::getInstance().registerRoute(SCREEN_SETTINGS, []() { return new SettingsScreen(); });
RouteTable::getInstance().registerRoute(SCREEN_WIFI, []() { return new WiFiScreen(); });
// ... 几十个路由注册
}
这样做的好处,我列一下:
- 解耦:按钮回调只需要知道目标页面的ID,不需要知道具体类名
- 可维护:所有页面跳转关系集中在一处,改路由不影响业务逻辑
- 可测试:可以轻松替换某个页面为Mock版本,方便单元测试
- 支持懒加载:页面只在被路由到时才创建,不占内存
核心思路:把「跳转到哪里」和「怎么跳转」分开。路由表只负责「创建哪个Screen」,导航控制器负责「怎么压栈、怎么传参」。各司其职,代码才清爽。
最后说一句,导航框架这东西,没有银弹。你项目的RAM有多大、页面有多少、交互有多复杂,直接决定了你该用哪种方案。但不管怎么选,Screen和Container的区别、屏幕栈的原理、路由表的思想,这些底层的东西是通用的。搞懂了它们,你就能搭出适合自己的导航框架。