将菜单的架构改成 MVP,并且进一步优化视图层和模型层的逻辑

This commit is contained in:
2026-04-01 19:42:05 +08:00
parent 0690d6a00e
commit 8b44b84d4c
54 changed files with 5362 additions and 2200 deletions

View File

@@ -30,14 +30,13 @@ add_executable(DTU-HMI
src/thread_utils.c
src/remoteDisplay.c
src/Drv/menu/app/menu.c
src/Drv/menu/model/display.c
src/Drv/menu/model/menu_model.c
src/Drv/menu/view/menu_view.c
src/Drv/menu/presenter/menu_presenter.c
src/Drv/menu/model/menu_tree_builder.c
src/Drv/menu/view/menu_layout.c
src/Drv/menu/presenter/menu_navigator.c
src/Drv/menu/view/menu_renderer_lcd.c
src/Drv/pages/page_manager.c
src/Drv/pages/global/global_state.c
src/Drv/pages/global/renderer_lcd.c
src/Drv/pages/menu/page.c
src/Drv/pages/menu/model.c
src/Drv/pages/menu/presenter.c
src/Drv/pages/menu/view.c
src/Drv/lcd/lcd.c
src/Drv/lcd/lcd_draw.c
src/Drv/lcd/lcd_text.c

View File

@@ -1 +0,0 @@
---

View File

@@ -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`入口
- 页面将事件转发给内部 ViewView 判断事件命中,转发给 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 的解耦原则,导致代码耦合混乱。
全局状态管理器的核心设计思路是:**实现一个全局单例的 ModelGlobalModel作为跨页面数据的唯一可信源通过观察者模式实现数据变化的同步通知完全不破坏各页面 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. 已知风险与改进建议

View File

@@ -0,0 +1,161 @@
<mxfile host="Electron" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/29.6.6 Chrome/144.0.7559.236 Electron/40.8.4 Safari/537.36" version="29.6.6" pages="6">
<diagram name="显示指定层级菜单项流程" id="etDREPkzm4HehLh3Wzss">
<mxGraphModel dx="752" dy="817" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="Dq_V7AeBsTtqcCJDGkD4-1" parent="1" style="swimlane;childLayout=stackLayout;horizontal=1;fillColor=none;horizontalStack=1;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=0;strokeColor=none;stackBorder=10;stackSpacing=-12;resizable=1;align=center;points=[];whiteSpace=wrap;html=1;" value="显示指定层级菜单项" vertex="1">
<mxGeometry height="100" width="610" x="100" y="560" as="geometry" />
</mxCell>
<mxCell id="Dq_V7AeBsTtqcCJDGkD4-2" parent="Dq_V7AeBsTtqcCJDGkD4-1" style="shape=step;perimeter=stepPerimeter;fixedSize=1;points=[];whiteSpace=wrap;html=1;" value="绘制该层级边框" vertex="1">
<mxGeometry height="57" width="160" x="10" y="33" as="geometry" />
</mxCell>
<mxCell id="Dq_V7AeBsTtqcCJDGkD4-3" parent="Dq_V7AeBsTtqcCJDGkD4-1" style="shape=step;perimeter=stepPerimeter;fixedSize=1;points=[];whiteSpace=wrap;html=1;" value="获取该层级选中菜单项" vertex="1">
<mxGeometry height="57" width="182" x="158" y="33" as="geometry" />
</mxCell>
<mxCell id="Dq_V7AeBsTtqcCJDGkD4-4" parent="Dq_V7AeBsTtqcCJDGkD4-1" style="shape=step;perimeter=stepPerimeter;fixedSize=1;points=[];whiteSpace=wrap;html=1;" value="绘制该层级下菜单项" vertex="1">
<mxGeometry height="57" width="162" x="328" y="33" as="geometry" />
</mxCell>
<mxCell id="Dq_V7AeBsTtqcCJDGkD4-5" parent="Dq_V7AeBsTtqcCJDGkD4-1" style="shape=step;perimeter=stepPerimeter;fixedSize=1;points=[];whiteSpace=wrap;html=1;" value="反显选中项" vertex="1">
<mxGeometry height="57" width="122" x="478" y="33" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="39Z0d1SsfJPrEY7NQ2dw" name="MVP 事件处理流程">
<mxGraphModel dx="1090" dy="1185" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="CHJpIxpMOG3BUcIp9TBN-1" edge="1" parent="1" source="CHJpIxpMOG3BUcIp9TBN-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="CHJpIxpMOG3BUcIp9TBN-3" value="">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-2" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;text-wrap-mode: nowrap;&quot;&gt;外部事件&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="120" x="354" y="490" as="geometry" />
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-3" parent="1" style="whiteSpace=wrap;html=1;rounded=0;" value="Presenter 协同层" vertex="1">
<mxGeometry height="60" width="120" x="354" y="570" as="geometry" />
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-4" parent="1" style="whiteSpace=wrap;html=1;rounded=0;" value="View 视图层" vertex="1">
<mxGeometry height="60" width="120" x="284" y="680" as="geometry" />
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-5" parent="1" style="whiteSpace=wrap;html=1;rounded=0;" value="Model 模型层&amp;nbsp;" vertex="1">
<mxGeometry height="60" width="120" x="424" y="680" as="geometry" />
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-6" edge="1" parent="1" source="CHJpIxpMOG3BUcIp9TBN-5" style="endArrow=classic;html=1;rounded=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;edgeStyle=orthogonalEdgeStyle;jumpStyle=arc;startArrow=classic;startFill=1;" target="CHJpIxpMOG3BUcIp9TBN-3" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<mxPoint x="594" y="630" as="sourcePoint" />
<mxPoint x="644" y="580" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-7" edge="1" parent="1" source="CHJpIxpMOG3BUcIp9TBN-3" style="endArrow=classic;html=1;rounded=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;edgeStyle=orthogonalEdgeStyle;" target="CHJpIxpMOG3BUcIp9TBN-4" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<mxPoint x="374" y="670" as="sourcePoint" />
<mxPoint x="484" y="660" as="targetPoint" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="X0IBgYVqs8-xy_G4TQXO" name="MVP 架构示意图">
<mxGraphModel dx="1090" dy="1185" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-14" connectable="0" parent="1" style="group" value="" vertex="1">
<mxGeometry height="300" width="600" x="100" y="300" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-1" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 21px;&quot;&gt;&lt;b&gt;MVP 架构示意图&lt;/b&gt;&lt;/font&gt;" vertex="1">
<mxGeometry height="50" width="600" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-2" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="" vertex="1">
<mxGeometry height="240" width="180" y="60" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-3" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="" vertex="1">
<mxGeometry height="240" width="185" x="207.5" y="60" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-4" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="" vertex="1">
<mxGeometry height="240" width="185" x="415" y="60" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-5" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 16px;&quot;&gt;&lt;b&gt;View (视图层)&lt;/b&gt;&lt;/font&gt;" vertex="1">
<mxGeometry height="40" width="160" x="10" y="70" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-6" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;b style=&quot;font-size: 16px;&quot;&gt;Presenter (协调层)&lt;/b&gt;" vertex="1">
<mxGeometry height="40" width="160" x="220" y="70" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-7" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;b style=&quot;font-size: 16px;&quot;&gt;Model (数据层)&lt;/b&gt;" vertex="1">
<mxGeometry height="40" width="160" x="430" y="70" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-8" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 16px;&quot;&gt;&lt;b&gt;负责显示内容&lt;/b&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="40" width="160" x="10" y="250" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-9" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 16px;&quot;&gt;&lt;b&gt;负责流程协调&lt;/b&gt;&lt;/font&gt;" vertex="1">
<mxGeometry height="40" width="160" x="220" y="250" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-10" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 16px;&quot;&gt;&lt;b&gt;负责数据和规则&lt;/b&gt;&lt;/font&gt;" vertex="1">
<mxGeometry height="40" width="160" x="430" y="250" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-11" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;rounded=0;" value="&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;LCD 显示&lt;/font&gt;&lt;/b&gt;&lt;div&gt;&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;LED 指示&lt;/font&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;按键输入&lt;/font&gt;&lt;/b&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="60" width="80" x="20" y="150" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-12" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;rounded=0;" value="&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;接收外部事件&lt;/font&gt;&lt;/b&gt;&lt;div&gt;&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;调用 Model 处理&lt;/font&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-size: 14px;&quot;&gt;&lt;b&gt;更新 View 状态&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="60" width="120" x="230" y="150" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-13" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;rounded=0;" value="&lt;span style=&quot;font-size: 14px;&quot;&gt;&lt;b&gt;菜单逻辑计算&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&lt;div&gt;&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;参数边界检查&lt;/font&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-size: 14px;&quot;&gt;&lt;b&gt;传感器数据获取&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-size: 14px;&quot;&gt;&lt;b&gt;数据存储读写&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="60" width="120" x="440" y="150" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="HxDEZlLnL0qG7Ur4KydZ" name="页面管理器架构设计">
<mxGraphModel dx="1363" dy="1481" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="LuWILqjna9OgU5SQR-9t-1" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 29px;&quot;&gt;页面管理器(&lt;span style=&quot;background-color: transparent; color: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));&quot;&gt;Page Manager&lt;/span&gt;&lt;/font&gt;&lt;div&gt;&lt;span style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="240" width="500" x="130" y="290" as="geometry" />
</mxCell>
<mxCell id="LuWILqjna9OgU5SQR-9t-2" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;页面栈 Page Stack&lt;/font&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="130" width="500" x="130" y="400" as="geometry" />
</mxCell>
<mxCell id="LuWILqjna9OgU5SQR-9t-3" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 23px;&quot;&gt;栈顶页面&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="240" x="140" y="460" as="geometry" />
</mxCell>
<mxCell id="LuWILqjna9OgU5SQR-9t-4" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 23px;&quot;&gt;后台页面1&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="240" x="380" y="460" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="R1sptnBL6Yvgqzv1JQT0" name="菜单页面架构设计">
<mxGraphModel dx="1090" dy="1185" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="_v9aWsQiSdQDmT4Fcf7S-1" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 29px;&quot;&gt;菜单页面&lt;/font&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="130" width="500" x="164" y="480" as="geometry" />
</mxCell>
<mxCell id="_v9aWsQiSdQDmT4Fcf7S-2" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 23px;&quot;&gt;page&lt;/font&gt;" vertex="1">
<mxGeometry height="60" width="120" x="174" y="540" as="geometry" />
</mxCell>
<mxCell id="_v9aWsQiSdQDmT4Fcf7S-3" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 23px;&quot;&gt;model&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="120" x="294" y="540" as="geometry" />
</mxCell>
<mxCell id="_v9aWsQiSdQDmT4Fcf7S-4" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 23px;&quot;&gt;view&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="120" x="414" y="540" as="geometry" />
</mxCell>
<mxCell id="_v9aWsQiSdQDmT4Fcf7S-5" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 23px;&quot;&gt;presenter&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="120" x="534" y="540" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="ae4TW034q6_u2rk01mG-" name="第 6 页">
<mxGraphModel dx="1090" dy="1185" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -0,0 +1,506 @@
<mxfile host="Electron" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/29.6.6 Chrome/144.0.7559.236 Electron/40.8.4 Safari/537.36" version="29.6.6" pages="8">
<diagram name="显示指定层级菜单项流程" id="etDREPkzm4HehLh3Wzss">
<mxGraphModel dx="752" dy="817" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="Dq_V7AeBsTtqcCJDGkD4-1" parent="1" style="swimlane;childLayout=stackLayout;horizontal=1;fillColor=none;horizontalStack=1;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=0;strokeColor=none;stackBorder=10;stackSpacing=-12;resizable=1;align=center;points=[];whiteSpace=wrap;html=1;" value="显示指定层级菜单项" vertex="1">
<mxGeometry height="100" width="610" x="100" y="560" as="geometry" />
</mxCell>
<mxCell id="Dq_V7AeBsTtqcCJDGkD4-2" parent="Dq_V7AeBsTtqcCJDGkD4-1" style="shape=step;perimeter=stepPerimeter;fixedSize=1;points=[];whiteSpace=wrap;html=1;" value="绘制该层级边框" vertex="1">
<mxGeometry height="57" width="160" x="10" y="33" as="geometry" />
</mxCell>
<mxCell id="Dq_V7AeBsTtqcCJDGkD4-3" parent="Dq_V7AeBsTtqcCJDGkD4-1" style="shape=step;perimeter=stepPerimeter;fixedSize=1;points=[];whiteSpace=wrap;html=1;" value="获取该层级选中菜单项" vertex="1">
<mxGeometry height="57" width="182" x="158" y="33" as="geometry" />
</mxCell>
<mxCell id="Dq_V7AeBsTtqcCJDGkD4-4" parent="Dq_V7AeBsTtqcCJDGkD4-1" style="shape=step;perimeter=stepPerimeter;fixedSize=1;points=[];whiteSpace=wrap;html=1;" value="绘制该层级下菜单项" vertex="1">
<mxGeometry height="57" width="162" x="328" y="33" as="geometry" />
</mxCell>
<mxCell id="Dq_V7AeBsTtqcCJDGkD4-5" parent="Dq_V7AeBsTtqcCJDGkD4-1" style="shape=step;perimeter=stepPerimeter;fixedSize=1;points=[];whiteSpace=wrap;html=1;" value="反显选中项" vertex="1">
<mxGeometry height="57" width="122" x="478" y="33" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="39Z0d1SsfJPrEY7NQ2dw" name="MVP 事件处理流程">
<mxGraphModel dx="1090" dy="1185" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="CHJpIxpMOG3BUcIp9TBN-1" edge="1" parent="1" source="CHJpIxpMOG3BUcIp9TBN-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="CHJpIxpMOG3BUcIp9TBN-3" value="">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-2" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;text-wrap-mode: nowrap;&quot;&gt;外部事件&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="120" x="354" y="490" as="geometry" />
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-3" parent="1" style="whiteSpace=wrap;html=1;rounded=0;" value="Presenter 协同层" vertex="1">
<mxGeometry height="60" width="120" x="354" y="570" as="geometry" />
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-4" parent="1" style="whiteSpace=wrap;html=1;rounded=0;" value="View 视图层" vertex="1">
<mxGeometry height="60" width="120" x="284" y="680" as="geometry" />
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-5" parent="1" style="whiteSpace=wrap;html=1;rounded=0;" value="Model 模型层&amp;nbsp;" vertex="1">
<mxGeometry height="60" width="120" x="424" y="680" as="geometry" />
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-6" edge="1" parent="1" source="CHJpIxpMOG3BUcIp9TBN-5" style="endArrow=classic;html=1;rounded=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;edgeStyle=orthogonalEdgeStyle;jumpStyle=arc;startArrow=classic;startFill=1;" target="CHJpIxpMOG3BUcIp9TBN-3" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<mxPoint x="594" y="630" as="sourcePoint" />
<mxPoint x="644" y="580" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-7" edge="1" parent="1" source="CHJpIxpMOG3BUcIp9TBN-3" style="endArrow=classic;html=1;rounded=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;edgeStyle=orthogonalEdgeStyle;" target="CHJpIxpMOG3BUcIp9TBN-4" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<mxPoint x="374" y="670" as="sourcePoint" />
<mxPoint x="484" y="660" as="targetPoint" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="X0IBgYVqs8-xy_G4TQXO" name="MVP 架构示意图">
<mxGraphModel dx="1090" dy="1185" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-14" connectable="0" parent="1" style="group" value="" vertex="1">
<mxGeometry height="300" width="600" x="100" y="300" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-1" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 21px;&quot;&gt;&lt;b&gt;MVP 架构示意图&lt;/b&gt;&lt;/font&gt;" vertex="1">
<mxGeometry height="50" width="600" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-2" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="" vertex="1">
<mxGeometry height="240" width="180" y="60" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-3" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="" vertex="1">
<mxGeometry height="240" width="185" x="207.5" y="60" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-4" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="" vertex="1">
<mxGeometry height="240" width="185" x="415" y="60" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-5" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 16px;&quot;&gt;&lt;b&gt;View (视图层)&lt;/b&gt;&lt;/font&gt;" vertex="1">
<mxGeometry height="40" width="160" x="10" y="70" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-6" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;b style=&quot;font-size: 16px;&quot;&gt;Presenter (协调层)&lt;/b&gt;" vertex="1">
<mxGeometry height="40" width="160" x="220" y="70" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-7" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;b style=&quot;font-size: 16px;&quot;&gt;Model (数据层)&lt;/b&gt;" vertex="1">
<mxGeometry height="40" width="160" x="430" y="70" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-8" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 16px;&quot;&gt;&lt;b&gt;负责显示内容&lt;/b&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="40" width="160" x="10" y="250" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-9" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 16px;&quot;&gt;&lt;b&gt;负责流程协调&lt;/b&gt;&lt;/font&gt;" vertex="1">
<mxGeometry height="40" width="160" x="220" y="250" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-10" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 16px;&quot;&gt;&lt;b&gt;负责数据和规则&lt;/b&gt;&lt;/font&gt;" vertex="1">
<mxGeometry height="40" width="160" x="430" y="250" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-11" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;rounded=0;" value="&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;LCD 显示&lt;/font&gt;&lt;/b&gt;&lt;div&gt;&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;LED 指示&lt;/font&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;按键输入&lt;/font&gt;&lt;/b&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="60" width="80" x="20" y="150" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-12" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;rounded=0;" value="&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;接收外部事件&lt;/font&gt;&lt;/b&gt;&lt;div&gt;&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;调用 Model 处理&lt;/font&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-size: 14px;&quot;&gt;&lt;b&gt;更新 View 状态&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="60" width="120" x="230" y="150" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-13" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;rounded=0;" value="&lt;span style=&quot;font-size: 14px;&quot;&gt;&lt;b&gt;菜单逻辑计算&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&lt;div&gt;&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;参数边界检查&lt;/font&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-size: 14px;&quot;&gt;&lt;b&gt;传感器数据获取&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-size: 14px;&quot;&gt;&lt;b&gt;数据存储读写&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="60" width="120" x="440" y="150" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="HxDEZlLnL0qG7Ur4KydZ" name="页面管理器架构设计">
<mxGraphModel dx="1363" dy="1481" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="LuWILqjna9OgU5SQR-9t-1" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 29px;&quot;&gt;页面管理器(&lt;span style=&quot;background-color: transparent; color: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));&quot;&gt;Page Manager&lt;/span&gt;&lt;/font&gt;&lt;div&gt;&lt;span style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="240" width="500" x="130" y="290" as="geometry" />
</mxCell>
<mxCell id="LuWILqjna9OgU5SQR-9t-2" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;页面栈 Page Stack&lt;/font&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="130" width="500" x="130" y="400" as="geometry" />
</mxCell>
<mxCell id="LuWILqjna9OgU5SQR-9t-3" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 23px;&quot;&gt;栈顶页面&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="240" x="140" y="460" as="geometry" />
</mxCell>
<mxCell id="LuWILqjna9OgU5SQR-9t-4" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 23px;&quot;&gt;后台页面1&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="240" x="380" y="460" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="R1sptnBL6Yvgqzv1JQT0" name="菜单页面架构设计">
<mxGraphModel dx="1090" dy="1185" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="_v9aWsQiSdQDmT4Fcf7S-1" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 29px;&quot;&gt;菜单页面&lt;/font&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="130" width="500" x="164" y="480" as="geometry" />
</mxCell>
<mxCell id="_v9aWsQiSdQDmT4Fcf7S-2" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 23px;&quot;&gt;page&lt;/font&gt;" vertex="1">
<mxGeometry height="60" width="120" x="174" y="540" as="geometry" />
</mxCell>
<mxCell id="_v9aWsQiSdQDmT4Fcf7S-3" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 23px;&quot;&gt;model&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="120" x="294" y="540" as="geometry" />
</mxCell>
<mxCell id="_v9aWsQiSdQDmT4Fcf7S-4" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 23px;&quot;&gt;view&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="120" x="414" y="540" as="geometry" />
</mxCell>
<mxCell id="_v9aWsQiSdQDmT4Fcf7S-5" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 23px;&quot;&gt;presenter&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="120" x="534" y="540" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="ae4TW034q6_u2rk01mG-" name="菜单树构建逻辑">
<mxGraphModel dx="1224" dy="1192" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="uTcgnOhw-1hiChn4308x-321" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="主菜单(class=0)" vertex="1">
<mxGeometry height="54" width="171" x="36" y="20" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-322" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="参数设置(class=1)" vertex="1">
<mxGeometry height="54" width="187" x="260" y="140" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-323" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="信息查看(class=1)" vertex="1">
<mxGeometry height="54" width="187" x="250" y="276" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-324" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="关于系统(class=1)" vertex="1">
<mxGeometry height="54" width="187" x="250" y="430" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-325" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="定值设置(class=2)" vertex="1">
<mxGeometry height="54" width="187" x="520" y="107" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-326" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="开关设置(class=2)" vertex="1">
<mxGeometry height="54" width="187" x="520" y="207" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-327" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="运行信息(class=2)" vertex="1">
<mxGeometry height="54" width="187" x="520" y="294" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-328" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-321" style="startArrow=none;endArrow=block;entryX=0.64;entryY=0;rounded=0;edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" target="uTcgnOhw-1hiChn4308x-322" value="lower">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="380" y="47" />
</Array>
<mxPoint x="240" y="40" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-329" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-322" style="startArrow=none;endArrow=block;exitX=0.667;exitY=1.037;entryX=0.72;entryY=0;rounded=0;exitDx=0;exitDy=0;exitPerimeter=0;" target="uTcgnOhw-1hiChn4308x-323" value="behind">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-330" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-323" style="startArrow=none;endArrow=block;exitX=0.75;exitY=1;entryX=0.75;entryY=0;rounded=0;exitDx=0;exitDy=0;entryDx=0;entryDy=0;" target="uTcgnOhw-1hiChn4308x-324" value="behind">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-331" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-324" style="startArrow=none;endArrow=block;exitX=0.75;exitY=1;entryX=-0.001;entryY=0.171;rounded=0;edgeStyle=orthogonalEdgeStyle;exitDx=0;exitDy=0;entryDx=0;entryDy=0;entryPerimeter=0;" target="uTcgnOhw-1hiChn4308x-322" value="behind 回环">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="390" y="520" />
<mxPoint x="110" y="520" />
<mxPoint x="110" y="149" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-332" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-322" style="startArrow=none;endArrow=block;exitX=0;exitY=0.75;entryX=0.25;entryY=1;rounded=0;edgeStyle=orthogonalEdgeStyle;entryDx=0;entryDy=0;exitDx=0;exitDy=0;" target="uTcgnOhw-1hiChn4308x-324" value="before 回环">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="180" y="180" />
<mxPoint x="180" y="500" />
<mxPoint x="297" y="500" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-333" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-323" style="startArrow=none;endArrow=block;exitX=0.269;exitY=0.012;entryX=0.33;entryY=1;rounded=0;exitDx=0;exitDy=0;exitPerimeter=0;" value="before">
<mxGeometry relative="1" as="geometry">
<mxPoint x="321.79" y="276" as="sourcePoint" />
<mxPoint x="300.00000000000006" y="194" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-334" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-324" style="startArrow=none;endArrow=block;exitX=0.4;exitY=0;entryX=0.399;entryY=0.986;rounded=0;entryDx=0;entryDy=0;entryPerimeter=0;" target="uTcgnOhw-1hiChn4308x-323" value="before">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-335" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-322" style="startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;entryDx=0;entryDy=0;exitDx=0;exitDy=0;edgeStyle=orthogonalEdgeStyle;" target="uTcgnOhw-1hiChn4308x-325" value="lower">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-336" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-325" style="startArrow=none;endArrow=block;exitX=0.912;exitY=0.975;entryX=0.91;entryY=0;rounded=0;edgeStyle=orthogonalEdgeStyle;exitDx=0;exitDy=0;exitPerimeter=0;" target="uTcgnOhw-1hiChn4308x-326" value="behind">
<mxGeometry relative="1" x="0.1907" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-337" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-326" style="startArrow=none;endArrow=block;exitX=0.332;exitY=-0.003;entryX=0.33;entryY=1;rounded=0;exitDx=0;exitDy=0;exitPerimeter=0;" target="uTcgnOhw-1hiChn4308x-325" value="behind 回环">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-338" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-325" style="startArrow=none;endArrow=block;exitX=0.669;exitY=0.986;rounded=0;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.673;entryY=-0.017;entryDx=0;entryDy=0;entryPerimeter=0;" target="uTcgnOhw-1hiChn4308x-326" value="before 回环">
<mxGeometry relative="1" x="0.1635" y="-6" as="geometry">
<mxPoint as="offset" />
<mxPoint x="647" y="167" as="sourcePoint" />
<mxPoint x="645" y="187" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-339" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-326" style="startArrow=none;endArrow=block;exitX=0.133;exitY=-0.013;entryX=0.13;entryY=1;rounded=0;exitDx=0;exitDy=0;exitPerimeter=0;" target="uTcgnOhw-1hiChn4308x-325" value="before">
<mxGeometry relative="1" x="0.0153" y="9" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-340" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-323" style="startArrow=none;endArrow=block;exitX=0.999;exitY=0.807;entryX=0;entryY=0.5;rounded=0;edgeStyle=orthogonalEdgeStyle;entryDx=0;entryDy=0;exitDx=0;exitDy=0;exitPerimeter=0;" target="uTcgnOhw-1hiChn4308x-327" value="lower">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-341" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-322" style="startArrow=none;endArrow=block;exitX=0.21;exitY=0;entryX=0.5;entryY=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" target="uTcgnOhw-1hiChn4308x-321" value="higher">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-342" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-323" style="startArrow=none;endArrow=block;exitX=0;exitY=0.5;entryX=0.26;entryY=1;rounded=0;edgeStyle=orthogonalEdgeStyle;exitDx=0;exitDy=0;" target="uTcgnOhw-1hiChn4308x-321" value="higher">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="80" y="303" />
<mxPoint x="80" y="276" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-343" edge="1" parent="1" style="startArrow=none;endArrow=block;entryX=0;entryY=0.83;rounded=0;edgeStyle=orthogonalEdgeStyle;" value="higher">
<mxGeometry relative="1" x="0.4125" as="geometry">
<mxPoint as="offset" />
<Array as="points">
<mxPoint x="250" y="460" />
<mxPoint x="20" y="460" />
<mxPoint x="20" y="40" />
</Array>
<mxPoint x="250" y="460" as="sourcePoint" />
<mxPoint x="36" y="40.019999999999996" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-344" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-325" style="startArrow=none;endArrow=block;exitX=0.5;exitY=0;entryX=0.905;entryY=0.019;rounded=0;edgeStyle=orthogonalEdgeStyle;entryDx=0;entryDy=0;entryPerimeter=0;exitDx=0;exitDy=0;" target="uTcgnOhw-1hiChn4308x-322" value="higher">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="614" y="80" />
<mxPoint x="429" y="80" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-345" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-326" style="startArrow=none;endArrow=block;exitX=0;exitY=0.5;rounded=0;edgeStyle=orthogonalEdgeStyle;exitDx=0;exitDy=0;" value="higher">
<mxGeometry relative="1" x="-0.3947" as="geometry">
<mxPoint as="offset" />
<Array as="points">
<mxPoint x="420" y="234" />
</Array>
<mxPoint x="420" y="194" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-346" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-327" style="startArrow=none;endArrow=block;exitX=0.43;exitY=0;entryX=1.005;entryY=0.163;rounded=0;edgeStyle=orthogonalEdgeStyle;entryDx=0;entryDy=0;entryPerimeter=0;" target="uTcgnOhw-1hiChn4308x-323" value="higher">
<mxGeometry relative="1" x="0.4124" y="-3" as="geometry">
<mxPoint as="offset" />
<Array as="points">
<mxPoint x="600" y="285" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-347" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-327" style="endArrow=classic;html=1;rounded=0;exitX=0.677;exitY=0.972;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.709;entryY=0.015;entryDx=0;entryDy=0;entryPerimeter=0;curved=1;" target="uTcgnOhw-1hiChn4308x-327" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<Array as="points">
<mxPoint x="710" y="380" />
<mxPoint x="730" y="330" />
<mxPoint x="730" y="300" />
<mxPoint x="720" y="260" />
</Array>
<mxPoint x="657" y="460" as="sourcePoint" />
<mxPoint x="707" y="410" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-348" connectable="0" parent="uTcgnOhw-1hiChn4308x-347" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" value="before" vertex="1">
<mxGeometry relative="1" x="-0.0353" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-351" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-327" style="curved=1;endArrow=classic;html=1;rounded=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;" target="uTcgnOhw-1hiChn4308x-327" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<Array as="points">
<mxPoint x="750" y="230" />
<mxPoint x="750" y="400" />
<mxPoint x="590" y="390" />
</Array>
<mxPoint x="570" y="484" as="sourcePoint" />
<mxPoint x="620" y="434" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-352" connectable="0" parent="uTcgnOhw-1hiChn4308x-351" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" value="behind" vertex="1">
<mxGeometry relative="1" x="0.0226" y="-5" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="5RVJ2jwpCgCB7Hji9JxO" name="菜单树判断逻辑">
<mxGraphModel dx="2720" dy="2649" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-1" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="byCurClass &lt; byNextClass" vertex="1">
<mxGeometry height="54" width="241" x="250" y="380" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-2" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="进入子菜单" vertex="1">
<mxGeometry height="54" width="140" x="510" y="380" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-3" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="current.lower = next" vertex="1">
<mxGeometry height="54" width="190" x="670" y="380" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-4" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="next.higher = current" vertex="1">
<mxGeometry height="54" width="209" x="891" y="380" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-5" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="byCurClass == byNextClass" vertex="1">
<mxGeometry height="54" width="240" x="250" y="484" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-6" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="同层兄弟" vertex="1">
<mxGeometry height="54" width="140" x="511" y="484" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-7" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="current.behind = next" vertex="1">
<mxGeometry height="54" width="190" x="670" y="484" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-8" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="next.before = current" vertex="1">
<mxGeometry height="54" width="210" x="890" y="484" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-9" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="next.higher = current.higher" vertex="1">
<mxGeometry height="78" width="260" x="1140" y="472" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-10" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="byCurClass &gt; byNextClass" vertex="1">
<mxGeometry height="54" width="241" x="250" y="591" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-11" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="从深层回退" vertex="1">
<mxGeometry height="54" width="140" x="510" y="591" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-12" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="next.before = ptLast[nextClass]" vertex="1">
<mxGeometry height="54" width="190" x="670" y="591" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-27" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-13" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="HZ-q9eH6YA3dt1o5nvaf-26" value="">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-13" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="ptLast[nextClass].behind=next" vertex="1">
<mxGeometry height="54" width="190" x="1140" y="591" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-14" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-1" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-2" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-15" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-2" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-3" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-16" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-3" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-4" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-17" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-5" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-6" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-18" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-6" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-7" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-19" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-7" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-8" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-20" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-8" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-9" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-21" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-10" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-11" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-22" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-11" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-12" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-23" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-12" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;rounded=0;exitDx=0;exitDy=0;" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
<mxPoint x="890" y="618" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-25" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="HZ-q9eH6YA3dt1o5nvaf-13" value="">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-24" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="next.behind=ptLast[nextClass].higher" vertex="1">
<mxGeometry height="54" width="209" x="891" y="591" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-26" parent="1" style="whiteSpace=wrap;html=1;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);strokeWidth=2;" value="形成回环" vertex="1">
<mxGeometry height="55" width="69" x="1351" y="591" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="zG06bic9s9vmrHAmIL5z" name="模型层初始化顺序">
<mxGraphModel dx="628" dy="611" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="4pC_cjyCFYns3d6entyY-21" edge="1" parent="1" source="4pC_cjyCFYns3d6entyY-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" target="4pC_cjyCFYns3d6entyY-3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-1" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);rounded=1;" value="开始" vertex="1">
<mxGeometry height="24" width="210" x="320" y="120" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-3" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="计算菜单个数 maxItem " vertex="1">
<mxGeometry height="30" width="210" x="320" y="171" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-5" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="统计 0 级菜单数量 by0LevelNum " vertex="1">
<mxGeometry height="30" width="210" x="320" y="225" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-6" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="构建菜单树链路" vertex="1">
<mxGeometry height="30" width="210" x="320" y="279" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-7" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="补充带子菜单项的右侧箭头》" vertex="1">
<mxGeometry height="40" width="210" x="320" y="328" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-8" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="计算各节点 wPos 与 wNum" vertex="1">
<mxGeometry height="30" width="210" x="320" y="387" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-9" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="计算整棵菜单树的显示位置" vertex="1">
<mxGeometry height="30" width="210" x="320" y="441" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-10" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);rounded=1;" value="函数结束" vertex="1">
<mxGeometry height="20" width="210" x="320" y="500" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-13" edge="1" parent="1" source="4pC_cjyCFYns3d6entyY-3" style="curved=1;startArrow=none;endArrow=block;exitX=0.5;exitY=1;entryX=0.5;entryY=0;rounded=0;entryDx=0;entryDy=0;" target="4pC_cjyCFYns3d6entyY-5" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
<mxPoint x="426" y="470" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-15" edge="1" parent="1" source="4pC_cjyCFYns3d6entyY-5" style="curved=1;startArrow=none;endArrow=block;exitX=0.5;exitY=1;entryX=0.5;entryY=0;rounded=0;" target="4pC_cjyCFYns3d6entyY-6" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-16" edge="1" parent="1" source="4pC_cjyCFYns3d6entyY-6" style="curved=1;startArrow=none;endArrow=block;exitX=0.5;exitY=1;entryX=0.5;entryY=0;rounded=0;" target="4pC_cjyCFYns3d6entyY-7" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-17" edge="1" parent="1" source="4pC_cjyCFYns3d6entyY-7" style="curved=1;startArrow=none;endArrow=block;exitX=0.5;exitY=1;entryX=0.5;entryY=0;rounded=0;" target="4pC_cjyCFYns3d6entyY-8" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-18" edge="1" parent="1" source="4pC_cjyCFYns3d6entyY-8" style="curved=1;startArrow=none;endArrow=block;exitX=0.5;exitY=1;entryX=0.5;entryY=0;rounded=0;" target="4pC_cjyCFYns3d6entyY-9" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-19" edge="1" parent="1" source="4pC_cjyCFYns3d6entyY-9" style="curved=1;startArrow=none;endArrow=block;exitX=0.5;exitY=1;entryX=0.5;entryY=0;rounded=0;" target="4pC_cjyCFYns3d6entyY-10" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -0,0 +1,506 @@
<mxfile host="Electron" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/29.6.6 Chrome/144.0.7559.236 Electron/40.8.4 Safari/537.36" version="29.6.6" pages="8">
<diagram name="显示指定层级菜单项流程" id="etDREPkzm4HehLh3Wzss">
<mxGraphModel dx="752" dy="817" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="Dq_V7AeBsTtqcCJDGkD4-1" parent="1" style="swimlane;childLayout=stackLayout;horizontal=1;fillColor=none;horizontalStack=1;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=0;strokeColor=none;stackBorder=10;stackSpacing=-12;resizable=1;align=center;points=[];whiteSpace=wrap;html=1;" value="显示指定层级菜单项" vertex="1">
<mxGeometry height="100" width="610" x="100" y="560" as="geometry" />
</mxCell>
<mxCell id="Dq_V7AeBsTtqcCJDGkD4-2" parent="Dq_V7AeBsTtqcCJDGkD4-1" style="shape=step;perimeter=stepPerimeter;fixedSize=1;points=[];whiteSpace=wrap;html=1;" value="绘制该层级边框" vertex="1">
<mxGeometry height="57" width="160" x="10" y="33" as="geometry" />
</mxCell>
<mxCell id="Dq_V7AeBsTtqcCJDGkD4-3" parent="Dq_V7AeBsTtqcCJDGkD4-1" style="shape=step;perimeter=stepPerimeter;fixedSize=1;points=[];whiteSpace=wrap;html=1;" value="获取该层级选中菜单项" vertex="1">
<mxGeometry height="57" width="182" x="158" y="33" as="geometry" />
</mxCell>
<mxCell id="Dq_V7AeBsTtqcCJDGkD4-4" parent="Dq_V7AeBsTtqcCJDGkD4-1" style="shape=step;perimeter=stepPerimeter;fixedSize=1;points=[];whiteSpace=wrap;html=1;" value="绘制该层级下菜单项" vertex="1">
<mxGeometry height="57" width="162" x="328" y="33" as="geometry" />
</mxCell>
<mxCell id="Dq_V7AeBsTtqcCJDGkD4-5" parent="Dq_V7AeBsTtqcCJDGkD4-1" style="shape=step;perimeter=stepPerimeter;fixedSize=1;points=[];whiteSpace=wrap;html=1;" value="反显选中项" vertex="1">
<mxGeometry height="57" width="122" x="478" y="33" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="39Z0d1SsfJPrEY7NQ2dw" name="MVP 事件处理流程">
<mxGraphModel dx="1090" dy="1185" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="CHJpIxpMOG3BUcIp9TBN-1" edge="1" parent="1" source="CHJpIxpMOG3BUcIp9TBN-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="CHJpIxpMOG3BUcIp9TBN-3" value="">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-2" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;text-wrap-mode: nowrap;&quot;&gt;外部事件&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="120" x="354" y="490" as="geometry" />
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-3" parent="1" style="whiteSpace=wrap;html=1;rounded=0;" value="Presenter 协同层" vertex="1">
<mxGeometry height="60" width="120" x="354" y="570" as="geometry" />
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-4" parent="1" style="whiteSpace=wrap;html=1;rounded=0;" value="View 视图层" vertex="1">
<mxGeometry height="60" width="120" x="284" y="680" as="geometry" />
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-5" parent="1" style="whiteSpace=wrap;html=1;rounded=0;" value="Model 模型层&amp;nbsp;" vertex="1">
<mxGeometry height="60" width="120" x="424" y="680" as="geometry" />
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-6" edge="1" parent="1" source="CHJpIxpMOG3BUcIp9TBN-5" style="endArrow=classic;html=1;rounded=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;edgeStyle=orthogonalEdgeStyle;jumpStyle=arc;startArrow=classic;startFill=1;" target="CHJpIxpMOG3BUcIp9TBN-3" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<mxPoint x="594" y="630" as="sourcePoint" />
<mxPoint x="644" y="580" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="CHJpIxpMOG3BUcIp9TBN-7" edge="1" parent="1" source="CHJpIxpMOG3BUcIp9TBN-3" style="endArrow=classic;html=1;rounded=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;edgeStyle=orthogonalEdgeStyle;" target="CHJpIxpMOG3BUcIp9TBN-4" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<mxPoint x="374" y="670" as="sourcePoint" />
<mxPoint x="484" y="660" as="targetPoint" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="X0IBgYVqs8-xy_G4TQXO" name="MVP 架构示意图">
<mxGraphModel dx="1090" dy="1185" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-14" connectable="0" parent="1" style="group" value="" vertex="1">
<mxGeometry height="300" width="600" x="100" y="300" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-1" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 21px;&quot;&gt;&lt;b&gt;MVP 架构示意图&lt;/b&gt;&lt;/font&gt;" vertex="1">
<mxGeometry height="50" width="600" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-2" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="" vertex="1">
<mxGeometry height="240" width="180" y="60" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-3" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="" vertex="1">
<mxGeometry height="240" width="185" x="207.5" y="60" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-4" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="" vertex="1">
<mxGeometry height="240" width="185" x="415" y="60" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-5" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 16px;&quot;&gt;&lt;b&gt;View (视图层)&lt;/b&gt;&lt;/font&gt;" vertex="1">
<mxGeometry height="40" width="160" x="10" y="70" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-6" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;b style=&quot;font-size: 16px;&quot;&gt;Presenter (协调层)&lt;/b&gt;" vertex="1">
<mxGeometry height="40" width="160" x="220" y="70" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-7" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;b style=&quot;font-size: 16px;&quot;&gt;Model (数据层)&lt;/b&gt;" vertex="1">
<mxGeometry height="40" width="160" x="430" y="70" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-8" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 16px;&quot;&gt;&lt;b&gt;负责显示内容&lt;/b&gt;&lt;/span&gt;" vertex="1">
<mxGeometry height="40" width="160" x="10" y="250" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-9" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 16px;&quot;&gt;&lt;b&gt;负责流程协调&lt;/b&gt;&lt;/font&gt;" vertex="1">
<mxGeometry height="40" width="160" x="220" y="250" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-10" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="rounded=1;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 16px;&quot;&gt;&lt;b&gt;负责数据和规则&lt;/b&gt;&lt;/font&gt;" vertex="1">
<mxGeometry height="40" width="160" x="430" y="250" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-11" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;rounded=0;" value="&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;LCD 显示&lt;/font&gt;&lt;/b&gt;&lt;div&gt;&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;LED 指示&lt;/font&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;按键输入&lt;/font&gt;&lt;/b&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="60" width="80" x="20" y="150" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-12" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;rounded=0;" value="&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;接收外部事件&lt;/font&gt;&lt;/b&gt;&lt;div&gt;&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;调用 Model 处理&lt;/font&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-size: 14px;&quot;&gt;&lt;b&gt;更新 View 状态&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="60" width="120" x="230" y="150" as="geometry" />
</mxCell>
<mxCell id="wkRVSMCZ4WoaQlwrK3vp-13" parent="wkRVSMCZ4WoaQlwrK3vp-14" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;rounded=0;" value="&lt;span style=&quot;font-size: 14px;&quot;&gt;&lt;b&gt;菜单逻辑计算&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&lt;div&gt;&lt;b&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;参数边界检查&lt;/font&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-size: 14px;&quot;&gt;&lt;b&gt;传感器数据获取&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-size: 14px;&quot;&gt;&lt;b&gt;数据存储读写&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="60" width="120" x="440" y="150" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="HxDEZlLnL0qG7Ur4KydZ" name="页面管理器架构设计">
<mxGraphModel dx="1363" dy="1481" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="LuWILqjna9OgU5SQR-9t-1" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 29px;&quot;&gt;页面管理器(&lt;span style=&quot;background-color: transparent; color: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));&quot;&gt;Page Manager&lt;/span&gt;&lt;/font&gt;&lt;div&gt;&lt;span style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="240" width="500" x="130" y="290" as="geometry" />
</mxCell>
<mxCell id="LuWILqjna9OgU5SQR-9t-2" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;页面栈 Page Stack&lt;/font&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="130" width="500" x="130" y="400" as="geometry" />
</mxCell>
<mxCell id="LuWILqjna9OgU5SQR-9t-3" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 23px;&quot;&gt;栈顶页面&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="240" x="140" y="460" as="geometry" />
</mxCell>
<mxCell id="LuWILqjna9OgU5SQR-9t-4" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 23px;&quot;&gt;后台页面1&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="240" x="380" y="460" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="R1sptnBL6Yvgqzv1JQT0" name="菜单页面架构设计">
<mxGraphModel dx="1090" dy="1185" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="_v9aWsQiSdQDmT4Fcf7S-1" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 29px;&quot;&gt;菜单页面&lt;/font&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 29px;&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/div&gt;" vertex="1">
<mxGeometry height="130" width="500" x="164" y="480" as="geometry" />
</mxCell>
<mxCell id="_v9aWsQiSdQDmT4Fcf7S-2" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;font style=&quot;font-size: 23px;&quot;&gt;page&lt;/font&gt;" vertex="1">
<mxGeometry height="60" width="120" x="174" y="540" as="geometry" />
</mxCell>
<mxCell id="_v9aWsQiSdQDmT4Fcf7S-3" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 23px;&quot;&gt;model&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="120" x="294" y="540" as="geometry" />
</mxCell>
<mxCell id="_v9aWsQiSdQDmT4Fcf7S-4" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 23px;&quot;&gt;view&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="120" x="414" y="540" as="geometry" />
</mxCell>
<mxCell id="_v9aWsQiSdQDmT4Fcf7S-5" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="&lt;span style=&quot;font-size: 23px;&quot;&gt;presenter&lt;/span&gt;" vertex="1">
<mxGeometry height="60" width="120" x="534" y="540" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="ae4TW034q6_u2rk01mG-" name="菜单树构建逻辑">
<mxGraphModel dx="1224" dy="1192" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="uTcgnOhw-1hiChn4308x-321" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="主菜单(class=0)" vertex="1">
<mxGeometry height="54" width="171" x="36" y="20" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-322" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="参数设置(class=1)" vertex="1">
<mxGeometry height="54" width="187" x="260" y="140" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-323" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="信息查看(class=1)" vertex="1">
<mxGeometry height="54" width="187" x="250" y="276" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-324" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="关于系统(class=1)" vertex="1">
<mxGeometry height="54" width="187" x="250" y="430" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-325" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="定值设置(class=2)" vertex="1">
<mxGeometry height="54" width="187" x="520" y="107" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-326" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="开关设置(class=2)" vertex="1">
<mxGeometry height="54" width="187" x="520" y="207" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-327" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="运行信息(class=2)" vertex="1">
<mxGeometry height="54" width="187" x="520" y="294" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-328" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-321" style="startArrow=none;endArrow=block;entryX=0.64;entryY=0;rounded=0;edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" target="uTcgnOhw-1hiChn4308x-322" value="lower">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="380" y="47" />
</Array>
<mxPoint x="240" y="40" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-329" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-322" style="startArrow=none;endArrow=block;exitX=0.667;exitY=1.037;entryX=0.72;entryY=0;rounded=0;exitDx=0;exitDy=0;exitPerimeter=0;" target="uTcgnOhw-1hiChn4308x-323" value="behind">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-330" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-323" style="startArrow=none;endArrow=block;exitX=0.75;exitY=1;entryX=0.75;entryY=0;rounded=0;exitDx=0;exitDy=0;entryDx=0;entryDy=0;" target="uTcgnOhw-1hiChn4308x-324" value="behind">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-331" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-324" style="startArrow=none;endArrow=block;exitX=0.75;exitY=1;entryX=-0.001;entryY=0.171;rounded=0;edgeStyle=orthogonalEdgeStyle;exitDx=0;exitDy=0;entryDx=0;entryDy=0;entryPerimeter=0;" target="uTcgnOhw-1hiChn4308x-322" value="behind 回环">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="390" y="520" />
<mxPoint x="110" y="520" />
<mxPoint x="110" y="149" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-332" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-322" style="startArrow=none;endArrow=block;exitX=0;exitY=0.75;entryX=0.25;entryY=1;rounded=0;edgeStyle=orthogonalEdgeStyle;entryDx=0;entryDy=0;exitDx=0;exitDy=0;" target="uTcgnOhw-1hiChn4308x-324" value="before 回环">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="180" y="180" />
<mxPoint x="180" y="500" />
<mxPoint x="297" y="500" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-333" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-323" style="startArrow=none;endArrow=block;exitX=0.269;exitY=0.012;entryX=0.33;entryY=1;rounded=0;exitDx=0;exitDy=0;exitPerimeter=0;" value="before">
<mxGeometry relative="1" as="geometry">
<mxPoint x="321.79" y="276" as="sourcePoint" />
<mxPoint x="300.00000000000006" y="194" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-334" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-324" style="startArrow=none;endArrow=block;exitX=0.4;exitY=0;entryX=0.399;entryY=0.986;rounded=0;entryDx=0;entryDy=0;entryPerimeter=0;" target="uTcgnOhw-1hiChn4308x-323" value="before">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-335" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-322" style="startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;entryDx=0;entryDy=0;exitDx=0;exitDy=0;edgeStyle=orthogonalEdgeStyle;" target="uTcgnOhw-1hiChn4308x-325" value="lower">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-336" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-325" style="startArrow=none;endArrow=block;exitX=0.912;exitY=0.975;entryX=0.91;entryY=0;rounded=0;edgeStyle=orthogonalEdgeStyle;exitDx=0;exitDy=0;exitPerimeter=0;" target="uTcgnOhw-1hiChn4308x-326" value="behind">
<mxGeometry relative="1" x="0.1907" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-337" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-326" style="startArrow=none;endArrow=block;exitX=0.332;exitY=-0.003;entryX=0.33;entryY=1;rounded=0;exitDx=0;exitDy=0;exitPerimeter=0;" target="uTcgnOhw-1hiChn4308x-325" value="behind 回环">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-338" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-325" style="startArrow=none;endArrow=block;exitX=0.669;exitY=0.986;rounded=0;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.673;entryY=-0.017;entryDx=0;entryDy=0;entryPerimeter=0;" target="uTcgnOhw-1hiChn4308x-326" value="before 回环">
<mxGeometry relative="1" x="0.1635" y="-6" as="geometry">
<mxPoint as="offset" />
<mxPoint x="647" y="167" as="sourcePoint" />
<mxPoint x="645" y="187" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-339" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-326" style="startArrow=none;endArrow=block;exitX=0.133;exitY=-0.013;entryX=0.13;entryY=1;rounded=0;exitDx=0;exitDy=0;exitPerimeter=0;" target="uTcgnOhw-1hiChn4308x-325" value="before">
<mxGeometry relative="1" x="0.0153" y="9" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-340" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-323" style="startArrow=none;endArrow=block;exitX=0.999;exitY=0.807;entryX=0;entryY=0.5;rounded=0;edgeStyle=orthogonalEdgeStyle;entryDx=0;entryDy=0;exitDx=0;exitDy=0;exitPerimeter=0;" target="uTcgnOhw-1hiChn4308x-327" value="lower">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-341" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-322" style="startArrow=none;endArrow=block;exitX=0.21;exitY=0;entryX=0.5;entryY=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" target="uTcgnOhw-1hiChn4308x-321" value="higher">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-342" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-323" style="startArrow=none;endArrow=block;exitX=0;exitY=0.5;entryX=0.26;entryY=1;rounded=0;edgeStyle=orthogonalEdgeStyle;exitDx=0;exitDy=0;" target="uTcgnOhw-1hiChn4308x-321" value="higher">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="80" y="303" />
<mxPoint x="80" y="276" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-343" edge="1" parent="1" style="startArrow=none;endArrow=block;entryX=0;entryY=0.83;rounded=0;edgeStyle=orthogonalEdgeStyle;" value="higher">
<mxGeometry relative="1" x="0.4125" as="geometry">
<mxPoint as="offset" />
<Array as="points">
<mxPoint x="250" y="460" />
<mxPoint x="20" y="460" />
<mxPoint x="20" y="40" />
</Array>
<mxPoint x="250" y="460" as="sourcePoint" />
<mxPoint x="36" y="40.019999999999996" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-344" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-325" style="startArrow=none;endArrow=block;exitX=0.5;exitY=0;entryX=0.905;entryY=0.019;rounded=0;edgeStyle=orthogonalEdgeStyle;entryDx=0;entryDy=0;entryPerimeter=0;exitDx=0;exitDy=0;" target="uTcgnOhw-1hiChn4308x-322" value="higher">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="614" y="80" />
<mxPoint x="429" y="80" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-345" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-326" style="startArrow=none;endArrow=block;exitX=0;exitY=0.5;rounded=0;edgeStyle=orthogonalEdgeStyle;exitDx=0;exitDy=0;" value="higher">
<mxGeometry relative="1" x="-0.3947" as="geometry">
<mxPoint as="offset" />
<Array as="points">
<mxPoint x="420" y="234" />
</Array>
<mxPoint x="420" y="194" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-346" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-327" style="startArrow=none;endArrow=block;exitX=0.43;exitY=0;entryX=1.005;entryY=0.163;rounded=0;edgeStyle=orthogonalEdgeStyle;entryDx=0;entryDy=0;entryPerimeter=0;" target="uTcgnOhw-1hiChn4308x-323" value="higher">
<mxGeometry relative="1" x="0.4124" y="-3" as="geometry">
<mxPoint as="offset" />
<Array as="points">
<mxPoint x="600" y="285" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-347" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-327" style="endArrow=classic;html=1;rounded=0;exitX=0.677;exitY=0.972;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.709;entryY=0.015;entryDx=0;entryDy=0;entryPerimeter=0;curved=1;" target="uTcgnOhw-1hiChn4308x-327" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<Array as="points">
<mxPoint x="710" y="380" />
<mxPoint x="730" y="330" />
<mxPoint x="730" y="300" />
<mxPoint x="720" y="260" />
</Array>
<mxPoint x="657" y="460" as="sourcePoint" />
<mxPoint x="707" y="410" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-348" connectable="0" parent="uTcgnOhw-1hiChn4308x-347" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" value="before" vertex="1">
<mxGeometry relative="1" x="-0.0353" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-351" edge="1" parent="1" source="uTcgnOhw-1hiChn4308x-327" style="curved=1;endArrow=classic;html=1;rounded=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;" target="uTcgnOhw-1hiChn4308x-327" value="">
<mxGeometry height="50" relative="1" width="50" as="geometry">
<Array as="points">
<mxPoint x="750" y="230" />
<mxPoint x="750" y="400" />
<mxPoint x="590" y="390" />
</Array>
<mxPoint x="570" y="484" as="sourcePoint" />
<mxPoint x="620" y="434" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="uTcgnOhw-1hiChn4308x-352" connectable="0" parent="uTcgnOhw-1hiChn4308x-351" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" value="behind" vertex="1">
<mxGeometry relative="1" x="0.0226" y="-5" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="5RVJ2jwpCgCB7Hji9JxO" name="菜单树判断逻辑">
<mxGraphModel dx="2720" dy="2649" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-1" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="byCurClass &lt; byNextClass" vertex="1">
<mxGeometry height="54" width="241" x="250" y="380" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-2" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="进入子菜单" vertex="1">
<mxGeometry height="54" width="140" x="510" y="380" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-3" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="current.lower = next" vertex="1">
<mxGeometry height="54" width="190" x="670" y="380" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-4" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="next.higher = current" vertex="1">
<mxGeometry height="54" width="209" x="891" y="380" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-5" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="byCurClass == byNextClass" vertex="1">
<mxGeometry height="54" width="240" x="250" y="484" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-6" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="同层兄弟" vertex="1">
<mxGeometry height="54" width="140" x="511" y="484" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-7" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="current.behind = next" vertex="1">
<mxGeometry height="54" width="190" x="670" y="484" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-8" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="next.before = current" vertex="1">
<mxGeometry height="54" width="210" x="890" y="484" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-9" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="next.higher = current.higher" vertex="1">
<mxGeometry height="78" width="260" x="1140" y="472" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-10" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="byCurClass &gt; byNextClass" vertex="1">
<mxGeometry height="54" width="241" x="250" y="591" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-11" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="从深层回退" vertex="1">
<mxGeometry height="54" width="140" x="510" y="591" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-12" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="next.before = ptLast[nextClass]" vertex="1">
<mxGeometry height="54" width="190" x="670" y="591" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-27" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-13" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="HZ-q9eH6YA3dt1o5nvaf-26" value="">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-13" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="ptLast[nextClass].behind=next" vertex="1">
<mxGeometry height="54" width="190" x="1140" y="591" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-14" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-1" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-2" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-15" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-2" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-3" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-16" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-3" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-4" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-17" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-5" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-6" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-18" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-6" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-7" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-19" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-7" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-8" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-20" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-8" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-9" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-21" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-10" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-11" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-22" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-11" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;entryX=0;entryY=0.5;rounded=0;" target="HZ-q9eH6YA3dt1o5nvaf-12" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-23" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-12" style="curved=1;startArrow=none;endArrow=block;exitX=1;exitY=0.5;rounded=0;exitDx=0;exitDy=0;" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
<mxPoint x="890" y="618" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-25" edge="1" parent="1" source="HZ-q9eH6YA3dt1o5nvaf-24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="HZ-q9eH6YA3dt1o5nvaf-13" value="">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-24" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="next.behind=ptLast[nextClass].higher" vertex="1">
<mxGeometry height="54" width="209" x="891" y="591" as="geometry" />
</mxCell>
<mxCell id="HZ-q9eH6YA3dt1o5nvaf-26" parent="1" style="whiteSpace=wrap;html=1;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);strokeWidth=2;" value="形成回环" vertex="1">
<mxGeometry height="55" width="69" x="1351" y="591" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="zG06bic9s9vmrHAmIL5z" name="第 8 页">
<mxGraphModel dx="628" dy="611" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="4pC_cjyCFYns3d6entyY-21" edge="1" parent="1" source="4pC_cjyCFYns3d6entyY-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" target="4pC_cjyCFYns3d6entyY-3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-1" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);rounded=1;" value="开始" vertex="1">
<mxGeometry height="24" width="210" x="320" y="120" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-3" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="计算菜单个数 maxItem " vertex="1">
<mxGeometry height="30" width="210" x="320" y="171" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-5" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="统计 0 级菜单数量 by0LevelNum " vertex="1">
<mxGeometry height="30" width="210" x="320" y="225" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-6" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="构建菜单树链路" vertex="1">
<mxGeometry height="30" width="210" x="320" y="279" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-7" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="补充带子菜单项的右侧箭头》" vertex="1">
<mxGeometry height="40" width="210" x="320" y="328" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-8" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="计算各节点 wPos 与 wNum" vertex="1">
<mxGeometry height="30" width="210" x="320" y="387" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-9" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);" value="计算整棵菜单树的显示位置" vertex="1">
<mxGeometry height="30" width="210" x="320" y="441" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-10" parent="1" style="whiteSpace=wrap;strokeWidth=2;fillColor=light-dark(#eeeeee,#1f2020);strokeColor=light-dark(#999999,#cccccc);fontColor=light-dark(#333333,#cccccc);rounded=1;" value="函数结束" vertex="1">
<mxGeometry height="20" width="210" x="320" y="500" as="geometry" />
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-13" edge="1" parent="1" source="4pC_cjyCFYns3d6entyY-3" style="curved=1;startArrow=none;endArrow=block;exitX=0.5;exitY=1;entryX=0.5;entryY=0;rounded=0;entryDx=0;entryDy=0;" target="4pC_cjyCFYns3d6entyY-5" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
<mxPoint x="426" y="470" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-15" edge="1" parent="1" source="4pC_cjyCFYns3d6entyY-5" style="curved=1;startArrow=none;endArrow=block;exitX=0.5;exitY=1;entryX=0.5;entryY=0;rounded=0;" target="4pC_cjyCFYns3d6entyY-6" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-16" edge="1" parent="1" source="4pC_cjyCFYns3d6entyY-6" style="curved=1;startArrow=none;endArrow=block;exitX=0.5;exitY=1;entryX=0.5;entryY=0;rounded=0;" target="4pC_cjyCFYns3d6entyY-7" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-17" edge="1" parent="1" source="4pC_cjyCFYns3d6entyY-7" style="curved=1;startArrow=none;endArrow=block;exitX=0.5;exitY=1;entryX=0.5;entryY=0;rounded=0;" target="4pC_cjyCFYns3d6entyY-8" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-18" edge="1" parent="1" source="4pC_cjyCFYns3d6entyY-8" style="curved=1;startArrow=none;endArrow=block;exitX=0.5;exitY=1;entryX=0.5;entryY=0;rounded=0;" target="4pC_cjyCFYns3d6entyY-9" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="4pC_cjyCFYns3d6entyY-19" edge="1" parent="1" source="4pC_cjyCFYns3d6entyY-9" style="curved=1;startArrow=none;endArrow=block;exitX=0.5;exitY=1;entryX=0.5;entryY=0;rounded=0;" target="4pC_cjyCFYns3d6entyY-10" value="">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -1,6 +1,8 @@
#ifndef __TYPES__H__
#define __TYPES__H__
#include <stdio.h>
#define uint8_t unsigned char
#define uint16_t unsigned short
#define uint32_t unsigned int
@@ -15,4 +17,18 @@
typedef int (*FUNCPTR) ( );
// 调试模式断言
#ifdef DEBUG
#define ASSERT(expr) \
do { \
if (expr) { \
printf("Assertion failed: %s, file %s, line %d\n", \
#expr, __FILE__, __LINE__); \
} \
} while (0)
#else
#define ASSERT(expr) ((void)0) // 发布模式禁用断言
#endif
#endif

View File

@@ -1,191 +1,54 @@
#include "menu.h"
#include <stddef.h>
#include <stdio.h>
#include "../../pages/menu/page.h"
#include "../model/display.h"
#include "../../key.h"
#include "../view/menu_layout.h"
#include "../presenter/menu_presenter.h"
#include "../view/menu_render_port.h"
#include "../common/menu_state_types.h"
/* -------------------------------------------------------------------------
* 运行态状态收口App 层私有)
*
* 设计意图:
* 1) 避免全局变量四处散落,统一由 app/menu.c 托管生命周期;
* 2) 外部模块只能经由 MenuApp_* 接口访问,降低耦合;
* 3) Presenter/Model/View 依赖都在初始化时一次性装配。
*
* 变量职责:
* - s_dspCtrl:
* 显示刷新控制状态(例如首帧标记 bFirst
* - s_menuCtrl:
* 导航与路由控制状态(当前节点、各级 route、0 级入口等)。
* - s_menuItems[300]:
* 运行时菜单节点池(树构建后常驻内存),由 Model 初始化、View 读取布局/渲染。
* - g_menuPresenter:
* MVP 调度中枢实例,封装输入处理与刷新协同。
* - g_menuLayoutConfig:
* 布局参数快照(屏幕尺寸、行高、边距、菜单宽度等),初始化时写入一次。
* ------------------------------------------------------------------------- */
static tagDspCtrl s_dspCtrl;
static tagMenuCtrl s_menuCtrl;
static tagMenuItem s_menuItems[300];
static MenuPresenter g_menuPresenter;
static MenuLayoutConfig g_menuLayoutConfig;
void MenuApp_Render(void)
void MenuProc_See_AppInfo(void)
{
/* 统一渲染入口:由 Presenter 决定全量/增量刷新策略。 */
MenuPresenter_Refresh(&g_menuPresenter);
MenuPage_TriggerCurrentAction();
}
void MenuApp_HandleInput(uint8_t keyVal)
void MenuProc_See_YC(void)
{
/* 单次输入事件入口:按键解释与状态迁移都在 Presenter 内完成。 */
MenuPresenter_HandleInput(&g_menuPresenter, keyVal);
MenuPage_TriggerCurrentAction();
}
void MenuApp_PollInput(void)
{
/* 轮询输入设备(键盘/按键),再复用事件处理入口。 */
MenuApp_HandleInput(Key_Read());
}
void MenuProc_Set_Value(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Set_Soft(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Cfg_Time(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Cfg_RevEvent(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Cfg_ManualWave(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_See_Input(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_See_Set(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_See_Soft(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_YX_SetCommType(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_YX_SetWidth(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_YX_SetTwin(void) { MenuPage_TriggerCurrentAction(); }
void MenuApp_Init(void)
{
const MenuRenderPort *renderPort = MenuRenderPort_Lcd();
uint16_t lcdSizeX;
uint16_t lcdSizeY;
/* 1) 初始化底层渲染设备LCD/模拟器具体实现由 renderPort 决定)。 */
renderPort->init();
/* 2) 通过渲染端口查询设备能力,避免 app 层直接依赖 lcd.h 宏。 */
lcdSizeX = renderPort->get_size_x();
lcdSizeY = renderPort->get_size_y();
/* 3) 组装布局配置(后续由 View 布局计算与渲染阶段复用)。 */
g_menuLayoutConfig.lcdSizeX = lcdSizeX;
g_menuLayoutConfig.lcdSizeY = lcdSizeY;
g_menuLayoutConfig.menuYMin = MENU_YMIN;
g_menuLayoutConfig.menuYMax = MENU_YMAX_FROM_LCD(lcdSizeY);
g_menuLayoutConfig.lineHeight = LINE_HEIGHT;
g_menuLayoutConfig.menuWidth = MENU_WITDTH;
g_menuLayoutConfig.menuXAdd = MENU_XADD;
g_menuLayoutConfig.menuYAdd = MENU_YADD;
/* 4) 装配 Presenter注入控制状态、菜单池、渲染端口与布局配置。 */
MenuPresenter_Setup(&g_menuPresenter,
&s_dspCtrl,
&s_menuCtrl,
s_menuItems,
renderPort,
&g_menuLayoutConfig);
/* 5) 初始化模型数据并建立初始路由/布局。 */
MenuPresenter_InitModel(&g_menuPresenter, g_tMenuModelTab, MENU_MAX_ITEM);
}
const tagMenuItem *MenuApp_GetMenuItems(uint16_t *count)
{
/* 诊断/测试只读访问接口:
* - 返回内部菜单池首地址;
* - 若 count 非空,同步返回数组容量。 */
if (count != NULL)
{
*count = (uint16_t)(sizeof(s_menuItems) / sizeof(s_menuItems[0]));
}
return s_menuItems;
}
void MenuProc_See_AppInfo()
{
printf("MenuProc_See_AppInfo\n");
}
void MenuProc_See_YC()
{
printf("MenuProc_See_YC\n");
}
void MenuProc_Set_Value()
{
printf("MenuProc_Set_Value\n");
}
void MenuProc_Set_Soft()
{
printf("MenuProc_Set_Soft\n");
}
void MenuProc_Cfg_Time()
{
printf("MenuProc_Cfg_Time\n");
}
void MenuProc_Cfg_RevEvent()
{
printf("MenuProc_Cfg_RevEvent\n");
}
void MenuProc_Cfg_ManualWave()
{
printf("MenuProc_Cfg_ManualWave\n");
}
void MenuProc_See_Input()
{
printf("MenuProc_See_Input\n");
}
void MenuProc_See_Set()
{
printf("MenuProc_See_Set\n");
}
void MenuProc_See_Soft()
{
printf("MenuProc_See_Soft\n");
}
void MenuProc_YX_SetCommType()
{
printf("MenuProc_YX_SetCommType\n");
}
void MenuProc_YX_SetWidth()
{
printf("MenuProc_YX_SetWidth\n");
}
void MenuProc_YX_SetTwin()
{
printf("MenuProc_YX_SetTwin\n");
}
/* ---------- 以下为 display.c 菜单表引用、暂无具体实现的桩函数,避免链接未定义符号 ---------- */
void Menu_NonPfunc(void) { printf("Menu_NonPfunc\n"); }
void MenuProc_AllInf_Default(void) { printf("MenuProc_AllInf_Default\n"); }
void MenuProc_Para_Default(void) { printf("MenuProc_Para_Default\n"); }
void MenuProc_Set_Default(void) { printf("MenuProc_Set_Default\n"); }
void MenuProc_Resume_Soft(void) { printf("MenuProc_Resume_Soft\n"); }
void MenuProc_Cfg_CellDef(void) { printf("MenuProc_Cfg_CellDef\n"); }
void MenuProc_Cfg_ShowAnaType(void) { printf("MenuProc_Cfg_ShowAnaType\n"); }
void MenuProc_Dbg_XuYX(void) { printf("MenuProc_Dbg_XuYX\n"); }
void MenuProc_Dbg_XuYC(void) { printf("MenuProc_Dbg_XuYC\n"); }
void MenuProc_Dbg_XuEvent(void) { printf("MenuProc_Dbg_XuEvent\n"); }
void MenuProc_Dbg_Relay(void) { printf("MenuProc_Dbg_Relay\n"); }
void MenuProc_See_VersionBoard(void) { printf("MenuProc_See_VersionBoard\n"); }
void MenuProc_Cfg_ClrRec(void) { printf("MenuProc_Cfg_ClrRec\n"); }
void MenuProc_Cfg_ComPara(void) { printf("MenuProc_Cfg_ComPara\n"); }
void MenuProc_Cfg_EditIP(void) { printf("MenuProc_Cfg_EditIP\n"); }
void MenuProc_Cfg_EditSntp(void) { printf("MenuProc_Cfg_EditSntp\n"); }
void MenuProc_Cfg_CellConf(void) { printf("MenuProc_Cfg_CellConf\n"); }
void MenuProc_See_RecSOE(void) { printf("MenuProc_See_RecSOE\n"); }
void MenuProc_See_RecAct(void) { printf("MenuProc_See_RecAct\n"); }
void MenuProc_See_RecOpt(void) { printf("MenuProc_See_RecOpt\n"); }
void MenuProc_See_RecAlm(void) { printf("MenuProc_See_RecAlm\n"); }
void MenuProc_See_RecStart(void) { printf("MenuProc_See_RecStart\n"); }
void MenuProc_See_RecYK(void) { printf("MenuProc_See_RecYK\n"); }
void MenuProc_See_RecChk(void) { printf("MenuProc_See_RecChk\n"); }
void MenuProc_See_RecRun(void) { printf("MenuProc_See_RecRun\n"); }
void MenuProc_See_RecFault(void) { printf("MenuProc_See_RecFault\n"); }
void MenuProc_YC_SetSqValue(void) { printf("MenuProc_YC_SetSqValue\n"); }
void MenuProc_YC_SetAdjCoe(void) { printf("MenuProc_YC_SetAdjCoe\n"); }
void Menu_NonPfunc(void) {}
void MenuProc_AllInf_Default(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Para_Default(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Set_Default(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Resume_Soft(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Cfg_CellDef(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Cfg_ShowAnaType(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Dbg_XuYX(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Dbg_XuYC(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Dbg_XuEvent(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Dbg_Relay(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_See_VersionBoard(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Cfg_ClrRec(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Cfg_ComPara(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Cfg_EditIP(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Cfg_EditSntp(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_Cfg_CellConf(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_See_RecSOE(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_See_RecAct(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_See_RecOpt(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_See_RecAlm(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_See_RecStart(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_See_RecYK(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_See_RecChk(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_See_RecRun(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_See_RecFault(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_YC_SetSqValue(void) { MenuPage_TriggerCurrentAction(); }
void MenuProc_YC_SetAdjCoe(void) { MenuPage_TriggerCurrentAction(); }

View File

@@ -10,14 +10,6 @@
* - 结构采用 MVPModel(状态) / View(渲染) / Presenter(调度)
*/
#include "../common/menu_item_types.h"
void MenuApp_Init(void);
void MenuApp_HandleInput(uint8_t keyVal);
void MenuApp_PollInput(void);
void MenuApp_Render(void);
const tagMenuItem *MenuApp_GetMenuItems(uint16_t *count);
/* 非功能键处理 */
void Menu_NonPfunc();
void MenuProc_AllInf_Default();

View File

@@ -1,52 +0,0 @@
#ifndef MENU_ITEM_TYPES_H
#define MENU_ITEM_TYPES_H
#include "types.h"
typedef struct MenuItem MenuItem;
typedef MenuItem *tagPMenuItem;
typedef MenuItem tagMenuItem;
typedef struct
{
MenuItem *higher;
MenuItem *lower;
MenuItem *before;
MenuItem *behind;
} MenuLinks;
typedef struct
{
uint16_t wSPosX;
uint16_t wSPosY;
uint16_t wEPosX;
uint16_t wEPosY;
} MenuRect;
struct MenuItem
{
/* 导航关系 */
MenuItem *ptHigher;
MenuItem *ptLower;
MenuItem *ptBefore;
MenuItem *ptBehind;
/* 菜单元数据 */
uint8_t byClass;
uint8_t byName[50];
uint8_t byTip[50];
uint8_t byAttrib;
uint16_t wPassword;
uint16_t wPara;
FUNCPTR pfnWinProc;
/* 视图与布局 */
uint16_t wPos;
uint16_t wNum;
uint16_t wSPosX;
uint16_t wSPosY;
uint16_t wEPosX;
uint16_t wEPosY;
};
#endif

View File

@@ -1,23 +0,0 @@
#ifndef MENU_STATE_TYPES_H
#define MENU_STATE_TYPES_H
#include "menu_item_types.h"
typedef struct
{
/* 当前菜单显示只依赖首帧刷新标记 */
unsigned bFirst : 1;
} tagDspCtrl;
typedef struct
{
uint8_t by0LevelNum;
tagPMenuItem ptHead;
tagPMenuItem ptCurrent;
tagPMenuItem ptRoute[4];
tagPMenuItem ptCurBak;
tagPMenuItem pt0Level;
} tagMenuCtrl;
#endif

View File

@@ -1,147 +0,0 @@
#include "../app/menu.h"
#include "display.h"
//========================================== //
// 字符串代号常量定义
enum _STR_FLAG_ {
EN_STR_FLAG_FUNSET = 0, // 投入,退出
EN_STR_FLAG_OKCANCEL,
EN_VALUE_FLAG_SLOTTYPE, // 板件类型
EN_VALUE_FLAG_YESNO, // 是否
EN_VALUE_FLAG_CHN_NUM, // 中文数字
EN_VALUE_FLAG_YXSET, // 遥信设置
EN_VALUE_FLAG_YKSTEP, // 遥控步骤
EN_VALUE_FLAG_YKACTION, // 遥控操作类型
EN_VALUE_FLAG_CHK, // 自检信息
EN_VALUE_FLAG_BEUSED, // 是否使用
EN_VALUE_FLAG_PORTTYPE, // 端口类型
EN_VALUE_FLAG_MAPNAME, // 转发表名称
EN_VALUE_FLAG_RECTITLE, // 记录类型表
EN_VALUE_FLAG_BHINFO, // 保护动作/复归
EN_VALUE_FLAG_BHSET, // 保护动作/复归
EN_VALUE_FLAG_OKCANCEL, // 确定/取消
EN_VALUE_FLAG_FILETYPE, // 文件类型
EN_VALUE_FLAG_ROLE, // 备份角色
EN_VALUE_FLAG_COMMSTATE, // 通讯状态
EN_VALUE_FLAG_ALARM, // 告警状态
EN_VALUE_FLAG_YXTYPE, // 遥信类型
EN_STR_FLAG_MENUALAUTO,
EN_STR_FLAG_LINKBREAK,
EN_STR_FLAG_DONEUNDONE,
EN_STR_FLAG_ACTBACK,
EN_STR_FLAG_RESUMEBREAK,
EN_STR_FLAG_USESTOP,
EN_STR_FLAG_BAUDRATE,
EN_STR_FLAG_VERIFY,
EN_STR_FLAG_PROTOL,
EN_STR_FLAG_RECORD,
EN_STR_FLAG_MESSAGE,
EN_STR_FLAG_COM,
EN_STR_FLAG_MODE,
EN_STR_FLAG_COMBAUD,
EN_STR_FLAG_DATABIT,
EN_STR_FLAG_STOPBIT,
EN_STR_FLAG_COMTYPE,
EN_STR_FLAG_SAMEV,
EN_STR_FLAG_PT,
EN_STR_FLAG_CT,
EN_STR_FLAG_TRANSTYPE,
EN_STR_FLAG_LINEPHS,
EN_STR_FLAG_RETOUT,
EN_STR_FLAG_YXDI,
EN_STR_FLAG_YKPUL,
EN_STR_FLAG_WAVE,
EN_STR_FLAG_MEMTYPE,
EN_STR_FLAG_CHANNEL,
EN_STR_FLAG_POWERTYPE,
EN_STR_FLAG_ZEROTYPE,
EN_STR_FLAG_YCCOMMTYPE,
EN_STR_FLAG_YXCOMMTYPE,
EN_STR_FLAG_CTRLWORD,
EN_STR_FLAG_BUSNAME,
EN_STR_FLAG_UNITSHOWMODE,
EN_STR_FLAG_BAYMODE,
EN_STR_FLAG_BASEANA,
EN_STR_FLAG_BASEPHASE,
EN_STR_FLAG_BAYCODE,
EN_STR_FLAG_SET_NUM,
EN_STR_FLAG_DCTYPE,
EN_STR_FLAG_DCINTYPE,
EN_STR_FLAG_DWINTYPE,
EN_STR_FLAG_PTCT,
EN_STR_FLAG_HARMOTYPE,
EN_STR_FLAG_YXSIN_DI, //设置双点遥信
EN_STR_FLAG_SHOWANATYPE,
EN_STR_FLAG_ANAPOLARTYPE, // 交流通道极性
EN_STR_FLAG_WIRDIAGRAMTYPE // 增加主接线图类型识别.modified by zhanggl 111010
} enumStrType;
/* 静态菜单表
注意:该表定义时顺序不能乱,需要从 0 级开始,一级一经按顺序写入
*/
const tagMenuModel g_tMenuModelTab[] =
{
{ 0, "主界面", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 1, "装置信息", "查看装置信息", 0, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_AppInfo },
{ 1, "实时数据", "装置实时数据", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 2, "交流量", "查看遥测交流量", EN_MEA_AC, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_YC },
{ 2, "直流量", "查看遥测直流量", EN_MEA_DC, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_YC },
{ 2, "遥信量", "查看遥信开入量", EN_INPUT_RLY_ALL, 0x0000, EN_INPUT_0, (FUNCPTR)MenuProc_See_Input },
{ 1, "参数定值", "保护参数查看与修改", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 2, "整定", "整定装置保护参数", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 3, "参数", "查看设备参数定值", EN_FIGURE_SET, CN_USER_PWD, EN_SIDE_BASIC, (FUNCPTR)MenuProc_Set_Value },
{ 3, "定值", "设置装置数值定值", EN_FIGURE_SET, CN_USER_PWD, EN_SIDE_DEF, (FUNCPTR)MenuProc_Set_Value },
{ 3, "控制字", "设置装置控制字", EN_SOFT_SET, CN_USER_PWD, EN_SIDE_DEF, (FUNCPTR)MenuProc_Set_Value },
{ 3, "软压板", "设置软压板", 0, CN_USER_PWD, EN_SOFT_PRO, (FUNCPTR)MenuProc_Set_Soft },
{ 2, "查看", "查看装置保护参数", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 3, "参数", "设置设备参数定值", EN_FIGURE_SET, 0x0000, EN_SIDE_BASIC, (FUNCPTR)MenuProc_See_Set },
{ 3, "定值", "查看数值型定值", EN_FIGURE_SET, 0x0000, EN_SIDE_DEF, (FUNCPTR)MenuProc_See_Set },
{ 3, "控制字", "查看控制字定值", EN_SOFT_SET, 0x0000, EN_SIDE_DEF, (FUNCPTR)MenuProc_See_Set },
{ 3, "软压板", "查看软压板", 0, 0x0000, EN_SOFT_PRO, (FUNCPTR)MenuProc_See_Soft },
{ 1, "三遥设置", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 2, "遥测死区", "设置遥测量死区门槛", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YC_SetSqValue },
{ 2, "遥测系数", "设置遥测量微调系数", EN_MEA_ADJ, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YC_SetAdjCoe },
{ 2, "遥信类型", "设置遥信类型", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetCommType },
{ 2, "遥信防抖", "设置遥信防抖时间", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetWidth },
{ 2, "双点遥信", "设置双点遥信虚端子", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetTwin },
{ 1, "装置维护", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 2, "时钟设置", "设置系统时钟", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_Time },
{ 2, "强制复归", "可复归未返回事件", EN_REV_FORCE, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_RevEvent },
{ 2, "手动录波", "启动手动录波", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ManualWave },
{ 2, "清除记录", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ClrRec },
{ 1, "通讯参数", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 2, "通讯设置", "外部通讯设置", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ComPara },
{ 2, "网口设置", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_EditIP },
{ 2, "SNTP设置", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_EditSntp },
{ 1, "记录查询", "查看各种装置记录", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 2, "SOE记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecSOE },
{ 2, "事故记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecAct },
{ 2, "操作记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecOpt },
{ 2, "保护告警", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecAlm },
{ 2, "保护启动", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecStart },
{ 2, "遥控记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecYK },
{ 2, "自检记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecChk },
{ 2, "运行记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecRun },
{ 2, "运行报告", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecFault },
{ 0, "厂家设置", "设置装置相关参数", 0, CN_COP_PWD, 0, (FUNCPTR)Menu_NonPfunc },
{ 1, "元件配置", "配置元件配置", 0, CN_COP_PWD, EN_FACTORY_PASSWORD,(FUNCPTR)MenuProc_Cfg_CellConf },
{ 1, "恢复默认", "恢复默认元件定值参数", 0, CN_COP_PWD, 0, (FUNCPTR)Menu_NonPfunc },
{ 2, "全部恢复", "全部参数恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_AllInf_Default },
{ 2, "默认参数", "当前参数恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Para_Default },
{ 2, "默认定值", "当前定值区恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Set_Default },
{ 2, "软压板", "当前软压板恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Resume_Soft },
{ 2, "元件配置", "元件配置恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Cfg_CellDef },
{ 1, "交流显示", "交流显示方式设置", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Cfg_ShowAnaType },
{ 1, "装置调试", "调试装置", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 2, "虚拟遥信", "设置虚拟遥信值", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuYX },
{ 2, "交流虚遥测", "设置虚拟交流遥测值", EN_MEA_AC, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC },
{ 2, "直流虚遥测", "设置虚拟直流遥测值", EN_MEA_DC, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC },
{ 2, "电度虚遥测", "设置虚拟电度遥测值", EN_MEA_POWER, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC },
{ 2, "动作虚事件", "设置虚拟动作事件", EN_ACT_REC, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuEvent },
{ 2, "告警虚事件", "设置虚拟告警事件", EN_ALM_REC, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuEvent },
{ 2, "动作出口", "进入此菜单保护退出", EN_OUTPUT_TRIP, CN_COP_PWD, EN_INPUT_0, (FUNCPTR)MenuProc_Dbg_Relay },
{ 2, "信号出口", "进入此菜单保护退出", EN_OUTPUT_SIGN, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_Relay },
{ 1, "版本信息", "查看板件版本信息", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_See_VersionBoard },
};
const uint32_t MENU_MAX_ITEM = sizeof(g_tMenuModelTab) / sizeof(tagMenuModel);

View File

@@ -1,180 +0,0 @@
#ifndef __DISPLAY__H__
#define __DISPLAY__H__
#include "types.h"
/* 静态菜定义需要的属性 */
typedef struct
{
uint8_t byClass; // 菜单分级标志;
uint8_t byName[50]; // 菜单字符串;
uint8_t byTip[50]; // 菜单提示文本;
uint8_t byAttrib; // 菜单属性,设置菜单特殊显示效果;
uint16_t wPassword; // 访问密码0x0000表示没有密码;
uint16_t wPara; // 菜单执行函数参数;
FUNCPTR pfnWinProc; // 界面执行函数指针;
}tagMenuModel,*tagPMenuModel;
extern const tagMenuModel g_tMenuModelTab[];
extern const uint32_t MENU_MAX_ITEM;
//菜单密码设置
//==============================================================================
#define CN_COP_PWD (321) // 厂家密码:可修改保护元件配置、查看内存等
#define CN_USER_PWD (700) // 用户超级密码:可用于修改普通密码、定值,预设等
#define CN_SUPER_PWD (620)
enum _REV_TYPE_ // 复归类型
{
EN_REV_FREE, // 非强制复归
EN_REV_FORCE // 强制复归
};
enum _SET_SIDE_TYPE_ // 定值类别
{
EN_SIDE_START = 0, // 备用侧
EN_SIDE_BASIC, // 基本信息
EN_SIDE_DEVINF, // 装置参数
EN_SIDE_COP, // 内部定值
EN_SIDE_MATRIX, // 内部定值 出口矩阵
EN_SIDE_ALL, // 全侧
EN_SIDE_HIGH, // 高压侧
EN_SIDE_MED1, // 中压侧
EN_SIDE_MED2, // 中压侧
EN_SIDE_LOW1, // 低压1侧
EN_SIDE_LOW2, // 低压2侧
EN_SIDE_LK, // 电抗器
EN_SIDE_Z, // Z变
EN_SIDE_DEF, // 自定义
EN_SIDE_NONE = 0xFF // 不存在
};
// ============================================================================
// Const_Soft.h 常量固定部分
// ============================================================================
// 软压板类型
enum _SOFT_TYPE_NUMBER
{
EN_SOFT_PRO = 0, // 保护功能软压板
EN_SOFT_GOOSE, // GOOSE软压板
EN_SOFT_MU, // MU软压板
EN_SOFT_SWITCH, // 刀闸强制软压板
EN_SOFT_BAK, // 备用软压板
//----------------------------------------------//
EN_SOFT_TYPE_END // 软压板类型总数目,不可改动
};
//============================================================================
//实时数据类型标志
//============================================================================
enum _MEA_TYPE_{
//保护交流量
EN_MEA_RLY, // 保护交流量1
EN_MEA_RLY2, // 保护交流量2
EN_MEA_RLY3, // 保护交流量3
EN_MEA_ANA, // 保护测量量1
EN_MEA_ANA2, // 保护测量量2
EN_MEA_ANA3, // 保护测量量3
//遥测
EN_MEA_AC, // 遥测交流量
EN_MEA_DC, // 保护直流量
EN_MEA_SYN, // 保护谐波量
EN_MEA_POWER, // 保护电能量
EN_MEA_DD, // 电度量
EN_MEA_JLYC,
EN_MEA_GEAR, // 保护档位量
EN_MEA_TQ, // 保护同期量
EN_MEA_INPUT1, // 保护交流量1
EN_MEA_INPUT2, // 保护交流量2
EN_MEA_INPUT3, // 保护交流量3
EN_INPUT_RLY_ALL, //开入加遥信
EN_INPUT_RLY_FAULT,
EN_INPUT_RLY_OTHER,
EN_INPUT_BS_ALL,
EN_INPUT_BS_FAULT,
EN_INPUT_BS_OTHER,
EN_MEA_ADJ, // 遥测微调系数
EN_MEA_YX, // 遥信状态
EN_OUTPUT_TRIP, // 保护出口调试
EN_OUTPUT_SIGN, // 信号出口调试
EN_MEA_LS, // 联锁信号.zhanggl
EN_MEA_SCRLY // 顺控配置信号.zhanggl
};
enum _INDEX_VALUE_TYPE_{
EN_SOFT_SET,
EN_FIGURE_SET
};
//==============================================================================
// 菜单常量表相关常量
//============================================================================
//交流量菜单对应wParameter执行菜单参数应用于菜单常量表
//============================================================================
enum _ANA_PARA_ {
EN_ANA_0 = 1,
EN_ANA_1
};
//============================================================================
//开入量菜单对应wParameter执行菜单参数应用于菜单常量表
//============================================================================
enum _INPUT_PARA_ {
EN_INPUT_0 = 1,
EN_INPUT_1
};
enum _ANA_TYPE_
{
EN_TYPE_DIF_CURRENT=0 ,
EN_TYPE_UNIT_CURRENT ,
EN_TYPE_UNIT_VOLTAGE
};
enum _NO_USER_PASSWORD_{
EN_NO_USER_PWD = 0x55
};
enum _FACTORY_PASSWORD_{
EN_FACTORY_PASSWORD = 0x55
};
// 记录类型
enum _REC_TYPE_
{
EN_ACT_REC = 0, // 事件记录
EN_ALM_REC, // 告警记录
EN_CHK_REC, // 自检记录
EN_SOE_REC, // SOE记录
EN_COS_REC, // COS记录
EN_LOCK_REC, // 瞬时闭锁记录
EN_OVER_REC, // 越限记录
EN_START_REC, // 启动记录
EN_RUN_REC, // 运行记录
EN_INPUT_REC, // 开入变位记录
EN_ONOFF_REC, // 保护投退记录
EN_OPT_REC, // 保护操作记录
EN_YK_REC, // 保护遥控记录
EN_SC_REC, // 装置顺控记录
EN_SCSTEPINFO_REC, // 装置顺控单步记录
EN_FAULT_REC, // 事故报告记录
EN_ACTWAVE_REC, // 动作录波信息记录
EN_STARTWAVE_REC, // 启动录波信息记录
EN_HANDWAVE_REC, // 手动录波信息记录
EN_FAULT_NO, // 故障序号
EN_ALL_REC = 0xFF, // 所有记录
EN_NO_REC = 0xFFFF // 无效记录
};
#endif

View File

@@ -1,52 +0,0 @@
#include "menu_model.h"
#include <stddef.h>
#include "menu_tree_builder.h"
/* 初始化阶段一次性修饰菜单名称:对有子项的节点追加 '\x10' 指示符。 */
static void MenuModel_DecorateDisplayNames(tagMenuItem *items, uint32_t maxItem)
{
for (uint32_t i = 0; i < maxItem; i++)
{
uint16_t nameCapacity = (uint16_t)sizeof(items[i].byName);
uint16_t maxTextLenWithoutNull = (uint16_t)(nameCapacity - 1);
uint16_t len = 0;
if (items[i].ptLower == NULL)
{
continue;
}
while ((len < maxTextLenWithoutNull) && (items[i].byName[len] != '\0'))
{
len++;
}
if ((len == 0) || (items[i].byName[len - 1] != '\x10'))
{
if (len < maxTextLenWithoutNull)
{
items[i].byName[len] = '\x10';
items[i].byName[len + 1] = '\0';
}
}
}
}
void MenuModel_Bootstrap(tagMenuCtrl *menuCtrl,
tagMenuItem *menuItems,
const tagMenuModel *modelTab,
uint32_t modelCount)
{
MenuTree_0LevelNumCal(menuCtrl, modelTab, modelCount);
MenuTree_MainCreate(menuItems, modelTab, modelCount);
MenuModel_DecorateDisplayNames(menuItems, modelCount);
menuCtrl->ptHead = &menuItems[0];
menuCtrl->ptRoute[0] = &menuItems[0];
menuCtrl->pt0Level = menuCtrl->ptRoute[0];
menuCtrl->ptCurrent = menuCtrl->ptHead->ptLower;
menuCtrl->ptCurBak = menuCtrl->ptCurrent;
menuCtrl->ptRoute[1] = menuCtrl->ptCurrent;
menuCtrl->ptRoute[2] = menuCtrl->ptCurrent;
menuCtrl->ptRoute[3] = menuCtrl->ptCurrent;
}

View File

@@ -1,12 +0,0 @@
#ifndef MENU_MODEL_H
#define MENU_MODEL_H
#include "display.h"
#include "../common/menu_state_types.h"
void MenuModel_Bootstrap(tagMenuCtrl *menuCtrl,
tagMenuItem *menuItems,
const tagMenuModel *modelTab,
uint32_t modelCount);
#endif

View File

@@ -1,119 +0,0 @@
#include <string.h>
#include "menu_tree_builder.h"
void MenuTree_0LevelNumCal(tagMenuCtrl *menuCtrl, const tagMenuModel *modelTab, uint32_t maxItem)
{
uint8_t by0LevelNum = 0;
uint32_t wLoop;
for (wLoop = 0; wLoop < maxItem; wLoop++)
{
if (modelTab[wLoop].byClass == 0)
{
by0LevelNum++;
}
}
menuCtrl->by0LevelNum = by0LevelNum;
}
void MenuTree_MainCreate(tagMenuItem *menuItems, const tagMenuModel *modelTab, uint32_t maxItem)
{
tagPMenuItem ptFirst[4];
tagPMenuItem ptLast[4];
tagPMenuItem ptCurrent;
tagPMenuItem ptNextNode;
uint32_t wLoop1;
uint8_t byCurClass;
uint8_t byNextClass;
uint8_t wLoop2;
ptFirst[0] = &menuItems[0];
ptLast[0] = ptFirst[0];
ptCurrent = &menuItems[0];
ptCurrent->wPos = 0;
ptCurrent->wNum = 0;
ptCurrent->wSPosX = 0;
ptCurrent->wSPosY = 0;
ptCurrent->wEPosX = 0;
ptCurrent->wEPosY = 0;
ptCurrent->ptHigher = NULL;
ptCurrent->ptLower = NULL;
ptCurrent->ptBefore = NULL;
ptCurrent->ptBehind = NULL;
ptCurrent->byClass = modelTab[0].byClass;
ptCurrent->byAttrib = modelTab[0].byAttrib;
ptCurrent->wPassword = modelTab[0].wPassword;
ptCurrent->wPara = modelTab[0].wPara;
ptCurrent->pfnWinProc = modelTab[0].pfnWinProc;
memcpy(ptCurrent->byName, modelTab[0].byName, 50);
memcpy(ptCurrent->byTip, modelTab[0].byTip, 50);
for (wLoop1 = 1; wLoop1 < maxItem; wLoop1++)
{
ptNextNode = &menuItems[wLoop1];
ptNextNode->wPos = 0;
ptNextNode->wNum = 0;
ptNextNode->wSPosX = 0;
ptNextNode->wSPosY = 0;
ptNextNode->wEPosX = 0;
ptNextNode->wEPosY = 0;
ptNextNode->ptLower = NULL;
ptNextNode->ptBehind = NULL;
ptNextNode->ptBefore = NULL;
ptNextNode->ptHigher = NULL;
ptNextNode->byClass = modelTab[wLoop1].byClass;
ptNextNode->byAttrib = modelTab[wLoop1].byAttrib;
ptNextNode->wPassword = modelTab[wLoop1].wPassword;
ptNextNode->wPara = modelTab[wLoop1].wPara;
ptNextNode->pfnWinProc = modelTab[wLoop1].pfnWinProc;
memcpy(ptNextNode->byName, modelTab[wLoop1].byName, 50);
memcpy(ptNextNode->byTip, modelTab[wLoop1].byTip, 50);
byCurClass = ptCurrent->byClass;
byNextClass = ptNextNode->byClass;
if (byCurClass < byNextClass)
{
ptCurrent->ptLower = ptNextNode;
ptNextNode->ptHigher = ptCurrent;
ptFirst[byNextClass] = ptNextNode;
ptLast[byNextClass] = ptNextNode;
}
else if (byCurClass == byNextClass)
{
ptNextNode->ptBefore = ptCurrent;
ptNextNode->ptHigher = ptCurrent->ptHigher;
ptCurrent->ptBehind = ptNextNode;
ptLast[byNextClass] = ptNextNode;
}
else if (byCurClass > byNextClass)
{
ptNextNode->ptBefore = ptLast[byNextClass];
ptNextNode->ptHigher = ptFirst[byNextClass]->ptHigher;
ptLast[byNextClass]->ptBehind = ptNextNode;
ptLast[byNextClass] = ptNextNode;
for (wLoop2 = byCurClass; wLoop2 > byNextClass; wLoop2--)
{
ptLast[wLoop2]->ptBehind = ptFirst[wLoop2];
ptFirst[wLoop2]->ptBefore = ptLast[wLoop2];
}
}
ptCurrent = ptNextNode;
}
byCurClass = ptCurrent->byClass;
for (wLoop1 = 0; wLoop1 <= byCurClass; wLoop1++)
{
ptLast[wLoop1]->ptBehind = ptFirst[wLoop1];
ptFirst[wLoop1]->ptBefore = ptLast[wLoop1];
}
}

View File

@@ -1,10 +0,0 @@
#ifndef MENU_TREE_BUILDER_H
#define MENU_TREE_BUILDER_H
#include "display.h"
#include "../common/menu_state_types.h"
void MenuTree_0LevelNumCal(tagMenuCtrl *menuCtrl, const tagMenuModel *modelTab, uint32_t maxItem);
void MenuTree_MainCreate(tagMenuItem *menuItems, const tagMenuModel *modelTab, uint32_t maxItem);
#endif

View File

@@ -1,15 +0,0 @@
#ifndef MENU_NAV_TYPES_H
#define MENU_NAV_TYPES_H
#include "../common/menu_item_types.h"
typedef struct
{
tagPMenuItem ptHead;
tagPMenuItem ptCurrent;
tagPMenuItem ptRoute[4];
tagPMenuItem ptCurBak;
tagPMenuItem pt0Level;
} MenuNavState;
#endif

View File

@@ -1,111 +0,0 @@
#include <stddef.h>
#include "menu_navigator.h"
#include "../../key.h"
MenuNavResult MenuNavigator_ProcessKey(MenuNavState *navState, uint8_t keyVal)
{
MenuNavResult result;
tagPMenuItem ptCurrent;
tagPMenuItem ptHead;
tagPMenuItem *ptRoute;
result.needRefresh = 0;
result.skipRenderThisRound = 0;
ptCurrent = navState->ptCurrent;
ptHead = navState->ptHead;
ptRoute = navState->ptRoute;
switch (keyVal)
{
case KEY_F1:
case KEY_F2:
break;
case KEY_U:
ptCurrent = ptCurrent->ptBefore;
result.needRefresh = 1;
break;
case KEY_D:
ptCurrent = ptCurrent->ptBehind;
result.needRefresh = 1;
break;
case KEY_L:
ptCurrent = ptRoute[ptCurrent->byClass - 1];
if (ptCurrent->byClass == 0)
{
if (ptCurrent->wPos == 1)
{
ptCurrent = ptCurrent->ptBefore->ptBefore;
}
else
{
ptCurrent = ptCurrent->ptBefore;
}
ptCurrent = ptCurrent->ptLower;
}
result.needRefresh = 1;
break;
case KEY_R:
case KEY_ENT:
if (ptCurrent->ptLower != NULL)
{
ptCurrent = ptCurrent->ptLower;
result.needRefresh = 1;
}
else if (ptCurrent->pfnWinProc != NULL)
{
ptCurrent->pfnWinProc();
}
break;
case KEY_ESC:
if (ptCurrent->byClass == 1)
{
navState->pt0Level = ptHead;
navState->ptRoute[0] = ptHead;
navState->ptCurrent = ptHead->ptLower;
navState->ptCurBak = navState->ptCurrent;
navState->ptRoute[1] = navState->ptCurrent;
result.skipRenderThisRound = 1;
return result;
}
ptCurrent = ptRoute[ptCurrent->byClass - 1];
result.needRefresh = 1;
break;
default:
break;
}
navState->ptCurrent = ptCurrent;
return result;
}
void MenuNavigator_RebuildRoute(MenuNavState *navState, uint32_t maxItem)
{
tagPMenuItem ptIndex;
if (navState->ptCurBak == navState->ptCurrent)
{
return;
}
ptIndex = navState->ptCurrent;
for (uint32_t index = 0; index < maxItem; index++)
{
if (ptIndex->ptHigher == NULL)
{
ptIndex = ptIndex->ptBefore;
}
else
{
ptIndex = ptIndex->ptHigher;
navState->ptRoute[ptIndex->byClass] = ptIndex;
}
if (ptIndex->byClass == 0)
{
break;
}
}
navState->ptRoute[navState->ptCurrent->byClass] = navState->ptCurrent;
}

View File

@@ -1,15 +0,0 @@
#ifndef MENU_NAVIGATOR_H
#define MENU_NAVIGATOR_H
#include "menu_nav_types.h"
typedef struct
{
uint8_t needRefresh;
uint8_t skipRenderThisRound;
} MenuNavResult;
MenuNavResult MenuNavigator_ProcessKey(MenuNavState *navState, uint8_t keyVal);
void MenuNavigator_RebuildRoute(MenuNavState *navState, uint32_t maxItem);
#endif

View File

@@ -1,176 +0,0 @@
#include "menu_presenter.h"
#include "../model/menu_model.h"
#include "menu_navigator.h"
/* -------------------------------------------------------------------------
* 函数: MenuPresenter_FillNavState
* 作用: 将“运行时控制结构 tagMenuCtrl”中的导航相关字段复制到
* “导航临时结构 MenuNavState”。
*
* 设计原因:
* - MenuNavigator_* 仅关心导航字段,不应直接操作完整控制结构;
* - 通过拷贝到轻量结构,可在 Presenter 内控制读写边界。
* ------------------------------------------------------------------------- */
static void MenuPresenter_FillNavState(const tagMenuCtrl *ctrl, MenuNavState *nav)
{
/* 根节点指针:用于 ESC 回到顶层时复位。 */
nav->ptHead = ctrl->ptHead;
/* 当前焦点节点:导航计算的主要输入。 */
nav->ptCurrent = ctrl->ptCurrent;
/* 路径第 0 级:顶层菜单路径缓存。 */
nav->ptRoute[0] = ctrl->ptRoute[0];
/* 路径第 1 级:一级菜单路径缓存。 */
nav->ptRoute[1] = ctrl->ptRoute[1];
/* 路径第 2 级:二级菜单路径缓存。 */
nav->ptRoute[2] = ctrl->ptRoute[2];
/* 路径第 3 级:三级菜单路径缓存。 */
nav->ptRoute[3] = ctrl->ptRoute[3];
/* 上一次焦点备份:用于判断是否发生焦点迁移。 */
nav->ptCurBak = ctrl->ptCurBak;
/* 当前可见的 0 级入口:用于判断是否需要全屏刷新。 */
nav->pt0Level = ctrl->pt0Level;
}
/* -------------------------------------------------------------------------
* 函数: MenuPresenter_ApplyNavState
* 作用: 将导航临时结构 MenuNavState 的结果回写到 tagMenuCtrl。
*
* 说明:
* - FillNavState + Navigator + ApplyNavState 构成一次输入处理闭环;
* - 这样 Presenter 明确“读输入状态 -> 计算 -> 写回状态”三个阶段。
* ------------------------------------------------------------------------- */
static void MenuPresenter_ApplyNavState(tagMenuCtrl *ctrl, const MenuNavState *nav)
{
/* 回写根节点(通常不变,但保持对称拷贝便于维护)。 */
ctrl->ptHead = nav->ptHead;
/* 回写当前焦点。 */
ctrl->ptCurrent = nav->ptCurrent;
/* 回写 0 级路径缓存。 */
ctrl->ptRoute[0] = nav->ptRoute[0];
/* 回写 1 级路径缓存。 */
ctrl->ptRoute[1] = nav->ptRoute[1];
/* 回写 2 级路径缓存。 */
ctrl->ptRoute[2] = nav->ptRoute[2];
/* 回写 3 级路径缓存。 */
ctrl->ptRoute[3] = nav->ptRoute[3];
/* 回写焦点备份。 */
ctrl->ptCurBak = nav->ptCurBak;
/* 回写 0 级入口指针。 */
ctrl->pt0Level = nav->pt0Level;
}
/* -------------------------------------------------------------------------
* 函数: MenuPresenter_Setup
* 作用: 完成 Presenter 的依赖注入和 View 绑定。
*
* 输入:
* - dspCtrl/menuCtrl/menuItems: 运行态状态与数据池
* - renderPort: 渲染端口抽象LCD/模拟器实现)
* - layoutConfig: 布局参数
* ------------------------------------------------------------------------- */
void MenuPresenter_Setup(MenuPresenter *presenter,
tagDspCtrl *dspCtrl,
tagMenuCtrl *menuCtrl,
tagMenuItem *menuItems,
const MenuRenderPort *renderPort,
const MenuLayoutConfig *layoutConfig)
{
/* 保存显示控制状态句柄。 */
presenter->dspCtrl = dspCtrl;
/* 保存菜单控制状态句柄。 */
presenter->menuCtrl = menuCtrl;
/* 保存菜单节点池句柄。 */
presenter->menuItems = menuItems;
/* 保存布局配置句柄。 */
presenter->layoutConfig = layoutConfig;
/* 初始化 View并注入具体渲染端口。 */
MenuView_Init(&presenter->view, renderPort);
}
/* -------------------------------------------------------------------------
* 函数: MenuPresenter_InitModel
* 作用: 启动时初始化模型与布局。
*
* 步骤:
* 1) 设置首帧标志,确保首次进入能触发完整绘制;
* 2) 通过 Model 完成菜单树构建与初始路由;
* 3) 通知 View 计算每个节点的布局坐标。
* ------------------------------------------------------------------------- */
void MenuPresenter_InitModel(MenuPresenter *presenter, const tagMenuModel *modelTab, uint32_t modelCount)
{
/* 标记“首帧未绘制”,后续输入循环会先触发一次刷新。 */
presenter->dspCtrl->bFirst = 1;
/* 构建运行时菜单树并初始化控制状态。 */
MenuModel_Bootstrap(presenter->menuCtrl,
presenter->menuItems,
modelTab,
modelCount);
/* 根据模型结果计算显示矩形位置。 */
MenuView_Layout(&presenter->view, presenter->menuCtrl, presenter->layoutConfig);
}
/* -------------------------------------------------------------------------
* 函数: MenuPresenter_Refresh
* 作用: 统一刷新入口,把“当前状态”交给 View 决定具体渲染策略。
* ------------------------------------------------------------------------- */
void MenuPresenter_Refresh(MenuPresenter *presenter)
{
/* View 根据 bFirst / ptCurBak / route 差异决定增量或全量刷新。 */
MenuView_RenderByState(&presenter->view, presenter->menuCtrl, presenter->dspCtrl);
}
/* -------------------------------------------------------------------------
* 函数: MenuPresenter_HandleInput
* 作用: 处理一次输入事件(按键),驱动导航状态迁移与视图刷新。
*
* 核心流程:
* 1) 首帧短路:先渲染再返回;
* 2) 拷贝导航状态 -> 调用导航器 -> 回写结果;
* 3) 按导航结果决定是否跳过渲染、是否重建路径并刷新。
* ------------------------------------------------------------------------- */
void MenuPresenter_HandleInput(MenuPresenter *presenter, uint8_t keyVal)
{
/* 本轮输入使用的导航状态快照。 */
MenuNavState navState;
/* 导航器输出:是否需要刷新/是否跳过本轮渲染。 */
MenuNavResult navResult;
/* 若仍是首帧阶段:优先做一次渲染,不消费导航逻辑。 */
if (presenter->dspCtrl->bFirst)
{
/* 执行首帧刷新。 */
MenuPresenter_Refresh(presenter);
/* 首帧路径到此结束。 */
return;
}
/* 从控制结构提取导航子集,作为导航器输入。 */
MenuPresenter_FillNavState(presenter->menuCtrl, &navState);
/* 基于按键执行导航状态机,得到状态变化与渲染建议。 */
navResult = MenuNavigator_ProcessKey(&navState, keyVal);
/* 将导航器结果回写到运行时控制结构。 */
MenuPresenter_ApplyNavState(presenter->menuCtrl, &navState);
/* 某些按键(如特定 ESC 场景)要求本轮跳过渲染。 */
if (navResult.skipRenderThisRound)
{
/* 按导航器要求直接返回。 */
return;
}
/* 只有导航器明确要求刷新时,才进行路径重建与渲染。 */
if (navResult.needRefresh)
{
/* 用于“自底向上”重建 route[] 的临时状态。 */
MenuNavState rebuildState;
/* 以最新控制状态为基准生成重建输入。 */
MenuPresenter_FillNavState(presenter->menuCtrl, &rebuildState);
/* 重建各级 route 缓存,保证后续渲染路径一致。 */
MenuNavigator_RebuildRoute(&rebuildState, MENU_MAX_ITEM);
/* 回写重建后的 route 与相关导航状态。 */
MenuPresenter_ApplyNavState(presenter->menuCtrl, &rebuildState);
/* 触发刷新,让界面体现本次输入结果。 */
MenuPresenter_Refresh(presenter);
}
}

View File

@@ -1,30 +0,0 @@
#ifndef MENU_PRESENTER_H
#define MENU_PRESENTER_H
#include "../model/display.h"
#include "../view/menu_layout.h"
#include "../view/menu_render_port.h"
#include "../common/menu_state_types.h"
#include "../view/menu_view.h"
typedef struct
{
tagDspCtrl *dspCtrl;
tagMenuCtrl *menuCtrl;
tagMenuItem *menuItems;
MenuView view;
const MenuLayoutConfig *layoutConfig;
} MenuPresenter;
void MenuPresenter_Setup(MenuPresenter *presenter,
tagDspCtrl *dspCtrl,
tagMenuCtrl *menuCtrl,
tagMenuItem *menuItems,
const MenuRenderPort *renderPort,
const MenuLayoutConfig *layoutConfig);
void MenuPresenter_InitModel(MenuPresenter *presenter, const tagMenuModel *modelTab, uint32_t modelCount);
void MenuPresenter_HandleInput(MenuPresenter *presenter, uint8_t keyVal);
void MenuPresenter_Refresh(MenuPresenter *presenter);
#endif

View File

@@ -1,447 +0,0 @@
#include <stddef.h>
#include "menu_layout.h"
#include "utf8.h"
/******************************************************************************
* 模块: menu_layout.c
* 职责: 根据菜单树结构计算各级菜单在 LCD 上的显示矩形区域
*
* 设计要点:
* 1) 采用“先算当前层,再递归/级联算下一层”的方式布点;
* 2) 每级菜单都维护:
* - ptFirst[level]: 当前层循环链表的起点;
* - ptIndex[level]: 当前层遍历游标;
* - byMenuNum[level]: 当前层项数量(或当前位置计数);
* 3) 对 Y 方向越界提供多级回退策略(向上翻转/贴顶重排),尽量保证菜单可见。
*****************************************************************************/
/* 读取菜单节点导航指针集合(本文件局部辅助,避免 core 泄漏行为函数)。 */
static MenuLinks MenuLayout_GetLinks(const MenuItem *item)
{
MenuLinks links;
links.higher = item->ptHigher;
links.lower = item->ptLower;
links.before = item->ptBefore;
links.behind = item->ptBehind;
return links;
}
/* 读取菜单节点矩形(本文件局部辅助)。 */
static MenuRect MenuLayout_GetRect(const MenuItem *item)
{
MenuRect rect;
rect.wSPosX = item->wSPosX;
rect.wSPosY = item->wSPosY;
rect.wEPosX = item->wEPosX;
rect.wEPosY = item->wEPosY;
return rect;
}
/* 给菜单项写入矩形坐标的轻量封装,避免到处重复构造 MenuRect。 */
static void MenuLayout_SetRect(MenuItem *item, uint16_t sx, uint16_t sy, uint16_t ex, uint16_t ey)
{
item->wSPosX = sx;
item->wSPosY = sy;
item->wEPosX = ex;
item->wEPosY = ey;
}
/* 将理论挂接行压缩映射到可视区,避免超出可显示行数。 */
static uint16_t MenuLayout_MapMenuPos(uint16_t menuPos, uint8_t itemNum, uint8_t maxNum)
{
if ((itemNum > maxNum) && (menuPos >= maxNum))
{
return (uint16_t)(maxNum - (itemNum - menuPos));
}
return menuPos;
}
/* 子菜单 X 方向统一规则:从父框右边展开。 */
static void MenuLayout_CalcSubMenuX(const MenuRect *parentRect,
uint8_t maxLen,
const MenuLayoutConfig *config,
uint16_t *startX,
uint16_t *endX)
{
*startX = parentRect->wEPosX;
*endX = (uint16_t)(*startX + maxLen * config->menuWidth + config->menuXAdd);
}
/* 尝试将菜单框整体翻转到上方显示;成功返回 1失败返回 0。 */
static uint8_t MenuLayout_TryFlipUp(uint16_t *startY,
uint16_t *endY,
uint8_t itemNum,
uint16_t flipMinY,
const MenuLayoutConfig *config)
{
uint16_t candidateEndY;
uint16_t temp;
candidateEndY = (uint16_t)(*startY - (itemNum - 1) * config->lineHeight - config->menuYAdd);
if ((candidateEndY > flipMinY) && (candidateEndY < config->menuYMax))
{
temp = *startY;
*startY = candidateEndY;
*endY = (uint16_t)(temp + config->lineHeight);
return 1;
}
return 0;
}
/* Sub2 兜底:回退到顶层起始 Y并在底部做截断。 */
static void MenuLayout_FallbackClampToTop(const tagPMenuItem *ptIndex,
uint8_t itemNum,
uint16_t *startY,
uint16_t *endY,
const MenuLayoutConfig *config)
{
*startY = ptIndex[0]->wSPosY;
*endY = (uint16_t)(*startY + itemNum * config->lineHeight + config->menuYAdd);
if (*endY > config->menuYMax)
{
*endY = config->menuYMax;
}
}
/* Sub1 兜底:从顶层起始 Y 逐行上探,直到菜单框不越界。 */
static void MenuLayout_FallbackProbeUp(const tagPMenuItem *ptIndex,
uint8_t itemNum,
uint16_t *startY,
uint16_t *endY,
const MenuLayoutConfig *config)
{
*startY = ptIndex[0]->wSPosY;
*endY = (uint16_t)(*startY + itemNum * config->lineHeight + config->menuYAdd);
for (uint16_t i = 1; i < (uint16_t)(ptIndex[0]->wSPosY / config->lineHeight + 1); i++)
{
if (*endY > config->menuYMax)
{
*startY = (uint16_t)(ptIndex[0]->wSPosY - config->lineHeight * i);
if (*startY < config->lineHeight)
{
*startY = 0;
}
*endY = (uint16_t)(*startY + itemNum * config->lineHeight + config->menuYAdd);
}
else
{
break;
}
}
}
/* 计算单个菜单项“显示宽度”:
* - 中文等多字节字符按 2 列计宽ASCII 按 1 列;
* - 若该项有下级菜单且名称末尾未带特殊标记 '\x10',额外预留 1 列用于层级指示。 */
static uint8_t MenuLayout_ItemDisplayLen(const MenuItem *item)
{
uint8_t displayLen;
displayLen = MenuLayout_Utf8LenCal((uint8_t *)item->byName);
if (item->ptLower != NULL)
{
uint8_t byteLen = 0;
while ((byteLen < 50) && (item->byName[byteLen] != '\0'))
{
byteLen++;
}
if ((byteLen == 0) || (item->byName[byteLen - 1] != '\x10'))
{
displayLen += 1;
}
}
return displayLen;
}
/* UTF-8 显示长度统计:
* utf8_next() 每次返回一个 Unicode 字符占用的字节数 n
* 约定 n>1 视为“宽字符”(占 2 列),否则占 1 列。 */
uint8_t MenuLayout_Utf8LenCal(uint8_t *str)
{
uint8_t strLen = 0;
uint32_t unicode;
uint8_t index = 0;
uint8_t n = 0;
while (str[index] != '\0')
{
n = utf8_next(str + index, &unicode);
strLen += (n > 1) ? 2 : 1;
index += n;
}
return strLen;
}
/******************************************************************************
* 函数: MenuLayout_CharLenCal
* 作用:
* 1) 统计 bylevel 下一层bylevel+1菜单项总数
* 2) 计算该层中最长显示宽度(用于决定菜单框宽度);
* 3) 为该层每个节点写 wPos从 1 开始的位置序号);
* 4) 把“下一层项数”写回父节点 wNum。
*
* 变量字典:
* - bylevel: 输入为父层级,函数内部会自增到“子层级”;
* - byMenuNum[level]: level 层累计项数(同时作为 wPos 的来源);
* - ptFirst[level]: level 层循环链表的起点节点;
* - ptIndex[level]: level 层当前遍历节点;
* - byMaxLen: 本层所有项显示宽度的最大值(单位: 字符列)。
*****************************************************************************/
uint8_t MenuLayout_CharLenCal(uint8_t bylevel, uint8_t *byMenuNum, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex)
{
uint8_t displayLen;
uint8_t byMaxLen = 0;
MenuLinks links;
/* 进入下一层,从父节点的 lower 作为该层环形链表起点。 */
ptFirst[bylevel + 1] = ptIndex[bylevel]->ptLower;
bylevel = bylevel + 1;
ptIndex[bylevel] = ptFirst[bylevel];
byMenuNum[bylevel] = 1;
ptIndex[bylevel]->wPos = 1;
/* 固定上限 300 作为保护,避免链表异常导致死循环。 */
for (uint16_t wLoop = 0; wLoop < 300; wLoop++)
{
links = MenuLayout_GetLinks(ptIndex[bylevel]);
displayLen = MenuLayout_ItemDisplayLen(ptIndex[bylevel]);
if (byMaxLen < displayLen)
{
byMaxLen = displayLen;
}
ptIndex[bylevel] = links.behind;
if (ptIndex[bylevel] == ptFirst[bylevel])
{
break;
}
byMenuNum[bylevel]++;
ptIndex[bylevel]->wPos = byMenuNum[bylevel];
}
/* 父节点记录子项数量,供分页/显示逻辑复用。 */
ptIndex[bylevel - 1]->wNum = byMenuNum[bylevel];
return byMaxLen;
}
/******************************************************************************
* 函数: MenuLayout_Sub2PosCal
* 作用: 计算二级子菜单(及其同层兄弟)的显示矩形
* 策略:
* - 先按“父项对齐”得到理想 Y
* - 若底部越界,尝试向上翻转;
* - 仍不满足则使用贴近顶层菜单的兜底摆放。
*
* 变量字典:
* - byMaxNum: 当前配置下可完整显示的最大行数;
* - byMenuPos: 当前菜单相对父菜单的挂接行(理论位置);
* - byItemNum: 当前层或下一层项数(代码中按阶段复用);
* - wSPosY/wEPosY: 菜单框起止 Y 坐标(含间距边界);
* - parentRect: 父菜单矩形,用于对子菜单做相对定位。
*
* 关键公式:
* - X 起点 = parentRect.wEPosX总是从父框右侧展开
* - X 终点 = X 起点 + byMaxLen * menuWidth + menuXAdd
* - Y 起点 = parentRect.wSPosY + (byMenuPos - 1) * lineHeight
* - Y 终点 = Y 起点 + 子项数 * lineHeight + menuYAdd。
*****************************************************************************/
void MenuLayout_Sub2PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config)
{
uint16_t wSPosY;
uint16_t wEPosY;
uint8_t byMaxLen;
uint8_t byMaxNum;
uint16_t byMenuPos;
uint8_t byItemNum;
uint8_t byMenuNum[4];
MenuLinks links;
MenuRect parentRect;
/* 当前可完整容纳的最大行数(预留边框/间距)。 */
byMaxNum = (config->menuYMax - config->menuYMin - 6) / config->lineHeight;
ptIndex[bylevel] = ptFirst[bylevel];
for (uint16_t wLoop = 0; wLoop < 300; wLoop++)
{
links = MenuLayout_GetLinks(ptIndex[bylevel]);
if (links.lower != NULL)
{
byMaxLen = MenuLayout_CharLenCal(bylevel, byMenuNum, ptFirst, ptIndex);
parentRect = MenuLayout_GetRect(ptIndex[bylevel - 1]);
MenuLayout_CalcSubMenuX(&parentRect,
byMaxLen,
config,
&ptIndex[bylevel]->wSPosX,
&ptIndex[bylevel]->wEPosX);
/* byMenuPos: 当前项在同层中的理论挂接位置(必要时压缩映射)。 */
byMenuPos = ptIndex[bylevel]->wPos;
byItemNum = byMenuNum[bylevel];
byMenuPos = MenuLayout_MapMenuPos(byMenuPos, byItemNum, byMaxNum);
wSPosY = parentRect.wSPosY + (byMenuPos - 1) * config->lineHeight;
byItemNum = byMenuNum[bylevel + 1];
wEPosY = wSPosY + byItemNum * config->lineHeight + config->menuYAdd;
if (wEPosY < config->menuYMax)
{
/* 理想位置不越界,直接应用。 */
MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY);
}
else
{
/* 底部越界:先尝试“向上翻转”放置整个菜单框。 */
if (!MenuLayout_TryFlipUp(&wSPosY, &wEPosY, byItemNum, config->menuYMin, config))
{
/* 仍不满足:回退到顶层菜单起始 Y再做截断兜底。 */
MenuLayout_FallbackClampToTop(ptIndex, byItemNum, &wSPosY, &wEPosY, config);
}
MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY);
}
}
ptIndex[bylevel] = links.behind;
if (ptIndex[bylevel] == ptFirst[bylevel])
{
break;
}
}
}
/******************************************************************************
* 函数: MenuLayout_Sub1PosCal
* 作用: 计算一级子菜单(通常是主菜单下拉)的位置,并联动计算二级子菜单
* 说明: 与 Sub2 逻辑相近,但在兜底时会逐步向上平移,直到菜单框可容纳。
*
* 变量字典:
* - byMaxNum: 屏幕当前可显示的最大菜单行数;
* - byMenuPos: 当前节点理论挂接行(可能被映射压缩);
* - byItemNum: 当前下拉框条目数;
* - wTemp: 用于“翻转布局”时交换起止 Y
* - byMenuNum[4]: 分层统计数组,索引与菜单层级一一对应。
*
* 与 Sub2 的核心差异:
* - Sub1 兜底策略增加“逐行上探”,尝试找到不越界的最靠下位置,
* 视觉上更适合主菜单下拉场景。
*****************************************************************************/
void MenuLayout_Sub1PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config)
{
uint16_t wSPosY;
uint16_t wEPosY;
uint8_t byMenuNum[4];
uint8_t byMaxLen;
uint8_t byMaxNum;
uint16_t byMenuPos;
uint8_t byItemNum;
MenuLinks links;
MenuRect parentRect;
byMaxNum = config->menuYMax / config->lineHeight;
ptIndex[bylevel] = ptFirst[bylevel];
for (uint16_t wLoop = 0; wLoop < 300; wLoop++)
{
links = MenuLayout_GetLinks(ptIndex[bylevel]);
if (links.lower != NULL)
{
byMaxLen = MenuLayout_CharLenCal(bylevel, byMenuNum, ptFirst, ptIndex);
parentRect = MenuLayout_GetRect(ptIndex[bylevel - 1]);
MenuLayout_CalcSubMenuX(&parentRect,
byMaxLen,
config,
&ptIndex[bylevel]->wSPosX,
&ptIndex[bylevel]->wEPosX);
byMenuPos = ptIndex[bylevel]->wPos;
byItemNum = byMenuNum[bylevel];
byMenuPos = MenuLayout_MapMenuPos(byMenuPos, byItemNum, byMaxNum);
wSPosY = parentRect.wSPosY + (byMenuPos - 1) * config->lineHeight;
byItemNum = byMenuNum[bylevel + 1];
wEPosY = wSPosY + byItemNum * config->lineHeight + config->menuYAdd;
if (wEPosY < config->menuYMax)
{
MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY);
}
else
{
/* 底部越界:优先尝试一次整体翻转到上方。 */
if (!MenuLayout_TryFlipUp(&wSPosY, &wEPosY, byItemNum, config->lineHeight, config))
{
/* 二次兜底:从顶层菜单起始 Y 往上逐行试探,直到不越界。 */
MenuLayout_FallbackProbeUp(ptIndex, byItemNum, &wSPosY, &wEPosY, config);
}
MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY);
}
/* 一级菜单定位完成后,继续计算二级菜单。 */
MenuLayout_Sub2PosCal(2, ptFirst, ptIndex, config);
}
ptIndex[bylevel] = links.behind;
if (ptIndex[bylevel] == ptFirst[bylevel])
{
break;
}
}
}
/******************************************************************************
* 函数: MenuLayout_PositionCal
* 作用: 入口函数,计算从 0 级到 2 级菜单的显示坐标
* 流程:
* 1) 先给 0 级菜单按屏宽等分;
* 2) 每个 0 级项若有下级,则计算其下拉框;
* 3) 再级联计算 1/2 级子菜单。
*
* 变量字典:
* - by0LevelNum: 顶层菜单数量;
* - byInterval: 顶层菜单等分后的列宽;
* - byMenuNum[0]: 当前顶层节点在同层中的序号(从 1 开始);
* - byMenuNum[1]: 顶层节点的直接子项数量(由 CharLenCal 写入);
* - ptFirst/ptIndex: 各层遍历上下文,在本函数中作为全流程共享状态。
*****************************************************************************/
void MenuLayout_PositionCal(tagPMenuItem ptMenuHead, uint8_t by0LevelNum, const MenuLayoutConfig *config)
{
tagPMenuItem ptFirst[4];
tagPMenuItem ptIndex[4];
uint8_t byMenuNum[4];
uint8_t byMaxLen;
uint8_t byInterval;
MenuLinks links;
/* 输入保护:头指针/配置为空,或 0 级菜单数量为 0 时直接返回。 */
if ((ptMenuHead == NULL) || (config == NULL) || (by0LevelNum == 0))
{
return;
}
ptFirst[0] = ptMenuHead;
ptIndex[0] = ptFirst[0];
byMenuNum[0] = 1;
ptIndex[0]->wPos = 1;
/* 顶层菜单按等分宽度布局。 */
byInterval = config->lcdSizeX / by0LevelNum;
for (uint16_t wLoop = 0; wLoop < by0LevelNum; wLoop++)
{
links = MenuLayout_GetLinks(ptIndex[0]);
if (links.lower != NULL)
{
/* 先统计该分支子项宽高需求,再写入 0 级菜单框。 */
byMaxLen = MenuLayout_CharLenCal(0, byMenuNum, ptFirst, ptIndex);
MenuLayout_SetRect(
ptIndex[0],
(byMenuNum[0] - 1) * byInterval,
config->lcdSizeY - config->lineHeight - byMenuNum[1] * config->lineHeight - config->menuYAdd,
(byMenuNum[0] - 1) * byInterval + byMaxLen * config->menuWidth + config->menuXAdd,
config->lcdSizeY - config->lineHeight);
/* 0 级完成后继续展开 1/2 级。 */
MenuLayout_Sub1PosCal(1, ptFirst, ptIndex, config);
}
ptIndex[0] = links.behind;
if (ptIndex[0] == ptFirst[0])
{
break;
}
byMenuNum[0]++;
ptIndex[0]->wPos = byMenuNum[0];
}
}

View File

@@ -1,34 +0,0 @@
#ifndef MENU_LAYOUT_H
#define MENU_LAYOUT_H
#include "../common/menu_item_types.h"
#define CN_HEIGHT 12
#define CN_ROWSPACE 2
#define LINE_HEIGHT (CN_HEIGHT + CN_ROWSPACE)
#define MENU_XADD 4
#define MENU_YADD 4
#define MENU_WITDTH 7
typedef struct
{
uint16_t lcdSizeX;
uint16_t lcdSizeY;
uint16_t menuYMin;
uint16_t menuYMax;
uint16_t lineHeight;
uint16_t menuWidth;
uint16_t menuXAdd;
uint16_t menuYAdd;
} MenuLayoutConfig;
#define MENU_YMIN 0
#define MENU_YMAX_FROM_LCD(ySize) ((uint16_t)((ySize) - 12U))
uint8_t MenuLayout_Utf8LenCal(uint8_t *str);
uint8_t MenuLayout_CharLenCal(uint8_t bylevel, uint8_t *byMenuNum, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex);
void MenuLayout_Sub2PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config);
void MenuLayout_Sub1PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config);
void MenuLayout_PositionCal(tagPMenuItem ptMenuHead, uint8_t by0LevelNum, const MenuLayoutConfig *config);
#endif

View File

@@ -1,45 +0,0 @@
#include "menu_render_port.h"
#include "lcd.h"
#include "lcd_draw.h"
#include "lcd_text.h"
static unsigned short MenuRenderPort_LcdSizeX(void)
{
return LCD_SIZE_X;
}
static unsigned short MenuRenderPort_LcdSizeY(void)
{
return LCD_SIZE_Y;
}
static unsigned char MenuRenderPort_LcdColorFont(void)
{
return LCD_FONT;
}
static unsigned char MenuRenderPort_LcdColorBack(void)
{
return LCD_BACK;
}
static const MenuRenderPort g_lcd_port = {
.init = Lcd_Init,
.get_size_x = MenuRenderPort_LcdSizeX,
.get_size_y = MenuRenderPort_LcdSizeY,
.get_color_font = MenuRenderPort_LcdColorFont,
.get_color_back = MenuRenderPort_LcdColorBack,
.fill_rect = Lcd_FillRect,
.line_h = Lcd_LineH,
.line_v = Lcd_LineV,
.line = Lcd_Line,
.set_pixel = Lcd_SetPixel,
.invert = Lcd_Invert,
.show_str = Lcd_ShowStr,
};
const MenuRenderPort *MenuRenderPort_Lcd(void)
{
return &g_lcd_port;
}

View File

@@ -1,114 +0,0 @@
#include "menu_view.h"
static void MenuView_DrawMeitou(MenuView *view, uint16_t yStart, uint16_t width)
{
uint8_t fontColor = view->port->get_color_font();
view->port->line_h(16, 144, yStart, width, fontColor);
view->port->line(8, yStart - 8, 16, yStart, width, fontColor);
view->port->line(144, yStart, 152, yStart - 8, width, fontColor);
}
static void MenuView_DrawBoundaryBox(MenuView *view, uint16_t leftX, uint16_t topY, uint16_t rightX, uint16_t bottomY)
{
uint8_t backColor = view->port->get_color_back();
uint8_t fontColor = view->port->get_color_font();
for (uint16_t y = topY; y < bottomY; y++)
{
for (uint16_t x = leftX; x < rightX; x++)
{
view->port->set_pixel(x, y, backColor);
}
}
view->port->line_h(leftX, rightX, topY, 1, fontColor);
view->port->line_v(topY, bottomY, leftX, 1, fontColor);
view->port->line_h(leftX, rightX + 1, bottomY, 1, fontColor);
view->port->line_v(topY, bottomY, rightX, 1, fontColor);
}
static void MenuView_ShowOtherLevel(MenuView *view, const tagMenuCtrl *menuCtrl, uint8_t level)
{
tagPMenuItem ptIndex = menuCtrl->ptRoute[level];
tagPMenuItem ptRoute;
uint16_t wPosX;
uint16_t wPosY;
MenuView_DrawBoundaryBox(view, ptIndex->wSPosX, ptIndex->wSPosY, ptIndex->wEPosX, ptIndex->wEPosY);
ptRoute = menuCtrl->ptRoute[level + 1];
ptIndex = ptRoute;
wPosX = menuCtrl->ptRoute[level]->wSPosX + 4;
for (uint16_t index = 0; index < menuCtrl->ptRoute[level]->wNum; index++)
{
wPosY = menuCtrl->ptRoute[level]->wSPosY + (ptIndex->wPos - 1) * LINE_HEIGHT + 3;
view->port->show_str(wPosX, wPosY, ptIndex->byName);
if (ptRoute == ptIndex)
{
view->port->invert(menuCtrl->ptRoute[level]->wSPosX + 2, wPosY - 1, menuCtrl->ptRoute[level]->wEPosX - 2, wPosY + 14);
}
ptIndex = ptIndex->ptBehind;
}
}
static void MenuView_ShowTopLevel(MenuView *view)
{
uint16_t lcdSizeX = view->port->get_size_x();
view->port->fill_rect(0, 0, lcdSizeX - 1, 32, view->port->get_color_back());
MenuView_DrawMeitou(view, 16, 2);
view->port->show_str(16, 20, (uint8_t *)"当前模式: 无模式");
}
void MenuView_Init(MenuView *view, const MenuRenderPort *renderPort)
{
view->port = renderPort;
}
void MenuView_Layout(MenuView *view, tagMenuCtrl *menuCtrl, const MenuLayoutConfig *config)
{
(void)view;
MenuLayout_PositionCal(menuCtrl->ptHead, menuCtrl->by0LevelNum, config);
}
void MenuView_RenderByState(MenuView *view, tagMenuCtrl *menuCtrl, tagDspCtrl *dspCtrl)
{
uint8_t needFullRefresh = 0;
uint8_t backColor = view->port->get_color_back();
uint16_t lcdSizeX = view->port->get_size_x();
uint16_t menuYMax = MENU_YMAX_FROM_LCD(view->port->get_size_y());
if (dspCtrl->bFirst)
{
dspCtrl->bFirst = 0;
needFullRefresh = 1;
}
if (menuCtrl->pt0Level != menuCtrl->ptRoute[0])
{
needFullRefresh = 1;
view->port->fill_rect(0, MENU_YMIN, lcdSizeX - 1, menuYMax, backColor);
menuCtrl->pt0Level = menuCtrl->ptRoute[0];
menuCtrl->ptCurBak = menuCtrl->ptCurrent;
}
else if (menuCtrl->ptCurBak != menuCtrl->ptCurrent)
{
if (menuCtrl->ptCurrent->byClass >= menuCtrl->ptCurBak->byClass)
{
MenuView_ShowOtherLevel(view, menuCtrl, menuCtrl->ptCurrent->byClass - 1);
}
else
{
view->port->fill_rect(0, MENU_YMIN, lcdSizeX - 1, menuYMax, backColor);
needFullRefresh = 1;
}
menuCtrl->ptCurBak = menuCtrl->ptCurrent;
}
if (needFullRefresh > 0)
{
MenuView_ShowTopLevel(view);
for (uint8_t index = 0; index < menuCtrl->ptCurrent->byClass; index++)
{
MenuView_ShowOtherLevel(view, menuCtrl, index);
}
}
}

View File

@@ -1,17 +0,0 @@
#ifndef MENU_VIEW_H
#define MENU_VIEW_H
#include "menu_layout.h"
#include "menu_render_port.h"
#include "../common/menu_state_types.h"
typedef struct
{
const MenuRenderPort *port;
} MenuView;
void MenuView_Init(MenuView *view, const MenuRenderPort *renderPort);
void MenuView_Layout(MenuView *view, tagMenuCtrl *menuCtrl, const MenuLayoutConfig *config);
void MenuView_RenderByState(MenuView *view, tagMenuCtrl *menuCtrl, tagDspCtrl *dspCtrl);
#endif

View File

@@ -0,0 +1,85 @@
#include <string.h>
#include "global_state.h"
static GlobalModel s_globalModel;
static void GlobalModel_CopyText(uint8_t *dst, uint16_t dstSize, const uint8_t *src)
{
uint16_t i;
if ((dst == NULL) || (dstSize == 0))
{
return;
}
if (src == NULL)
{
dst[0] = '\0';
return;
}
for (i = 0; i < (uint16_t)(dstSize - 1); i++)
{
dst[i] = src[i];
if (src[i] == '\0')
{
return;
}
}
dst[dstSize - 1] = '\0';
}
void GlobalModel_Init(void)
{
memset(&s_globalModel, 0, sizeof(s_globalModel));
}
GlobalModel *GlobalModel_Instance(void)
{
return &s_globalModel;
}
void GlobalModel_SetActiveYcType(uint16_t ycType)
{
s_globalModel.activeYcType = ycType;
}
uint16_t GlobalModel_GetActiveYcType(void)
{
return s_globalModel.activeYcType;
}
void GlobalModel_SetMenuAction(uint16_t actionId,
const uint8_t *menuName,
const uint8_t *menuTip,
uint16_t menuPara)
{
s_globalModel.activeActionId = actionId;
s_globalModel.activeMenuPara = menuPara;
GlobalModel_CopyText(s_globalModel.activeMenuName,
(uint16_t)sizeof(s_globalModel.activeMenuName),
menuName);
GlobalModel_CopyText(s_globalModel.activeMenuTip,
(uint16_t)sizeof(s_globalModel.activeMenuTip),
menuTip);
}
uint16_t GlobalModel_GetMenuActionId(void)
{
return s_globalModel.activeActionId;
}
uint16_t GlobalModel_GetMenuActionPara(void)
{
return s_globalModel.activeMenuPara;
}
const uint8_t *GlobalModel_GetMenuActionName(void)
{
return s_globalModel.activeMenuName;
}
const uint8_t *GlobalModel_GetMenuActionTip(void)
{
return s_globalModel.activeMenuTip;
}

View File

@@ -0,0 +1,71 @@
#ifndef PAGE_GLOBAL_STATE_H
#define PAGE_GLOBAL_STATE_H
#include "types.h"
typedef enum
{
MENU_ACTION_NONE = 0,
MENU_ACTION_SET_VALUE,
MENU_ACTION_SET_SOFT,
MENU_ACTION_CFG_TIME,
MENU_ACTION_CFG_REV_EVENT,
MENU_ACTION_CFG_MANUAL_WAVE,
MENU_ACTION_SEE_INPUT,
MENU_ACTION_SEE_SET,
MENU_ACTION_SEE_SOFT,
MENU_ACTION_YX_SET_COMM_TYPE,
MENU_ACTION_YX_SET_WIDTH,
MENU_ACTION_YX_SET_TWIN,
MENU_ACTION_ALLINF_DEFAULT,
MENU_ACTION_PARA_DEFAULT,
MENU_ACTION_SET_DEFAULT,
MENU_ACTION_RESUME_SOFT,
MENU_ACTION_CFG_CELL_DEF,
MENU_ACTION_CFG_SHOW_ANA_TYPE,
MENU_ACTION_DBG_XU_YX,
MENU_ACTION_DBG_XU_YC,
MENU_ACTION_DBG_XU_EVENT,
MENU_ACTION_DBG_RELAY,
MENU_ACTION_SEE_VERSION_BOARD,
MENU_ACTION_CFG_CLR_REC,
MENU_ACTION_CFG_COM_PARA,
MENU_ACTION_CFG_EDIT_IP,
MENU_ACTION_CFG_EDIT_SNTP,
MENU_ACTION_CFG_CELL_CONF,
MENU_ACTION_SEE_REC_SOE,
MENU_ACTION_SEE_REC_ACT,
MENU_ACTION_SEE_REC_OPT,
MENU_ACTION_SEE_REC_ALM,
MENU_ACTION_SEE_REC_START,
MENU_ACTION_SEE_REC_YK,
MENU_ACTION_SEE_REC_CHK,
MENU_ACTION_SEE_REC_RUN,
MENU_ACTION_SEE_REC_FAULT,
MENU_ACTION_YC_SET_SQ_VALUE,
MENU_ACTION_YC_SET_ADJ_COE
} menu_action_id_t;
typedef struct
{
uint16_t activeYcType;
uint16_t activeMenuPara;
uint16_t activeActionId;
uint8_t activeMenuName[50];
uint8_t activeMenuTip[50];
} GlobalModel;
void GlobalModel_Init(void);
GlobalModel *GlobalModel_Instance(void);
void GlobalModel_SetActiveYcType(uint16_t ycType);
uint16_t GlobalModel_GetActiveYcType(void);
void GlobalModel_SetMenuAction(uint16_t actionId,
const uint8_t *menuName,
const uint8_t *menuTip,
uint16_t menuPara);
uint16_t GlobalModel_GetMenuActionId(void);
uint16_t GlobalModel_GetMenuActionPara(void);
const uint8_t *GlobalModel_GetMenuActionName(void);
const uint8_t *GlobalModel_GetMenuActionTip(void);
#endif

View File

@@ -0,0 +1,44 @@
#include "renderer_lcd.h"
#include "lcd.h"
#include "lcd_draw.h"
#include "lcd_text.h"
static unsigned short PageRenderer_LcdSizeX(void)
{
return LCD_SIZE_X;
}
static unsigned short PageRenderer_LcdSizeY(void)
{
return LCD_SIZE_Y;
}
static unsigned char PageRenderer_LcdColorFont(void)
{
return LCD_FONT;
}
static unsigned char PageRenderer_LcdColorBack(void)
{
return LCD_BACK;
}
static const PageRenderPort g_lcd_port = {
.get_size_x = PageRenderer_LcdSizeX,
.get_size_y = PageRenderer_LcdSizeY,
.get_color_font = PageRenderer_LcdColorFont,
.get_color_back = PageRenderer_LcdColorBack,
.fill_rect = Lcd_FillRect,
.line_h = Lcd_LineH,
.line_v = Lcd_LineV,
.line = Lcd_Line,
.set_pixel = Lcd_SetPixel,
.invert = Lcd_Invert,
.show_str = Lcd_ShowStr,
};
const PageRenderPort *PageRenderer_Lcd(void)
{
return &g_lcd_port;
}

View File

@@ -1,9 +1,8 @@
#ifndef MENU_RENDER_PORT_H
#define MENU_RENDER_PORT_H
#ifndef PAGE_RENDERER_LCD_H
#define PAGE_RENDERER_LCD_H
typedef struct
{
void (*init)(void);
unsigned short (*get_size_x)(void);
unsigned short (*get_size_y)(void);
unsigned char (*get_color_font)(void);
@@ -15,8 +14,8 @@ typedef struct
signed char (*set_pixel)(unsigned short x, unsigned short y, unsigned char color);
signed char (*invert)(unsigned short left_x, unsigned short top_y, unsigned short right_x, unsigned short bottom_y);
signed char (*show_str)(unsigned short x, unsigned short y, unsigned char *text);
} MenuRenderPort;
} PageRenderPort;
const MenuRenderPort *MenuRenderPort_Lcd(void);
const PageRenderPort *PageRenderer_Lcd(void);
#endif

145
src/Drv/pages/menu/def.h Normal file
View File

@@ -0,0 +1,145 @@
#ifndef MENU_def_H
#define MENU_def_H
/* layout constants are unified into menu_view */
#define MENU_SIZE_Y (160)
#define MENU_SIZE_X (160)
#define CN_HEIGHT 12
#define CN_ROWSPACE 2
#define LINE_HEIGHT (CN_HEIGHT + CN_ROWSPACE)
#define MENU_XADD 4
#define MENU_YADD 4
#define MENU_WITDTH 7
#define MENU_YMIN 0
#define MENU_YMAX (MENU_SIZE_Y - CN_HEIGHT)
/* -------------------------------------------------------------------------
* 枚举名: MenuMode
* 功能:
* 定义菜单页顶部状态栏显示所使用的运行模式。
*
* 枚举值说明:
* MODE_NONE - 无模式/默认模式
* MODE_OVERFLOW_PROTECTION - 过流保护模式
* MODE_LOCAL_FEEDER_SEGMENT - 就地馈线分段模式
* MODE_LOCAL_FEEDER_CONTACT - 就地馈线联络模式
* ------------------------------------------------------------------------- */
typedef enum
{
MODE_NONE = 0,
MODE_OVERFLOW_PROTECTION = 1,
MODE_LOCAL_FEEDER_SEGMENT = 2,
MODE_LOCAL_FEEDER_CONTACT = 3
} MenuMode;
typedef struct MenuItem MenuItem;
typedef MenuItem *tagPMenuItem;
typedef MenuItem tagMenuItem;
/* -------------------------------------------------------------------------
* 结构体名: tagMenuModel
* 功能:
* 菜单项的静态业务定义数据(名称、属性、参数与动作入口)。
*
* 字段说明:
* byClass - 菜单层级0 表示根层,其它值表示子层级深度)
* byName - 菜单显示名称UTF-8 字节数组)
* byTip - 菜单提示信息UTF-8 字节数组)
* byAttrib - 菜单属性标志(读写权限/类型等)
* wPassword - 菜单访问密码或权限码
* wPara - 菜单动作参数(业务扩展字段)
* pfnWinProc - 菜单动作回调函数(叶子节点触发)
* ------------------------------------------------------------------------- */
typedef struct tagMenuModel
{
uint8_t byClass;
uint8_t byName[50];
uint8_t byTip[50];
uint8_t byAttrib;
uint16_t wPassword;
uint16_t wPara;
FUNCPTR pfnWinProc;
} tagMenuModel;
/* -------------------------------------------------------------------------
* 结构体名: MenuLinks
* 功能:
* 菜单运行时节点的四向链路关系,描述父子与同层前后关系。
*
* 字段说明:
* higher - 指向父节点
* lower - 指向第一个子节点
* before - 指向同层前一个节点
* behind - 指向同层后一个节点
* ------------------------------------------------------------------------- */
typedef struct
{
MenuItem *higher;
MenuItem *lower;
MenuItem *before;
MenuItem *behind;
} MenuLinks;
/* -------------------------------------------------------------------------
* 结构体名: MenuRect
* 功能:
* 菜单项/菜单框的布局计算结果,供 View 层绘制与反显定位使用。
*
* 字段说明:
* wPos - 当前项在本层中的行号(从 0 开始)
* wNum - 当前层可见项数量
* wSPosX - 绘制区域左上角 X
* wSPosY - 绘制区域左上角 Y
* wEPosX - 绘制区域右下角 X
* wEPosY - 绘制区域右下角 Y
* ------------------------------------------------------------------------- */
typedef struct
{
uint16_t wPos; /* 当前项在本层中的行号(从 0 开始) */
uint16_t wNum;
uint16_t wSPosX;
uint16_t wSPosY;
uint16_t wEPosX;
uint16_t wEPosY;
} MenuRect;
/* -------------------------------------------------------------------------
* 结构体名: MenuItem
* 功能:
* 菜单运行时节点,聚合链路关系、业务定义与布局结果。
*
* 字段说明:
* links - 节点拓扑关系(父子/同层)
* menuDef - 节点业务定义(名称、属性、回调等)
* rect - 节点绘制位置与层级布局信息
* ------------------------------------------------------------------------- */
struct MenuItem
{
MenuLinks links;
tagMenuModel menuDef;
MenuRect rect;
};
/* -------------------------------------------------------------------------
* 结构体名: tagMenuCtrl
* 功能:
* 菜单导航与渲染过程中的运行时控制上下文Presenter 持有)。
*
* 字段说明:
* ptHead - 菜单根节点
* ptCurrent - 当前选中节点
* ptRoute - 当前路径路由(按层级保存各级节点,固定深度 4
* ptCurBak - 上一次选中节点(用于差分刷新)
* pt0Level - 当前顶层节点缓存(用于判断是否需要整页刷新)
* ------------------------------------------------------------------------- */
typedef struct
{
tagPMenuItem ptHead;
tagPMenuItem ptCurrent;
tagPMenuItem ptRoute[4];
tagPMenuItem ptCurBak;
tagPMenuItem pt0Level;
} tagMenuCtrl;
#endif

View File

@@ -0,0 +1,138 @@
#ifndef __DISPLAY__H__
#define __DISPLAY__H__
#include "types.h"
#define CN_COP_PWD (321)
#define CN_USER_PWD (700)
#define CN_SUPER_PWD (620)
enum _REV_TYPE_
{
EN_REV_FREE,
EN_REV_FORCE
};
enum _SET_SIDE_TYPE_
{
EN_SIDE_START = 0,
EN_SIDE_BASIC,
EN_SIDE_DEVINF,
EN_SIDE_COP,
EN_SIDE_MATRIX,
EN_SIDE_ALL,
EN_SIDE_HIGH,
EN_SIDE_MED1,
EN_SIDE_MED2,
EN_SIDE_LOW1,
EN_SIDE_LOW2,
EN_SIDE_LK,
EN_SIDE_Z,
EN_SIDE_DEF,
EN_SIDE_NONE = 0xFF
};
enum _SOFT_TYPE_NUMBER
{
EN_SOFT_PRO = 0,
EN_SOFT_GOOSE,
EN_SOFT_MU,
EN_SOFT_SWITCH,
EN_SOFT_BAK,
EN_SOFT_TYPE_END
};
enum _MEA_TYPE_
{
EN_MEA_RLY,
EN_MEA_RLY2,
EN_MEA_RLY3,
EN_MEA_ANA,
EN_MEA_ANA2,
EN_MEA_ANA3,
EN_MEA_AC,
EN_MEA_DC,
EN_MEA_SYN,
EN_MEA_POWER,
EN_MEA_DD,
EN_MEA_JLYC,
EN_MEA_GEAR,
EN_MEA_TQ,
EN_MEA_INPUT1,
EN_MEA_INPUT2,
EN_MEA_INPUT3,
EN_INPUT_RLY_ALL,
EN_INPUT_RLY_FAULT,
EN_INPUT_RLY_OTHER,
EN_INPUT_BS_ALL,
EN_INPUT_BS_FAULT,
EN_INPUT_BS_OTHER,
EN_MEA_ADJ,
EN_MEA_YX,
EN_OUTPUT_TRIP,
EN_OUTPUT_SIGN,
EN_MEA_LS,
EN_MEA_SCRLY
};
enum _INDEX_VALUE_TYPE_
{
EN_SOFT_SET,
EN_FIGURE_SET
};
enum _ANA_PARA_
{
EN_ANA_0 = 1,
EN_ANA_1
};
enum _INPUT_PARA_
{
EN_INPUT_0 = 1,
EN_INPUT_1
};
enum _ANA_TYPE_
{
EN_TYPE_DIF_CURRENT = 0,
EN_TYPE_UNIT_CURRENT,
EN_TYPE_UNIT_VOLTAGE
};
enum _NO_USER_PASSWORD_
{
EN_NO_USER_PWD = 0x55
};
enum _FACTORY_PASSWORD_
{
EN_FACTORY_PASSWORD = 0x55
};
enum _REC_TYPE_
{
EN_ACT_REC = 0,
EN_ALM_REC,
EN_CHK_REC,
EN_SOE_REC,
EN_COS_REC,
EN_LOCK_REC,
EN_OVER_REC,
EN_START_REC,
EN_RUN_REC,
EN_INPUT_REC,
EN_ONOFF_REC,
EN_OPT_REC,
EN_YK_REC,
EN_SC_REC,
EN_SCSTEPINFO_REC,
EN_FAULT_REC,
EN_ACTWAVE_REC,
EN_STARTWAVE_REC,
EN_HANDWAVE_REC,
EN_FAULT_NO,
EN_ALL_REC = 0xFF,
EN_NO_REC = 0xFFFF
};
#endif

View File

@@ -0,0 +1,69 @@
#ifndef MENU_MODEL_H
#define MENU_MODEL_H
#include "display.h"
typedef struct tagMenuModel
{
uint8_t byClass;
uint8_t byName[50];
uint8_t byTip[50];
uint8_t byAttrib;
uint16_t wPassword;
uint16_t wPara;
FUNCPTR pfnWinProc;
} tagMenuModel;
typedef struct MenuItem MenuItem;
typedef MenuItem *tagPMenuItem;
typedef MenuItem tagMenuItem;
typedef struct
{
MenuItem *higher;
MenuItem *lower;
MenuItem *before;
MenuItem *behind;
} MenuLinks;
typedef struct
{
uint16_t wPos;
uint16_t wNum;
uint16_t wSPosX;
uint16_t wSPosY;
uint16_t wEPosX;
uint16_t wEPosY;
} MenuRect;
struct MenuItem
{
MenuLinks links;
tagMenuModel menuDef;
MenuRect rect;
};
typedef struct
{
uint8_t by0LevelNum;
tagPMenuItem ptHead;
tagPMenuItem ptCurrent;
tagPMenuItem ptRoute[4];
tagPMenuItem ptCurBak;
tagPMenuItem pt0Level;
} tagMenuCtrl;
typedef struct menu_model_t menu_model_t;
struct menu_model_t
{
uint16_t maxItem;
tagMenuItem menuItems[300];
uint16_t by0LevelNum;
};
void MenuModel_Init(menu_model_t *model);
#endif

891
src/Drv/pages/menu/model.c Normal file
View File

@@ -0,0 +1,891 @@
#include "model.h"
#include <string.h>
#include "../../menu/app/menu.h"
#include "utf8.h"
/* -------------------------------------------------------------------------
* 函数名: MenuModel_Utf8LenCal
* 功能:
* 计算 UTF-8 字符串在当前菜单显示规则下的“显示宽度”。
*
* 参数:
* str - 待计算的 UTF-8 字符串
*
* 边界处理:
* - 本函数不做空指针校验,调用方需保证 `str` 非空且为 `\0` 结尾。
* - 依赖 `utf8_next()` 逐字符解析 UTF-8 编码;若输入非法,结果由底层解析行为决定。
*
* 说明:
* - 当前显示规则约定:
* 1) ASCII/单字节字符宽度记为 1
* 2) 多字节 UTF-8 字符(如中文)宽度记为 2
* - 本函数返回的是“显示占位宽度”,不是原始字节长度。
* - 该结果会被菜单布局函数用于计算菜单框宽度与字符串显示长度。
*
* 返回值:
* - UTF-8 字符串的显示宽度
* ------------------------------------------------------------------------- */
uint8_t MenuModel_Utf8LenCal(uint8_t *str)
{
uint8_t strLen = 0;
uint32_t unicode;
uint8_t index = 0;
uint8_t n = 0;
while (str[index] != '\0')
{
n = utf8_next(str + index, &unicode);
strLen += (n > 1) ? 2 : 1;
index += n;
}
return strLen;
}
/* -------------------------------------------------------------------------
* 函数名: MenuLayout_ItemDisplayLen
* 功能:
* 补充右箭头并计算单个菜单项最终用于布局的显示宽度。
*
* 参数:
* item - 菜单项节点指针
*
* 边界处理:
* - 本函数不做空指针校验,调用方需保证 `item` 有效。
* - 菜单名称最大扫描长度固定为 50与 `byName` 缓冲区容量保持一致。
*
* 说明:
* - 先通过 `MenuModel_Utf8LenCal()` 计算菜单名称本身的显示宽度。
* - 若该菜单项存在子节点(`links.lower != NULL`),则该项在界面上通常会显示
* 一个“可下钻”装饰符(例如 `\x10` 右箭头)。
* - 如果当前名称末尾尚未包含该装饰符,则额外为布局宽度加 1
* 以保证菜单框宽度计算时为装饰符预留空间。
* - 本函数只做宽度估算,不修改菜单项内容。
*
* 返回值:
* - 该菜单项的显示宽度
* ------------------------------------------------------------------------- */
static uint8_t MenuLayout_ItemDisplayLen(const MenuItem *item)
{
uint8_t displayLen;
displayLen = MenuModel_Utf8LenCal((uint8_t *)item->menuDef.byName);
if (item->links.lower != NULL)
{
uint8_t byteLen = 0;
while ((byteLen < 50) && (item->menuDef.byName[byteLen] != '\0'))
{
byteLen++;
}
if ((byteLen == 0) || (item->menuDef.byName[byteLen - 1] != '\x10'))
{
displayLen += 1;
}
}
return displayLen;
}
/* -------------------------------------------------------------------------
* 函数名: MenuModel_GetMenuMaxDisplayLen
* 功能:
* 统计当前层级菜单项的最大显示宽度,并返回当前层级菜单项中的最大显示宽度。
*
* 参数:
* ptFirst - 当前层的首节点指针
*
* 边界处理:
* - 本函数不做空指针校验,调用方需保证 `ptFirst` 有效。
*
* 说明:
* - 本函数只负责遍历下一层菜单项,并调用 `MenuLayout_ItemDisplayLen()`
* 计算每个节点的显示宽度。
*
* 返回值:
* - 当前层级菜单项中的最大显示宽度
* ------------------------------------------------------------------------- */
uint8_t MenuModel_GetMenuMaxDisplayLen(tagPMenuItem ptFirst)
{
uint8_t displayLen;
uint8_t byMaxLen = 0;
tagPMenuItem ptIndex = ptFirst;
for (uint16_t index = 0; index < MENU_MODEL_MAX_ITEM; index++)
{
displayLen = MenuLayout_ItemDisplayLen(ptIndex);
if (byMaxLen < displayLen)
{
byMaxLen = displayLen;
}
ptIndex = ptIndex->links.behind;
if (ptIndex == ptFirst)
{
break;
}
}
return byMaxLen;
}
/* -------------------------------------------------------------------------
* 函数名: MenuLayout_MapMenuPos
* 功能:
* 根据当前菜单项在本层中的原始序号,以及当前层可显示的最大行数,
* 计算该菜单项在菜单框中的实际显示行号。
*
* 参数:
* menuPos - 当前菜单项在本层中的原始位置序号(从 0 开始)
* itemNum - 当前层菜单项总数
* maxNum - 当前菜单框在可视区域内最多可容纳的显示行数
*
* 边界处理:
* - 本函数不做参数合法性校验,调用方需保证 `menuPos / itemNum / maxNum` 有效。
* - 若 `itemNum <= maxNum`,说明当前层菜单可完整显示,直接返回原始序号。
* - 若 `itemNum > maxNum` 且 `menuPos < maxNum`,说明当前项仍落在可直接显示区域,
* 直接返回原始序号。
*
* 说明:
* - 当当前层菜单项总数未超过窗口容量时,菜单项在框内的显示行号与其原始序号一致。
* - 当菜单项总数超过窗口容量时,需要把靠后的菜单项映射到有限的可视行范围内。
* - 当前实现策略为:
* 1) 若当前项仍位于前 `maxNum - 1` 个可显示位置内,则保持不变
* 2) 若当前项已落到窗口容量之外,则将其映射到靠近窗口底部的可视区域
* - 该函数的作用是为后续 Y 坐标计算提供“菜单框内部显示行号”,
* 避免菜单项过多时直接按原始序号计算导致超出可显示范围。
*
* 返回值:
* - 当前菜单项在菜单框中的实际显示行号
* ------------------------------------------------------------------------- */
static uint16_t MenuLayout_MapMenuPos(uint16_t menuPos, uint16_t itemNum, uint8_t maxNum)
{
if ((itemNum > maxNum) && (menuPos >= maxNum))
{
return (uint16_t)(maxNum - (itemNum - menuPos));
}
return menuPos;
}
/* -------------------------------------------------------------------------
* 函数名: MenuLayout_TryFlipUp
* 功能:
* 当子菜单默认向下展开可能越界时,尝试将菜单框改为“向上展开”。
*
* 参数:
* startY - 菜单框起始 Y 坐标(输入/输出)
* endY - 菜单框结束 Y 坐标(输入/输出)
* itemNum - 当前子菜单项数量
* flipMinY - 允许上翻后的最小 Y 边界
*
* 边界处理:
* - 本函数不做空指针校验,调用方需保证 `startY / endY` 有效。
* - 若根据 `itemNum` 推导出的上翻后起点小于等于 `flipMinY`
* 则认为顶部空间不足,上翻失败。
* - 若候选起点超出 `MENU_YMAX`,则视为异常坐标,上翻失败。
*
* 说明:
* - 本函数的目标是在“尽量保持父子菜单行对齐”的前提下,
* 把原本会向下越界的子菜单框整体上移。
* - 计算步骤:
* 1) 以当前 `startY` 为锚点,反推出上翻后的候选顶部 `candidateEndY`
* 2) 判断该候选顶部是否落在允许范围 `(flipMinY, MENU_YMAX)` 内
* 3) 若可上翻,则将 `startY` 更新为候选顶部
* 4) 同时使用旧 `startY` 反推出新的 `endY`
* - 返回成功后,调用方可直接使用更新后的 `startY / endY`
* 若失败,则由调用方继续执行 clamp/probe 等后备布局策略。
*
* 返回值:
* - 1 : 上翻成功
* - 0 : 上翻失败
* ------------------------------------------------------------------------- */
static uint8_t MenuLayout_TryFlipUp(uint16_t *startY, uint16_t *endY, uint16_t itemNum, uint16_t flipMinY)
{
uint16_t candidateEndY;
uint16_t temp;
candidateEndY = (uint16_t)(*startY - (itemNum - 1) * LINE_HEIGHT - MENU_YADD);
if ((candidateEndY > flipMinY) && (candidateEndY < MENU_YMAX))
{
temp = *startY;
*startY = candidateEndY;
*endY = (uint16_t)(temp + LINE_HEIGHT);
return 1;
}
return 0;
}
/* -------------------------------------------------------------------------
* 函数名: MenuLayout_FallbackProbeUp
* 功能:
* 当子菜单无法直接放入可视区域时,按“逐行向上试探”的方式寻找可容纳位置。
*
* 参数:
* ptIndex - 当前菜单路径节点数组(输入)
* itemNum - 当前子菜单项数量
* startY - 菜单框起始 Y 坐标(输出)
* endY - 菜单框结束 Y 坐标(输出)
*
* 边界处理:
* - 本函数不做空指针校验,调用方需保证 `ptIndex / startY / endY` 有效。
* - 默认要求 `ptIndex[0]` 已指向 0 级菜单节点,且其 `rect.wSPosY` 已完成布局计算。
* - 当试探上移后 `startY` 小于 1 行高度时,直接钳制为 0避免出现负坐标。
* - 若多次上移后仍无法完全放入 `MENU_YMAX` 范围,本函数保留最后一次试探结果,
* 由调用方或后续绘制逻辑继续处理。
*
* 说明:
* - 该函数是介于“直接上翻失败”和“强制贴顶”之间的一种更平滑兜底策略。
* - 处理步骤:
* 1) 先以 0 级菜单顶部 `ptIndex[0]->rect.wSPosY` 作为初始起点
* 2) 按 `itemNum * LINE_HEIGHT + MENU_YADD` 计算当前理论底部
* 3) 若底部仍超出 `MENU_YMAX`,则每次向上移动 1 个 `LINE_HEIGHT`
* 4) 每移动一次都重新计算 `endY`,直到菜单框进入可显示区域或试探结束
* - 这种策略比直接贴顶更保守,尽量保留与原始父菜单位置的相对关系,
* 同时逐步寻找一个更自然的显示位置。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
static void MenuLayout_FallbackProbeUp(uint16_t w0LevelSPosY, uint16_t itemNum, uint16_t *startY, uint16_t *endY)
{
*startY = w0LevelSPosY;
*endY = (uint16_t)(*startY + itemNum * LINE_HEIGHT + MENU_YADD);
for (uint16_t i = 1; i < (uint16_t)(w0LevelSPosY / LINE_HEIGHT + 1); i++)
{
if (*endY > MENU_YMAX)
{
*startY = (uint16_t)(w0LevelSPosY - LINE_HEIGHT * i);
if (*startY < LINE_HEIGHT)
{
*startY = 0;
}
*endY = (uint16_t)(*startY + itemNum * LINE_HEIGHT + MENU_YADD);
}
else
{
break;
}
}
}
/* -------------------------------------------------------------------------
* 函数名: MenuView_SubPosCal
* 功能:
* 以递归方式计算从当前层首节点开始的所有子菜单框位置,
* 完成当前层以及更深层级菜单的矩形布局。
*
* 参数:
* ptFirst - 当前层首节点指针
* ptParent - 当前层所属父菜单框节点
* w0LevelSPosY - 0 级菜单框顶部 Y 坐标,用于越界时的回退布局基准
*
* 边界处理:
* - 若 `ptFirst == NULL`,说明当前层不存在有效菜单链,直接返回。
* - 本函数不对 `ptParent` 做空指针校验,调用方需保证其有效,
* 且其 `rect` 已完成布局计算。
* - 当前层遍历仍使用 `MENU_MODEL_MAX_ITEM` 作为保护上限,
* 防止异常链表导致死循环。
* - 当子菜单框底部超出 `MENU_YMAX` 时,优先尝试上翻;
* 若上翻失败,则退化到 `MenuLayout_FallbackProbeUp()` 继续寻找可显示位置。
*
* 说明:
* - 本函数是子菜单布局的递归核心,处理流程如下:
* 1) 以 `ptFirst` 为起点遍历当前层所有节点
* 2) 对每个存在子菜单的节点,先取其下一层首节点 `ptNextFirst`
* 3) 通过 `MenuModel_GetMenuMaxDisplayLen()` 计算该下一层菜单的最大显示宽度
* 4) 根据父菜单框右边界,计算当前子菜单框的 X 坐标
* 5) 根据当前节点在本层中的 0 基序号 `wPos`、父层项数 `wNum`
* 计算当前子菜单框的理论 Y 坐标
* 6) 若理论底部越界,则尝试上翻或执行回退探测
* 7) 当前节点对应子菜单框位置确定后,
* 再递归处理其更深一层的子菜单布局
* - `ptParent->rect.wNum` 表示父节点直接子节点总数,
* 用于把当前节点的原始层内 0 基序号映射到菜单框可显示行号。
* - `w0LevelSPosY` 作为递归过程中统一的顶部参考,
* 保证深层菜单在空间不足时仍能回退到稳定的显示区域。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
static void MenuView_SubPosCal(tagPMenuItem ptFirst, tagPMenuItem ptParent, uint16_t w0LevelSPosY)
{
uint16_t wSPosY;
uint16_t wEPosY;
uint8_t byMaxLen;
uint8_t byMaxNum;
uint16_t byMenuPos;
uint16_t byItemNum;
tagPMenuItem ptNextFirst;
tagPMenuItem ptIndex;
if (ptFirst == NULL)
{
return;
}
byMaxNum = (MENU_YMAX - MENU_YMIN) / LINE_HEIGHT;
ptIndex = ptFirst;
for (uint16_t wLoop = 0; wLoop < MENU_MODEL_MAX_ITEM; wLoop++)
{
if (ptIndex->links.lower != NULL)
{
ptNextFirst = ptIndex->links.lower; /* 保存下一层的第一个节点 */
byMaxLen = MenuModel_GetMenuMaxDisplayLen(ptNextFirst); /* 获取下一层菜单项中的最大显示宽度 */
/* 计算子菜单框的 X 坐标 */
ptIndex->rect.wSPosX = ptParent->rect.wEPosX; /* 子菜单框左边界紧贴父菜单框右边界 */
/* 子菜单框右边界由最大显示宽度 * 单字符宽度 + 额外边距推导 */
ptIndex->rect.wEPosX = (uint16_t)(ptIndex->rect.wSPosX + byMaxLen * MENU_WITDTH + MENU_XADD);
/* 计算子菜单框的 Y 坐标 */
byMenuPos = ptIndex->rect.wPos; /* 当前菜单项的 0 基序号 */
byItemNum = ptParent->rect.wNum; /* 父菜单项的项数 */
byMenuPos = MenuLayout_MapMenuPos(byMenuPos, byItemNum, byMaxNum);
wSPosY = ptParent->rect.wSPosY + byMenuPos * LINE_HEIGHT;
byItemNum = ptIndex->rect.wNum;
wEPosY = wSPosY + byItemNum * LINE_HEIGHT + MENU_YADD;
if (wEPosY < MENU_YMAX)
{
ptIndex->rect.wSPosY = wSPosY;
ptIndex->rect.wEPosY = wEPosY;
}
else
{
if (!MenuLayout_TryFlipUp(&wSPosY, &wEPosY, byItemNum, MENU_YMIN))
{
MenuLayout_FallbackProbeUp(w0LevelSPosY, byItemNum, &wSPosY, &wEPosY);
}
ptIndex->rect.wSPosY = wSPosY;
ptIndex->rect.wEPosY = wEPosY;
}
MenuView_SubPosCal(ptNextFirst, ptIndex, w0LevelSPosY);
}
ptIndex = ptIndex->links.behind;
if (ptIndex == ptFirst)
{
break;
}
}
}
/* -------------------------------------------------------------------------
* 函数名: MenuModel_PositionCal
* 功能:
* 计算整棵菜单树的初始显示位置,完成 0 级菜单以及后续子菜单层级的矩形布局。
*
* 参数:
* ptMenuHead - 菜单头节点0 级起点)
* by0LevelNum - 0 级菜单数量
*
* 边界处理:
* - 若 ptMenuHead 为 NULL、config 为 NULL 或 by0LevelNum 为 0则直接返回。
* - 若 by0LevelNum 超过本函数内部固定工作数组容量 4则直接返回。
* - 当 0 级菜单数量大于 LCD 宽度时byInterval 可能被截断为 0
* 当前通过断言暴露异常输入,并保持返回保护。
*
* 说明:
* - 本函数是菜单布局计算入口:
* 1) 先建立 0 级菜单的遍历起点、当前位置与 0 基序号
* 2) 根据 lcdSizeX / by0LevelNum 计算 0 级菜单的水平分布间隔
* 3) 逐个遍历 0 级菜单项
* 4) 对存在子菜单的节点,计算其子菜单框体矩形
* 5) 递归调用 MenuView_Sub1PosCal() 继续完成更深层布局
* - 本函数只计算 rect不负责实际绘制。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
void MenuModel_PositionCal(tagPMenuItem ptMenuHead, uint16_t by0LevelNum)
{
tagPMenuItem ptFirst = ptMenuHead;
tagPMenuItem ptIndex = ptMenuHead;
uint8_t byMaxLen;
uint8_t byInterval;
tagPMenuItem ptNextFirst;
ASSERT((ptMenuHead == NULL) || (by0LevelNum == 0));
if ((ptMenuHead == NULL) || (by0LevelNum == 0))
{
return;
}
byInterval = MENU_SIZE_X / by0LevelNum;
for (uint16_t wLoop = 0; wLoop < by0LevelNum; wLoop++)
{
if (ptIndex->links.lower != NULL)
{
ptNextFirst = ptIndex->links.lower; /* 保存下一层的第一个节点 */
byMaxLen = MenuModel_GetMenuMaxDisplayLen(ptNextFirst);
ptIndex->rect.wSPosX = ptIndex->rect.wPos * byInterval;
ptIndex->rect.wSPosY = MENU_SIZE_Y - LINE_HEIGHT - ptIndex->rect.wNum * LINE_HEIGHT - MENU_YADD;
ptIndex->rect.wEPosX = ptIndex->rect.wPos * byInterval + byMaxLen * MENU_WITDTH + MENU_XADD;
ptIndex->rect.wEPosY = MENU_SIZE_Y - LINE_HEIGHT;
MenuView_SubPosCal(ptNextFirst, ptIndex, ptIndex->rect.wSPosY);
}
ptIndex = ptIndex->links.behind;
if (ptIndex == ptFirst)
{
break;
}
}
}
enum _STR_FLAG_ {
EN_STR_FLAG_FUNSET = 0,
EN_STR_FLAG_OKCANCEL,
EN_VALUE_FLAG_SLOTTYPE,
EN_VALUE_FLAG_YESNO,
EN_VALUE_FLAG_CHN_NUM,
EN_VALUE_FLAG_YXSET,
EN_VALUE_FLAG_YKSTEP,
EN_VALUE_FLAG_YKACTION,
EN_VALUE_FLAG_CHK,
EN_VALUE_FLAG_BEUSED,
EN_VALUE_FLAG_PORTTYPE,
EN_VALUE_FLAG_MAPNAME,
EN_VALUE_FLAG_RECTITLE,
EN_VALUE_FLAG_BHINFO,
EN_VALUE_FLAG_BHSET,
EN_VALUE_FLAG_OKCANCEL,
EN_VALUE_FLAG_FILETYPE,
EN_VALUE_FLAG_ROLE,
EN_VALUE_FLAG_COMMSTATE,
EN_VALUE_FLAG_ALARM,
EN_VALUE_FLAG_YXTYPE,
EN_STR_FLAG_MENUALAUTO,
EN_STR_FLAG_LINKBREAK,
EN_STR_FLAG_DONEUNDONE,
EN_STR_FLAG_ACTBACK,
EN_STR_FLAG_RESUMEBREAK,
EN_STR_FLAG_USESTOP,
EN_STR_FLAG_BAUDRATE,
EN_STR_FLAG_VERIFY,
EN_STR_FLAG_PROTOL,
EN_STR_FLAG_RECORD,
EN_STR_FLAG_MESSAGE,
EN_STR_FLAG_COM,
EN_STR_FLAG_MODE,
EN_STR_FLAG_COMBAUD,
EN_STR_FLAG_DATABIT,
EN_STR_FLAG_STOPBIT,
EN_STR_FLAG_COMTYPE,
EN_STR_FLAG_SAMEV,
EN_STR_FLAG_PT,
EN_STR_FLAG_CT,
EN_STR_FLAG_TRANSTYPE,
EN_STR_FLAG_LINEPHS,
EN_STR_FLAG_RETOUT,
EN_STR_FLAG_YXDI,
EN_STR_FLAG_YKPUL,
EN_STR_FLAG_WAVE,
EN_STR_FLAG_MEMTYPE,
EN_STR_FLAG_CHANNEL,
EN_STR_FLAG_POWERTYPE,
EN_STR_FLAG_ZEROTYPE,
EN_STR_FLAG_YCCOMMTYPE,
EN_STR_FLAG_YXCOMMTYPE,
EN_STR_FLAG_CTRLWORD,
EN_STR_FLAG_BUSNAME,
EN_STR_FLAG_UNITSHOWMODE,
EN_STR_FLAG_BAYMODE,
EN_STR_FLAG_BASEANA,
EN_STR_FLAG_BASEPHASE,
EN_STR_FLAG_BAYCODE,
EN_STR_FLAG_SET_NUM,
EN_STR_FLAG_DCTYPE,
EN_STR_FLAG_DCINTYPE,
EN_STR_FLAG_DWINTYPE,
EN_STR_FLAG_PTCT,
EN_STR_FLAG_HARMOTYPE,
EN_STR_FLAG_YXSIN_DI,
EN_STR_FLAG_SHOWANATYPE,
EN_STR_FLAG_ANAPOLARTYPE,
EN_STR_FLAG_WIRDIAGRAMTYPE
} enumStrType;
const tagMenuModel menuTab[] =
{
{ 0, "主界面", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 1, "装置信息", "查看装置信息", 0, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_AppInfo },
{ 1, "实时数据", "装置实时数据", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 2, "交流量", "查看遥测交流量", EN_MEA_AC, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_YC },
{ 2, "直流量", "查看遥测直流量", EN_MEA_DC, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_YC },
{ 2, "遥信量", "查看遥信开入量", EN_INPUT_RLY_ALL, 0x0000, EN_INPUT_0, (FUNCPTR)MenuProc_See_Input },
{ 1, "参数定值", "保护参数查看与修改", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 2, "整定", "整定装置保护参数", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 3, "参数", "查看设备参数定值", EN_FIGURE_SET, CN_USER_PWD, EN_SIDE_BASIC, (FUNCPTR)MenuProc_Set_Value },
{ 3, "定值", "设置装置数值定值", EN_FIGURE_SET, CN_USER_PWD, EN_SIDE_DEF, (FUNCPTR)MenuProc_Set_Value },
{ 3, "控制字", "设置装置控制字", EN_SOFT_SET, CN_USER_PWD, EN_SIDE_DEF, (FUNCPTR)MenuProc_Set_Value },
{ 3, "软压板", "设置软压板", 0, CN_USER_PWD, EN_SOFT_PRO, (FUNCPTR)MenuProc_Set_Soft },
{ 2, "查看", "查看装置保护参数", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 3, "参数", "设置设备参数定值", EN_FIGURE_SET, 0x0000, EN_SIDE_BASIC, (FUNCPTR)MenuProc_See_Set },
{ 3, "定值", "查看数值型定值", EN_FIGURE_SET, 0x0000, EN_SIDE_DEF, (FUNCPTR)MenuProc_See_Set },
{ 3, "控制字", "查看控制字定值", EN_SOFT_SET, 0x0000, EN_SIDE_DEF, (FUNCPTR)MenuProc_See_Set },
{ 3, "软压板", "查看软压板", 0, 0x0000, EN_SOFT_PRO, (FUNCPTR)MenuProc_See_Soft },
{ 1, "三遥设置", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 2, "遥测死区", "设置遥测量死区门槛", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YC_SetSqValue },
{ 2, "遥测系数", "设置遥测量微调系数", EN_MEA_ADJ, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YC_SetAdjCoe },
{ 2, "遥信类型", "设置遥信类型", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetCommType },
{ 2, "遥信防抖", "设置遥信防抖时间", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetWidth },
{ 2, "双点遥信", "设置双点遥信虚端子", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetTwin },
{ 1, "装置维护", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 2, "时钟设置", "设置系统时钟", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_Time },
{ 2, "强制复归", "可复归未返回事件", EN_REV_FORCE, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_RevEvent },
{ 2, "手动录波", "启动手动录波", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ManualWave },
{ 2, "清除记录", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ClrRec },
{ 1, "通讯参数", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 2, "通讯设置", "外部通讯设置", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ComPara },
{ 2, "网口设置", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_EditIP },
{ 2, "SNTP设置", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_EditSntp },
{ 1, "记录查询", "查看各种装置记录", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 2, "SOE记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecSOE },
{ 2, "事故记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecAct },
{ 2, "操作记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecOpt },
{ 2, "保护告警", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecAlm },
{ 2, "保护启动", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecStart },
{ 2, "遥控记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecYK },
{ 2, "自检记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecChk },
{ 2, "运行记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecRun },
{ 2, "运行报告", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecFault },
{ 0, "厂家设置", "设置装置相关参数", 0, CN_COP_PWD, 0, (FUNCPTR)Menu_NonPfunc },
{ 1, "元件配置", "配置元件配置", 0, CN_COP_PWD, EN_FACTORY_PASSWORD,(FUNCPTR)MenuProc_Cfg_CellConf },
{ 1, "恢复默认", "恢复默认元件定值参数", 0, CN_COP_PWD, 0, (FUNCPTR)Menu_NonPfunc },
{ 2, "全部恢复", "全部参数恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_AllInf_Default },
{ 2, "默认参数", "当前参数恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Para_Default },
{ 2, "默认定值", "当前定值区恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Set_Default },
{ 2, "软压板", "当前软压板恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Resume_Soft },
{ 2, "元件配置", "元件配置恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Cfg_CellDef },
{ 1, "交流显示", "交流显示方式设置", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Cfg_ShowAnaType },
{ 1, "装置调试", "调试装置", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
{ 2, "虚拟遥信", "设置虚拟遥信值", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuYX },
{ 2, "交流虚遥测", "设置虚拟交流遥测值", EN_MEA_AC, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC },
{ 2, "直流虚遥测", "设置虚拟直流遥测值", EN_MEA_DC, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC },
{ 2, "电度虚遥测", "设置虚拟电度遥测值", EN_MEA_POWER, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC },
{ 2, "动作虚事件", "设置虚拟动作事件", EN_ACT_REC, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuEvent },
{ 2, "告警虚事件", "设置虚拟告警事件", EN_ALM_REC, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuEvent },
{ 2, "动作出口", "进入此菜单保护退出", EN_OUTPUT_TRIP, CN_COP_PWD, EN_INPUT_0, (FUNCPTR)MenuProc_Dbg_Relay },
{ 2, "信号出口", "进入此菜单保护退出", EN_OUTPUT_SIGN, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_Relay },
{ 1, "版本信息", "查看板件版本信息", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_See_VersionBoard },
};
/* -------------------------------------------------------------------------
* 函数名: MenuModel_CountTopLevel
* 功能:
* 统计静态菜单定义表中 0 级菜单项的数量,用于后续顶部菜单布局计算。
*
* 参数:
* maxItem - 需要参与统计的菜单项总数
*
* 边界处理:
* - 本函数默认 `maxItem` 不超过 `menuTab` 实际长度,调用方需保证该前提成立。
* - 当 `maxItem == 0` 时for 循环不进入,函数返回 0。
*
* 说明:
* - 在当前菜单模型中,`byClass == 0` 表示顶层菜单节点。
* - 本函数通过顺序扫描 `menuTab[0 ~ maxItem-1]`
* 统计所有 0 级节点数量,并将结果返回给布局初始化流程。
* - 返回值通常会被用于计算顶层菜单在 LCD 上的水平分布间隔。
*
* 返回值:
* - 静态菜单表中的 0 级菜单数量
* ------------------------------------------------------------------------- */
uint16_t MenuModel_CountTopLevel(uint16_t maxItem)
{
uint8_t by0LevelNum = 0;
uint32_t wLoop;
for (wLoop = 0; wLoop < maxItem; wLoop++)
{
if (menuTab[wLoop].byClass == 0)
{
by0LevelNum++;
}
}
return by0LevelNum;
}
/* -------------------------------------------------------------------------
* 函数名: MenuModel_BuildTree
* 功能:
* 根据按层级顺序排列的静态菜单定义表,构建菜单运行时树,并建立
* `higher / lower / before / behind` 四类链路关系。
*
* 参数:
* menuItems - 运行时菜单节点数组(输出)
* modelTab - 静态菜单定义表(输入)
* modelCount - 菜单项总数量
*
* 边界处理:
* - 本函数不做 menuItems/modelTab 空指针校验,调用方需保证输入有效。
* - 默认要求 modelCount > 0且 `modelTab` 已按菜单显示顺序正确排列:
* 同层节点连续,子节点紧跟在父节点之后。
* - 当前函数内部使用固定深度数组 `ptFirst[4] / ptLast[4]`
* 因此默认支持的最大层级索引为 3。
*
* 说明:
* - 构建流程:
* 1) 先初始化 `menuItems[0]`,作为整棵树的起点
* 2) 使用 `ptFirst[level] / ptLast[level]` 记录每一层当前的首节点和尾节点
* 3) 依次读取后续静态定义项,复制 `menuDef` 到运行时节点
* 4) 比较“当前节点层级”和“下一个节点层级”,决定链路挂接方式:
* a) byCurClass < byNextClass
* 表示进入更深一层,`ptNextNode` 是 `ptCurrent` 的第一个子节点:
* - `ptCurrent->links.lower = ptNextNode`
* - `ptNextNode->links.higher = ptCurrent`
* - 该层的 `ptFirst/ptLast` 都初始化为 `ptNextNode`
* b) byCurClass == byNextClass
* 表示接入同层兄弟节点:
* - `ptNextNode->links.before = ptCurrent`
* - `ptCurrent->links.behind = ptNextNode`
* - `ptNextNode->links.higher` 继承当前节点的父节点
* - 更新该层 `ptLast`
* c) byCurClass > byNextClass
* 表示从更深层回退到某个上层后,再接入该层的新兄弟节点:
* - `ptNextNode->links.before = ptLast[byNextClass]`
* - `ptNextNode->links.higher = ptFirst[byNextClass]->links.higher`
* 即:新节点的父节点与该层第一个节点保持一致
* - 将原尾节点的 `behind` 指向新节点,并更新该层 `ptLast`
* - 对回退过程中已经结束的更深层链表做首尾闭环
* 5) 全部节点处理完成后,再将 `0 ~ byCurClass` 各层链表统一闭环
* - 构建结果:
* `higher` - 父节点
* `lower` - 第一个子节点
* `before` - 同层前一个节点
* `behind` - 同层后一个节点
* - 同层链表最终被组织为环形结构,便于菜单上下移动时循环导航。
* - 本函数只负责树与链表关系构建,不负责菜单位置计算和显示名称装饰。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
void MenuModel_BuildTree(tagMenuItem *menuItems, const tagMenuModel *modelTab, uint32_t modelCount)
{
tagPMenuItem ptFirst[MENU_MODEL_MAX_LEVEL]; /* 每层第一个节点 */
tagPMenuItem ptLast[MENU_MODEL_MAX_LEVEL]; /* 每层最后一个节点 */
tagPMenuItem ptCurrent; /* 当前节点 */
tagPMenuItem ptNextNode; /* 下一个节点 */
uint8_t byCurClass; /* 当前节点层级 */
uint8_t byNextClass; /* 下一个节点层级 */
/* 初始化第 0 个节点,作为整棵树的起点 */
memset(&menuItems[0], 0, sizeof(menuItems[0]));
/* 初始化第 0 个节点的业务字段 */
memcpy(&menuItems[0].menuDef, &modelTab[0], sizeof(menuItems[0].menuDef));
/* 初始化第 0 个节点的链路 */
ptFirst[0] = &menuItems[0];
ptLast[0] = ptFirst[0];
ptCurrent = &menuItems[0];
/* 构建菜单树 */
for (uint16_t index = 1; index < modelCount; index++)
{
/* 初始化第 index 个节点的业务字段 */
ptNextNode = &menuItems[index];
memset(ptNextNode, 0, sizeof(*ptNextNode));
memcpy(&ptNextNode->menuDef, &modelTab[index], sizeof(ptNextNode->menuDef));
byCurClass = ptCurrent->menuDef.byClass;
byNextClass = ptNextNode->menuDef.byClass;
if (byCurClass < byNextClass) /* 当前节点层级小于下一个节点层级 */
{
ptCurrent->links.lower = ptNextNode;
ptNextNode->links.higher = ptCurrent;
/* 更新该层 first/last */
ptFirst[byNextClass] = ptNextNode;
ptLast[byNextClass] = ptNextNode;
}
else if (byCurClass == byNextClass) /* 当前节点层级等于下一个节点层级 */
{
ptNextNode->links.before = ptCurrent;
ptNextNode->links.higher = ptCurrent->links.higher;
ptCurrent->links.behind = ptNextNode;
/* 更新该层 last */
ptLast[byNextClass] = ptNextNode;
}
else /* 当前节点层级大于下一个节点层级,回退层级*/
{
ptNextNode->links.before = ptLast[byNextClass];
ptNextNode->links.higher = ptFirst[byNextClass]->links.higher;
/* 更新该层 last 的 behind */
ptLast[byNextClass]->links.behind = ptNextNode;
ptLast[byNextClass] = ptNextNode;
/* 形成回环链表 */
for (uint16_t index2 = byCurClass; index2 > byNextClass; index2--)
{
ptLast[index2]->links.behind = ptFirst[index2];
ptFirst[index2]->links.before = ptLast[index2];
}
}
/* 更新当前节点 */
ptCurrent = ptNextNode;
}
/* 形成回环链表 */
byCurClass = ptCurrent->menuDef.byClass;
for (uint16_t index = 0; index <= byCurClass; index++)
{
ptLast[index]->links.behind = ptFirst[index];
ptFirst[index]->links.before = ptLast[index];
}
}
/* -------------------------------------------------------------------------
* 函数名: MenuModel_DecorateDisplayNames
* 功能:
* 为存在子菜单的菜单项名称追加显示装饰符,提示该节点可继续进入下一级。
*
* 参数:
* menuItems - 菜单项数组首地址
* modelCount - 菜单项总数量
*
* 边界处理:
* - 本函数不做 menuItems 空指针校验,调用方需保证数组有效。
* - 若菜单项没有子节点links.lower == NULL直接跳过不追加装饰符。
* - 通过 `sizeof(byName) - 1` 预留字符串结尾 `\0`,避免越界写入。
* - 若名称已以 `\x10` 结尾,则不重复追加。
*
* 说明:
* - `\x10` 在当前菜单显示逻辑中作为“右箭头/可下钻”标记,
* 用于提示用户该项后面还有子菜单。
* - 处理流程:
* 1) 遍历所有菜单项
* 2) 判断是否存在子菜单
* 3) 统计当前名称长度(不超过缓冲区上限)
* 4) 若末尾尚未追加装饰符,则写入 `\x10` 和字符串结束符 `\0`
* - 该函数只修改显示名称,不改变菜单树链路关系和业务属性。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
void MenuModel_DecorateDisplayNames(tagMenuItem *menuItems, uint32_t modelCount)
{
for (uint32_t i = 0; i < modelCount; i++)
{
uint16_t nameCapacity = (uint16_t)sizeof(menuItems[i].menuDef.byName);
uint16_t maxTextLenWithoutNull = (uint16_t)(nameCapacity - 1);
uint16_t len = 0;
/* 如果没有子菜单,直接跳过 */
if (menuItems[i].links.lower == NULL)
{
continue;
}
/* 计算名称长度 */
while ((len < maxTextLenWithoutNull) && (menuItems[i].menuDef.byName[len] != '\0'))
{
len++;
}
/* 补充右箭头 */
if ((len == 0) || (menuItems[i].menuDef.byName[len - 1] != '\x10'))
{
if (len < maxTextLenWithoutNull)
{
menuItems[i].menuDef.byName[len] = '\x10';
menuItems[i].menuDef.byName[len + 1] = '\0';
}
}
}
}
/* -------------------------------------------------------------------------
* 函数名: MenuModel_IndexMenuItems
* 功能:
* 统计整棵菜单树中所有菜单项的 `wPos` 与 `wNum`。
*
* 参数:
* menuItems - 菜单项数组
* modelCount - 菜单项总数
*
* 边界处理:
* - 若 `menuItems == NULL` 或 `modelCount == 0`,则直接返回。
*
* 说明:
* - 本函数分两部分初始化:
* 1) 先遍历 0 级同层链表,为所有顶层节点写入 `rect.wPos`
* 2) 再遍历每个节点:
* a) 若无子节点,则其 `rect.wNum = 0`
* b) 若有子节点,则沿子层 `behind` 链统计子节点总数
* c) 同时为该组子节点写入各自的 `rect.wPos`
* d) 最后把子节点总数写回当前父节点的 `rect.wNum`
* - 因此:
* `wPos` 表示“当前节点在本层中的 0 基序号”
* `wNum` 表示“当前节点拥有的直接子节点总数”
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
void MenuModel_IndexMenuItems(tagMenuItem *menuItems, uint32_t modelCount)
{
tagPMenuItem current;
tagPMenuItem parent;
tagPMenuItem child;
uint16_t count;
if ((menuItems == NULL) || (modelCount == 0))
{
return;
}
/* 1. 记录 0 级节点的位置和子节点数量 */
current = &menuItems[0];
count = 0;
for (uint16_t index = 0; index < modelCount; index++)
{
current->rect.wPos = count;
current = current->links.behind;
if ((current == NULL) || (current == &menuItems[0]))
{
break;
}
count++;
}
/* 2. 记录其他层级的位置和子节点数量 */
for (uint32_t i = 0; i < modelCount; i++)
{
parent = &menuItems[i];
current = parent->links.lower;
/* a. 如果当前节点没有子节点,说明是最深一层,记录当前节点子节点数量为 0 */
if (current == NULL)
{
parent->rect.wNum = 0;
continue;
}
/*b. 若有子节点,则沿子层 `behind` 链统计子节点总数 */
child = current;
count = 0;
for (uint16_t index = 0; index < MENU_MODEL_MAX_ITEM; index++)
{
/*c. 为该组子节点写入各自的 `rect.wPos` */
child->rect.wPos = count;
count++;
child = child->links.behind;
if ((child == NULL) || (child == current))
{
break;
}
}
/* d. 最后把子节点总数写回当前父节点的 `rect.wNum` */
parent->rect.wNum = count;
}
}
void MenuModel_Init(menu_model_t *model)
{
uint16_t by0LevelNum;
model->maxItem = (uint16_t)(sizeof(menuTab) / sizeof(tagMenuModel));
memset(model->menuItems, 0, sizeof(model->menuItems));
by0LevelNum = MenuModel_CountTopLevel(model->maxItem);
MenuModel_BuildTree(model->menuItems, menuTab, model->maxItem);
MenuModel_DecorateDisplayNames(model->menuItems, model->maxItem);
MenuModel_IndexMenuItems(model->menuItems, model->maxItem);
MenuModel_PositionCal(&model->menuItems[0], by0LevelNum);
}

View File

@@ -0,0 +1,36 @@
#ifndef MENU_MODEL_H
#define MENU_MODEL_H
#include "display.h"
#include "def.h"
#define MENU_MODEL_MAX_ITEM 300 /* 最大菜单项数 */
#define MENU_MODEL_MAX_LEVEL 4 /* 最大菜单层级 */
typedef struct menu_model_t menu_model_t;
struct menu_model_t
{
uint16_t maxItem;
tagMenuItem menuItems[MENU_MODEL_MAX_ITEM];
};
void MenuModel_Init(menu_model_t *model);
/* whitebox test hooks */
#ifdef UNIT_TEST
void MenuModel_IndexMenuItems(tagMenuItem *menuItems, uint32_t modelCount);
void MenuModel_PositionCal(tagPMenuItem ptMenuHead, uint16_t by0LevelNum);
uint8_t MenuModel_Utf8LenCal(uint8_t *str);
uint8_t MenuModel_GetMenuMaxDisplayLen(tagPMenuItem ptFirst);
uint16_t MenuModel_CountTopLevel(uint16_t maxItem);
void MenuModel_BuildTree(tagMenuItem *menuItems, const tagMenuModel *modelTab, uint32_t modelCount);
void MenuModel_DecorateDisplayNames(tagMenuItem *menuItems, uint32_t modelCount);
#endif
#endif

255
src/Drv/pages/menu/page.c Normal file
View File

@@ -0,0 +1,255 @@
#include <stdio.h>
#include <string.h>
#include "page.h"
#include "display.h"
#include "presenter.h"
/* -------------------------------------------------------------------------
* 模块内静态对象说明:
* s_model - 菜单页 Model 实例(菜单数据与运行时结构)
* s_view - 菜单页 View 实例(布局与渲染能力)
* s_presenter - 菜单页 Presenter 实例(输入处理与状态驱动)
* s_menuPage - 页面管理器可注册的 page_t 描述对象
*
* 说明:
* - 以上对象均为文件内静态单例,生命周期覆盖进程运行期。
* - 通过 MenuPage_GetInstance() 暴露 s_menuPage 给 PageManager 注册。
* ------------------------------------------------------------------------- */
static menu_model_t s_model;
static menu_view_t s_view;
static menu_presenter_t s_presenter;
static page_t s_menuPage;
/* -------------------------------------------------------------------------
* 函数名: MenuPage_OnEnter
* 功能:
* 页面进入回调:将菜单渲染标记为“首帧全量刷新”,并立即触发一次刷新。
*
* 参数:
* page - 当前页面对象指针(本实现未直接使用)
*
* 边界处理:
* - 本函数不依赖 page 内容,统一转为 (void)page 消除未使用告警。
*
* 说明:
* - 通过 dspCtrl.bFirst = 1 告知 Presenter 下一次刷新走首帧路径。
* - 进入页面后立即刷新,确保界面可见状态与内部状态同步。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
static void MenuPage_OnEnter(page_t *page)
{
(void)page;
s_presenter.dspCtrl.bFirst = 1;
MenuPresenter_Refresh(&s_presenter);
}
/* -------------------------------------------------------------------------
* 函数名: MenuPage_OnExit
* 功能:
* 页面退出回调占位点;当前版本无额外退出动作。
*
* 参数:
* page - 当前页面对象指针(本实现未直接使用)
*
* 边界处理:
* - 使用 (void)page 防止未使用参数告警。
*
* 说明:
* - 预留给后续扩展(如停止定时任务、冻结动画、保存瞬时 UI 状态等)。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
static void MenuPage_OnExit(page_t *page)
{
(void)page;
}
/* -------------------------------------------------------------------------
* 函数名: MenuPage_OnDestroy
* 功能:
* 页面销毁回调清空菜单页内部三层对象Model/View/Presenter状态。
*
* 参数:
* page - 当前页面对象指针(本实现未直接使用)
*
* 边界处理:
* - 使用 memset 全量清零静态对象,避免残留状态影响后续重建。
*
* 说明:
* - 与 is_cached 策略配合:当页面被标记为非缓存并弹栈销毁时,该函数用于复位。
* - page_t 元信息不在此函数复位,由页面生命周期创建阶段重新赋值。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
static void MenuPage_OnDestroy(page_t *page)
{
(void)page;
memset(&s_model, 0, sizeof(s_model));
memset(&s_view, 0, sizeof(s_view));
memset(&s_presenter, 0, sizeof(s_presenter));
}
/* -------------------------------------------------------------------------
* 函数名: MenuPage_OnEvent
* 功能:
* 页面事件回调:校验输入事件后,将按键转交 Presenter 处理。
*
* 参数:
* page - 当前页面对象指针(本实现未直接使用)
* event - 输入事件指针
*
* 边界处理:
* - event 为 NULL 时返回 EVENT_UNHANDLED。
* - 仅处理 PAGE_EVENT_KEY 事件类型,其它类型返回 EVENT_UNHANDLED。
* - keyVal 为 0 视为无效按键,返回 EVENT_UNHANDLED。
*
* 说明:
* - 事件有效时调用 MenuPresenter_HandleInput() 执行业务输入流转。
* - 返回 EVENT_HANDLED表示该事件已被菜单页消费不再交给上层页面逻辑。
*
* 返回值:
* - EVENT_HANDLED : 事件已处理
* - EVENT_UNHANDLED : 事件无效或不属于本页处理范围
* ------------------------------------------------------------------------- */
static event_result_t MenuPage_OnEvent(page_t *page, input_event_t *event)
{
(void)page;
if ((event == NULL) || (event->type != PAGE_EVENT_KEY) || (event->keyVal == 0))
{
return EVENT_UNHANDLED;
}
MenuPresenter_HandleInput(&s_presenter, event->keyVal);
return EVENT_HANDLED;
}
/* -------------------------------------------------------------------------
* 函数名: MenuPage_OnLoop
* 功能:
* 页面循环回调:周期性驱动 Presenter 执行刷新逻辑。
*
* 参数:
* page - 当前页面对象指针(本实现未直接使用)
*
* 边界处理:
* - 使用 (void)page 防止未使用参数告警。
*
* 说明:
* - 实际刷新策略(全量/增量)由 Presenter 内部状态控制。
* - 该函数通常由 PageManager_Loop() 在主循环节拍中调用。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
static void MenuPage_OnLoop(page_t *page)
{
(void)page;
MenuPresenter_Refresh(&s_presenter);
}
/* -------------------------------------------------------------------------
* 函数名: MenuPage_OnCreate
* 功能:
* 页面创建回调:按 Model -> View -> Presenter 顺序完成菜单页运行时装配,
* 并初始化 page_t 描述对象字段。
*
* 参数:
* page - 当前页面对象指针(由 PageManager 传入)
*
* 边界处理:
* - 先清零 s_menuPage再统一重建其元信息与回调绑定避免脏状态遗留。
* - 假定 page 非空且来自 PageManager 生命周期调用链。
*
* 说明:
* - 初始化顺序固定:
* 1) MenuModel_Init(&s_model)
* 2) MenuView_Init(&s_view)
* 3) MenuPresenter_Init(&s_presenter, &s_model, &s_view)
* - 将 model/presenter/view 回填到 page 与 s_menuPage便于调试与统一访问。
* - s_menuPage 作为静态页面实例,对外由 MenuPage_GetInstance() 返回。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
static void MenuPage_OnCreate(page_t *page)
{
/* 1) model init */
MenuModel_Init(&s_model);
/* 2) view init */
MenuView_Init(&s_view);
/* 3) presenter setup + runtime build */
MenuPresenter_Init(&s_presenter, &s_model, &s_view);
page->model = &s_model;
page->presenter = &s_presenter;
page->view = &s_view;
s_menuPage.presenter = &s_presenter;
s_menuPage.view = &s_view;
s_menuPage.model = &s_model;
}
/* -------------------------------------------------------------------------
* 函数名: MenuPage_GetInstance
* 功能:
* 获取菜单页静态实例指针,供 PageManager_Register() 注册使用。
*
* 参数:
* 无
*
* 边界处理:
* - 返回文件内静态对象地址,无空指针分支。
*
* 说明:
* - 该函数仅暴露页面入口,不负责初始化;初始化由 on_create 生命周期完成。
*
* 返回值:
* - 指向静态页面对象 s_menuPage 的 page_t* 指针
* ------------------------------------------------------------------------- */
page_t *MenuPage_GetInstance(void)
{
/* 确保在注册到 PageManager 前,页面生命周期回调已就绪 */
memset(&s_menuPage, 0, sizeof(s_menuPage));
s_menuPage.page_id = PAGE_ID_MENU;
s_menuPage.is_cached = 1;
s_menuPage.on_create = MenuPage_OnCreate;
s_menuPage.on_enter = MenuPage_OnEnter;
s_menuPage.on_exit = MenuPage_OnExit;
s_menuPage.on_destroy = MenuPage_OnDestroy;
s_menuPage.on_event = MenuPage_OnEvent;
s_menuPage.on_loop = MenuPage_OnLoop;
return &s_menuPage;
}
/* -------------------------------------------------------------------------
* 函数名: MenuPage_TriggerCurrentAction
* 功能:
* 触发当前菜单项动作的占位接口(当前版本仅输出调试日志)。
*
* 参数:
* 无
*
* 边界处理:
* - 当前实现不依赖外部输入,不涉及参数校验。
*
* 说明:
* - 现阶段用于保留动作触发扩展点,后续可接入真实业务动作执行链路。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
void MenuPage_TriggerCurrentAction(void)
{
printf("MenuPage_TriggerCurrentAction\n");
}

10
src/Drv/pages/menu/page.h Normal file
View File

@@ -0,0 +1,10 @@
#ifndef MENU_PAGE_H
#define MENU_PAGE_H
#include "view.h"
#include "../page.h"
page_t *MenuPage_GetInstance(void);
void MenuPage_TriggerCurrentAction(void);
#endif

View File

@@ -0,0 +1,272 @@
#include "presenter.h"
#include <stddef.h>
#include <string.h>
#include "../../key.h"
typedef struct
{
tagPMenuItem ptHead;
tagPMenuItem ptCurrent;
tagPMenuItem ptRoute[4];
tagPMenuItem ptCurBak;
tagPMenuItem pt0Level;
} MenuNavState;
typedef struct
{
uint8_t needRefresh;
uint8_t skipRenderThisRound;
} MenuNavResult;
static MenuNavResult MenuNavigator_ProcessKey(MenuNavState *navState, uint8_t keyVal)
{
MenuNavResult result;
tagPMenuItem ptCurrent;
tagPMenuItem ptHead;
tagPMenuItem *ptRoute;
result.needRefresh = 0;
result.skipRenderThisRound = 0;
ptCurrent = navState->ptCurrent;
ptHead = navState->ptHead;
ptRoute = navState->ptRoute;
switch (keyVal)
{
case KEY_F1:
case KEY_F2:
break;
case KEY_U:
ptCurrent = ptCurrent->links.before;
result.needRefresh = 1;
break;
case KEY_D:
ptCurrent = ptCurrent->links.behind;
result.needRefresh = 1;
break;
case KEY_L:
ptCurrent = ptRoute[ptCurrent->menuDef.byClass - 1];
if (ptCurrent->menuDef.byClass == 0)
{
if (ptCurrent->rect.wPos == 1)
{
ptCurrent = ptCurrent->links.before->links.before;
}
else
{
ptCurrent = ptCurrent->links.before;
}
ptCurrent = ptCurrent->links.lower;
}
result.needRefresh = 1;
break;
case KEY_R:
case KEY_ENT:
if (ptCurrent->links.lower != NULL)
{
ptCurrent = ptCurrent->links.lower;
result.needRefresh = 1;
}
else if (ptCurrent->menuDef.pfnWinProc != NULL)
{
ptCurrent->menuDef.pfnWinProc();
}
break;
case KEY_ESC:
if (ptCurrent->menuDef.byClass == 1)
{
navState->pt0Level = ptHead;
navState->ptRoute[0] = ptHead;
navState->ptCurrent = ptHead->links.lower;
navState->ptCurBak = navState->ptCurrent;
navState->ptRoute[1] = navState->ptCurrent;
result.skipRenderThisRound = 1;
return result;
}
ptCurrent = ptRoute[ptCurrent->menuDef.byClass - 1];
result.needRefresh = 1;
break;
default:
break;
}
navState->ptCurrent = ptCurrent;
return result;
}
static void MenuNavigator_RebuildRoute(MenuNavState *navState, uint32_t maxItem)
{
tagPMenuItem ptIndex;
if (navState->ptCurBak == navState->ptCurrent)
{
return;
}
ptIndex = navState->ptCurrent;
for (uint32_t index = 0; index < maxItem; index++)
{
if (ptIndex->links.higher == NULL)
{
ptIndex = ptIndex->links.before;
}
else
{
ptIndex = ptIndex->links.higher;
navState->ptRoute[ptIndex->menuDef.byClass] = ptIndex;
}
if (ptIndex->menuDef.byClass == 0)
{
break;
}
}
navState->ptRoute[navState->ptCurrent->menuDef.byClass] = navState->ptCurrent;
}
static void MenuPresenter_FillNavState(const tagMenuCtrl *ctrl, MenuNavState *nav)
{
nav->ptHead = ctrl->ptHead;
nav->ptCurrent = ctrl->ptCurrent;
nav->ptRoute[0] = ctrl->ptRoute[0];
nav->ptRoute[1] = ctrl->ptRoute[1];
nav->ptRoute[2] = ctrl->ptRoute[2];
nav->ptRoute[3] = ctrl->ptRoute[3];
nav->ptCurBak = ctrl->ptCurBak;
nav->pt0Level = ctrl->pt0Level;
}
static void MenuPresenter_ApplyNavState(tagMenuCtrl *ctrl, const MenuNavState *nav)
{
ctrl->ptHead = nav->ptHead;
ctrl->ptCurrent = nav->ptCurrent;
ctrl->ptRoute[0] = nav->ptRoute[0];
ctrl->ptRoute[1] = nav->ptRoute[1];
ctrl->ptRoute[2] = nav->ptRoute[2];
ctrl->ptRoute[3] = nav->ptRoute[3];
ctrl->ptCurBak = nav->ptCurBak;
ctrl->pt0Level = nav->pt0Level;
}
/* -------------------------------------------------------------------------
* 函数名: MenuPresenter_RenderByState
* 功能:
* 根据当前菜单状态差异选择渲染策略(整页刷新 / 同层局部刷新 / 单层补绘)。
*
* 参数:
* presenter - 菜单 Presenter 实例,内部持有 menuCtrl 与 view 接口
*
* 边界处理:
* - 本函数不做空指针校验,默认由调用链保证 presenter/view 有效。
* - 当菜单路由发生跨 0 级变化时,强制走整页刷新,避免多层残影。
* - 仅在“同层且同父”场景使用局部反显更新,其他变化按层级关系降级处理。
*
* 说明:
* - 该函数是 Presenter 的渲染决策核心,不直接参与菜单导航计算。
* - 决策顺序如下:
* 1) 若 pt0Level 与 route[0] 不一致:
* 说明当前顶层上下文已切换,先同步 pt0Level/ptCurBak再 full_refresh。
* 2) 否则若 ptCurBak 与 ptCurrent 不一致:
* a) 同层 + 同父:调用 update_selection_same_level执行“旧选中恢复 + 新选中高亮”
* b) 新层级 >= 旧层级:调用 show_other_level仅补绘受影响层
* c) 其他(通常是回退到更高层):调用 full_refresh保证界面一致性
* 3) 渲染后统一将 ptCurBak 更新为 ptCurrent作为下一轮差异比较基准。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
static void MenuPresenter_RenderByState(MenuPresenter *presenter)
{
tagMenuCtrl *menuCtrl = &presenter->menuCtrl;
MenuView *view = presenter->view;
if (menuCtrl->pt0Level != menuCtrl->ptRoute[0])
{
menuCtrl->pt0Level = menuCtrl->ptRoute[0];
menuCtrl->ptCurBak = menuCtrl->ptCurrent;
presenter->view->full_refresh(presenter->view, menuCtrl, MODE_NONE);
}
else if (menuCtrl->ptCurBak != menuCtrl->ptCurrent)
{
if ((menuCtrl->ptCurrent->menuDef.byClass == menuCtrl->ptCurBak->menuDef.byClass) &&
(menuCtrl->ptCurrent->menuDef.byClass > 0) &&
(menuCtrl->ptCurrent->links.higher == menuCtrl->ptCurBak->links.higher))
{
view->update_selection_same_level(view, menuCtrl->ptCurBak, menuCtrl->ptCurrent);
}
else if (menuCtrl->ptCurrent->menuDef.byClass >= menuCtrl->ptCurBak->menuDef.byClass)
{
view->update_selection_new_level(view, menuCtrl->ptRoute[menuCtrl->ptCurrent->menuDef.byClass - 1], menuCtrl->ptRoute[menuCtrl->ptCurrent->menuDef.byClass]);
}
else
{
presenter->view->full_refresh(presenter->view, menuCtrl, MODE_NONE);
}
menuCtrl->ptCurBak = menuCtrl->ptCurrent;
}
}
void MenuPresenter_Init(MenuPresenter *presenter, menu_model_t *model, menu_view_t *view)
{
presenter->dspCtrl.bFirst = 0;
memset(&presenter->menuCtrl, 0, sizeof(presenter->menuCtrl));
presenter->model = model;
presenter->view = view;
presenter->dspCtrl.bFirst = 1;
presenter->menuCtrl.ptHead = &presenter->model->menuItems[0];
presenter->menuCtrl.pt0Level = &presenter->model->menuItems[0];
presenter->menuCtrl.ptCurrent = presenter->menuCtrl.ptHead->links.lower;
presenter->menuCtrl.ptCurBak = presenter->menuCtrl.ptCurrent;
presenter->menuCtrl.ptRoute[0] = &presenter->model->menuItems[0];
presenter->menuCtrl.ptRoute[1] = presenter->menuCtrl.ptCurrent;
presenter->menuCtrl.ptRoute[2] = presenter->menuCtrl.ptCurrent;
presenter->menuCtrl.ptRoute[3] = presenter->menuCtrl.ptCurrent;
}
void MenuPresenter_Refresh(MenuPresenter *presenter)
{
if (presenter->dspCtrl.bFirst)
{
presenter->dspCtrl.bFirst = 0;
presenter->view->full_refresh(presenter->view, &presenter->menuCtrl, MODE_NONE);
}
else
{
MenuPresenter_RenderByState(presenter);
}
}
void MenuPresenter_HandleInput(MenuPresenter *presenter, uint8_t keyVal)
{
MenuNavState navState;
MenuNavResult navResult;
if (presenter->dspCtrl.bFirst)
{
MenuPresenter_Refresh(presenter);
return;
}
MenuPresenter_FillNavState(&presenter->menuCtrl, &navState);
navResult = MenuNavigator_ProcessKey(&navState, keyVal);
MenuPresenter_ApplyNavState(&presenter->menuCtrl, &navState);
if (navResult.skipRenderThisRound)
{
return;
}
if (navResult.needRefresh)
{
MenuNavState rebuildState;
MenuPresenter_FillNavState(&presenter->menuCtrl, &rebuildState);
MenuNavigator_RebuildRoute(&rebuildState, presenter->model->maxItem);
MenuPresenter_ApplyNavState(&presenter->menuCtrl, &rebuildState);
MenuPresenter_Refresh(presenter);
}
}

View File

@@ -0,0 +1,33 @@
#ifndef MENU_PRESENTER_H
#define MENU_PRESENTER_H
#include "display.h"
#include "model.h"
#include "view.h"
typedef struct menu_presenter_t menu_presenter_t;
typedef struct
{
uint8_t bFirst;
} tagDspCtrl;
struct menu_presenter_t
{
tagDspCtrl dspCtrl;
tagMenuCtrl menuCtrl;
menu_model_t *model;
menu_view_t *view;
};
typedef menu_presenter_t MenuPresenter;
void MenuPresenter_Init(MenuPresenter *presenter,
menu_model_t *model,
menu_view_t *view);
void MenuPresenter_HandleInput(MenuPresenter *presenter, uint8_t keyVal);
void MenuPresenter_Refresh(MenuPresenter *presenter);
#endif

313
src/Drv/pages/menu/view.c Normal file
View File

@@ -0,0 +1,313 @@
#include <stddef.h>
#include <string.h>
#include "view.h"
static const PageRenderPort *s_port = NULL;
/* -------------------------------------------------------------------------
* 函数名: MenuView_DrawMeitou
* 功能:
* 绘制菜单标题装饰线(“眉头”样式):中间横线 + 左右两端斜线。
*
* 参数:
* view - 菜单视图对象,提供底层渲染端口
* yStart - 装饰线基准 Y 坐标(横线所在行)
* width - 线宽参数(传递给 line_h/line
*
* 边界处理:
* - 本函数不做空指针判定,调用方需保证 view 与 s_port 有效。
* - 坐标范围合法性由上层布局计算保证。
*
* 说明:
* - 颜色统一使用字体前景色get_color_font确保装饰与文字风格一致。
* - 绘制顺序:
* 1) 中间横线x:16~144
* 2) 左斜线8, yStart-8 -> 16, yStart
* 3) 右斜线144, yStart -> 152, yStart-8
* - 该函数用于视觉分隔与层次强调,不改变菜单状态数据。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
static void MenuView_DrawMeitou(MenuView *view, uint16_t yStart, uint16_t width)
{
uint8_t fontColor = s_port->get_color_font();
s_port->line_h(16, 144, yStart, width, fontColor);
s_port->line(8, yStart - 8, 16, yStart, width, fontColor);
s_port->line(144, yStart, 152, yStart - 8, width, fontColor);
}
/* -------------------------------------------------------------------------
* 函数名: MenuView_DrawBoundaryBox
* 功能:
* 绘制矩形边界框:先清空内部区域,再绘制四条边线。
*
* 参数:
* view - 菜单视图对象,提供像素与线段绘制接口
* leftX - 左边界 X
* topY - 上边界 Y
* rightX - 右边界 X
* bottomY - 下边界 Y
*
* 边界处理:
* - 本函数不校验 view/port 空指针,调用方需保证有效。
* - 默认要求 leftX <= rightX、topY <= bottomY
* 若参数异常,像素循环或画线结果由底层驱动行为决定。
*
* 说明:
* - 背景填充逐像素将内部区域置为背景色get_color_back
* 避免残影或旧内容叠加。
* - 边框绘制统一使用字体前景色get_color_font绘制上/左/下/右四边。
* - 下边框使用 rightX + 1目的是与其它边界在视觉上闭合对齐。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
static void MenuView_DrawBoundaryBox(MenuView *view, uint16_t leftX, uint16_t topY, uint16_t rightX, uint16_t bottomY)
{
uint8_t backColor = s_port->get_color_back();
uint8_t fontColor = s_port->get_color_font();
for (uint16_t y = topY; y < bottomY; y++)
{
for (uint16_t x = leftX; x < rightX; x++)
{
s_port->set_pixel(x, y, backColor);
}
}
s_port->line_h(leftX, rightX, topY, 1, fontColor);
s_port->line_v(topY, bottomY, leftX, 1, fontColor);
s_port->line_h(leftX, rightX + 1, bottomY, 1, fontColor);
s_port->line_v(topY, bottomY, rightX, 1, fontColor);
}
/* -------------------------------------------------------------------------
* 函数名: MenuView_UpdateSelectionNewLevel
* 功能:
* 在“进入新层级”场景下,重绘目标层级窗口内容(边框 + 文本)
* 并对当前选中项执行反显高亮。
*
* 参数:
* view - 菜单视图对象,提供底层渲染接口
* currentLevel - 当前层级的“层级框拥有者”节点,提供窗口矩形与数量信息
* nextLevel - 当前层级内的选中节点(也是该层列表起始遍历节点)
*
* 边界处理:
* - 本函数不做空指针判定,调用方需保证 view/currentLevel/nextLevel 有效。
* - 默认要求 currentLevel 与 nextLevel 来自同一层级链路,且 behind 链可遍历。
* - 当 currentLevel->rect.wNum 为 0 时,文本循环不执行,
* 但边框仍会被绘制。
*
* 说明:
* - 绘制步骤:
* 1) 使用 currentLevel->rect 绘制层级边框区域
* 2) 从 nextLevel 起,按 wNum 数量沿 behind 链逐项绘制名称
* 3) 基于 nextLevel 的 wPos 计算行区域并执行 invert 高亮
* - 坐标策略:
* wPosX = currentLevel->rect.wSPosX + 4文本左内边距
* wPosY = currentLevel->rect.wSPosY + item->rect.wPos * LINE_HEIGHT + 3
* - 该函数对应“新层级内容补绘”路径,与同层移动的局部反显函数职责分离。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
static void MenuView_UpdateSelectionNewLevel(MenuView *view, const tagPMenuItem currentLevel, const tagPMenuItem nextLevel)
{
tagPMenuItem ptIndex;
uint16_t wPosX;
uint16_t wPosY;
/* 1. 绘制当前层级的边界框 */
MenuView_DrawBoundaryBox(view, currentLevel->rect.wSPosX, currentLevel->rect.wSPosY, currentLevel->rect.wEPosX, currentLevel->rect.wEPosY);
/*2. 获取当前层级选中菜单项并计算X起始坐标*/
ptIndex = nextLevel;
/* 计算当前菜单项的 X 坐标,对于下一层级起始 X 坐标都是一致的 */
wPosX = currentLevel->rect.wSPosX + 4;
/* 3. 绘制当前层级的所有菜单项 */
for (uint16_t index = 0; index < currentLevel->rect.wNum; index++)
{
wPosY = currentLevel->rect.wSPosY + ptIndex->rect.wPos * LINE_HEIGHT + 3;
s_port->show_str(wPosX, wPosY, ptIndex->menuDef.byName);
/* 移动到下一个菜单项 */
ptIndex = ptIndex->links.behind;
}
/* 4. 绘制当前选中项的反显 */
wPosY = currentLevel->rect.wSPosY + nextLevel->rect.wPos * LINE_HEIGHT + 3;
s_port->invert(currentLevel->rect.wSPosX + 2, wPosY - 1, currentLevel->rect.wEPosX - 2, wPosY + LINE_HEIGHT);
}
/* -------------------------------------------------------------------------
* 函数名: MenuView_ShowTopLevel
* 功能:
* 绘制菜单页顶部区域0 级标题栏):清空标题背景、绘制装饰线,
* 并按传入模式显示顶部提示文案。
*
* 参数:
* view - 菜单视图对象,提供渲染端口能力
* mode - 当前需要显示的运行模式MenuMode
*
* 边界处理:
* - 本函数不做 view/port 空指针校验,调用方需保证对象已正确初始化。
* - 模式值若不在已定义枚举分支内,走默认分支显示“当前模式: 无模式”。
*
* 说明:
* - 先清空顶部区域y: 0~32避免旧文案残留。
* - 再调用 MenuView_DrawMeitou() 绘制标题装饰线,保持页面视觉一致性。
* - 模式文案映射:
* MODE_OVERFLOW_PROTECTION -> "当前模式: 过流保护"
* MODE_LOCAL_FEEDER_SEGMENT -> "就地馈线: 分段模式"
* MODE_LOCAL_FEEDER_CONTACT -> "就地馈线: 联络模式"
* 其他/未设置 -> "当前模式: 无模式"
* - 本函数只负责“顶部区域”渲染,不处理菜单列表区内容刷新。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
static void MenuView_ShowTopLevel(MenuView *view, MenuMode mode)
{
uint16_t lcdSizeX = s_port->get_size_x();
s_port->fill_rect(0, 0, lcdSizeX - 1, 32, s_port->get_color_back());
MenuView_DrawMeitou(view, 16, 2);
/* 在 0 级菜单标题栏下方显示当前运行模式提示文字 */
if (mode == MODE_OVERFLOW_PROTECTION)
{
s_port->show_str(16, 20, (uint8_t *)"当前模式: 过流保护");
}
else if (mode == MODE_LOCAL_FEEDER_SEGMENT)
{
s_port->show_str(16, 20, (uint8_t *)"就地馈线: 分段模式");
}
else if (mode == MODE_LOCAL_FEEDER_CONTACT)
{
s_port->show_str(16, 20, (uint8_t *)"就地馈线: 联络模式");
}
else
{
s_port->show_str(16, 20, (uint8_t *)"当前模式: 无模式");
}
}
/* -------------------------------------------------------------------------
* 函数名: MenuView_UpdateSelectionSameLevel
* 功能:
* 在“同层同父节点”移动场景下执行局部刷新,仅更新旧/新选中行的反显状态,
* 避免整块区域重绘带来的闪烁。
*
* 参数:
* view - 菜单视图对象,提供底层 invert 绘制能力
* oldItem - 变更前的选中菜单项(旧焦点)
* newItem - 变更后的选中菜单项(新焦点)
*
* 边界处理:
* - 本函数不做空指针与父链校验,调用方需保证:
* 1) view/s_port 有效
* 2) oldItem/newItem 非空
* 3) oldItem->links.higher 非空且与 newItem 属于同一父节点
* - 若上述前置条件不满足,坐标计算与反显结果将不可预期。
*
* 说明:
* - 该函数是 Presenter 在“同层同父移动”场景(典型 U/D的优化渲染路径。
* - 通过两次 invert 完成“旧选中恢复 + 新选中高亮”:
* 1) 对旧行区域 invert撤销旧高亮
* 2) 对新行区域 invert施加新高亮
* - 坐标基准来自 oldItem 的父节点levelRectOwner = oldItem->links.higher
* posY = levelRectOwner->rect.wSPosY + (item->rect.wPos - 1) * LINE_HEIGHT + 3
* - X 方向使用边框内缩(+2/-2避免覆盖边框线条保持视觉完整性。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
static void MenuView_UpdateSelectionSameLevel(MenuView *view, const tagPMenuItem oldItem, const tagPMenuItem newItem)
{
tagPMenuItem levelRectOwner = oldItem->links.higher;
uint16_t leftX = (uint16_t)(levelRectOwner->rect.wSPosX + 2);
uint16_t rightX = (uint16_t)(levelRectOwner->rect.wEPosX - 2);
uint16_t oldPosY = (uint16_t)(levelRectOwner->rect.wSPosY + oldItem->rect.wPos * LINE_HEIGHT + 3);
uint16_t newPosY = (uint16_t)(levelRectOwner->rect.wSPosY + newItem->rect.wPos * LINE_HEIGHT + 3);
/* 旧选中行反显恢复 + 新选中行反显高亮 */
s_port->invert(leftX, (uint16_t)(oldPosY - 1), rightX, (uint16_t)(oldPosY + LINE_HEIGHT));
s_port->invert(leftX, (uint16_t)(newPosY - 1), rightX, (uint16_t)(newPosY + LINE_HEIGHT));
}
/* -------------------------------------------------------------------------
* 函数名: MenuView_FullRefresh
* 功能:
* 执行菜单页整页刷新:清空工作区、重绘顶部标题区域、按当前层级路径重绘各级菜单框。
*
* 参数:
* view - 菜单视图对象,提供布局参数与绘制能力
* menuCtrl - 菜单运行时控制结构,提供当前层级与路由链信息
* mode - 顶部标题显示使用的运行模式
*
* 边界处理:
* - 本函数不做空指针校验,调用方需保证 view/menuCtrl/s_port 均有效。
* - 当 byClass 为 0 时for 循环不进入,仅刷新顶部区域。
* - 默认要求 ptRoute[index] 与 ptRoute[index + 1] 在有效层级范围内可访问。
*
* 说明:
* - 刷新步骤:
* 1) 使用背景色清空菜单工作区域MENU_YMIN ~ menuYMax
* 2) 调用 MenuView_ShowTopLevel() 重绘顶部模式文案与装饰
* 3) 按当前菜单层级byClass逐层调用 MenuView_UpdateSelectionNewLevel()
* 重绘各层边框、文本和选中态
* - 该函数用于“整页一致性恢复”场景(如跨层回退、顶层切换等)。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
static void MenuView_FullRefresh(MenuView *view, const tagMenuCtrl *menuCtrl, MenuMode mode)
{
uint8_t backColor = s_port->get_color_back();
uint16_t lcdSizeX = s_port->get_size_x();
s_port->fill_rect(0, MENU_YMIN, lcdSizeX - 1, MENU_YMAX, backColor);
MenuView_ShowTopLevel(view, mode);
for (uint8_t index = 0; index < menuCtrl->ptCurrent->menuDef.byClass; index++)
{
MenuView_UpdateSelectionNewLevel(view, menuCtrl->ptRoute[index], menuCtrl->ptRoute[index + 1]);
}
}
/* -------------------------------------------------------------------------
* 函数名: MenuView_Init
* 功能:
* 初始化菜单视图对象 MenuView准备渲染端口引用与布局参数
* 并设置当前模式状态的默认值以及渲染入口函数指针。
*
* 参数:
* view - 菜单视图实例地址(由调用方分配,函数内填充其字段)
*
* 边界处理:
* - 本函数不校验 view 空指针,调用方需保证 view 非空。
* - 渲染端口的物理 init 不在视图内执行;
* 由入口main统一完成屏幕初始化符合 PageRenderPort 共享策略)。
*
* 说明:
* - 初始化内容包含:
* 1) s_port = PageRenderer_Lcd():绑定 LCD 渲染端口表指针
* 4) 设置当前模式为 MODE_NONE避免进入绘制前读取未定义值
* 5) 视图层仅保留绘制原子动作,刷新策略由 Presenter 调度
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
void MenuView_Init(MenuView *view)
{
s_port = PageRenderer_Lcd();
view->update_selection_new_level = MenuView_UpdateSelectionNewLevel;
view->update_selection_same_level = MenuView_UpdateSelectionSameLevel;
view->full_refresh = MenuView_FullRefresh;
}
#ifdef UNIT_TEST
const PageRenderPort *MenuView_GetPortForTest(void)
{
return s_port;
}
#endif

44
src/Drv/pages/menu/view.h Normal file
View File

@@ -0,0 +1,44 @@
#ifndef MENU_VIEW_H
#define MENU_VIEW_H
#include "types.h"
#include "display.h"
#include "def.h"
#include "../global/renderer_lcd.h"
typedef struct menu_view_t menu_view_t;
/* -------------------------------------------------------------------------
* 结构体名: menu_view_t
* 功能:
* 菜单页面 View 层对象,封装对外绘制接口函数指针。
*
* 字段说明:
* update_selection_new_level - 新层级切换时的层级窗口重绘与高亮更新
* update_selection_same_level - 同层移动时的局部反显更新(降低闪烁)
* full_refresh - 整页刷新入口(顶部 + 多层菜单内容)
*
* 说明:
* - Presenter 通过该结构体调用 View 能力,避免直接依赖 view.c 内部实现细节。
* - 函数指针在 MenuView_Init() 中完成绑定。
* ------------------------------------------------------------------------- */
struct menu_view_t
{
void (*update_selection_new_level)(struct menu_view_t *self, const tagPMenuItem currentLevel, const tagPMenuItem nextLevel);
void (*update_selection_same_level)(struct menu_view_t *self, const tagPMenuItem oldItem, const tagPMenuItem newItem);
void (*full_refresh)(struct menu_view_t *self, const tagMenuCtrl *menuCtrl, MenuMode mode);
};
typedef menu_view_t MenuView;
void MenuView_Init(MenuView *view);
#ifdef UNIT_TEST
const PageRenderPort *MenuView_GetPortForTest(void);
#endif
#endif

125
src/Drv/pages/page.h Normal file
View File

@@ -0,0 +1,125 @@
#ifndef PAGE_H
#define PAGE_H
#include "types.h"
/* -------------------------------------------------------------------------
* 枚举名: page_id_t
* 作用:
* 定义系统内可导航页面的逻辑标识Page ID
*
* 取值说明:
* PAGE_ID_NONE - 无效页面 ID / 未初始化占位值
* PAGE_ID_MENU - 菜单页 ID当前主运行页
* PAGE_ID_APP_INFO - 预留页面 ID当前版本可注册与否由上层决定
* PAGE_ID_MAX - 上界哨兵,不可作为有效页面 ID 使用
*
* 使用约束:
* - PageManager_Register() 会校验 page_idPAGE_ID_NONE 与 >= PAGE_ID_MAX
* 均视为非法值。
* ------------------------------------------------------------------------- */
typedef enum
{
PAGE_ID_NONE = 0,
PAGE_ID_MENU = 1,
PAGE_ID_APP_INFO = 2, /* 预留ID当前版本未注册运行 */
PAGE_ID_MAX
} page_id_t;
/* -------------------------------------------------------------------------
* 枚举名: page_event_type_t
* 作用:
* 定义页面层可分发的事件类型。
*
* 当前约定:
* PAGE_EVENT_KEY - 按键输入事件
*
* 扩展说明:
* - 后续可按需扩展触摸、定时器、通信消息等事件类型。
* ------------------------------------------------------------------------- */
typedef enum
{
PAGE_EVENT_KEY = 1
} page_event_type_t;
/* -------------------------------------------------------------------------
* 结构体名: input_event_t
* 作用:
* 页面事件分发的数据载体,由 PageManager_DispatchEvent() 传入页面 on_event。
*
* 字段说明:
* type - 事件类型,取值来自 page_event_type_t
* keyVal - 按键值(当 type 为 PAGE_EVENT_KEY 时有效)
*
* 使用约束:
* - 调用方应保证 type/keyVal 与事件来源一致;
* - 页面 on_event 可基于该结构返回 EVENT_HANDLED / EVENT_UNHANDLED。
* ------------------------------------------------------------------------- */
typedef struct
{
uint8_t type;
uint8_t keyVal;
} input_event_t;
/* -------------------------------------------------------------------------
* 枚举名: event_result_t
* 作用:
* 定义页面事件处理结果,用于控制事件链是否继续兜底处理。
*
* 取值说明:
* EVENT_UNHANDLED - 页面未消费事件;允许交由管理器执行全局兜底逻辑
* EVENT_HANDLED - 页面已消费事件;事件链终止
* ------------------------------------------------------------------------- */
typedef enum
{
EVENT_UNHANDLED = 0,
EVENT_HANDLED = 1
} event_result_t;
/* 前置声明:支持在回调签名中使用 page_t* */
typedef struct page_t page_t;
/* -------------------------------------------------------------------------
* 结构体名: page_t
* 作用:
* 页面抽象基元,描述“一个可被 PageManager 管理的页面实例”。
*
* 字段分组:
* 1) 基础元信息
* page_id - 页面逻辑标识
* is_cached - 是否缓存页1: Pop 后不销毁0: Pop 后可销毁)
* is_created - 是否已执行过 on_create由管理器维护
*
* 2) 生命周期与事件回调
* on_create - 首次创建时调用
* on_enter - 页面进入前台时调用
* on_exit - 页面离开前台时调用
* on_destroy - 页面销毁时调用(常用于非缓存页)
* on_event - 输入事件处理回调
* on_loop - 周期循环回调
*
* 3) 三层对象挂载指针MVP
* presenter / view / model - 页面内部层对象地址(可为空,按页面实现决定)
*
* 生命周期约定(由 PageManager 驱动):
* - Push 到新页面时:旧页 on_exit -> 新页(必要时 on_create-> 新页 on_enter
* - Pop 回退时:当前页 on_exit ->(非缓存页可 on_destroy-> 新栈顶 on_enter
* ------------------------------------------------------------------------- */
struct page_t
{
page_id_t page_id;
uint8_t is_cached;
uint8_t is_created;
void (*on_create)(page_t *page);
void (*on_enter)(page_t *page);
void (*on_exit)(page_t *page);
void (*on_destroy)(page_t *page);
event_result_t (*on_event)(page_t *page, input_event_t *event);
void (*on_loop)(page_t *page);
void *presenter;
void *view;
void *model;
};
#endif

View File

@@ -0,0 +1,448 @@
#include <stddef.h>
#include <string.h>
#include "page_manager.h"
#include "../key.h"
/* 默认全局页面管理器实例(便于主流程直接使用) */
static page_manager_t s_pageManager;
/* -------------------------------------------------------------------------
* 函数名: PageManager_GlobalEventHandle
* 功能:
* 页面管理器的全局事件兜底处理入口,用于处理未被页面消费的通用按键逻辑。
*
* 参数:
* event - 输入事件指针
*
* 边界处理:
* - 若 event 为 NULL函数直接返回。
* - 若事件类型不是 PAGE_EVENT_KEY函数直接返回不做处理。
* - 仅当 keyVal 为 KEY_ESC 时触发返回上一页,其它按键忽略。
*
* 说明:
* - 该函数通常由 PageManager_DispatchEvent 在页面返回
* EVENT_UNHANDLED 时调用。
* - 当前全局策略为“ESC = 返回”:内部通过 PageManager_Pop()
* 执行页面栈回退。
* - 设计目标是在不侵入页面私有逻辑的前提下,提供统一系统级快捷行为。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
static void PageManager_GlobalEventHandle(input_event_t *event)
{
ASSERT((event == NULL) || (event->type != PAGE_EVENT_KEY));
if ((event == NULL) || (event->type != PAGE_EVENT_KEY))
{
return;
}
if (event->keyVal == KEY_ESC)
{
(void)PageManager_Pop();
}
}
/* -------------------------------------------------------------------------
* 函数名: PageManager_Init
* 功能:
* 初始化单例页面管理器 s_pageManager使其进入“空栈、可注册”的初始状态。
*
* 参数:
* 无
*
* 边界处理:
* - 本函数无外部入参,不涉及空指针校验。
* - 通过 memset 全量清零,确保历史运行残留状态(栈、注册表、计数)被彻底复位。
*
* 说明:
* - stack_top 采用 -1 表示页面栈为空,这是后续 Push/GetTop/Depth 的统一判定基准。
* - 该函数通常在系统启动阶段调用一次;若重复调用,将清空当前页面管理上下文。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
void PageManager_Init(void)
{
memset(&s_pageManager, 0, sizeof(s_pageManager));
s_pageManager.stack_top = -1;
}
/* -------------------------------------------------------------------------
* 函数名: PageManager_Register
* 功能:
* 将页面对象注册到页面管理器的注册表中,供后续按 page_id 查找与导航。
*
* 参数:
* page - 待注册页面指针
*
* 边界处理:
* - 若 page 为 NULL返回 PAGE_MANAGER_ERR_NULL_PARAM不执行注册。
* - 若 page_id 非法PAGE_ID_NONE 或超出 PAGE_ID_MAX返回 PAGE_MANAGER_ERR_INVALID_ID。
* - 若注册表已满page_count >= MAX_PAGE_COUNT返回 PAGE_MANAGER_ERR_REGISTRY_FULL。
*
* 说明:
* - 若检测到相同 page_id 已存在,不新增条目,而是覆盖旧指针(热更新语义)。
* - 新页面按 page_count 尾插,成功后 page_count 自增 1。
*
* 返回值:
* - PAGE_MANAGER_OK : 注册成功(含覆盖更新成功)
* - PAGE_MANAGER_ERR_NULL_PARAM : 参数无效
* - PAGE_MANAGER_ERR_INVALID_ID : page_id 非法
* - PAGE_MANAGER_ERR_REGISTRY_FULL : 注册表容量不足
* ------------------------------------------------------------------------- */
page_manager_result_t PageManager_Register(page_t *page)
{
ASSERT(page == NULL);
if (page == NULL)
{
return PAGE_MANAGER_ERR_NULL_PARAM;
}
ASSERT((page->page_id == PAGE_ID_NONE) || (page->page_id >= PAGE_ID_MAX));
if ((page->page_id == PAGE_ID_NONE) || (page->page_id >= PAGE_ID_MAX))
{
return PAGE_MANAGER_ERR_INVALID_ID;
}
for (uint8_t i = 0; i < s_pageManager.page_count; i++)
{
/* 支持重复注册:同 page_id 视为更新页面指针 */
if (s_pageManager.page_registry[i]->page_id == page->page_id)
{
s_pageManager.page_registry[i] = page;
return PAGE_MANAGER_OK;
}
}
ASSERT(s_pageManager.page_count >= MAX_PAGE_COUNT);
if (s_pageManager.page_count >= MAX_PAGE_COUNT)
{
return PAGE_MANAGER_ERR_REGISTRY_FULL;
}
s_pageManager.page_registry[s_pageManager.page_count] = page;
s_pageManager.page_count++;
return PAGE_MANAGER_OK;
}
/* -------------------------------------------------------------------------
* 函数名: PageManager_Find
* 功能:
* 在管理器的页面注册表中按 page_id 线性查找,返回对应的 page_t 指针。
*
* 参数:
* pageId - 待查找的页面标识
*
* 边界处理:
* - 若注册表为空page_count 为 0循环不执行返回 NULL。
* - 若不存在匹配的 page_id返回 NULL。
*
* 说明:
* - 查找顺序与注册顺序一致;同 id 在 Register 中已保证唯一或覆盖更新。
* - 常与 PageManager_Navigate 配合:先 Find 再 Push上层也可直接 Find 获取页面对象。
*
* 返回值:
* - 命中时返回注册表中的 page_t 指针
* - 未命中返回 NULL
* ------------------------------------------------------------------------- */
page_t *PageManager_Find(page_id_t pageId)
{
for (uint8_t i = 0; i < s_pageManager.page_count; i++)
{
if (s_pageManager.page_registry[i]->page_id == pageId)
{
return s_pageManager.page_registry[i];
}
}
return NULL;
}
/* -------------------------------------------------------------------------
* 函数名: PageManager_GetTop
* 功能:
* 返回当前页面栈栈顶元素,即当前“前台/可见”页面对应的 page_t 指针。
*
* 参数:
* 无
*
* 边界处理:
* - stack_top < 0 表示空栈,返回 NULL。
* - 正常栈顶索引由 Push/Pop 维护,假定栈槽位与 stack_top 一致。
*
* 说明:
* - DispatchEvent、Loop、Navigate 等逻辑均以栈顶页为“当前页”。
* - 不修改栈状态,仅查询。
*
* 返回值:
* - 非空栈时返回栈顶 page_t 指针
* - 空栈时返回 NULL
* ------------------------------------------------------------------------- */
page_t *PageManager_GetTop(void)
{
if (s_pageManager.stack_top < 0)
{
return NULL;
}
return s_pageManager.page_stack[s_pageManager.stack_top];
}
/* -------------------------------------------------------------------------
* 函数名: PageManager_Push
* 功能:
* 将指定页面压入页面栈并切换为当前页:必要时触发旧页 on_exit、新页 on_create/on_enter。
*
* 参数:
* newPage - 要入栈并成为栈顶的目标页面指针
*
* 边界处理:
* - 若 newPage 为 NULL返回 PAGE_MANAGER_ERR_NULL_PARAM。
* - 若栈已满stack_top >= MAX_PAGE_STACK_DEPTH - 1返回 PAGE_MANAGER_ERR_STACK_FULL。
* - 若当前栈顶与 newPage 的 page_id 相同,视为重复导航,直接返回 PAGE_MANAGER_OK
* 不再次调用 on_exit/on_enter幂等
*
* 说明:
* - 切换顺序:旧栈顶 on_exit若存在-> 新页入栈 ->
* 若 is_created 为 0 则 on_create 并置 is_created再 on_enter。
* - 页面“创建一次”语义由 is_created 控制;非缓存页在 Pop 时可销毁并重置 is_created。
*
* 返回值:
* - PAGE_MANAGER_OK : 成功(含幂等重复入同一逻辑页)
* - PAGE_MANAGER_ERR_NULL_PARAM : 参数无效
* - PAGE_MANAGER_ERR_STACK_FULL : 栈满
* ------------------------------------------------------------------------- */
page_manager_result_t PageManager_Push(page_t *newPage)
{
page_t *currentTop;
ASSERT(newPage == NULL);
if (newPage == NULL)
{
return PAGE_MANAGER_ERR_NULL_PARAM;
}
ASSERT(s_pageManager.stack_top >= (MAX_PAGE_STACK_DEPTH - 1));
if (s_pageManager.stack_top >= (MAX_PAGE_STACK_DEPTH - 1))
{
return PAGE_MANAGER_ERR_STACK_FULL;
}
currentTop = PageManager_GetTop();
ASSERT((currentTop != NULL) && (currentTop->page_id == newPage->page_id));
if ((currentTop != NULL) && (currentTop->page_id == newPage->page_id))
{
/* 重复导航到当前页面:按幂等处理 */
return PAGE_MANAGER_OK;
}
ASSERT((currentTop != NULL) && (currentTop->on_exit != NULL));
if ((currentTop != NULL) && (currentTop->on_exit != NULL))
{
currentTop->on_exit(currentTop);
}
s_pageManager.stack_top++;
s_pageManager.page_stack[s_pageManager.stack_top] = newPage;
if (!newPage->is_created)
{
/* 页面只创建一次(除非在 Pop 时被销毁并重置) */
if (newPage->on_create != NULL)
{
newPage->on_create(newPage);
}
newPage->is_created = 1;
}
if (newPage->on_enter != NULL)
{
newPage->on_enter(newPage);
}
return PAGE_MANAGER_OK;
}
/* -------------------------------------------------------------------------
* 函数名: PageManager_Pop
* 功能:
* 弹出当前栈顶页面并回到下一层:触发当前页 on_exit非缓存页可 on_destroy
* 再对新栈顶调用 on_enter。
*
* 参数:
* 无
*
* 边界处理:
* - 若 stack_top <= 0仅余底页或空栈不允许继续弹出
* 返回 PAGE_MANAGER_ERR_STACK_BOTTOM。
* - 当前页指针为 NULL 时跳过对应回调(防御性,正常不应出现)。
*
* 说明:
* - 设计为至少保留栈底一页:不能无限 Pop 到空栈,故 stack_top <= 0 即失败。
* - 非缓存页is_cached == 0在 Pop 后会 on_destroy 且 is_created 清零,下次 Push 会重新 on_create。
* - 缓存页仅 on_exit不销毁便于保留状态。
* - 全局 ESC 兜底通过本函数回退上一页。
*
* 返回值:
* - PAGE_MANAGER_OK : 出栈成功,新栈顶已 on_enter若实现
* - PAGE_MANAGER_ERR_STACK_BOTTOM : 无法弹出(已到底或空栈)
* ------------------------------------------------------------------------- */
page_manager_result_t PageManager_Pop(void)
{
page_t *currentPage;
page_t *newTop;
ASSERT(s_pageManager.stack_top <= 0);
if (s_pageManager.stack_top <= 0)
{
return PAGE_MANAGER_ERR_STACK_BOTTOM;
}
currentPage = s_pageManager.page_stack[s_pageManager.stack_top];
ASSERT((currentPage != NULL) && (currentPage->on_exit != NULL));
if ((currentPage != NULL) && (currentPage->on_exit != NULL))
{
currentPage->on_exit(currentPage);
}
if ((currentPage != NULL) && !currentPage->is_cached)
{
/* 非缓存页离开即销毁,下次进入会重新 on_create */
if (currentPage->on_destroy != NULL)
{
currentPage->on_destroy(currentPage);
}
currentPage->is_created = 0;
}
s_pageManager.page_stack[s_pageManager.stack_top] = NULL;
s_pageManager.stack_top--;
newTop = s_pageManager.page_stack[s_pageManager.stack_top];
ASSERT((newTop != NULL) && (newTop->on_enter != NULL));
if ((newTop != NULL) && (newTop->on_enter != NULL))
{
newTop->on_enter(newTop);
}
return PAGE_MANAGER_OK;
}
/* -------------------------------------------------------------------------
* 函数名: PageManager_Navigate
* 功能:
* 根据目标 page_id 执行页面导航:先在注册表中查找页面,再将其压入页面栈。
*
* 参数:
* pageId - 目标页面标识
*
* 边界处理:
* - 若注册表中未找到对应 pageId返回 PAGE_MANAGER_ERR_NOT_FOUND
* 不执行页面切换。
* - 目标页找到后Push 过程中的边界条件(如栈满、重复入栈等)
* 由 PageManager_Push 统一处理并返回对应错误码。
*
* 说明:
* - 本函数是“按 ID 导航”的统一入口,避免上层直接操作 page_t 指针。
* - 导航成功后将触发标准生命周期链路:
* 旧页 on_exit -> 新页(必要时 on_create-> 新页 on_enter。
*
* 返回值:
* - PAGE_MANAGER_OK : 导航成功
* - PAGE_MANAGER_ERR_NOT_FOUND : 未找到目标页面
* - 其他 : 透传 PageManager_Push 的返回码(如栈满等)
* ------------------------------------------------------------------------- */
page_manager_result_t PageManager_Navigate(page_id_t pageId)
{
page_t *target;
target = PageManager_Find(pageId);
ASSERT(target == NULL);
if (target == NULL)
{
return PAGE_MANAGER_ERR_NOT_FOUND;
}
return PageManager_Push(target);
}
/* -------------------------------------------------------------------------
* 函数名: PageManager_DispatchEvent
* 功能:
* 将输入事件分发到当前栈顶页面,并在页面未处理时执行全局兜底处理。
*
* 参数:
* event - 待分发的输入事件指针(键值、事件类型等)
*
* 边界处理:
* - 若 event 为 NULL返回 PAGE_MANAGER_ERR_NULL_PARAM不进行任何处理。
* - 若当前页面栈为空GetTop 返回 NULL返回 PAGE_MANAGER_ERR_NO_ACTIVE_PAGE。
* - 若栈顶页面未实现 on_event 回调,则视为未处理并进入兜底判定流程。
*
* 说明:
* - 分发优先级:页面私有事件处理 > 管理器全局兜底处理。
* - top->on_event 返回 EVENT_UNHANDLED 时,才调用
* PageManager_GlobalEventHandle(event)。
* - 该机制允许页面覆盖默认按键行为,同时保留统一的系统级快捷逻辑
* (例如 ESC 返回上页)。
*
* 返回值:
* - PAGE_MANAGER_OK : 分发流程执行完成
* - PAGE_MANAGER_ERR_NULL_PARAM : 事件参数为空
* - PAGE_MANAGER_ERR_NO_ACTIVE_PAGE : 当前无活动页面
* ------------------------------------------------------------------------- */
page_manager_result_t PageManager_DispatchEvent(input_event_t *event)
{
page_t *top;
event_result_t result;
ASSERT(event == NULL);
if (event == NULL)
{
return PAGE_MANAGER_ERR_NULL_PARAM;
}
top = PageManager_GetTop();
ASSERT(top == NULL);
if (top == NULL)
{
return PAGE_MANAGER_ERR_NO_ACTIVE_PAGE;
}
result = EVENT_UNHANDLED;
if (top->on_event != NULL)
{
result = top->on_event(top, event);
}
/* 页面未处理时交给管理器处理全局快捷行为 */
if (result == EVENT_UNHANDLED)
{
PageManager_GlobalEventHandle(event);
}
return PAGE_MANAGER_OK;
}
/* -------------------------------------------------------------------------
* 函数名: PageManager_Loop
* 功能:
* 驱动当前栈顶页面的循环回调on_loop用于执行周期性任务。
*
* 参数:
* 无
*
* 边界处理:
* - 若当前页面栈为空GetTop 返回 NULL函数直接返回。
* - 若栈顶页面未实现 on_loop 回调,函数直接返回。
* - 通过双重判空避免空指针调用导致崩溃。
*
* 说明:
* - 本函数只处理“当前可见页面”(栈顶页),不遍历其它页面。
* - 典型调用位置是主循环中的固定节拍点(例如每 N ms 调用一次)。
* - 页面切换、事件分发不在本函数处理范围内,分别由 Navigate/Push/Pop、
* DispatchEvent 负责。
*
* 返回值:
* - 无
* ------------------------------------------------------------------------- */
void PageManager_Loop(void)
{
page_t *top = PageManager_GetTop();
ASSERT((top == NULL) || (top->on_loop == NULL));
if ((top != NULL) && (top->on_loop != NULL))
{
top->on_loop(top);
}
}

View File

@@ -0,0 +1,77 @@
#ifndef PAGE_MANAGER_H
#define PAGE_MANAGER_H
#include "page.h"
/* -------------------------------------------------------------------------
* 结构体名: page_manager_t
* 作用:
* 页面管理器运行时上下文,维护“页面栈 + 页面注册表”两套核心状态。
*
* 设计说明:
* - page_stack:
* 运行时导航栈,表示当前页面跳转路径。
* 栈顶元素是当前可见/可交互页面。
* - stack_top:
* 栈顶索引。约定 -1 表示空栈。
* 当前栈深可由 (stack_top + 1) 推导。
* - page_registry:
* 可导航页面的注册表,按 page_id 查找目标页面时使用。
* - page_count:
* 当前注册表有效页面数量,范围 [0, MAX_PAGE_COUNT]。
*
* 约束关系:
* - page_stack 最大深度由 MAX_PAGE_STACK_DEPTH 限制;
* - page_registry 最大容量由 MAX_PAGE_COUNT 限制;
* - 调用方需通过 PageManager_Init 初始化后再使用。
* ------------------------------------------------------------------------- */
#define MAX_PAGE_STACK_DEPTH 5
#define MAX_PAGE_COUNT 16
/* -------------------------------------------------------------------------
* 枚举名: page_manager_result_t
* 作用:
* 定义 PageManager 相关接口的统一返回码,避免使用无语义的裸整数。
*
* 取值说明:
* PAGE_MANAGER_OK - 操作成功
* PAGE_MANAGER_ERR_NULL_PARAM - 传入参数为空
* PAGE_MANAGER_ERR_INVALID_ID - 页面 ID 非法
* PAGE_MANAGER_ERR_NOT_FOUND - 目标页面未注册/未找到
* PAGE_MANAGER_ERR_STACK_FULL - 页面栈已满,无法继续压栈
* PAGE_MANAGER_ERR_STACK_BOTTOM - 页面栈已到栈底,无法继续弹栈
* PAGE_MANAGER_ERR_REGISTRY_FULL - 页面注册表已满
* PAGE_MANAGER_ERR_NO_ACTIVE_PAGE - 当前无活动页面可供分发/循环驱动
* ------------------------------------------------------------------------- */
typedef enum
{
PAGE_MANAGER_OK = 0,
PAGE_MANAGER_ERR_NULL_PARAM = -1,
PAGE_MANAGER_ERR_INVALID_ID = -2,
PAGE_MANAGER_ERR_NOT_FOUND = -3,
PAGE_MANAGER_ERR_STACK_FULL = -4,
PAGE_MANAGER_ERR_STACK_BOTTOM = -5,
PAGE_MANAGER_ERR_REGISTRY_FULL = -6,
PAGE_MANAGER_ERR_NO_ACTIVE_PAGE = -7
} page_manager_result_t;
typedef struct
{
page_t *page_stack[MAX_PAGE_STACK_DEPTH]; /* 页面导航栈 */
int8_t stack_top; /* 栈顶索引(-1 表示空栈) */
page_t *page_registry[MAX_PAGE_COUNT]; /* 页面注册表 */
uint8_t page_count; /* 注册页面数量 */
} page_manager_t;
void PageManager_Init(void);
page_manager_result_t PageManager_Register(page_t *page);
page_manager_result_t PageManager_Navigate(page_id_t pageId);
page_manager_result_t PageManager_DispatchEvent(input_event_t *event);
page_t *PageManager_GetTop(void);
page_manager_result_t PageManager_Push(page_t *newPage);
page_manager_result_t PageManager_Pop(void);
page_t *PageManager_Find(page_id_t pageId);
void PageManager_Loop(void);
#endif

View File

@@ -30,10 +30,12 @@ static int getch(void)
}
#endif
#include "Drv/menu/app/menu.h"
#include "Drv/pages/page_manager.h"
#include "Drv/pages/menu/page.h"
#include "TCP/tcp.h"
#include "remoteDisplay.h"
#include "Drv/key.h"
#include "Drv/lcd/lcd.h"
#include "thread_utils.h"
/* ----------------------------------------------------------------------------
@@ -42,6 +44,7 @@ static int getch(void)
int main(void)
{
uint8_t count = 0;
input_event_t event;
#ifdef _WIN32
/* Windows将控制台代码页设为 UTF-8避免中文乱码如“监听”“退出”等 */
@@ -61,9 +64,12 @@ int main(void)
server_arg.p_server_sock = &server_sock;
server_arg.p_quit = &server_quit;
printf("开始初始化菜单树...\n");
printf("开始初始化页面管理器与菜单页面...\n");
MenuApp_Init(); /* 初始化菜单应用Model + View + Presenter */
PageManager_Init();
Lcd_Init(); /* 初始化屏幕显存:由入口统一完成 */
(void)PageManager_Register(MenuPage_GetInstance());
(void)PageManager_Navigate(PAGE_ID_MENU);
Key_Init(); /* 初始化按键 */
printf("PC 端 HMI 菜单模拟启动TCP 服务在单独线程,端口 7003\n");
/* 7003 为 RemoDispBus 默认端口,与 remo_disp_server.py 一致 */
@@ -73,13 +79,19 @@ int main(void)
}
while(1)
{
MenuApp_PollInput();
uint8_t keyVal = Key_Read();
if (keyVal != KEY_NONE)
{
event.type = PAGE_EVENT_KEY;
event.keyVal = keyVal;
PageManager_DispatchEvent(&event);
}
Sleep(20);
count++;
if(count > 50)
{
count = 0;
MenuApp_Render();
PageManager_Loop();
}
}

View File

@@ -7,7 +7,6 @@ set(DTU_TEST_COMMON_SOURCES
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c"
"${CMAKE_SOURCE_DIR}/src/Drv/key.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/app/menu.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/display.c"
"${CMAKE_SOURCE_DIR}/src/TCP/tcp.c"
"${CMAKE_SOURCE_DIR}/src/thread_utils.c"
"${CMAKE_SOURCE_DIR}/src/remoteDisplay.c"
@@ -70,11 +69,10 @@ add_dtu_test(
# ------------------------------------------------------------
# P1业务核心计算/状态流转测试
# ------------------------------------------------------------
add_dtu_test(
test_p1_key
test_p1_key.c
"${CMAKE_SOURCE_DIR}/src/Drv/key.c"
)
# ------------------------------------------------------------
# P1lcd基本测试
# ------------------------------------------------------------
add_dtu_test(
test_p1_lcd_basic
test_p1_lcd_basic.c
@@ -83,40 +81,40 @@ add_dtu_test(
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c"
)
# ------------------------------------------------------------
# P1菜单管理器测试
# ------------------------------------------------------------
add_dtu_test(
test_p1_page_manager
test_p1_page_manager.c
"${CMAKE_SOURCE_DIR}/src/Drv/pages/page_manager.c"
)
# ------------------------------------------------------------
# P1菜单测试
# ------------------------------------------------------------
add_dtu_test(
test_p1_menu
test_p1_menu.c
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_tree_builder.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_layout.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c"
)
add_dtu_test(
test_p1_menu_nav_legacy
test_p1_menu_nav_legacy.c
"${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_navigator.c"
)
add_dtu_test(
test_p1_menu_navigator
test_p1_menu_navigator.c
"${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_navigator.c"
)
add_dtu_test(
test_p1_menu_tree_builder
test_p1_menu_tree_builder.c
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_tree_builder.c"
)
add_dtu_test(
test_p1_menu_layout
test_p1_menu_layout.c
"${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_layout.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/app/menu.c"
"${CMAKE_SOURCE_DIR}/src/Drv/pages/menu/model.c"
"${CMAKE_SOURCE_DIR}/src/Drv/pages/menu/view.c"
"${CMAKE_SOURCE_DIR}/src/Drv/pages/menu/presenter.c"
"${CMAKE_SOURCE_DIR}/src/Drv/pages/menu/page.c"
"${CMAKE_SOURCE_DIR}/src/Drv/pages/global/renderer_lcd.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_draw.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c"
)
target_compile_definitions(test_p1_menu PRIVATE UNIT_TEST)
# ------------------------------------------------------------
# P1key测试
# ------------------------------------------------------------
add_dtu_test(
test_p1_key
test_p1_key.c
"${CMAKE_SOURCE_DIR}/src/Drv/key.c"
)
# ------------------------------------------------------------
# P2集成测试网络回环等
# ------------------------------------------------------------
@@ -126,21 +124,3 @@ add_dtu_test(
"${CMAKE_SOURCE_DIR}/src/TCP/tcp.c"
"${CMAKE_SOURCE_DIR}/src/thread_utils.c"
)
add_dtu_test(
test_p2_menu_runtime_startup
test_p2_menu_runtime_startup.c
"${CMAKE_SOURCE_DIR}/src/Drv/menu/app/menu.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/display.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_model.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_view.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_presenter.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_tree_builder.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_layout.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_navigator.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_renderer_lcd.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_draw.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c"
"${CMAKE_SOURCE_DIR}/src/Drv/key.c"
)

View File

@@ -2,46 +2,199 @@
#include "test_common.h"
#include "../src/Drv/menu/view/menu_layout.h"
#include "../src/Drv/menu/model/menu_tree_builder.h"
static int noop_proc(void)
{
return 0;
}
#include "../src/Drv/pages/menu/view.h"
#include "../src/Drv/pages/menu/model.h"
#include "../src/Drv/pages/menu/page.h"
/* -------------------------------------------------------------------------
* 函数名: main
* 功能:
* 覆盖菜单模块的白盒/黑盒关键能力,验证:
* 1) Model 初始化与辅助函数
* 2) 菜单树构建与显示名称装饰
* 3) View 初始化后的端口与接口绑定
* 4) MenuPage 生命周期入口是否完整绑定
*
* 参数:
* 无
*
* 返回值:
* - 0 : 测试通过
* ------------------------------------------------------------------------- */
int main(void)
{
tagMenuCtrl ctrl;
tagMenuItem items[4];
uint8_t menu_num[4] = {0};
/* 用于 MenuView_CharLenCal / Position 相关白盒测试的临时数组 */
tagPMenuItem first[4] = {0};
tagPMenuItem index[4] = {0};
uint8_t max_len;
const tagMenuModel model[4] = {
{0, "Root", "", 0, 0, 0, (FUNCPTR)noop_proc},
{1, "设置", "", 0, 0, 0, (FUNCPTR)noop_proc},
{2, "子项", "", 0, 0, 0, (FUNCPTR)noop_proc},
{1, "查看", "", 0, 0, 0, (FUNCPTR)noop_proc},
/* 页面实例:用于验证 MenuPage_GetInstance 生命周期回调绑定 */
page_t *menuPage;
/* 真实菜单模型对象:用于测试 MenuModel_Init 及其派生结果 */
menu_model_t menuModel;
/* 视图对象:用于验证 MenuView_Init 的接口与渲染端口初始化 */
MenuView view;
/* 自定义最小菜单树:用于单独测试 BuildTree / DecorateDisplayNames */
tagMenuItem customItems[4];
tagMenuItem customItems2[7];
tagMenuModel customModelTab[4] =
{
{0, "Root", "", 0, 0x0000, 0, NULL},
{1, "ChildA", "", 0, 0x0000, 0, NULL},
{2, "Leaf", "", 0, 0x0000, 0, NULL},
{1, "ChildB", "", 0, 0x0000, 0, NULL}
};
tagMenuModel customModelTab2[7] =
{
{0, "RootA", "", 0, 0x0000, 0, NULL},
{1, "A1", "", 0, 0x0000, 0, NULL},
{2, "A1a", "", 0, 0x0000, 0, NULL},
{1, "A2", "", 0, 0x0000, 0, NULL},
{0, "RootB", "", 0, 0x0000, 0, NULL},
{1, "B1", "", 0, 0x0000, 0, NULL},
{0, "RootC", "", 0, 0x0000, 0, NULL}
};
memset(&ctrl, 0, sizeof(ctrl));
memset(items, 0, sizeof(items));
/* ---------------------------------------------------------------------
* Case 1:
* 测试 MenuModel_Init / MenuModel_CountTopLevel
* - 初始化真实菜单模型
* - 验证 0 级菜单数量统计结果有效
* --------------------------------------------------------------------- */
memset(&menuModel, 0, sizeof(menuModel));
MenuModel_Init(&menuModel);
MenuTree_0LevelNumCal(&ctrl, model, 4);
ASSERT_EQ_INT(1, ctrl.by0LevelNum);
MenuTree_MainCreate(items, model, 4);
/* ---------------------------------------------------------------------
* Case 2:
* 测试 MenuView_Utf8LenCal
* - 验证英文与中文字符串长度计算逻辑
* --------------------------------------------------------------------- */
ASSERT_EQ_INT(3, MenuModel_Utf8LenCal((uint8_t *)"ABC"));
ASSERT_EQ_INT(2, MenuModel_Utf8LenCal((uint8_t *)""));
ASSERT_EQ_INT(3, MenuLayout_Utf8LenCal((uint8_t *)"ABC"));
ASSERT_EQ_INT(2, MenuLayout_Utf8LenCal((uint8_t *)""));
first[0] = &items[0];
index[0] = &items[0];
max_len = MenuLayout_CharLenCal(0, menu_num, first, index);
/* ---------------------------------------------------------------------
* Case 3:
* 测试 MenuModel_IndexMenuItems / MenuModel_GetMenuMaxDisplayLen
* - 使用真实菜单模型验证序号初始化、子节点数量统计与子菜单宽度统计
* --------------------------------------------------------------------- */
first[0] = &menuModel.menuItems[0];
index[0] = &menuModel.menuItems[0];
first[1] = menuModel.menuItems[0].links.lower;
max_len = MenuModel_GetMenuMaxDisplayLen(first[1]);
ASSERT_TRUE(max_len > 0);
ASSERT_TRUE(menu_num[1] > 0);
ASSERT_STREQ("设置", (const char *)items[1].byName);
ASSERT_EQ_INT(0, menuModel.menuItems[0].rect.wPos);
ASSERT_EQ_INT(0, first[1]->rect.wPos);
ASSERT_TRUE(menuModel.menuItems[0].rect.wNum > 0);
if (first[1]->links.lower == NULL)
{
ASSERT_EQ_INT(0, first[1]->rect.wNum);
}
else
{
ASSERT_TRUE(first[1]->rect.wNum > 0);
}
/* ---------------------------------------------------------------------
* Case 4:
* 测试 MenuModel_DecorateDisplayNames 的真实初始化结果
* - 验证存在子菜单的项名称非空,并被正确用于显示
* --------------------------------------------------------------------- */
/* 有子菜单的项会被装饰追加 '\x10'(右箭头标记) */
ASSERT_TRUE(((const char *)menuModel.menuItems[1].menuDef.byName)[0] != '\0');
/* ---------------------------------------------------------------------
* Case 5:
* 测试 MenuModel_BuildTree
* - 使用自定义 4 节点菜单表验证父子、同层、闭环链路关系
* --------------------------------------------------------------------- */
memset(customItems, 0, sizeof(customItems));
MenuModel_BuildTree(customItems, customModelTab, 4);
MenuModel_IndexMenuItems(customItems, 4);
ASSERT_TRUE(customItems[0].links.lower == &customItems[1]);
ASSERT_TRUE(customItems[1].links.higher == &customItems[0]);
ASSERT_TRUE(customItems[1].links.lower == &customItems[2]);
ASSERT_TRUE(customItems[2].links.higher == &customItems[1]);
ASSERT_TRUE(customItems[1].links.behind == &customItems[3]);
ASSERT_TRUE(customItems[3].links.before == &customItems[1]);
ASSERT_TRUE(customItems[3].links.higher == &customItems[0]);
ASSERT_TRUE(customItems[3].links.behind == &customItems[1]); /* 同层闭环 */
ASSERT_EQ_INT(0, customItems[0].rect.wPos);
ASSERT_EQ_INT(2, customItems[0].rect.wNum);
ASSERT_EQ_INT(0, customItems[1].rect.wPos);
ASSERT_EQ_INT(1, customItems[1].rect.wNum);
ASSERT_EQ_INT(1, customItems[3].rect.wPos);
ASSERT_EQ_INT(0, customItems[3].rect.wNum);
ASSERT_EQ_INT(0, customItems[2].rect.wPos);
ASSERT_EQ_INT(0, customItems[2].rect.wNum);
/* ---------------------------------------------------------------------
* Case 5.1:
* 强化测试 MenuModel_IndexMenuItems
* - 覆盖多顶层、多兄弟、跨层回退后的 wPos / wNum 统计
* --------------------------------------------------------------------- */
memset(customItems2, 0, sizeof(customItems2));
MenuModel_BuildTree(customItems2, customModelTab2, 7);
MenuModel_IndexMenuItems(customItems2, 7);
ASSERT_EQ_INT(0, customItems2[0].rect.wPos);
ASSERT_EQ_INT(2, customItems2[0].rect.wNum);
ASSERT_EQ_INT(0, customItems2[1].rect.wPos);
ASSERT_EQ_INT(1, customItems2[1].rect.wNum);
ASSERT_EQ_INT(0, customItems2[2].rect.wPos);
ASSERT_EQ_INT(0, customItems2[2].rect.wNum);
ASSERT_EQ_INT(1, customItems2[3].rect.wPos);
ASSERT_EQ_INT(0, customItems2[3].rect.wNum);
ASSERT_EQ_INT(1, customItems2[4].rect.wPos);
ASSERT_EQ_INT(1, customItems2[4].rect.wNum);
ASSERT_EQ_INT(0, customItems2[5].rect.wPos);
ASSERT_EQ_INT(0, customItems2[5].rect.wNum);
ASSERT_EQ_INT(2, customItems2[6].rect.wPos);
ASSERT_EQ_INT(0, customItems2[6].rect.wNum);
/* ---------------------------------------------------------------------
* Case 6:
* 测试 MenuModel_DecorateDisplayNames
* - 验证有子节点的项追加 '\x10'
* - 验证叶子节点名称不被追加装饰符
* --------------------------------------------------------------------- */
MenuModel_DecorateDisplayNames(customItems, 4);
ASSERT_TRUE(customItems[0].menuDef.byName[4] == '\x10');
ASSERT_TRUE(customItems[0].menuDef.byName[5] == '\0');
ASSERT_TRUE(customItems[1].menuDef.byName[6] == '\x10');
ASSERT_TRUE(customItems[1].menuDef.byName[7] == '\0');
ASSERT_TRUE(customItems[2].menuDef.byName[4] == '\0'); /* 叶子节点不追加 */
/* ---------------------------------------------------------------------
* Case 7:
* 测试 MenuView_Init / MenuView_GetPortForTest
* - 验证 View 初始化后渲染端口、布局参数、接口函数指针均可用
* --------------------------------------------------------------------- */
/* View 初始化后,渲染端口派生数据和对外接口必须可用 */
memset(&view, 0, sizeof(view));
ASSERT_TRUE(MenuView_GetPortForTest() == NULL);
MenuView_Init(&view);
ASSERT_TRUE(MenuView_GetPortForTest() != NULL);
ASSERT_TRUE(view.update_selection_new_level != NULL);
ASSERT_TRUE(view.update_selection_same_level != NULL);
ASSERT_TRUE(view.full_refresh != NULL);
/* ---------------------------------------------------------------------
* Case 8:
* 测试 MenuPage_GetInstance
* - 验证页面生命周期入口已经完整绑定
* --------------------------------------------------------------------- */
/* 页面生命周期入口必须完整绑定,避免注册后生命周期不执行 */
menuPage = MenuPage_GetInstance();
ASSERT_TRUE(menuPage != NULL);
ASSERT_TRUE(menuPage->on_create != NULL);
ASSERT_TRUE(menuPage->on_enter != NULL);
ASSERT_TRUE(menuPage->on_exit != NULL);
ASSERT_TRUE(menuPage->on_destroy != NULL);
ASSERT_TRUE(menuPage->on_event != NULL);
ASSERT_TRUE(menuPage->on_loop != NULL);
return 0;
}

View File

@@ -1,47 +0,0 @@
#include "test_common.h"
#include <string.h>
#include "../src/Drv/lcd/lcd.h"
#include "../src/Drv/menu/app/menu.h"
#include "../src/Drv/menu/view/menu_layout.h"
int main(void)
{
tagMenuItem root;
tagMenuItem child;
MenuLayoutConfig config = {
LCD_SIZE_X,
LCD_SIZE_Y,
MENU_YMIN,
MENU_YMAX_FROM_LCD(LCD_SIZE_Y),
LINE_HEIGHT,
MENU_WITDTH,
MENU_XADD,
MENU_YADD};
memset(&root, 0, sizeof(root));
memset(&child, 0, sizeof(child));
memcpy(root.byName, "Root", 5);
memcpy(child.byName, "Child", 6);
root.byClass = 0;
child.byClass = 1;
root.ptLower = &child;
root.ptBehind = &root;
root.ptBefore = &root;
child.ptHigher = &root;
child.ptBehind = &child;
child.ptBefore = &child;
ASSERT_EQ_INT(3, MenuLayout_Utf8LenCal((uint8_t *)"ABC"));
ASSERT_EQ_INT(2, MenuLayout_Utf8LenCal((uint8_t *)""));
MenuLayout_PositionCal(&root, 1, &config);
ASSERT_TRUE(root.wEPosX > root.wSPosX);
ASSERT_TRUE(root.wEPosY > root.wSPosY);
ASSERT_TRUE(root.wEPosY <= LCD_SIZE_Y);
return 0;
}

View File

@@ -1,93 +0,0 @@
#include <string.h>
#include "test_common.h"
#include "../src/Drv/key.h"
#include "../src/Drv/menu/presenter/menu_navigator.h"
static int g_exec_count = 0;
static int on_exec(void)
{
g_exec_count++;
return 0;
}
static void build_legacy_like_tree(MenuNavState *nav, tagMenuItem *root, tagMenuItem *m1, tagMenuItem *m2, tagMenuItem *m1_sub)
{
memset(nav, 0, sizeof(*nav));
memset(root, 0, sizeof(*root));
memset(m1, 0, sizeof(*m1));
memset(m2, 0, sizeof(*m2));
memset(m1_sub, 0, sizeof(*m1_sub));
root->byClass = 0;
root->ptLower = m1;
root->ptBefore = root;
root->ptBehind = root;
root->wPos = 1;
m1->byClass = 1;
m1->wPos = 1;
m1->ptHigher = root;
m1->ptBefore = m2;
m1->ptBehind = m2;
m1->ptLower = m1_sub;
m1->pfnWinProc = on_exec;
m2->byClass = 1;
m2->wPos = 2;
m2->ptHigher = root;
m2->ptBefore = m1;
m2->ptBehind = m1;
m2->pfnWinProc = on_exec;
m1_sub->byClass = 2;
m1_sub->wPos = 1;
m1_sub->ptHigher = m1;
m1_sub->ptBefore = m1_sub;
m1_sub->ptBehind = m1_sub;
m1_sub->pfnWinProc = on_exec;
nav->ptHead = root;
nav->ptCurrent = m1;
nav->ptCurBak = m1;
nav->ptRoute[0] = root;
nav->ptRoute[1] = m1;
}
int main(void)
{
MenuNavState nav;
tagMenuItem root;
tagMenuItem m1;
tagMenuItem m2;
tagMenuItem m1_sub;
MenuNavResult result;
build_legacy_like_tree(&nav, &root, &m1, &m2, &m1_sub);
result = MenuNavigator_ProcessKey(&nav, KEY_D);
ASSERT_EQ_INT(1, result.needRefresh);
ASSERT_TRUE(nav.ptCurrent == &m2);
result = MenuNavigator_ProcessKey(&nav, KEY_U);
ASSERT_EQ_INT(1, result.needRefresh);
ASSERT_TRUE(nav.ptCurrent == &m1);
result = MenuNavigator_ProcessKey(&nav, KEY_ENT);
ASSERT_EQ_INT(1, result.needRefresh);
ASSERT_TRUE(nav.ptCurrent == &m1_sub);
result = MenuNavigator_ProcessKey(&nav, KEY_ESC);
ASSERT_EQ_INT(1, result.needRefresh);
ASSERT_TRUE(nav.ptCurrent == &m1);
g_exec_count = 0;
nav.ptCurrent = &m2;
result = MenuNavigator_ProcessKey(&nav, KEY_ENT);
ASSERT_EQ_INT(0, result.needRefresh);
ASSERT_EQ_INT(1, g_exec_count);
return 0;
}

View File

@@ -1,79 +0,0 @@
#include "test_common.h"
#include <string.h>
#include "../src/Drv/key.h"
#include "../src/Drv/menu/presenter/menu_navigator.h"
static int g_exec_count = 0;
static int on_exec(void)
{
g_exec_count++;
return 0;
}
static void build_two_level(MenuNavState *nav, tagMenuItem *root, tagMenuItem *child_a, tagMenuItem *child_b)
{
memset(nav, 0, sizeof(*nav));
memset(root, 0, sizeof(*root));
memset(child_a, 0, sizeof(*child_a));
memset(child_b, 0, sizeof(*child_b));
root->byClass = 0;
root->wPos = 1;
root->ptLower = child_a;
root->ptBefore = root;
root->ptBehind = root;
child_a->byClass = 1;
child_a->wPos = 1;
child_a->ptHigher = root;
child_a->ptBefore = child_b;
child_a->ptBehind = child_b;
child_a->pfnWinProc = on_exec;
child_b->byClass = 1;
child_b->wPos = 2;
child_b->ptHigher = root;
child_b->ptBefore = child_a;
child_b->ptBehind = child_a;
child_b->pfnWinProc = on_exec;
nav->ptHead = root;
nav->ptCurrent = child_a;
nav->ptCurBak = child_a;
nav->ptRoute[0] = root;
nav->ptRoute[1] = child_a;
}
int main(void)
{
MenuNavState nav;
tagMenuItem root;
tagMenuItem a;
tagMenuItem b;
MenuNavResult result;
build_two_level(&nav, &root, &a, &b);
result = MenuNavigator_ProcessKey(&nav, KEY_D);
ASSERT_EQ_INT(1, result.needRefresh);
ASSERT_TRUE(nav.ptCurrent == &b);
result = MenuNavigator_ProcessKey(&nav, KEY_U);
ASSERT_EQ_INT(1, result.needRefresh);
ASSERT_TRUE(nav.ptCurrent == &a);
g_exec_count = 0;
result = MenuNavigator_ProcessKey(&nav, KEY_ENT);
ASSERT_EQ_INT(0, result.needRefresh);
ASSERT_EQ_INT(1, g_exec_count);
result = MenuNavigator_ProcessKey(&nav, KEY_ESC);
ASSERT_EQ_INT(1, result.skipRenderThisRound);
ASSERT_TRUE(nav.ptCurrent == root.ptLower);
ASSERT_TRUE(nav.ptRoute[0] == &root);
return 0;
}

View File

@@ -1,37 +0,0 @@
#include "test_common.h"
#include <string.h>
#include "../src/Drv/menu/model/menu_tree_builder.h"
static int noop_proc(void)
{
return 0;
}
int main(void)
{
tagMenuCtrl ctrl;
tagMenuItem items[4];
const tagMenuModel model[4] = {
{0, "Root", "", 0, 0, 0, (FUNCPTR)noop_proc},
{1, "A", "", 0, 0, 0, (FUNCPTR)noop_proc},
{1, "B", "", 0, 0, 0, (FUNCPTR)noop_proc},
{2, "C", "", 0, 0, 0, (FUNCPTR)noop_proc},
};
memset(&ctrl, 0, sizeof(ctrl));
memset(items, 0, sizeof(items));
MenuTree_0LevelNumCal(&ctrl, model, 4);
ASSERT_EQ_INT(1, ctrl.by0LevelNum);
MenuTree_MainCreate(items, model, 4);
ASSERT_TRUE(items[0].ptLower == &items[1]);
ASSERT_TRUE(items[1].ptBehind == &items[2]);
ASSERT_TRUE(items[2].ptBefore == &items[1]);
ASSERT_TRUE(items[2].ptLower == &items[3]);
ASSERT_TRUE(items[3].ptHigher == &items[2]);
return 0;
}

View File

@@ -0,0 +1,292 @@
#include <string.h>
#include "test_common.h"
#include "../src/Drv/key.h"
#include "../src/Drv/pages/page_manager.h"
/* -------------------------------------------------------------------------
* 结构体名: test_page_ctx_t
* 功能:
* 测试页面私有上下文,用于统计各生命周期回调与事件处理的触发次数。
*
* 字段说明:
* createCount - on_create 被调用次数
* enterCount - on_enter 被调用次数
* exitCount - on_exit 被调用次数
* destroyCount - on_destroy 被调用次数
* loopCount - on_loop 被调用次数
* eventResult - on_event 预设返回值(用于控制是否触发全局兜底)
* ------------------------------------------------------------------------- */
typedef struct
{
int createCount;
int enterCount;
int exitCount;
int destroyCount;
int loopCount;
event_result_t eventResult;
} test_page_ctx_t;
static test_page_ctx_t g_ctx_a;
static test_page_ctx_t g_ctx_b;
/* A 页生命周期/事件回调桩函数:将调用痕迹写入 page->model 对应的计数器 */
static void on_create_a(page_t *page) { ((test_page_ctx_t *)page->model)->createCount++; }
static void on_enter_a(page_t *page) { ((test_page_ctx_t *)page->model)->enterCount++; }
static void on_exit_a(page_t *page) { ((test_page_ctx_t *)page->model)->exitCount++; }
static void on_destroy_a(page_t *page) { ((test_page_ctx_t *)page->model)->destroyCount++; }
static event_result_t on_event_a(page_t *page, input_event_t *event)
{
(void)event;
return ((test_page_ctx_t *)page->model)->eventResult;
}
static void on_loop_a(page_t *page) { ((test_page_ctx_t *)page->model)->loopCount++; }
/* B 页生命周期/事件回调桩函数:用于验证多页切换与销毁链路 */
static void on_create_b(page_t *page) { ((test_page_ctx_t *)page->model)->createCount++; }
static void on_enter_b(page_t *page) { ((test_page_ctx_t *)page->model)->enterCount++; }
static void on_exit_b(page_t *page) { ((test_page_ctx_t *)page->model)->exitCount++; }
static void on_destroy_b(page_t *page) { ((test_page_ctx_t *)page->model)->destroyCount++; }
static event_result_t on_event_b(page_t *page, input_event_t *event)
{
(void)event;
return ((test_page_ctx_t *)page->model)->eventResult;
}
static void on_loop_b(page_t *page) { ((test_page_ctx_t *)page->model)->loopCount++; }
static void setup_page(page_t *page,
page_id_t pageId,
uint8_t isCached,
test_page_ctx_t *ctx,
void (*on_create)(page_t *),
void (*on_enter)(page_t *),
void (*on_exit)(page_t *),
void (*on_destroy)(page_t *),
event_result_t (*on_event)(page_t *, input_event_t *),
void (*on_loop)(page_t *))
{
memset(page, 0, sizeof(*page));
page->page_id = pageId;
page->is_cached = isCached;
page->on_create = on_create;
page->on_enter = on_enter;
page->on_exit = on_exit;
page->on_destroy = on_destroy;
page->on_event = on_event;
page->on_loop = on_loop;
page->model = ctx;
}
/* -------------------------------------------------------------------------
* 函数名: main
* 功能:
* 覆盖 PageManager 的核心行为链路,验证注册、导航、事件分发(含 ESC 兜底回退)、
* 生命周期回调顺序以及循环驱动是否符合预期。
*
* 参数:
* 无
*
* 边界处理:
* - 通过 memset 将页面对象和上下文清零,避免脏数据影响断言。
* - 使用 ASSERT_TRUE 先校验 GetTop 非空,再访问 page_id防止空指针解引用。
*
* 说明:
* - pageA 设为缓存页is_cached = 1用于验证回退后可继续进入而不销毁。
* - pageB 设为非缓存页is_cached = 0用于验证 ESC 回退时触发 on_destroy。
* - 将 pageB.on_event 设为 EVENT_UNHANDLED确保 DispatchEvent 走全局兜底,
* 从而触发 KEY_ESC -> PageManager_Pop() 的系统行为。
* - 关键验证点:
* 1) Navigate 到 AA create/enter 各一次;
* 2) Navigate 到 BA exit 一次B create/enter 各一次;
* 3) ESC 事件B exit + destroy栈顶回到 AA enter 再次触发;
* 4) Loop仅驱动当前栈顶 A 的 on_loop。
*
* 返回值:
* - 0测试通过
* ------------------------------------------------------------------------- */
int main(void)
{
page_t pageA;
page_t pageB;
page_t pageA2;
page_t pageB2;
page_t pageA3;
input_event_t event;
page_t invalidNonePage;
page_t invalidMaxPage;
/* ===== Case 1: Init 后的默认行为与空栈边界 ===== */
memset(&g_ctx_a, 0, sizeof(g_ctx_a));
memset(&g_ctx_b, 0, sizeof(g_ctx_b));
PageManager_Init();
ASSERT_TRUE(PageManager_GetTop() == NULL);
ASSERT_TRUE(PageManager_Find(PAGE_ID_MENU) == NULL);
ASSERT_EQ_INT(PAGE_MANAGER_ERR_NOT_FOUND, PageManager_Navigate(PAGE_ID_MENU));
ASSERT_EQ_INT(PAGE_MANAGER_ERR_STACK_BOTTOM, PageManager_Pop());
ASSERT_EQ_INT(PAGE_MANAGER_ERR_NULL_PARAM, PageManager_Register(NULL));
ASSERT_EQ_INT(PAGE_MANAGER_ERR_NULL_PARAM, PageManager_Push(NULL));
ASSERT_EQ_INT(PAGE_MANAGER_ERR_NULL_PARAM, PageManager_DispatchEvent(NULL));
PageManager_Loop();
/* ===== Case 2: Register 参数边界与重复注册覆盖 ===== */
setup_page(&invalidNonePage,
PAGE_ID_NONE,
1,
&g_ctx_a,
on_create_a,
on_enter_a,
on_exit_a,
on_destroy_a,
on_event_a,
on_loop_a);
setup_page(&invalidMaxPage,
PAGE_ID_MAX,
1,
&g_ctx_b,
on_create_b,
on_enter_b,
on_exit_b,
on_destroy_b,
on_event_b,
on_loop_b);
ASSERT_EQ_INT(PAGE_MANAGER_ERR_INVALID_ID, PageManager_Register(&invalidNonePage));
ASSERT_EQ_INT(PAGE_MANAGER_ERR_INVALID_ID, PageManager_Register(&invalidMaxPage));
setup_page(&pageA,
PAGE_ID_MENU,
1,
&g_ctx_a,
on_create_a,
on_enter_a,
on_exit_a,
on_destroy_a,
on_event_a,
on_loop_a);
setup_page(&pageB,
PAGE_ID_APP_INFO,
0,
&g_ctx_b,
on_create_b,
on_enter_b,
on_exit_b,
on_destroy_b,
on_event_b,
on_loop_b);
setup_page(&pageA2,
PAGE_ID_MENU,
1,
&g_ctx_a,
on_create_a,
on_enter_a,
on_exit_a,
on_destroy_a,
on_event_a,
on_loop_a);
setup_page(&pageB2,
PAGE_ID_APP_INFO,
0,
&g_ctx_b,
on_create_b,
on_enter_b,
on_exit_b,
on_destroy_b,
on_event_b,
on_loop_b);
setup_page(&pageA3,
PAGE_ID_MENU,
1,
&g_ctx_a,
on_create_a,
on_enter_a,
on_exit_a,
on_destroy_a,
on_event_a,
on_loop_a);
ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Register(&pageA));
ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Register(&pageB));
ASSERT_TRUE(PageManager_Find(PAGE_ID_MENU) == &pageA);
ASSERT_TRUE(PageManager_Find(PAGE_ID_APP_INFO) == &pageB);
ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Register(&pageA2)); /* 重复 ID 覆盖 */
ASSERT_TRUE(PageManager_Find(PAGE_ID_MENU) == &pageA2);
/* ===== Case 3: Navigate + 生命周期 + GetTop ===== */
ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Navigate(PAGE_ID_MENU));
ASSERT_EQ_INT(1, g_ctx_a.createCount);
ASSERT_EQ_INT(1, g_ctx_a.enterCount);
ASSERT_TRUE(PageManager_GetTop() != NULL);
ASSERT_EQ_INT(PAGE_ID_MENU, PageManager_GetTop()->page_id);
ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Navigate(PAGE_ID_MENU)); /* 幂等导航到当前页 */
ASSERT_EQ_INT(1, g_ctx_a.enterCount); /* 不应重复 enter */
ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Navigate(PAGE_ID_APP_INFO));
ASSERT_EQ_INT(1, g_ctx_a.exitCount);
ASSERT_EQ_INT(1, g_ctx_b.createCount);
ASSERT_EQ_INT(1, g_ctx_b.enterCount);
ASSERT_TRUE(PageManager_GetTop() != NULL);
ASSERT_EQ_INT(PAGE_ID_APP_INFO, PageManager_GetTop()->page_id);
/* ===== Case 4: DispatchEvent handled/unhandled 分支 ===== */
g_ctx_b.eventResult = EVENT_HANDLED;
event.type = PAGE_EVENT_KEY;
event.keyVal = KEY_ESC;
ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_DispatchEvent(&event));
ASSERT_TRUE(PageManager_GetTop() == &pageB); /* handled 时不走全局 Pop */
ASSERT_EQ_INT(0, g_ctx_b.exitCount);
ASSERT_EQ_INT(0, g_ctx_b.destroyCount);
g_ctx_b.eventResult = EVENT_UNHANDLED;
event.type = PAGE_EVENT_KEY;
event.keyVal = KEY_ESC;
ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_DispatchEvent(&event));
ASSERT_TRUE(PageManager_GetTop() == &pageA2);
ASSERT_EQ_INT(PAGE_ID_MENU, PageManager_GetTop()->page_id);
ASSERT_EQ_INT(1, g_ctx_b.exitCount);
ASSERT_EQ_INT(1, g_ctx_b.destroyCount);
ASSERT_EQ_INT(2, g_ctx_a.enterCount);
/* 未处理但非键盘事件,不应触发全局行为 */
event.type = 0;
event.keyVal = KEY_ESC;
ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_DispatchEvent(&event));
ASSERT_TRUE(PageManager_GetTop() == &pageA2);
/* ===== Case 5: Loop 仅驱动栈顶页 ===== */
PageManager_Loop();
ASSERT_EQ_INT(1, g_ctx_a.loopCount);
ASSERT_EQ_INT(0, g_ctx_b.loopCount);
/* ===== Case 6: Push/Pop 边界(含栈满) ===== */
ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Push(&pageB2));
ASSERT_EQ_INT(2, g_ctx_b.createCount); /* 上次被 destroy重新 push 应再 create */
ASSERT_EQ_INT(2, g_ctx_b.enterCount);
ASSERT_TRUE(PageManager_GetTop() == &pageB2);
ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Push(&pageA3));
ASSERT_TRUE(PageManager_GetTop() == &pageA3);
/* 当前栈深推进到 4A2 -> B2 -> A3 -> B */
ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Push(&pageB));
ASSERT_TRUE(PageManager_GetTop() == &pageB);
/* 再 Push 一次达到栈深 5上限 */
ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Push(&pageA3));
ASSERT_TRUE(PageManager_GetTop() == &pageA3);
/* 栈满后继续 Push 应失败 */
ASSERT_EQ_INT(PAGE_MANAGER_ERR_STACK_FULL, PageManager_Push(&pageA2));
/* 连续 Pop 到底页,最后一次应失败 */
ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Pop());
ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Pop());
ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Pop());
ASSERT_EQ_INT(PAGE_MANAGER_OK, PageManager_Pop());
ASSERT_EQ_INT(PAGE_MANAGER_ERR_STACK_BOTTOM, PageManager_Pop());
ASSERT_TRUE(PageManager_GetTop() == &pageA2);
/* ===== Case 7: Navigate 未注册页 ===== */
ASSERT_EQ_INT(PAGE_MANAGER_ERR_NOT_FOUND, PageManager_Navigate(PAGE_ID_NONE));
return 0;
}

View File

@@ -1,39 +0,0 @@
#include "test_common.h"
#include "../src/Drv/key.h"
#include "../src/Drv/menu/app/menu.h"
int main(void)
{
int decorated_found = 0;
uint16_t itemCount = 0;
const tagMenuItem *menuItems;
MenuApp_Init();
Key_Init();
menuItems = MenuApp_GetMenuItems(&itemCount);
for (uint16_t i = 0; i < itemCount; i++)
{
if (menuItems[i].ptLower != NULL)
{
uint8_t len = 0;
while ((len < 50) && (menuItems[i].byName[len] != '\0'))
{
len++;
}
ASSERT_TRUE(len > 0);
ASSERT_EQ_INT('\x10', menuItems[i].byName[len - 1]);
decorated_found = 1;
break;
}
}
ASSERT_TRUE(decorated_found == 1);
/* 首次路由应仅触发首帧绘制,不应崩溃 */
MenuApp_PollInput();
/* 二次刷新路径也不应崩溃 */
MenuApp_Render();
return 0;
}