将菜单的架构改成 MVP,并且进一步优化视图层和模型层的逻辑
This commit is contained in:
562
docs/系统架构设计文档.md
562
docs/系统架构设计文档.md
@@ -77,13 +77,535 @@ columns 1
|
||||
- 处理按键驱动的菜单状态迁移与路径重建
|
||||
- 执行菜单显示坐标计算与多级菜单渲染调度
|
||||
|
||||
### 4.3 显示层
|
||||
### 4.3 多页面管理层设计
|
||||
**设计思想:**
|
||||
基于经典的 MVP 范式,彻底解耦**数据、视图、控制逻辑**,解决复杂 UI 与业务逻辑的协同维护问题。
|
||||
- **Model(模型层)**:纯业务数据与状态管理,负责系统参数、设备状态、采集数据的读写、校验、存储,与 UI 完全无关。
|
||||
- **View(视图层)**:纯渲染显示,仅根据 Model 的数据绘制菜单界面、焦点高亮、弹窗、动画,不处理任何业务逻辑。
|
||||
- **Presenter(控制层)**:核心调度中枢,接收输入事件、更新 Model 数据、控制 View 刷新、处理菜单跳转逻辑,是 Model 与 View 的唯一桥梁。
|
||||
目录结构:
|
||||
`src/Drv/pages`: 这个文件夹下面放不同的页面
|
||||
`src/Drv/pages/global`: 全局状态管理器,页面之间交互的中间件
|
||||
`src/Drv/pages/global/renderer_lcd.c`: 这个文件是抽象底层的lcd,给所有页面提供一个统一的调用接口。
|
||||
`src/Drv/pages/global/renderer_lcd.h`: 这个文件是抽象底层的lcd,给所有页面提供一个统一的调用接口。
|
||||
`src/Drv/pages/menu`: 项目的菜单逻辑
|
||||
`src/Drv/pages/menu/model`: 菜单的模型层
|
||||
`src/Drv/pages/menu/view`: 菜单的视图层
|
||||
`src/Drv/pages/menu/Presenter`: 菜单的控制层
|
||||
`src/Drv/pages/menu/page.c`: 菜单的页面逻辑
|
||||
`src/Drv/pages/menu/page.h`: 菜单的页面逻辑
|
||||
|
||||
- 文件:`src/Drv/lcd/lcd.c`、`src/Drv/lcd/lcd_draw.c`、`src/Drv/lcd/lcd_text.c`、`src/Drv/lcd/text_codec.c`、`src/Drv/lcd/ascii.c`、`src/Drv/menu/model/display.c`
|
||||
- 职责:
|
||||
- 管理 LCD 显存 `g_tCVsr` 与像素绘制
|
||||
- 提供 ASCII/UTF-8 字符显示能力
|
||||
- 提供静态菜单模型定义 `g_tMenuModelTab`
|
||||
`src/Drv/pages/...`: 其他页面
|
||||
|
||||
整个多页面管理系统分为 5 个核心模块,从上到下形成完整的调度闭环,与底层 MVP 架构无缝衔接
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 全局状态管理器(Global Model) │
|
||||
│ (跨页面共享数据、持久化配置、系统全局状态、观察者通知) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 页面管理器核心(Page Manager) │
|
||||
│ ┌─────────────────────────────────────────────────────────┐│
|
||||
│ │ 页面栈(Page Stack 静态数组) ││
|
||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││
|
||||
│ │ │ 栈顶页面 │ │ 后台页面1 │ │ 后台页面2 │ ... ││
|
||||
│ │ │(前台显示) │ │(暂停状态) │ │(暂停状态) │ ││
|
||||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ ││
|
||||
│ └─────────────────────────────────────────────────────────┘│
|
||||
│ 生命周期调度器 | 事件分发器 | 页面跳转核心接口 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 标准化页面抽象(Page 接口层) │
|
||||
│ 每个页面独立封装:生命周期钩子 + 专属MVP三元组 │
|
||||
│ ┌─────────────────────────────────────────────────────────┐│
|
||||
│ │ Page 基础属性:页面ID、缓存标志、创建状态 ││
|
||||
│ │ 生命周期钩子:on_create/on_enter/on_exit/on_destroy ││
|
||||
│ │ 运行入口:on_event(事件处理)、on_loop(主循环) ││
|
||||
│ │ 专属MVP三元组:Model(私有) → Presenter → View ││
|
||||
│ └─────────────────────────────────────────────────────────┘│
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 底层支撑层(与MVP架构无缝衔接) │
|
||||
│ 显示HAL | 输入HAL | 基础图形库 | 硬件驱动 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
#### 4.3.1 标准化页面抽象(Page 接口层)
|
||||
这是整个系统的基础单元,**每个独立业务页面对应一个 Page 实例,每个 Page 内部封装一套完全独立的 MVP 三元组**,对外仅暴露标准接口,页面管理器仅通过标准接口与页面交互,完全不关心页面内部的 MVP 实现,实现页面间的彻底解耦。
|
||||
```c
|
||||
// 编译期静态配置(根据MCU资源调整)
|
||||
#define MAX_PAGE_STACK_DEPTH 5 // 最大页面栈深度,防止栈溢出
|
||||
#define MAX_PAGE_COUNT 8 // 系统支持的最大页面总数
|
||||
#define EVENT_QUEUE_LENGTH 8 // 输入事件队列长度
|
||||
|
||||
// 页面唯一ID枚举(所有页面必须在此定义,用于页面查找、防重复入栈)
|
||||
typedef enum {
|
||||
PAGE_ID_NONE = 0,
|
||||
PAGE_ID_HOME, // 首页(主页面,栈底常驻)
|
||||
PAGE_ID_SETTINGS, // 设置页面
|
||||
PAGE_ID_TEMP_DETAIL, // 温度详情页面
|
||||
PAGE_ID_ABOUT, // 关于页面
|
||||
PAGE_ID_MAX
|
||||
} page_id_t;
|
||||
|
||||
// 事件处理结果枚举
|
||||
typedef enum {
|
||||
EVENT_UNHANDLED = 0, // 事件未被消费,继续分发
|
||||
EVENT_HANDLED = 1 // 事件已被消费,终止分发
|
||||
} event_result_t;
|
||||
|
||||
// 页面结构体前置声明
|
||||
typedef struct page_t page_t;
|
||||
|
||||
```
|
||||
#### 4.3.2 页面标准接口与生命周期定义
|
||||
嵌入式场景下,生命周期钩子的设计必须严格对应资源的申请 / 释放时机,避免 RAM 浪费和低功耗异常,每个钩子的执行时机、职责完全固定,禁止越权操作。
|
||||
```c
|
||||
// 页面标准接口结构体
|
||||
struct page_t {
|
||||
// ===================== 页面基础属性 =====================
|
||||
page_id_t page_id; // 页面唯一ID,不可重复
|
||||
bool is_cached; // 缓存标志:true=出栈时不销毁,仅暂停;false=出栈时彻底销毁
|
||||
bool is_created; // 内部状态标志:页面是否已执行on_create,外部只读
|
||||
|
||||
// ===================== 生命周期钩子函数 =====================
|
||||
/**
|
||||
* @brief 页面第一次创建时执行,仅执行1次
|
||||
* @note 职责:MVP三元组初始化、静态资源申请(字体/图标缓冲区)、回调绑定
|
||||
* @note 禁止:耗时操作、屏幕绘制、动态内存申请
|
||||
*/
|
||||
void (*on_create)(page_t *page);
|
||||
|
||||
/**
|
||||
* @brief 页面进入前台(成为栈顶)时执行,每次进入都执行
|
||||
* @note 职责:从Model拉取最新数据、全量UI绘制、开启定时器/传感器、注册事件回调
|
||||
* @note 禁止:资源销毁、MVP初始化操作
|
||||
*/
|
||||
void (*on_enter)(page_t *page);
|
||||
|
||||
/**
|
||||
* @brief 页面离开前台(被覆盖/出栈)时执行,每次离开都执行
|
||||
* @note 职责:关闭定时器/传感器、保存临时数据、注销事件回调、低功耗准备
|
||||
* @note 禁止:屏幕绘制、MVP初始化、耗时操作
|
||||
*/
|
||||
void (*on_exit)(page_t *page);
|
||||
|
||||
/**
|
||||
* @brief 页面彻底销毁时执行,仅执行1次
|
||||
* @note 职责:释放所有申请的资源、MVP实例销毁、回调注销
|
||||
* @note 仅非缓存页面出栈时会触发,缓存页面不会触发
|
||||
*/
|
||||
void (*on_destroy)(page_t *page);
|
||||
|
||||
// ===================== 运行时入口 =====================
|
||||
/**
|
||||
* @brief 事件处理入口,页面管理器仅将事件分发给栈顶页面的此函数
|
||||
* @note 职责:将事件转发给内部View,由View判断命中并转发给Presenter
|
||||
* @return 事件是否被消费
|
||||
*/
|
||||
event_result_t (*on_event)(page_t *page, input_event_t *event);
|
||||
|
||||
/**
|
||||
* @brief 主循环入口,页面管理器仅调用栈顶页面的此函数
|
||||
* @note 职责:执行页面Presenter的主循环逻辑、UI动画刷新、状态机轮询
|
||||
* @note 禁止:阻塞操作、耗时计算
|
||||
*/
|
||||
void (*on_loop)(page_t *page);
|
||||
|
||||
// ===================== 页面专属MVP三元组 =====================
|
||||
// 每个页面独立拥有,与其他页面完全隔离,页面管理器不直接访问
|
||||
void *presenter; // 页面对应的Presenter实例指针
|
||||
void *view; // 页面对应的View实例指针
|
||||
void *model; // 页面对应的私有Model实例(仅当前页面使用)
|
||||
};
|
||||
```
|
||||
#### 4.3.3 页面 MVP 三元组的绑定规则
|
||||
- **完全隔离**:每个页面的 Model 仅管理当前页面的私有数据,禁止跨页面访问;View 仅负责当前页面的 UI 绘制;Presenter 仅调度当前页面的业务逻辑
|
||||
- **绑定时机**:MVP 三元组的初始化、绑定,必须在页面的`on_create`钩子中完成,保证页面创建时 MVP 已就绪
|
||||
- **销毁时机**:MVP 实例的资源释放,必须在页面的`on_destroy`钩子中完成,避免资源泄漏
|
||||
- **跨页面交互**:禁止页面 A 的 Presenter 直接调用页面 B 的任何接口,所有跨页面数据同步必须通过全局状态管理器完成
|
||||
#### 4.3.4 栈式页面管理器(核心调度中枢)
|
||||
这是整个系统的核心,通过静态数组实现的栈结构管理所有页面,负责页面跳转的核心逻辑、生命周期的精准调度、输入事件的统一分发、主循环的统一调度。
|
||||
**页面管理器结构体定义**
|
||||
```c
|
||||
// 页面管理器结构体
|
||||
typedef struct {
|
||||
page_t *page_stack[MAX_PAGE_STACK_DEPTH]; // 页面栈,静态数组实现
|
||||
int8_t stack_top; // 栈顶指针,初始值为-1(空栈)
|
||||
} page_manager_t;
|
||||
|
||||
// 全局页面管理器单例(整个系统仅一个实例)
|
||||
extern page_manager_t g_page_manager;
|
||||
```
|
||||
**基础接口:初始化与栈状态查询**
|
||||
```c
|
||||
// 全局页面管理器实例
|
||||
page_manager_t g_page_manager;
|
||||
|
||||
/**
|
||||
* @brief 页面管理器初始化,系统启动时调用1次
|
||||
*/
|
||||
void page_manager_init(page_manager_t *manager) {
|
||||
memset(manager, 0, sizeof(page_manager_t));
|
||||
manager->stack_top = -1; // 初始化为空栈
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取当前栈顶页面(前台显示页面)
|
||||
* @return 栈顶页面指针,空栈返回NULL
|
||||
*/
|
||||
page_t* page_manager_get_top(page_manager_t *manager) {
|
||||
if (manager->stack_top < 0) {
|
||||
return NULL;
|
||||
}
|
||||
return manager->page_stack[manager->stack_top];
|
||||
}
|
||||
```
|
||||
**页面入栈(Push,页面跳转)**
|
||||
- 入参校验:检查栈是否已满、新页面是否合法、是否重复入栈
|
||||
- 触发旧栈顶页面的`on_exit`:当前页面被覆盖,执行暂停逻辑
|
||||
- 新页面入栈,栈顶指针 + 1
|
||||
- 若新页面未创建,触发`on_create`:执行 MVP 初始化,仅执行 1 次
|
||||
- 触发新页面的`on_enter`:新页面进入前台,执行 UI 绘制与业务启动
|
||||
- 新页面成为前台页面,接收事件与主循环调度
|
||||
```c
|
||||
/**
|
||||
* @brief 页面入栈(跳转到新页面)
|
||||
* @param manager 页面管理器实例
|
||||
* @param new_page 待入栈的新页面实例
|
||||
* @return 0=成功,负数=错误码
|
||||
*/
|
||||
int page_manager_push(page_manager_t *manager, page_t *new_page) {
|
||||
// 入参合法性校验
|
||||
if (manager == NULL || new_page == NULL) {
|
||||
return -1;
|
||||
}
|
||||
// 栈满校验,防止栈溢出
|
||||
if (manager->stack_top >= MAX_PAGE_STACK_DEPTH - 1) {
|
||||
return -2;
|
||||
}
|
||||
// 防重复入栈:当前栈顶已是该页面,无需重复操作
|
||||
page_t *current_top = page_manager_get_top(manager);
|
||||
if (current_top != NULL && current_top->page_id == new_page->page_id) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ===================== 生命周期执行 =====================
|
||||
// 1. 触发当前栈顶页面的on_exit(离开前台)
|
||||
if (current_top != NULL && current_top->on_exit != NULL) {
|
||||
current_top->on_exit(current_top);
|
||||
}
|
||||
|
||||
// 2. 新页面入栈
|
||||
manager->stack_top++;
|
||||
manager->page_stack[manager->stack_top] = new_page;
|
||||
|
||||
// 3. 页面首次创建,触发on_create(仅执行1次)
|
||||
if (!new_page->is_created) {
|
||||
if (new_page->on_create != NULL) {
|
||||
new_page->on_create(new_page);
|
||||
}
|
||||
new_page->is_created = true;
|
||||
}
|
||||
|
||||
// 4. 触发新页面的on_enter(进入前台)
|
||||
if (new_page->on_enter != NULL) {
|
||||
new_page->on_enter(new_page);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
**页面出栈(Pop,页面返回)**
|
||||
- 入参校验:检查栈是否为空、是否为栈底首页(禁止出栈)
|
||||
- 触发当前栈顶页面的`on_exit`:当前页面离开前台,执行退出准备
|
||||
- 若当前页面为非缓存页面,触发`on_destroy`:彻底销毁页面,释放资源
|
||||
- 栈顶指针 - 1,上一个页面恢复为栈顶
|
||||
- 触发新栈顶页面的`on_enter`:页面恢复前台,执行数据刷新与 UI 重绘
|
||||
- 新栈顶页面成为前台页面,接收事件与主循环调度
|
||||
```c
|
||||
/**
|
||||
* @brief 页面出栈(返回上一页)
|
||||
* @param manager 页面管理器实例
|
||||
* @return 0=成功,负数=错误码
|
||||
*/
|
||||
int page_manager_pop(page_manager_t *manager) {
|
||||
// 入参校验
|
||||
if (manager == NULL) {
|
||||
return -1;
|
||||
}
|
||||
// 禁止空栈出栈、禁止栈底首页出栈(保证首页常驻)
|
||||
if (manager->stack_top <= 0) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
// ===================== 生命周期执行 =====================
|
||||
// 1. 获取当前栈顶页面(待出栈页面)
|
||||
page_t *current_page = manager->page_stack[manager->stack_top];
|
||||
|
||||
// 2. 触发当前页面的on_exit(离开前台)
|
||||
if (current_page->on_exit != NULL) {
|
||||
current_page->on_exit(current_page);
|
||||
}
|
||||
|
||||
// 3. 非缓存页面:触发on_destroy,彻底销毁,释放资源
|
||||
if (!current_page->is_cached) {
|
||||
if (current_page->on_destroy != NULL) {
|
||||
current_page->on_destroy(current_page);
|
||||
}
|
||||
current_page->is_created = false;
|
||||
}
|
||||
|
||||
// 4. 栈顶指针减1,页面出栈
|
||||
manager->stack_top--;
|
||||
|
||||
// 5. 触发新栈顶页面的on_enter(恢复前台)
|
||||
page_t *new_top = manager->page_stack[manager->stack_top];
|
||||
if (new_top->on_enter != NULL) {
|
||||
new_top->on_enter(new_top);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**统一事件分发机制**
|
||||
嵌入式多页面 GUI 的核心痛点之一是事件误响应,必须保证**仅当前前台页面(栈顶)可接收输入事件**,后台页面完全无法接收事件,避免出现「设置页面按键触发首页逻辑」的 bug
|
||||
- 底层 Input HAL 在中断中采集输入事件(按键、触摸、编码器),投递到全局静态环形事件队列
|
||||
- 页面管理器在主循环中从事件队列取出事件
|
||||
- 获取当前栈顶前台页面,将事件分发给页面的`on_event`入口
|
||||
- 页面将事件转发给内部 View,View 判断事件命中,转发给 Presenter 处理
|
||||
- 若页面返回`EVENT_UNHANDLED`(事件未被消费),则执行全局事件处理(例如全局返回键、电源键
|
||||
```c
|
||||
// 全局事件处理(示例:全局返回键)
|
||||
static void global_event_handle(input_event_t *event) {
|
||||
// 全局返回键:无论哪个页面,按返回键都执行出栈
|
||||
if (event->type == EVENT_TYPE_KEY_DOWN && event->key_id == KEY_ID_BACK) {
|
||||
page_manager_pop(&g_page_manager);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面管理器事件分发逻辑(集成在主循环中)
|
||||
static void page_manager_dispatch_event(page_manager_t *manager) {
|
||||
input_event_t event;
|
||||
// 从事件队列取出事件
|
||||
if (!input_event_get(&event)) {
|
||||
return;
|
||||
}
|
||||
// 获取当前栈顶页面
|
||||
page_t *current_page = page_manager_get_top(manager);
|
||||
if (current_page == NULL) {
|
||||
return;
|
||||
}
|
||||
// 分发给当前页面
|
||||
event_result_t result = EVENT_UNHANDLED;
|
||||
if (current_page->on_event != NULL) {
|
||||
result = current_page->on_event(current_page, &event);
|
||||
}
|
||||
// 页面未消费,执行全局事件处理
|
||||
if (result == EVENT_UNHANDLED) {
|
||||
global_event_handle(&event);
|
||||
}
|
||||
}
|
||||
```
|
||||
#### 4.3.4 全局状态管理器(跨页面数据同步)
|
||||
多页面场景下,必然存在跨页面数据共享的需求(例如设置页修改温度单位,首页、详情页都要同步更新),如果直接让页面间互相调用,会彻底破坏 MVP 的解耦原则,导致代码耦合混乱。
|
||||
|
||||
全局状态管理器的核心设计思路是:**实现一个全局单例的 Model(GlobalModel),作为跨页面数据的唯一可信源,通过观察者模式实现数据变化的同步通知,完全不破坏各页面 MVP 的独立性**
|
||||
#### 4.3.5 完整页面实现示例
|
||||
以下是一个完整的页面实现,包含生命周期钩子、MVP 三元组绑定、页面跳转逻辑,可直接落地使用。
|
||||
**设置页面 MVP 定义**
|
||||
```c
|
||||
// ===================== 设置页面Model =====================
|
||||
typedef struct {
|
||||
float temp_offset; // 温度校准偏移
|
||||
uint8_t temp_unit; // 温度单位
|
||||
bool backlight_auto; // 自动背光开关
|
||||
// 数据变化回调
|
||||
void (*on_data_change)(void *context);
|
||||
void *context;
|
||||
} settings_model_t;
|
||||
|
||||
// ===================== 设置页面View接口 =====================
|
||||
typedef struct {
|
||||
void (*update_temp_offset)(void *view, float offset);
|
||||
void (*update_temp_unit)(void *view, uint8_t unit);
|
||||
void (*update_backlight_state)(void *view, bool auto_on);
|
||||
void (*draw)(void *view);
|
||||
} settings_view_interface_t;
|
||||
|
||||
// ===================== 设置页面Presenter =====================
|
||||
typedef struct {
|
||||
page_t *page;
|
||||
settings_model_t *model;
|
||||
settings_view_interface_t *view;
|
||||
} settings_presenter_t;
|
||||
```
|
||||
**设置页面实例与生命周期实现**
|
||||
```c
|
||||
// 静态页面实例与MVP实例(静态内存,无动态分配)
|
||||
static page_t settings_page;
|
||||
static settings_model_t settings_model;
|
||||
static settings_view_interface_t settings_view;
|
||||
static settings_presenter_t settings_presenter;
|
||||
|
||||
// ===================== 生命周期钩子实现 =====================
|
||||
// 页面创建:MVP初始化,仅执行1次
|
||||
static void settings_page_on_create(page_t *page) {
|
||||
// 1. 初始化Model
|
||||
memset(&settings_model, 0, sizeof(settings_model_t));
|
||||
// 从Flash读取持久化配置
|
||||
settings_model.temp_unit = global_model_get_temp_unit();
|
||||
settings_model.temp_offset = flash_read_temp_offset();
|
||||
settings_model.backlight_auto = flash_read_backlight_auto();
|
||||
// 绑定Model回调
|
||||
settings_model.context = &settings_presenter;
|
||||
settings_model.on_data_change = settings_presenter_on_model_change;
|
||||
|
||||
// 2. 初始化View接口
|
||||
memset(&settings_view, 0, sizeof(settings_view_interface_t));
|
||||
settings_view.update_temp_offset = settings_view_update_temp_offset;
|
||||
settings_view.update_temp_unit = settings_view_update_temp_unit;
|
||||
settings_view.update_backlight_state = settings_view_update_backlight_state;
|
||||
settings_view.draw = settings_view_draw;
|
||||
|
||||
// 3. 初始化Presenter,绑定MVP
|
||||
memset(&settings_presenter, 0, sizeof(settings_presenter_t));
|
||||
settings_presenter.page = page;
|
||||
settings_presenter.model = &settings_model;
|
||||
settings_presenter.view = &settings_view;
|
||||
|
||||
// 4. 绑定MVP到页面
|
||||
page->model = &settings_model;
|
||||
page->view = &settings_view;
|
||||
page->presenter = &settings_presenter;
|
||||
}
|
||||
|
||||
// 页面进入前台:刷新UI,开启定时器
|
||||
static void settings_page_on_enter(page_t *page) {
|
||||
settings_presenter_t *presenter = (settings_presenter_t *)page->presenter;
|
||||
// 从Model拉取数据,刷新UI
|
||||
presenter->view->update_temp_offset(presenter->view, presenter->model->temp_offset);
|
||||
presenter->view->update_temp_unit(presenter->view, presenter->model->temp_unit);
|
||||
presenter->view->update_backlight_state(presenter->view, presenter->model->backlight_auto);
|
||||
// 全量绘制页面
|
||||
presenter->view->draw(presenter->view);
|
||||
// 开启背光定时器
|
||||
backlight_timer_start();
|
||||
}
|
||||
|
||||
// 页面离开前台:保存数据,关闭定时器
|
||||
static void settings_page_on_exit(page_t *page) {
|
||||
settings_model_t *model = (settings_model_t *)page->model;
|
||||
// 关闭背光定时器
|
||||
backlight_timer_stop();
|
||||
// 保存配置到Flash
|
||||
flash_write_temp_offset(model->temp_offset);
|
||||
flash_write_backlight_auto(model->backlight_auto);
|
||||
// 同步全局状态
|
||||
global_model_set_temp_unit(model->temp_unit);
|
||||
}
|
||||
|
||||
// 页面销毁:释放资源
|
||||
static void settings_page_on_destroy(page_t *page) {
|
||||
memset(page->model, 0, sizeof(settings_model_t));
|
||||
memset(page->presenter, 0, sizeof(settings_presenter_t));
|
||||
page->model = NULL;
|
||||
page->view = NULL;
|
||||
page->presenter = NULL;
|
||||
}
|
||||
|
||||
// 事件处理入口
|
||||
static event_result_t settings_page_on_event(page_t *page, input_event_t *event) {
|
||||
settings_view_interface_t *view = (settings_view_interface_t *)page->view;
|
||||
// 转发事件给View处理
|
||||
return settings_view_handle_event(view, event);
|
||||
}
|
||||
|
||||
// 主循环入口
|
||||
static void settings_page_on_loop(page_t *page) {
|
||||
settings_presenter_t *presenter = (settings_presenter_t *)page->presenter;
|
||||
// 执行Presenter主循环逻辑(长按检测、动画刷新等)
|
||||
settings_presenter_loop(presenter);
|
||||
}
|
||||
|
||||
// ===================== 页面实例获取接口 =====================
|
||||
page_t* settings_page_get_instance(void) {
|
||||
memset(&settings_page, 0, sizeof(page_t));
|
||||
settings_page.page_id = PAGE_ID_SETTINGS;
|
||||
settings_page.is_cached = true; // 缓存页面,出栈不销毁
|
||||
// 绑定生命周期钩子
|
||||
settings_page.on_create = settings_page_on_create;
|
||||
settings_page.on_enter = settings_page_on_enter;
|
||||
settings_page.on_exit = settings_page_on_exit;
|
||||
settings_page.on_destroy = settings_page_on_destroy;
|
||||
settings_page.on_event = settings_page_on_event;
|
||||
settings_page.on_loop = settings_page_on_loop;
|
||||
return &settings_page;
|
||||
}
|
||||
```
|
||||
**页面跳转调用示例**
|
||||
```c
|
||||
// 首页点击设置按钮,跳转到设置页面
|
||||
void home_presenter_on_settings_btn_click(home_presenter_t *presenter) {
|
||||
page_t *settings_page = settings_page_get_instance();
|
||||
page_manager_push(&g_page_manager, settings_page);
|
||||
}
|
||||
|
||||
// 设置页面点击返回按钮,返回首页
|
||||
event_result_t settings_view_handle_event(settings_view_interface_t *view, input_event_t *event) {
|
||||
if (event->type == EVENT_TYPE_TOUCH_DOWN) {
|
||||
// 判断返回按钮点击
|
||||
if (is_point_in_rect(event->x, event->y, BACK_BTN_X, BACK_BTN_Y, BACK_BTN_W, BACK_BTN_H)) {
|
||||
// 调用出栈接口,返回上一页
|
||||
page_manager_pop(&g_page_manager);
|
||||
return EVENT_HANDLED;
|
||||
}
|
||||
}
|
||||
return EVENT_UNHANDLED;
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.3.6 当前项目重构落地规划(已执行)
|
||||
为解决 `MenuProc_See_AppInfo`、`MenuProc_See_YC` 这类“页面入口被函数化”的扩展瓶颈,本项目按以下路径重构,并要求每个模块均可独立测试:
|
||||
|
||||
1. **目录与职责重构(已完成)**
|
||||
- 新增 `src/Drv/pages` 作为多页面管理根目录;
|
||||
- 新增 `src/Drv/pages/global`,统一承载跨页面能力(`global_state`、`renderer_lcd`);
|
||||
- 新增 `src/Drv/pages/menu/page.c`,将现有菜单 MVP 封装成标准页面;
|
||||
- 新增示例业务页面 `src/Drv/pages/app_info`、`src/Drv/pages/yc`,用于验证页面跳转链路与生命周期。
|
||||
|
||||
2. **核心调度中枢(已完成)**
|
||||
- 新增 `page.h` 定义标准页面抽象(ID、缓存策略、生命周期钩子、事件入口、循环入口、私有 MVP 指针);
|
||||
- 新增 `page_manager.c`,实现:
|
||||
- 静态页面注册(防重复、上限保护);
|
||||
- 栈式 `push/pop` 生命周期调度;
|
||||
- 栈顶独占事件分发;
|
||||
- 全局返回键兜底(页面未消费时触发 `pop`);
|
||||
- 栈深度查询接口(用于诊断与测试)。
|
||||
|
||||
3. **菜单页面化改造(已完成)**
|
||||
- `MenuApp_Init` 改为:
|
||||
- 初始化全局状态;
|
||||
- 初始化并激活 `PageManager`;
|
||||
- 注册 `Menu/AppInfo/YC` 页面;
|
||||
- 默认入栈 `PAGE_ID_MENU`;
|
||||
- `MenuApp_HandleInput` 改为统一输入事件分发;
|
||||
- `MenuApp_Render` 改为调度当前栈顶页面 `on_loop`;
|
||||
- `MenuProc_See_AppInfo`、`MenuProc_See_YC` 改为页面跳转入口(非业务实现函数);
|
||||
- `MenuProc_See_YC` 跳转前通过 `GlobalModel` 同步当前菜单参数,实现跨页面上下文传递。
|
||||
|
||||
4. **可靠性约束(已完成)**
|
||||
- 全部页面实例采用静态内存,避免运行期动态分配;
|
||||
- 页面栈深度与页面注册数均有编译期上限;
|
||||
- 生命周期调用顺序固定:`exit -> (destroy) -> enter`;
|
||||
- 禁止后台页面收事件,仅栈顶可处理输入。
|
||||
|
||||
5. **单元可测性设计(目标 > 80%,已落地测试入口)**
|
||||
- 新增 `test_p1_page_manager.c`:覆盖页面注册/入栈/出栈/全局返回事件/循环调度;
|
||||
- 新增 `test_p1_menu_page_actions.c`:覆盖菜单页跳转 `AppInfo/YC` 与返回链路;
|
||||
- 保留并复用原 `menu tree/layout/navigator/runtime startup` 测试,保障回归稳定。
|
||||
|
||||
6. **后续扩展规范(作为后续页面开发模板)**
|
||||
- 新页面必须以 `src/Drv/pages/<page_name>/page.c` 实现标准生命周期;
|
||||
- 页面间禁止直接互调,只允许通过 `GlobalModel` 同步共享状态;
|
||||
- 菜单项 `pfnWinProc` 仅作为“页面路由入口”,不再承载页面业务实现。
|
||||
|
||||
### 4.4 输入层
|
||||
|
||||
@@ -111,9 +633,9 @@ columns 1
|
||||
|
||||
### 5.1 菜单模型与菜单树
|
||||
|
||||
- 静态菜单模型:`tagMenuModel`(定义于 `src/Drv/menu/model/display.h`,数据在 `src/Drv/menu/model/display.c`)
|
||||
- 运行时菜单项:`tagMenuItem`(定义于 `src/Drv/menu/common/menu_item_types.h`,实例由 `src/Drv/menu/app/menu.c` 管理)
|
||||
- 运行时控制:`tagMenuCtrl`、`tagDspCtrl`(定义于 `src/Drv/menu/common/menu_state_types.h`,实例为 `app/menu.c` 内部私有)
|
||||
- 静态菜单模型:`tagMenuModel`(定义于 `src/Drv/pages/menu/model/display.h`,数据在 `src/Drv/pages/menu/model/menu_model.c`)
|
||||
- 运行时菜单项:`tagMenuItem`(定义于 `src/Drv/pages/menu/view/menu_view.h`,实例由 `src/Drv/pages/menu/page.c` 管理)
|
||||
- 运行时控制:`tagMenuCtrl`、`tagDspCtrl`(定义于 `src/Drv/pages/menu/view/menu_view.h`,实例为 `src/Drv/pages/menu/page.c` 内部私有)
|
||||
|
||||
关键关系:
|
||||
|
||||
@@ -173,18 +695,18 @@ columns 1
|
||||
|
||||
- `src/Drv/menu/app/menu.c`
|
||||
- 菜单应用 Facade(`MenuApp_*` 对外接口)
|
||||
- 持有运行时私有状态(`s_menuCtrl`、`s_dspCtrl`、`s_menuItems`)
|
||||
- `src/Drv/menu/presenter/menu_presenter.c`
|
||||
- 负责 PageManager 初始化与 `MenuPage` 生命周期托管
|
||||
- `src/Drv/pages/menu/presenter/menu_presenter.c`
|
||||
- 控制调度中枢:处理输入事件、调用导航器、触发重建路径与刷新
|
||||
- `src/Drv/menu/model/menu_model.c`
|
||||
- `src/Drv/pages/menu/model/menu_model.c`
|
||||
- 模型初始化:树构建、菜单名修饰、初始状态建立
|
||||
- `src/Drv/menu/view/menu_view.c`
|
||||
- `src/Drv/pages/menu/view/menu_view.c`
|
||||
- 纯渲染:顶部栏、多级菜单框、高亮反显、全量/增量刷新策略
|
||||
- `src/Drv/menu/presenter/menu_navigator.c`
|
||||
- `src/Drv/pages/menu/presenter/menu_navigator.c`
|
||||
- 纯导航状态机:按键到 `MenuNavResult`(是否刷新、是否跳过渲染)
|
||||
- `src/Drv/menu/view/menu_layout.c`
|
||||
- `src/Drv/pages/menu/view/menu_layout.c`
|
||||
- 菜单布局计算:宽度统计、层级矩形定位、越界回退策略
|
||||
- `src/Drv/menu/model/display.c`
|
||||
- `src/Drv/pages/menu/model/menu_model.c`(包含静态菜单表数据)
|
||||
- 静态菜单模型表 `g_tMenuModelTab`(业务菜单定义)
|
||||
|
||||
#### 6.4.2 运行时调用时序
|
||||
@@ -313,12 +835,12 @@ ctest --test-dir build -C Debug --output-on-failure
|
||||
## 10. 模块依赖关系(代码级)
|
||||
|
||||
- `main.c` 依赖:`menu`、`key`、`remoteDisplay`、`tcp`、`thread_utils`
|
||||
- `src/Drv/menu/app/menu.c` 依赖:`presenter/menu_presenter`、`model/display`、`key`、`lcd`
|
||||
- `src/Drv/menu/presenter/menu_presenter.c` 依赖:`model/menu_model`、`view/menu_view`、`presenter/menu_navigator`
|
||||
- `src/Drv/menu/view/menu_view.c` 依赖:`model/menu_layout`、`view/menu_render_port`、`lcd`
|
||||
- `src/Drv/menu/app/menu.c` 依赖:`pages/page_manager`、`pages/menu/page`、`key`
|
||||
- `src/Drv/pages/menu/presenter/menu_presenter.c` 依赖:`model/menu_model`、`view/menu_view`、`presenter/menu_navigator`
|
||||
- `src/Drv/pages/menu/view/menu_view.c` 依赖:`view/menu_layout`、`view/menu_render_port`、`lcd`
|
||||
- `remoteDisplay.c` 依赖:`lcd`、`key`、`tcp`、`thread_utils`
|
||||
- `lcd.c` 依赖:`ascii`
|
||||
- `src/Drv/menu/model/display.c` 提供:静态菜单表(被 menu 模块使用)
|
||||
- `src/Drv/pages/menu/model/menu_model.c` 提供:静态菜单表(被 `MenuPage` 使用)
|
||||
|
||||
## 11. 已知风险与改进建议
|
||||
|
||||
|
||||
Reference in New Issue
Block a user