4a4ad6bc5f
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名, 不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体 run_boot_test.py 的真实调用。 - AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改 「编译(/keil-build)」「串口回归(/serial-boot-test)」 - keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree 方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围) - 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入 等 41 行历史证据保留旧名 - gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑 - 顺手补并行方案同行漏网计数 51→52 钩子 验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52 钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
875 lines
56 KiB
Markdown
875 lines
56 KiB
Markdown
# 菜单页面设计
|
||
|
||
> **用途**:定义 DTU 主菜单的**内容(43 节点层级树)、视觉规范、三层代码架构**(数据层 / 显示层 / 逻辑层),以及叶子节点进入后的**内容页模块**(§九)。
|
||
> **范围**:菜单层级(节点/父子/层级/进出导航)+ 三个已实现的内容页(装置信息/交流量/直流量,§九);其余叶子的内容页后续按同模式扩展。
|
||
> **来源**:用户提供的主菜单截图 7 屏(根页 + 实时数据/参数定值/三遥设置/装置维护/通讯参数/记录查询各一级子页 + 参数定值·整定二级)。
|
||
> **导航模型**:树形**逐级进入**——`ENTER`/`→` 进入子页,`ESC`/`←` 返回上一级;最多 **4 级(主界面 → 三级菜单)**。
|
||
|
||
> **阅读指南**:
|
||
>
|
||
> | 你想… | 读 |
|
||
> |---|---|
|
||
> | 搞懂**哪段代码干什么、三层怎么配合** | **§一 架构总览**(职责速览 / 调用矩阵 / 一次按键的旅程 / 初始化时序 / 层间不变量),再按需深入 §三 / §五 / §六 |
|
||
> | 深入某一层 | §三 数据层 / §五 显示层 / §六 逻辑层(各章自包含:职责边界 + 字段 + API) |
|
||
> | **改菜单**(增删/改名/换序条目) | §二(内容)+ §3.7(只改文本源、重跑生成器,代码零改动) |
|
||
> | **改视觉**(框/行/几何) | §四(显示规范)+ §五(显示层实现) |
|
||
> | **内容页**(装置信息/交流量/直流量) | §九(对象树/生命周期/页面定义表/API/内存预算) |
|
||
> | 搞懂**内存**(什么存、什么现算) | §7.3 + §7.5 |
|
||
> | **验证 / 调试** | §八 |
|
||
|
||
---
|
||
|
||
## 一、架构总览:三层拆分与协作
|
||
|
||
### 1.1 为什么拆三层
|
||
|
||
"菜单表 + 导航状态 + LVGL 渲染" 三块职责若同处一个文件则互相耦合(改菜单要动导航代码、改渲染易破坏导航)。因此拆为三层,**一层一文件、各管一件事**:
|
||
|
||
- **数据层**——只回答"**有什么条目**":只读菜单表 + 4 个查询 API;
|
||
- **显示层**——只回答"**怎么画**":持有**全部** LVGL 对象,**零**导航状态;
|
||
- **逻辑层**——只回答"**用户在哪、选中哪个**":唯一 17 B 导航状态 + 键语义(移/进/退)。
|
||
|
||
3 个文件 **MCU / PC 模拟器双端共用**(双端注册:Keil `.uvprojx` + `simulator/CMakeLists.txt`,见 `UI_设计架构.md` §1.4 约束 5)。
|
||
|
||
### 1.2 三层职责速览
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
K(["按键(远端键 / 模拟器键)"]) --> L["逻辑层 ui_menu.c<br/>唯一 17 B 导航状态(path/选择/深度/滚动)<br/>键语义:移动 / 进入 / 退出"]
|
||
L -- "单向驱动: frame_build / frame_destroy / frame_refresh / focus" --> V["显示层 ui_menu_view.c/h<br/>持全部 LVGL 对象,纯渲染,零导航状态"]
|
||
L -- "只读查询: children / valid" --> T["数据层 ui_menu_tab.c/h(生成物,只读)<br/>g_menu_tab[43] 父子索引表 + 4 个查询 API"]
|
||
V -- "只读查询: children / has_children / name" --> T
|
||
```
|
||
|
||
| 层 | 文件 | 一句话职责 | 持 LVGL 对象? | 持导航状态? |
|
||
|---|---|---|---|---|
|
||
| **数据层** | `Drivers/BSP/UI/ui_menu_tab.c/h`(生成物) | 回答"有什么条目":菜单表 + 查询 API | 否 | 否 |
|
||
| **逻辑层** | `Drivers/BSP/UI/ui_menu.c/h` | 回答"用户在哪、选中哪个":导航状态 + 键语义 | 否 | **是(唯一,17 B)** |
|
||
| **显示层** | `Drivers/BSP/UI/ui_menu_view.c/h` | 回答"怎么画":级联框渲染 | **是(全部)** | 否 |
|
||
|
||
> 记忆口诀:**数据层管"料",逻辑层管"账",显示层管"画"**。
|
||
|
||
### 1.3 依赖方向与调用矩阵
|
||
|
||
依赖**严格单向**:`逻辑 → 显示 → 数据`,数据层不依赖任何人;**禁止反向调用**(显示层不调逻辑层;数据/显示层不引用导航状态)。
|
||
|
||
| 调用方 ↓ / 被调 → | 数据层 | 显示层 | 逻辑层 |
|
||
|---|---|---|---|
|
||
| **数据层** `ui_menu_tab` | — | ✗ | ✗ |
|
||
| **显示层** `ui_menu_view` | ✓ 查询 API | — | ✗ |
|
||
| **逻辑层** `ui_menu` | ✓ 查询 API | ✓ 8 个显示接口 | — |
|
||
|
||
层间"词汇"只有两组接口(各层章节有完整签名):
|
||
|
||
| 方向 | 接口 |
|
||
|---|---|
|
||
| 逻辑 → 显示 | `ui_menu_view_init / container / frame_build / frame_destroy / frame_refresh / set_scroll / get_scroll / focus`(§5.5) |
|
||
| 逻辑/显示 → 数据 | `ui_menu_tab_children / has_children / name / valid`(§3.4) |
|
||
|
||
### 1.4 按键协作:一次按键的旅程
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant K as 按键(远端键 / 模拟器键)
|
||
participant G as LVGL 默认输入组(lv_group)
|
||
participant L as 逻辑层 ui_menu.c
|
||
participant V as 显示层 ui_menu_view
|
||
participant T as 数据层 ui_menu_tab
|
||
K->>G: 键码
|
||
Note over G: 焦点 = 持久容器 s_cont(唯一键入口;帧深度由逻辑层 s_depth 决定)
|
||
G->>L: LV_EVENT_KEY → menu_key_cb(深度=s_depth)→ menu_key_handle
|
||
L->>L: 状态变更:移动 s_sel[d] / 进入 s_depth+1+s_path / 退出 s_depth−1
|
||
L->>V: frame_refresh(高亮+跟滚)/ frame_build(建新框)/ frame_destroy(拆最右框)+ focus(移焦点)
|
||
V->>T: children / has_children / name(条目现算不落盘)
|
||
V-->>L: get_scroll(读回滚动位置,逻辑层写入 s_scroll[d])
|
||
Note over V: LVGL 下一帧渲染 → 屏幕变化
|
||
```
|
||
|
||
两个要点:
|
||
- **逻辑层决定"改成什么状态",显示层决定"画面怎么变"**——显示层只执行、不决策;数据层全程不参与(只被显示层查询条目/名字/菜单叶子)。
|
||
- 全程**单向同步**:一次按键 = 一次完整的"状态变更 → 画面更新",无异步、无回调上行。
|
||
|
||
### 1.5 初始化时序(`ui_menu_init`)
|
||
|
||
`uiTask` 等 `SYS_EVT_NET_READY` 后调 `ui_menu_init()`,三层按此顺序配合:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant U as uiTask
|
||
participant L as 逻辑层 ui_menu.c
|
||
participant V as 显示层 ui_menu_view
|
||
participant T as 数据层 ui_menu_tab
|
||
U->>L: ui_menu_init()
|
||
L->>T: ui_menu_tab_valid() 表自检
|
||
alt 父索引越界(表损坏)
|
||
T-->>L: 不过 → 打 ERROR、放弃建菜单(返回 -1)
|
||
else 自检通过
|
||
T-->>L: 通过
|
||
end
|
||
L->>L: 清零 17 B 导航状态(s_path/s_sel/s_scroll=0,s_depth=0)
|
||
L->>V: ui_menu_view_init()(样式 → 屏幕根容器 → 持久容器(装饰/状态行自绘回调) → 根框本体(含自绘回调))
|
||
L->>V: 持久容器 lv_group_add_obj(默认组)+ 挂 menu_key_cb(深度取 s_depth)
|
||
L->>V: ui_menu_view_frame_build(0) 注册根框行数据 + focus()
|
||
Note over L: lv_mem_monitor 打印建前/建后 LVGL 池
|
||
L-->>U: 0(成功)
|
||
```
|
||
|
||
- 日志标记:`UI menu init: OK`(uiTask 打印)——`/serial-boot-test` 回归可作 Required 标记。
|
||
- LVGL 池实测(模拟器 16 KB 池):根框建完 `used=5368 B`(旧行对象实现为 13312 B,自绘 + 容器收敛省 8.3 KB);最深四级级联 `used=6040 B`(增量 1256 B = 3 个子框体),远小于池余量。
|
||
|
||
### 1.6 层间不变量(每层"绝不做")
|
||
|
||
| 层 | 绝不做 | 为什么 |
|
||
|---|---|---|
|
||
| 数据层 | 业务逻辑(4 个 API 是纯表过滤)、依赖 LVGL、修改表(`const`) | 表是唯一事实源;改表 = 改菜单(走生成器 §3.6) |
|
||
| 显示层 | 持导航状态(17 B 全在逻辑层)、解释按键(输入回调挂在逻辑层) | 显示层 = "状态的纯函数":同样状态必画同样画面 → 易验证、双端可复用 |
|
||
| 逻辑层 | 创建/修改 LVGL 对象(建对象全在显示层)、持 `lv_obj_t*` 句柄 | 唯一例外:输入接线 2 调 `lv_group_add_obj` / `lv_obj_add_event_cb`(容器须入输入组)+ 读事件参数 + `lv_mem_monitor` 内存监控 |
|
||
|
||
---
|
||
|
||
## 二、菜单内容:43 节点层级树
|
||
|
||
### 2.1 层级总览
|
||
|
||
主菜单为 4 级树:
|
||
|
||
| 层级 | 含义 | 例子 |
|
||
|---|---|---|
|
||
| **主界面(根)** | 主界面,7 个一级条目 | 主界面 |
|
||
| **一级菜单** | 根页的 7 个条目 | 装置信息、实时数据、参数定值、三遥设置、装置维护、通讯参数、记录查询 |
|
||
| **二级菜单** | 一级子菜单的子项 | 交流量、整定、遥测死区、时钟设置、通讯设置、SOE记录 … |
|
||
| **三级菜单** | 仅"参数定值 → 整定/查看"下的子项 | 参数、定值、控制字、软压板 |
|
||
|
||
节点分两类(决定导航行为):
|
||
|
||
| 类型 | 标识 | 行为 |
|
||
|---|---|---|
|
||
| **菜单节点** | 行尾 **▶** 箭头 | `ENTER` 进入 → 展开下一级子页 |
|
||
| **叶子节点** | 无箭头 | `ENTER` 进入 → 有内容页(node 1/3/4,§九)则切内容屏;否则进空白终点框 |
|
||
|
||
> 根页默认选中**第 1 项"装置信息"**(整行反色:白带、黑字)。
|
||
|
||
### 2.2 完整层级树
|
||
|
||
```text
|
||
主界面
|
||
├─ 装置信息 [叶子]
|
||
├─ 实时数据 ▶ [菜单]
|
||
│ ├─ 交流量 [叶子]
|
||
│ ├─ 直流量 [叶子]
|
||
│ └─ 遥信量 [叶子]
|
||
├─ 参数定值 ▶ [菜单]
|
||
│ ├─ 整定 ▶ [菜单]
|
||
│ │ ├─ 参数 [叶子]
|
||
│ │ ├─ 定值 [叶子]
|
||
│ │ ├─ 控制字 [叶子]
|
||
│ │ └─ 软压板 [叶子]
|
||
│ └─ 查看 ▶ [菜单]
|
||
│ ├─ 参数 [叶子]
|
||
│ ├─ 定值 [叶子]
|
||
│ ├─ 控制字 [叶子]
|
||
│ └─ 软压板 [叶子]
|
||
├─ 三遥设置 ▶ [菜单]
|
||
│ ├─ 遥测死区 [叶子]
|
||
│ ├─ 遥测系数 [叶子]
|
||
│ ├─ 遥信类型 [叶子]
|
||
│ ├─ 遥信防抖 [叶子]
|
||
│ └─ 双点遥信 [叶子]
|
||
├─ 装置维护 ▶ [菜单]
|
||
│ ├─ 时钟设置 [叶子]
|
||
│ ├─ 强制复归 [叶子]
|
||
│ ├─ 手动录波 [叶子]
|
||
│ └─ 清除记录 [叶子]
|
||
├─ 通讯参数 ▶ [菜单]
|
||
│ ├─ 通讯设置 [叶子]
|
||
│ ├─ 网口设置 [叶子]
|
||
│ └─ SNTP设置 [叶子]
|
||
└─ 记录查询 ▶ [菜单]
|
||
├─ SOE记录 [叶子]
|
||
├─ 事故记录 [叶子]
|
||
├─ 操作记录 [叶子]
|
||
├─ 保护告警 [叶子]
|
||
├─ 保护启动 [叶子]
|
||
├─ 遥控记录 [叶子]
|
||
├─ 自检记录 [叶子]
|
||
├─ 运行记录 [叶子]
|
||
├─ 运行报告 [叶子]
|
||
└─ 录波回放 [叶子]
|
||
```
|
||
|
||
**统计**:菜单节点 **9** 个(含根),叶子节点 **34** 个,共 **43** 个节点;最大深度 **4 级**。
|
||
|
||
### 2.3 节点定义表(逻辑层级定义)
|
||
|
||
> 扁平表,与 §3.3 生成物 `g_menu_tab[]` 一一对应:`#` = 数组下标,`父节点` = 父菜单的 `#`,`子项数` = 直接子项个数(叶子为 0)。
|
||
|
||
| # | 名称 | 层级 | 类型 | 父节点 | 子项数 |
|
||
|---|---|---|---|---|---|
|
||
| 0 | 主界面 | 主界面 | 菜单(根) | — | 7 |
|
||
| 1 | 装置信息 | 一级菜单 | 叶子 | 0 | 0 |
|
||
| 2 | 实时数据 | 一级菜单 | 菜单 | 0 | 3 |
|
||
| 3 | 交流量 | 二级菜单 | 叶子 | 2 | 0 |
|
||
| 4 | 直流量 | 二级菜单 | 叶子 | 2 | 0 |
|
||
| 5 | 遥信量 | 二级菜单 | 叶子 | 2 | 0 |
|
||
| 6 | 参数定值 | 一级菜单 | 菜单 | 0 | 2 |
|
||
| 7 | 整定 | 二级菜单 | 菜单 | 6 | 4 |
|
||
| 8 | 参数 | 三级菜单 | 叶子 | 7 | 0 |
|
||
| 9 | 定值 | 三级菜单 | 叶子 | 7 | 0 |
|
||
| 10 | 控制字 | 三级菜单 | 叶子 | 7 | 0 |
|
||
| 11 | 软压板 | 三级菜单 | 叶子 | 7 | 0 |
|
||
| 12 | 查看 | 二级菜单 | 菜单 | 6 | 4 |
|
||
| 13 | 参数 | 三级菜单 | 叶子 | 12 | 0 |
|
||
| 14 | 定值 | 三级菜单 | 叶子 | 12 | 0 |
|
||
| 15 | 控制字 | 三级菜单 | 叶子 | 12 | 0 |
|
||
| 16 | 软压板 | 三级菜单 | 叶子 | 12 | 0 |
|
||
| 17 | 三遥设置 | 一级菜单 | 菜单 | 0 | 5 |
|
||
| 18 | 遥测死区 | 二级菜单 | 叶子 | 17 | 0 |
|
||
| 19 | 遥测系数 | 二级菜单 | 叶子 | 17 | 0 |
|
||
| 20 | 遥信类型 | 二级菜单 | 叶子 | 17 | 0 |
|
||
| 21 | 遥信防抖 | 二级菜单 | 叶子 | 17 | 0 |
|
||
| 22 | 双点遥信 | 二级菜单 | 叶子 | 17 | 0 |
|
||
| 23 | 装置维护 | 一级菜单 | 菜单 | 0 | 4 |
|
||
| 24 | 时钟设置 | 二级菜单 | 叶子 | 23 | 0 |
|
||
| 25 | 强制复归 | 二级菜单 | 叶子 | 23 | 0 |
|
||
| 26 | 手动录波 | 二级菜单 | 叶子 | 23 | 0 |
|
||
| 27 | 清除记录 | 二级菜单 | 叶子 | 23 | 0 |
|
||
| 28 | 通讯参数 | 一级菜单 | 菜单 | 0 | 3 |
|
||
| 29 | 通讯设置 | 二级菜单 | 叶子 | 28 | 0 |
|
||
| 30 | 网口设置 | 二级菜单 | 叶子 | 28 | 0 |
|
||
| 31 | SNTP设置 | 二级菜单 | 叶子 | 28 | 0 |
|
||
| 32 | 记录查询 | 一级菜单 | 菜单 | 0 | 10 |
|
||
| 33 | SOE记录 | 二级菜单 | 叶子 | 32 | 0 |
|
||
| 34 | 事故记录 | 二级菜单 | 叶子 | 32 | 0 |
|
||
| 35 | 操作记录 | 二级菜单 | 叶子 | 32 | 0 |
|
||
| 36 | 保护告警 | 二级菜单 | 叶子 | 32 | 0 |
|
||
| 37 | 保护启动 | 二级菜单 | 叶子 | 32 | 0 |
|
||
| 38 | 遥控记录 | 二级菜单 | 叶子 | 32 | 0 |
|
||
| 39 | 自检记录 | 二级菜单 | 叶子 | 32 | 0 |
|
||
| 40 | 运行记录 | 二级菜单 | 叶子 | 32 | 0 |
|
||
| 41 | 运行报告 | 二级菜单 | 叶子 | 32 | 0 |
|
||
| 42 | 录波回放 | 二级菜单 | 叶子 | 32 | 0 |
|
||
|
||
---
|
||
|
||
## 三、数据层:ui_menu_tab(生成物,只读)
|
||
|
||
> **一句话职责**:提供"有什么条目"——一张扁平父子索引表(43 节点,`const` 存 Flash、只读)+ 4 个查询 API,供逻辑层 / 显示层共用。
|
||
> **绝不做**:持有任何状态、依赖 LVGL、被手改(改菜单走 §3.6 生成器)。
|
||
|
||
### 3.1 选型:扁平"父子索引表"(邻接表 / 关系表模型)
|
||
|
||
成熟树存储方案对比(参考 Wikipedia "Tree (abstract data type)"、文件系统/关系库的层级表示):
|
||
|
||
| 方案 | 原理 | 本方案取舍 |
|
||
|---|---|---|
|
||
| 指针树(动态节点,父/子指针) | 运行时动态增删 | ✗ 需堆 + 指针,嵌入端 GC/碎片/内存风险,`const` 入 Flash 困难 |
|
||
| 首子-兄弟(left-child right-sibling) | 每节点只存 first_child / next_sibling | 紧凑但"取某节点全部子项"需链式遍历,不如邻接直观,调试不友好 |
|
||
| 数组位置表示(堆式) | 靠下标位置推父子(2i/2i+1) | ✗ 要求满二叉/固定出度;菜单出度不定且有命名,不适用 |
|
||
| **邻接表 / 父子索引表(关系表)** | 每行自带 `parent` 索引,子项=parent 等于本下标的行 | ✓ **选用**:无指针、每行自描述、增删改=改行、可入 Flash、启动可校验 |
|
||
|
||
> LVGL 8 已移除 `lv_menu` 控件(`lv_conf.h` 中 `LV_USE_MENU 0`),本项目的扁平表方案与"无 lv_menu"约束一致,且比控件更贴合嵌入端。
|
||
|
||
**核心不变式**:
|
||
- 子项 = 表中所有 `parent == 本下标` 的行,**按表序**排列(表序即显示序)。
|
||
- **菜单 vs 叶子由"是否有子项"自动推导**:加子项→自动出现 ▶;删光子项→自动变叶子。无需单独的类型字段,替换时零维护。
|
||
|
||
### 3.2 数据结构定义
|
||
|
||
```c
|
||
#include <stdint.h>
|
||
#include <stdbool.h>
|
||
|
||
#define MENU_ROOT 0xFFu /* 根哨兵:仅作根节点(主界面 #0)的 parent,表示"它是顶层";8 位,上限 255 项 */
|
||
|
||
/* 菜单节点 = 扁平表一行;子项 = parent 等于本下标的所有行(按表序) */
|
||
typedef struct {
|
||
uint8_t parent; /* 父节点下标(0..254);仅根节点(主界面#0)为 MENU_ROOT(0xFF) */
|
||
const char *name; /* 显示文本(字面量,存 Flash) */
|
||
} ui_menu_node_t;
|
||
```
|
||
|
||
> 实现落在生成器产物 `Drivers/BSP/UI/ui_menu_tab.h`(见 §3.6);字段/语义与此一致。
|
||
|
||
### 3.3 完整 43 节点表(按 §2.3 节点定义表落地)
|
||
|
||
> 产物 `ui_menu_tab.c`(§3.6 生成):`const ui_menu_node_t g_menu_tab[UI_MENU_TAB_COUNT]`,内容与下表一致。
|
||
|
||
```c
|
||
const ui_menu_node_t g_menu_tab[UI_MENU_TAB_COUNT] = {
|
||
/* parent name 下标 */
|
||
{ MENU_ROOT, "主界面" }, /* 0 主界面菜单(根) */
|
||
{ 0, "装置信息" }, /* 1 一级菜单叶子 */
|
||
{ 0, "实时数据" }, /* 2 一级菜单 */
|
||
{ 2, "交流量" }, /* 3 二级菜单叶子 */
|
||
{ 2, "直流量" }, /* 4 二级菜单叶子 */
|
||
{ 2, "遥信量" }, /* 5 二级菜单叶子 */
|
||
{ 0, "参数定值" }, /* 6 一级菜单 */
|
||
{ 6, "整定" }, /* 7 二级菜单 */
|
||
{ 7, "参数" }, /* 8 三级菜单叶子 */
|
||
{ 7, "定值" }, /* 9 三级菜单叶子 */
|
||
{ 7, "控制字" }, /* 10 三级菜单叶子 */
|
||
{ 7, "软压板" }, /* 11 三级菜单叶子 */
|
||
{ 6, "查看" }, /* 12 二级菜单 */
|
||
{ 12, "参数" }, /* 13 三级菜单叶子 */
|
||
{ 12, "定值" }, /* 14 三级菜单叶子 */
|
||
{ 12, "控制字" }, /* 15 三级菜单叶子 */
|
||
{ 12, "软压板" }, /* 16 三级菜单叶子 */
|
||
{ 0, "三遥设置" }, /* 17 一级菜单 */
|
||
{ 17, "遥测死区" }, /* 18 二级菜单叶子 */
|
||
{ 17, "遥测系数" }, /* 19 二级菜单叶子 */
|
||
{ 17, "遥信类型" }, /* 20 二级菜单叶子 */
|
||
{ 17, "遥信防抖" }, /* 21 二级菜单叶子 */
|
||
{ 17, "双点遥信" }, /* 22 二级菜单叶子 */
|
||
{ 0, "装置维护" }, /* 23 一级菜单 */
|
||
{ 23, "时钟设置" }, /* 24 二级菜单叶子 */
|
||
{ 23, "强制复归" }, /* 25 二级菜单叶子 */
|
||
{ 23, "手动录波" }, /* 26 二级菜单叶子 */
|
||
{ 23, "清除记录" }, /* 27 二级菜单叶子 */
|
||
{ 0, "通讯参数" }, /* 28 一级菜单 */
|
||
{ 28, "通讯设置" }, /* 29 二级菜单叶子 */
|
||
{ 28, "网口设置" }, /* 30 二级菜单叶子 */
|
||
{ 28, "SNTP设置" }, /* 31 二级菜单叶子 */
|
||
{ 0, "记录查询" }, /* 32 一级菜单 */
|
||
{ 32, "SOE记录" }, /* 33 二级菜单叶子 */
|
||
{ 32, "事故记录" }, /* 34 二级菜单叶子 */
|
||
{ 32, "操作记录" }, /* 35 二级菜单叶子 */
|
||
{ 32, "保护告警" }, /* 36 二级菜单叶子 */
|
||
{ 32, "保护启动" }, /* 37 二级菜单叶子 */
|
||
{ 32, "遥控记录" }, /* 38 二级菜单叶子 */
|
||
{ 32, "自检记录" }, /* 39 二级菜单叶子 */
|
||
{ 32, "运行记录" }, /* 40 二级菜单叶子 */
|
||
{ 32, "运行报告" }, /* 41 二级菜单叶子 */
|
||
{ 32, "录波回放" }, /* 42 二级菜单叶子 */
|
||
};
|
||
```
|
||
|
||
> `UI_MENU_TAB_COUNT`(=43)与 `MENU_ROOT` 在 `ui_menu_tab.h` 定义。
|
||
|
||
### 3.4 查询 API(逻辑层 / 显示层共用)
|
||
|
||
4 个查询 API 统一模块前缀 `ui_menu_tab_*`(与逻辑层 `menu_*` 函数名区分),参数 `uint8_t`(菜单上限 255 项),随表生成在 `ui_menu_tab.c`:
|
||
|
||
```c
|
||
/* ui_menu_tab.h(生成物;逻辑层与显示层共用) */
|
||
uint8_t ui_menu_tab_children(uint8_t node, uint8_t out[], uint8_t max); /* 取 node 直接子项(按表序),最多 max 个;返回实际个数 */
|
||
bool ui_menu_tab_has_children(uint8_t node); /* 有子项=菜单(带▶);无=叶子 */
|
||
const char *ui_menu_tab_name(uint8_t node); /* 显示名 */
|
||
bool ui_menu_tab_valid(void); /* 启动自检:非根 parent 下标均有效 */
|
||
```
|
||
|
||
**各层用哪几个**(谁问数据层什么问题):
|
||
|
||
| API | 逻辑层(键分发) | 显示层(建框渲染) |
|
||
|---|---|---|
|
||
| `children` | ✓ 取子项/子项数(判空框、取选中子节点) | ✓ 取本框条目列表 |
|
||
| `has_children` | — | ✓ 判行尾有无 ▶ |
|
||
| `name` | — | ✓ 行文本 |
|
||
| `valid` | ✓ 初始化自检(§1.5 第 1 步) | — |
|
||
|
||
**建框 = 对"当前层节点"做一次过滤**(条目**现算不落地**,见 §7.3):
|
||
|
||
```c
|
||
/* 显示层 ui_menu_view_frame_build(depth, node, sel, scroll) 内: */
|
||
uint8_t kids[MENU_ITEM_MAX];
|
||
uint8_t cnt = ui_menu_tab_children(node, kids, MENU_ITEM_MAX); /* node = s_path[depth] */
|
||
for (uint8_t i = 0; i < cnt; i++) {
|
||
/* 注册一行:s_row_node[depth][i] = kids[i](框体自绘时现查名称/箭头) */
|
||
}
|
||
```
|
||
|
||
> 导航/显示一律按 `parent == 节点` 过滤,**改名/换序/增删子项均不影响导航逻辑**。
|
||
|
||
### 3.5 启动自检:`ui_menu_tab_valid()`
|
||
|
||
> 实现随表生成在 `ui_menu_tab.c`:校验**非根节点 parent 下标均 < `UI_MENU_TAB_COUNT`**(捕获表内笔误/生成器缺陷)。
|
||
> 由 `ui_menu_init()` 首行调用(§1.5 第 1 步);失败 → 打 `DBG_ERROR("menu table: INVALID…")` 并放弃建菜单(返回 -1)。
|
||
> 校验取 parent 界校验即够——生成器保证 parent 恒为"上级节点下标"(结构上无环),界校验足以兜底。
|
||
|
||
### 3.6 声明式生成器:`ui_menu_tree.txt` → `ui_menu_tab.c/.h`(与字体生成 `ui_cn_chars.txt → make_font.py` 同模式)
|
||
|
||
菜单层级声明在**缩进即层级**的文本源 `ui_menu_tree.txt`(仓库根,每级 2 空格、`#` 注释),
|
||
脚本读源、推导每个节点 `parent` 下标、产出 C 表。**改菜单只编辑文本源,重跑脚本即可**,导航/显示代码零改动。
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `ui_menu_tree.txt` | **人工编辑源**(缩进即层级);改菜单改这里 |
|
||
| `tools/menu_gen/gen_menu.py` | 生成器(同 `make_font.py` 模式):解析缩进→推导 parent→产出 C 表;含根唯一/缩进对齐/层级上限自检 |
|
||
| `Drivers/BSP/UI/ui_menu_tab.h` | 产物:`ui_menu_node_t` 定义 + `MENU_ROOT` + `UI_MENU_TAB_COUNT` + `extern g_menu_tab[]` |
|
||
| `Drivers/BSP/UI/ui_menu_tab.c` | 产物:`g_menu_tab[UI_MENU_TAB_COUNT]` 完整 43 节点表 |
|
||
|
||
重新生成:
|
||
```
|
||
python tools/menu_gen/gen_menu.py
|
||
```
|
||
|
||
> 产物 `ui_menu_tab.c/.h` 含 43 节点(9 菜单 + 34 叶子,最深 三级菜单;2026-10-29 录波道加两叶后由 42 升到 43);`ui_menu.c` 不持有本地表,导航/显示一律走 `ui_menu_tab_*` 查询 API(§3.4)。
|
||
|
||
> **编码陷阱(已踩)**:产物含中文**字符串字面量**,必须 **UTF-8 带 BOM**(同 `ui_menu.c`)。
|
||
> 无 BOM 时 cl.exe(代码页 936/GBK)会按 GBK 误读 UTF-8 字节 → 字符串解析错乱(C2001/C4819)。`gen_menu.py` 已用 `utf-8-sig` 输出。
|
||
|
||
### 3.7 改菜单流程
|
||
|
||
```text
|
||
1. 编辑 ui_menu_tree.txt —— 缩进即层级(每级 2 空格),# 为注释;增/删/改名/换序条目都只改这里;
|
||
行尾可选 ` #P` = 该叶子需密码(密码页按所输密码授权查看/修改权限,
|
||
密码值在 Drivers/BSP/UI/ui_pwd_def.h 编译期配置)
|
||
2. python tools/menu_gen/gen_menu.py —— 重跑生成器:表 / UI_MENU_TAB_COUNT / 8 位护栏 全部同步更新
|
||
3. 完成:导航(逻辑层)与渲染(显示层)代码零改动
|
||
```
|
||
|
||
> 注意:新增**中文汉字**时,须同步加入字体字符集 `ui_cn_chars.txt` 并重生成字体(见 `UI_中文字体生成与配置.md`),否则显示成方框。
|
||
|
||
### 3.8 Flash 占用(const,只读)
|
||
|
||
| 符号 | 定义处 | 内容 | 占用 |
|
||
|---|---|---|---|
|
||
| `ui_menu_node_t` | `ui_menu_tab.h` | `{ uint8_t parent; uint8_t pwd; const char *name; }`(32 位 8 B/项) | 类型定义 |
|
||
| `g_menu_tab[UI_MENU_TAB_COUNT]` | `ui_menu_tab.c`(生成) | 43 节点扁平父子表 | ≈ 43×8 B ≈ 344 B |
|
||
| 名字字面量 | 同上 | 43 个 `const char[]` | ≈ 400 B |
|
||
| `UI_MENU_TAB_COUNT` / `MENU_ROOT` | `ui_menu_tab.h` | 节点数 43 / 根哨兵 0xFF(8 位,上限 255 项) | 宏 |
|
||
|
||
> 逻辑层/显示层**只读**它(§3.4 的 API 都是对它的过滤/查询),从不修改。合计 ≈ **0.7 KB Flash**。
|
||
|
||
---
|
||
|
||
## 四、显示规范:横向级联子框模型
|
||
|
||
> **本章是显示层(§五)要实现的"图纸"**:讲"页"(每级的子项列表)在**屏上怎么摆**——每级一个**框**,贴靠在其**父框右侧**——一棵**横向的树**(往右生长)。
|
||
> 以下规则由参考图 02~07(`docs/UI设计/mockups/`)逐像素实测得出(设备 px,误差 ≤3px);参考图为**手工 mockup**,与实现的偏差见 §8.2 注 5。
|
||
|
||
### 4.1 总览
|
||
|
||

|
||
|
||
- 每级一个**框**,框 = 该级条目列表(紧凑列表,**无逐框页头/返回按钮**,返回靠 `←`);
|
||
- **无滑动动画**(1bpp 推流);
|
||
- 屏上几框 = 深度 × 各框宽 决定(不固定)。
|
||
|
||
### 4.2 水平布局(X):右贴靠 + 自适应宽
|
||
|
||
- 子框**左缘 = 父框右缘**:共享边框列、零间隙;逐级向右生长(实测:各截图子框均恰好贴在父框右缘 x≈67/106 处)。
|
||
- 框宽 = 该框最宽条目(文字 + ▶ + 边距 + 边框),按内容自适应(公式见 §5.3)。参考图实测:4字+箭头 ≈68、4字无箭头 ≈60、3字 ≈46、2字+箭头 ≈40;**实现实测**(模拟器像素统计):66 / 54 / 46 / 42,另有空终点框 6(偏差 2/8/0/2 px)。
|
||
- 最深框**不钉屏幕右缘**(实测:实时数据子框止于 x≈113/160;三遥/装置维护/记录查询子框止于 x≈127/160)。
|
||
- 本菜单最深路径宽 = 实现实测 66+42+42+6−3(共享边框)= **153 < 160** → 级联不会超屏宽;>160 参考图未覆盖,实际行为 = 框被全屏容器在屏缘裁切(右缘截断),无特殊处理。
|
||
|
||
### 4.3 垂直布局(Y):每框独立(顶对齐 + 底对齐上移)
|
||
|
||
- 默认:顶边 = 父框"选中行"**行顶**(实时数据/整定两例实测 Δ≤2px);框高 = 条目数 × 行高(**按内容**,不强顶屏底)。
|
||
- (顶边 + 框高) 超出屏底 → 整框上移使**子框底边对齐父"选中行"行底**(不是贴屏底):参考图实测 三遥设置 top=26 / 装置维护 top=54 / 通讯参数 top=82 / 记录查询 top=12;**实现实测 28 / 58 / 88 / 13**(Δ=2/4/6/1 px——四个样本不满足同一公式,实现取"底边对齐选中行行底"这一自洽规则,两端极例(三遥/记录查询)精确吻合,见 §8.2 注 5)。
|
||
- 上移后框可叠到屏顶状态行/装饰区(记录查询子框 top≈12)——**顶部不钳制**。
|
||
- 比整屏还高 → `↑`/`↓` 滚(初始对齐"选中行")。
|
||
|
||
### 4.4 选中态与焦点
|
||
|
||
- **选中态**:整行反色(白带、黑字);菜单行有 **▶**,叶子行无;**选中行保留 ▶**(箭头随反色变黑,参考图 02/04 实测);非选中行黑底白字(白箭头)。
|
||
- **焦点** = 最右(最深)框;框无页头/返回按钮(框即紧凑列表,返回靠 `←`);仅根框带主界面装饰(顶部"浅 V" + 状态行"当前模式 : 无模式")。
|
||
|
||
---
|
||
|
||
## 五、显示层:ui_menu_view(持有全部 LVGL 对象)
|
||
|
||
> **一句话职责**:按"状态 + 数据表"渲染级联框——持有**全部** LVGL 对象(样式 / 持久容器 / 框体;根框装饰与状态行由容器自绘、无对象),由逻辑层经 8 个接口(§5.5)驱动建/毁/刷新。行不是对象,由框体自绘。
|
||
> **绝不做**:持有导航状态(17 B 在逻辑层);解释按键(输入回调挂在逻辑层)。
|
||
|
||
### 5.1 职责边界
|
||
|
||
- 持有全部 LVGL 对象(样式/持久容器/框体);根框装饰与状态行由容器自绘(无对象)
|
||
- 按 `frame_build/destroy/refresh` 调用建/毁/刷新(依据"状态 + 数据表")
|
||
- 现算框几何(§5.3 公式)并按层缓存(`s_frame[]`)
|
||
- 提供滚动位读写(`set_scroll` / `get_scroll`)供逻辑层持久化
|
||
|
||
### 5.2 布局常量(设备 px,定义于 `ui_menu_view.c`)
|
||
|
||
| 常量 | 值 | 含义 |
|
||
|---|---|---|
|
||
| `MENU_ROW_H` | 15 | 行高(字体行高 13 + 上下各 1) |
|
||
| `MENU_ROW_TXT_TOP` | 1 | 行内文字垂直偏移(居中) |
|
||
| `MENU_ROW_PAD_L` / `MENU_ROW_PAD_R` | 2 / 2 | 行内左/右留白(紧凑,保证级联总宽 <160) |
|
||
| `MENU_ARROW_GAP` | 2 | 行内 文本→箭头 间距 |
|
||
| `MENU_FRAME_BORDER` | 1 | 框边框宽(子框与父框共享该边框列) |
|
||
| `MENU_ROOT_BOX_X` / `MENU_ROOT_BOX_Y` | 0 / 44 | 根框左上(左对齐、贴左缘) |
|
||
| `MENU_ROOT_STATUS_X` / `MENU_ROOT_STATUS_Y` | 17 / 20 | 状态行"当前模式 : 无模式" |
|
||
| `MENU_DECO_LEG_W` / `MENU_DECO_HORIZ_W` | 3 / 2 | 顶部"浅 V"腿线宽 / 水平线宽(1bpp 斜线边缘易被阈值吃掉,腿须比水平线粗) |
|
||
|
||
字体度量(`ui_font_cn_12`,宋体 12px 1bpp):汉字步进 12 px/字、▶ 步进 10 px(adv_w=165/16)、行高 13 px(见 `UI_中文字体生成与配置.md`)。
|
||
|
||
### 5.3 几何计算(`ui_menu_view_frame_build()`)
|
||
|
||
`frame_build` 拿到 (depth, node, sel, scroll) 后,先**算三个数**——框宽 w、位置 (x, y)、框高 h——再照数建框体、注册行数据、应用选中/滚动。四步各配一格图(下图 ①~④):
|
||
|
||

|
||
|
||
**① 框宽 w = 最宽行 + 2 条边框**(图①)
|
||
|
||
一行横向构成:`边框1 | 留白2 | 文本 |(菜单行: 间距2 + ▶10)| 留白2 | 边框1`。文本宽按字体实际步进量(`lv_text_get_width`,⚠️ 见本文 §8.2 第 1 条(`lv_text_get_width` 的 `length=0` 陷阱));同框各行宽度可以不同,**框宽只由最宽的一行决定**,窄行左对齐、右侧留空:
|
||
|
||
w = max_行( PAD_L + 文本_w + [GAP + ▶_w] + PAD_R ) + 2×BORDER
|
||
|
||
实测五类框宽(1× 原尺寸,px):
|
||
|
||
| 框 | 最宽行构成 | 框宽 |
|
||
|---|---|---|
|
||
| 根框(7 行,4 字 + ▶) | 2 + 48 + 2 + 10 + 2 | **66** |
|
||
| 4 字无箭头框 | 2 + 48 + 2 | 54 |
|
||
| 3 字无箭头框 | 2 + 36 + 2 | 46 |
|
||
| 2 字 + ▶ 框 | 2 + 24 + 2 + 10 + 2 | 42 |
|
||
| 空终点框(无行) | 仅留白 + 边框 | 6 |
|
||
|
||
**② 子框 X:贴在父框右缘,共享 1px 边框列**(图②)
|
||
|
||
x = 父.x + 父.w − 1
|
||
|
||
减掉的 1 = 父框右边框那列像素:子框的左边框与父框的右边框**是同一列**(边框重叠 1px),所以零间隙、不重画,逐级向右生长。
|
||
|
||
**③ 子框 Y(默认):顶边对齐父框"选中行"行顶**(图③)
|
||
|
||
y = 父.y + BORDER + 父sel × ROW_H
|
||
|
||
父框选中哪一行,子框顶边就落在那一行的**行顶**——父子垂直方向"肩并肩",选中行不动子框就不动。
|
||
|
||
**④ 子框 Y(溢出时):整框上移,底边对齐选中行行底**(图④)
|
||
|
||
y + h > 160 → y = 选中行行顶 + ROW_H − h
|
||
|
||
子框比"选中行以下到屏底"的空间高 → 整框上移,底边卡在**选中行行底**(不是贴屏底——贴屏底会让父子脱钩,选中行一移子框位置就乱跳)。上移后仍顶出屏顶(y<0)→ 钳到 0;再不够 → 框高截到可见区、行在框内滚动(§5.4 `LV_SCROLLBAR_MODE_AUTO`)。
|
||
|
||
**框高 h = 按内容,不强顶屏底**
|
||
|
||
h = 行数 × ROW_H + 2×BORDER
|
||
|
||
行数少框就矮(2 行 = 32 px)、行数多框就高;上限受屏高 160 约束(触发④的上移/截断)。
|
||
|
||
**公式汇总**(`ui_menu_view_frame_build()` 的实现即此五式):
|
||
|
||
```text
|
||
w = max_行(PAD_L + 文本_w + [菜单: GAP + ▶_w] + PAD_R) + 2×BORDER (文本宽经 lv_text_get_width)
|
||
h = 行数 × ROW_H + 2×BORDER
|
||
子框 x = 父.x + 父.w − 1 (d>0,共享边框列)
|
||
子框 y = 父.y + BORDER + 父sel×ROW_H (d>0,顶对齐选中行行顶)
|
||
y + h > 160 → y = 选中行行顶 + ROW_H − h;y < 0 → y = 0(截断/框内滚动兜底)
|
||
```
|
||
|
||
### 5.4 对象模型
|
||
|
||
- 屏幕根容器(全屏 flat)→ **单一持久全屏容器** `s_cont`(各层框均建其内;`LV_OBJ_FLAG_IGNORE_LAYOUT` + 绝对定位,透明,永不删;级联框须同屏叠加)
|
||
- 根框体 `s_frame_box[0]` 持久(位置 (0,44)、宽自适应);子框体进入时建 / 返回时毁(`lv_obj_create(s_cont)` + flat + 边框样式 + 自绘回调,`IGNORE_LAYOUT` 绝对定位)
|
||
- 框体:自动滚动条(`LV_SCROLLBAR_MODE_AUTO`);行自绘(无子对象),行内容超框高才框内滚动(内容高经 `LV_EVENT_GET_SELF_SIZE` 虚报)
|
||
- 行 = **数据**(节点下标注册 `s_row_node[d][i]`,无对象);框体 `LV_EVENT_DRAW_MAIN` 自绘:选中行 = 整行白带(`lv_draw_rect`),每行 = `lv_draw_label` 文本(左留白 2)+ 箭头(紧跟文本、间距 2;叶子无箭头),静态字符串(Flash 表、不拷贝);按滚动偏移枚举可见行、重绘区外的行跳过(culling)
|
||
- 选中行 = 整行反色(白带、黑字),**箭头随反色变黑**(§4.4);非选中行黑底白字(白箭头)
|
||
- 根框装饰(浅 V + 状态行):容器 `DRAW_MAIN` 自绘(3 段线 `lv_draw_line` + 1 行文本 `lv_draw_label`,无对象;两腿 3px、水平线 2px)
|
||
- 空终点框:叶子 ENTER 后建 w=6、h=2 的占位框(无行),仅 ESC 可返回
|
||
|
||
### 5.5 公开 API
|
||
|
||
| 接口 | 作用 | 调用方 |
|
||
|---|---|---|
|
||
| `int ui_menu_view_init(void)` | 样式 + 持久容器(装饰/状态行自绘回调)+ 根框框体(含自绘回调)(一次性) | 逻辑层 `ui_menu_init` |
|
||
| `lv_obj_t *ui_menu_view_container(void)` | 取持久容器(焦点/键唯一入口;逻辑层在其上绑输入组/键回调) | 逻辑层 |
|
||
| `void ui_menu_view_frame_build(uint8_t depth, uint8_t node, uint8_t sel, uint16_t scroll)` | 建该深度框:现算几何 + 注册行数据 + 应用选中/滚动(d=0 仅(重)注册行) | 逻辑层(进 / 初始化) |
|
||
| `void ui_menu_view_frame_destroy(uint8_t depth)` | 毁该深度框(框体连自绘回调整体删除、行数据清空;容器保留;根框 d=0 不可毁) | 逻辑层(退) |
|
||
| `void ui_menu_view_frame_refresh(uint8_t depth, uint8_t sel)` | 刷选中(选中数据 + 跟随滚动 + 重绘框体,不重建框) | 逻辑层(移选后) |
|
||
| `void ui_menu_view_set_scroll(uint8_t depth, uint16_t scroll)` | 应用滚动偏移(行数据重注册后按此还原) | 逻辑层 |
|
||
| `uint16_t ui_menu_view_get_scroll(uint8_t depth)` | 读当前滚动偏移(供逻辑层回读持久化) | 逻辑层 |
|
||
| `void ui_menu_view_focus(void)` | 焦点置持久容器(唯一键入口;框深度由 `s_depth` 决定) | 逻辑层(进/退) |
|
||
|
||
> 公共常量:`MENU_PAGE_MAX 4`(深度上限 = 根框 + 3 级子框)、`MENU_ITEM_MAX 16`(单框最大条目数)、`MENU_SCREEN_W/H 160`(与 `ui_port.h` / 模拟器一致)。
|
||
|
||
### 5.6 RAM 字段(显示层)
|
||
|
||
| 字段 | 类型 | 大小 | 用途 |
|
||
|---|---|---|---|
|
||
| `s_cont` | `lv_obj_t*` | 4 B | 单一全屏持久容器(焦点/键唯一入口;各层框建其内);透明常驻、**永不删**(防焦点对象悬空) |
|
||
| `s_row_node[4][16]` | `uint8_t` | 64 B | 行数据:节点下标(名称/箭头绘制时现查,无对象);**仅屏上框注册** |
|
||
| `s_style_frame/scrollbar` | `lv_style_t`×2(动态)+ flat 常量样式(Flash) | ~200 B | 样式(一次性初始化,常驻;flat 为常量样式零池占用,见 §9.5) |
|
||
| `s_frame_box[4]` | `lv_obj_t*`×4 | 16 B | 框体(d=0 持久;d>0 进入时建、返回时毁) |
|
||
| `s_frame[4]` | `menu_frame_t`(built/sel/item_cnt/x/y/w/h) | 48 B | 每框几何缓存:子框对齐要用**父框刚算出**的 x/w/选中行,故按层留一份;随时可由 `s_path`/`s_sel` 重算,**不属于导航状态** |
|
||
|
||
> **只给"屏上的框"注册行数据**(每框 ≤16 行);返回时清掉被关闭最右框的行数据 → 行自绘无对象,LVGL 池只占**屏上框体**(2~3 框),关闭即释放。
|
||
|
||
---
|
||
|
||
## 六、逻辑层:ui_menu(纯状态机)
|
||
|
||
> **一句话职责**:持有**唯一** 17 B 导航状态(路径/选中/深度/滚动)+ 键语义(移/进/退),单向驱动显示层建/毁/刷新级联框。
|
||
> **绝不做**:持 LVGL 对象句柄(唯一例外:输入接线 2 调 `lv_group_add_obj`/`lv_obj_add_event_cb`);渲染。
|
||
|
||
### 6.1 职责边界
|
||
|
||
- 持有 17 B 导航状态(唯一持有者,§6.2)
|
||
- 解释按键:移(↑↓)/ 进(→/ENTER)/ 退(←/ESC)(§6.4)
|
||
- 查询数据层(`children`/`valid`)判键行为(§3.4)
|
||
- 单向驱动显示层:`frame_build/destroy/refresh/focus` + 滚动位回读持久化
|
||
|
||
### 6.2 17 B 导航状态(唯一"必须持久"的状态)
|
||
|
||
| 字段 | 类型 | 大小 | 用途 |
|
||
|---|---|---|---|
|
||
| `s_path[4]` | `uint8_t`×4 | 4 B | **路径**:`s_path[d]`=第 d 层"展示的节点下标"(`s_path[0]=0` 主界面);即"从根到当前(最右)框"的节点链 |
|
||
| `s_sel[4]` | `uint8_t`×4 | 4 B | **各层选中下标**:返回时还原该层高亮(选中不跳) |
|
||
| `s_depth` | `uint8_t` | 1 B | **当前深度**(0=根框)= 焦点所在(最右)框 |
|
||
| `s_scroll[4]` | `uint16_t`×4 | 8 B | **每框滚动偏移**(px):该框销毁再进入、行重建时按此还原位置 |
|
||
| **合计** | | **17 B** | 其余(条目列表/名字/菜单叶子/框几何)全部**现算**,不落 RAM(见 §7.3) |
|
||
|
||
### 6.3 导航规则:数据如何驱动"移 / 进 / 退"
|
||
|
||
> **一句话**:导航**从不遍历树**,它只反复向 `g_menu_tab` 问一个问题——
|
||
> **"谁的 parent 等于当前节点?"** 答案(按表序)就是当前页要显示的条目。
|
||
> **进** = 把选中的子项记为新的"当前节点"再问一遍;**退** = 忘掉它、还原上一个"当前节点"。
|
||
> 表是**扁平**的(无嵌套指针),"树"只是"反复按 parent 过滤"营造出来的错觉。
|
||
|
||
**当前页 = 对表的一次过滤**(页面**按需构建**——进入时建、返回时毁,内存 = 当前导航路径,见 §7.2):
|
||
|
||
```text
|
||
显示"节点 N 的页面" ⇔ 取 g_menu_tab 中所有 parent == N 的行(按表序 = 显示序)
|
||
```
|
||
|
||
| 你所在的页 | 当前节点 N | 执行的查询 | 页面上出现的条目 |
|
||
|---|---|---|---|
|
||
| 主界面(根页) | 0(主界面) | `parent == 0` | 装置信息、实时数据▶、参数定值▶、三遥设置▶、装置维护▶、通讯参数▶、记录查询▶(**7 项**)|
|
||
| 参数定值页 | 6(参数定值) | `parent == 6` | 整定▶、查看▶(**2 项**)|
|
||
| 整定页 | 7(整定) | `parent == 7` | 参数、定值、控制字、软压板(**4 项,全叶子**)|
|
||
|
||
> 行尾有没有 **▶** 也由同一张表判:`parent == 该节点` **非空** → 菜单(有 ▶);**为空** → 叶子(无 ▶)。
|
||
|
||
**移(↑ / ↓)**:只改当前页的选中下标 `s_sel[d]`(**首尾环绕**),**不碰表**。
|
||
|
||
**进(ENTER / →)**:选中条目 = 子节点 **M**(`kids[s_sel[d]]`)。向表问一句"**M 还有孩子吗?**"(`parent == M` 非空?)
|
||
- **有** → M 是菜单:**压一层**,`s_path[d+1]=M`,再按"过滤"显示 M 的孩子。
|
||
- **没有** → M 是叶子:进**终点**(空终点框占位,本阶段不展开内容页),不再下钻。
|
||
|
||
**退(ESC / ←)**:**弹一层**——销毁最右框、回到 depth-1 的父框;父框选中项**保持**为"刚才那个 M 在父框里的下标"(= 按下 ENTER 前的位置,不跳)。
|
||
|
||
**完整走查**(`#` = `g_menu_tab` 下标):主界面 → 参数定值 → 整定 → 参数,再一级级退回
|
||
|
||
```text
|
||
【根页 node=0】 parent==0 → 7 项;选中 [2]=参数定值(#6)
|
||
ENTER ─▶ 问"6 有孩子吗"→ parent==6 非空(整定/查看) → 压层
|
||
【参数定值页 node=6】 parent==6 → [整定#7, 查看#12];选中 [0]=整定
|
||
ENTER ─▶ 问"7 有孩子吗"→ parent==7 非空(参数/定值/控制字/软压板) → 压层
|
||
【整定页 node=7】 parent==7 → [参数#8, 定值#9, 控制字#10, 软压板#11];选中 [0]=参数
|
||
ENTER ─▶ 问"8 有孩子吗"→ parent==8 为空 → 叶子 → 进终点(空框)
|
||
【参数终点 node=8】(占位,无条目,到此为止)
|
||
ESC ─▶ 弹层 → 回【整定页】,选中仍 [0]=参数
|
||
ESC ─▶ 弹层 → 回【参数定值页】,选中仍 [0]=整定
|
||
ESC ─▶ 弹层 → 回【根页】,选中仍 [2]=参数定值
|
||
```
|
||
|
||
**导航状态 = 一条"当前节点"链**(= 从根到当前框的路径)
|
||
|
||
```text
|
||
s_path[0]=0 ─▶ s_path[1]=6 ─▶ s_path[2]=7 ─▶ s_path[3]=8
|
||
根页 参数定值页 整定页 参数终点
|
||
depth=0 depth=1 depth=2 depth=3
|
||
```
|
||
|
||
- 链上**每个 node 都是上一个的孩子**(parent 关系)→ 这条链天然就是路径,**无需额外记录"我从哪来"**。
|
||
- **进 = 链尾追加**(选中的子节点接上去);**退 = 链尾弹掉**。
|
||
- 内存只占"当前这条链"(`s_path[0..s_depth]`)+ 屏上各框各一套行数据(屏外框行清空),退回即释放(§7.2/§7.5)。
|
||
|
||
**对应到代码**(逻辑层决定、显示层执行;三层见 §一):
|
||
|
||
| 逻辑 | 函数 | 读 / 写的数据 |
|
||
|---|---|---|
|
||
| 建框(显示某节点子项) | 显示层 `ui_menu_view_frame_build(d, node, sel, scroll)`(由逻辑层调用) | 读 `s_path[d]`+`g_menu_tab` 现算条目;算 x/顶边/框宽、注册 `s_row_node[d][*]`、恢复 `s_scroll[d]` |
|
||
| 进(压层 / 终点) | 逻辑层 `menu_open(child)` | 写 `s_depth+1`、`s_path[d+1]=M`、`s_sel[d+1]`(同节点还原 / 换节点清零);再 `frame_build` + `focus` |
|
||
| 退(弹层) | 逻辑层 `menu_back()` | `ui_menu_view_frame_destroy(s_depth)`;`s_depth-1`、焦点回父框(父框不动、无需重建,`s_sel`/`s_scroll` 原样保留) |
|
||
| 移选 | 逻辑层 `menu_key_handle(d, key)` → `menu_refresh_layer(d)` | 写 `s_sel[d]`(首尾环绕);`frame_refresh`(高亮 + 跟随滚动);滚动位回读入 `s_scroll[d]` |
|
||
|
||
### 6.4 键语义与分发
|
||
|
||
**键位语义**:
|
||
|
||
| 键 | 行为 |
|
||
|---|---|
|
||
| `↑` / `↓` | 当前(最右)框内上/下移动选中(**首尾环绕**);到边缘且该框可滚则**滚动该框** |
|
||
| `→` 或 `ENTER` | 选中**菜单节点** → 其右侧**弹出子框**;选中**叶子节点** → 进终点(空框) |
|
||
| `←` 或 `ESC` | 返回:**关最右框**(毁框体+行数据)、焦点回父框、选中态保持(不跳) |
|
||
| 焦点 / 框头 | 焦点 = 最右(最深)框;**无逐框页头/返回按钮**(框即紧凑列表,返回靠 `←`);仅根框带主界面装饰 |
|
||
|
||
**进入/返回示例**(路径,`→`/`←` = 进/退动作方向):
|
||
```
|
||
主界面 → 参数定值 → 整定 → 参数
|
||
主界面一级菜单 二级菜单三级菜单
|
||
返回:主界面 ← 参数定值 ← 整定 ← 参数
|
||
```
|
||
|
||
**分发**(`menu_key_handle(d, key)`,`d` = 焦点所在层):
|
||
|
||
```text
|
||
cnt = ui_menu_tab_children(s_path[d], …) ← 先问数据层:本框几个条目
|
||
UP/DOWN : cnt>0 → s_sel[d] 首尾环绕 → menu_refresh_layer(高亮+跟随滚动+滚动位回读)
|
||
ENTER/RIGHT: cnt>0 且 d+1<4 → menu_open(kids[s_sel[d]]) (叶子也压层 → 空终点框)
|
||
ESC/LEFT : d>0 → menu_back() (根框 d=0 不响应;空框只响应返回)
|
||
```
|
||
|
||
### 6.5 公开 API
|
||
|
||
- `int ui_menu_init(void)` —— **唯一公开入口**(uiTask 调用);五步时序见 §1.5,表自检见 §3.5。
|
||
- 内部(`static`):`menu_open` / `menu_back` / `menu_key_handle` / `menu_key_cb` / `menu_refresh_layer`(见 §6.3/§6.4)。
|
||
|
||
---
|
||
|
||
## 七、层间配合细节
|
||
|
||
### 7.1 按键全序列(逐键分层动作)
|
||
|
||
> 通用管线见 §1.4;本表按键列出**逻辑层改什么状态、显示层做什么**。
|
||
|
||
| 键 | 逻辑层(17 B 状态) | 显示层(画面) |
|
||
|---|---|---|
|
||
| **移** ↑/↓ | `s_sel[d] = (s_sel[d] ∓ 1 + cnt) % cnt`(首尾环绕);`cnt=0`(空框)不变 | `frame_refresh(d, s_sel[d])`:改选中数据 + 跟随滚动 + 重绘框体(不重建);随后逻辑层 `get_scroll(d)` 回读入 `s_scroll[d]` |
|
||
| **进** →/ENTER | `s_depth+1`;`s_path[d+1]=kids[s_sel[d]]`;`s_sel[d+1]`(同节点还原原选中/滚动,换节点清零) | `frame_build(d+1, …)`:现算几何(贴父框右缘、顶对齐/底对齐上移)→ 注册行数据 → 应用选中/滚动;`focus()`(焦点恒在持久容器);叶子 → 空终点框(w=6, h=2) |
|
||
| **退** ←/ESC | `s_depth−1`(根框 d=0 不响应) | `frame_destroy(d)`:毁框体(含自绘回调,容器保留,池内存即回落);`focus()`(焦点恒在持久容器);父框不动、无需重建,`s_sel`/`s_scroll` 原样保留 |
|
||
|
||
### 7.2 框生命周期(进建退毁)
|
||
|
||
- **进时建**(`frame_build`):现算几何(§5.3;父框几何取父层 `s_frame` 缓存)→ 注册行数据(行 = 子项,现算不落地)→ 应用选中高亮 + 滚动位。
|
||
- **退时毁**(`frame_destroy`):框体(含自绘回调)整体销毁、容器保留(永不删,防焦点对象悬空);LVGL 池内存随即回落。
|
||
- **根框持久**(`ui_menu_view_init` 建一次,永不毁);子框随进/退建/毁。
|
||
- **焦点**:持久容器为唯一焦点对象(初始化时入输入组并获焦,永不删、焦点不迁移);框深度由逻辑层 `s_depth` 决定,无焦点争抢。
|
||
- **空终点框**:叶子 ENTER 后建 w=6、h=2 占位框(无行),仅 ESC 可返回。
|
||
|
||
### 7.3 存 vs 算(关键区分)
|
||
|
||
> **核心原则:能推导的就不存**——某页的条目列表可由 `g_menu_tab` 的 `parent==节点` 随时重算;
|
||
> 故**只持久化"不可推导"的导航历史(路径 + 各层选中 + 各框滚动)**,页面内容进出时按需重建。
|
||
|
||
| 数据 | 存 / 算 | 依据 |
|
||
|---|---|---|
|
||
| 路径(在哪些层) | **存** `s_path`(逻辑层) | 用户导航历史,不可推导 |
|
||
| 各层选中 | **存** `s_sel`(逻辑层) | 用户选择,不可推导 |
|
||
| 每框滚动位 | **存** `s_scroll`(逻辑层) | 用户 `↑`/`↓` 滚动,不可推导 |
|
||
| 某框条目列表 | **算** | `ui_menu_tab_children(s_path[d])` = 过滤 `parent==s_path[d]` |
|
||
| 条目是菜单/叶子(有无 ▶) | **算** | `ui_menu_tab_has_children(node)` = 存不存在 `parent==node` |
|
||
| 条目显示名 | **算** | `g_menu_tab[node].name` |
|
||
| 每框 x / 顶边 / 框宽 | **算** | 右贴靠布局 + 顶对齐/底对齐上移(§四),现算(按层缓存在 `s_frame[]`) |
|
||
|
||
> **结论:持久导航状态 = `s_path` + `s_sel` + `s_depth` + `s_scroll`(共 17 B,全在逻辑层)**;
|
||
> 框的条目/名字/菜单叶子/位置(框宽·x·顶边) 100% 现算,进/退时重建。
|
||
|
||
### 7.4 导航操作 × 字段读写
|
||
|
||
| 操作 | 读 | 写 |
|
||
|---|---|---|
|
||
| **建框**(进 / 初始化) | `s_path[d]`、`g_menu_tab`(现算条目)、`s_sel[d]`、`s_scroll[d]` | 算该框 x/顶边/框宽(§四);注册 `s_row_node[d][*]`;恢复滚动位 |
|
||
| **移**(↑/↓) | `s_sel[d]`、`s_scroll[d]`、`s_row_node[d][*]` | `s_sel[d]`(首尾环绕);到边缘则改 `s_scroll[d]`(滚);刷新高亮 |
|
||
| **进**(ENTER/→,菜单) | `s_sel[d]`→子节点 M、`ui_menu_tab_has_children(M)`=真 | `s_depth+1`、`s_path[d+1]=M`、`s_sel[d+1]=0`;新框贴父框右缘生成(既有框不动、不重排) |
|
||
| **进**(ENTER/→,叶子) | `ui_menu_tab_has_children(M)`=假 | `s_depth+1`、`s_path[d+1]=M`(终点框) |
|
||
| **退**(ESC/←) | `s_depth` | `s_depth-1`、销毁最右框行、焦点回父框(父框不动、无需重建,`s_sel[d-1]`/`s_scroll[d-1]` 原样保留) |
|
||
|
||
### 7.5 内存预算
|
||
|
||
**Flash(const,数据层)**:`g_menu_tab` 336 B + 名字 ~400 B ≈ **~0.7 KB**
|
||
|
||
**RAM(静态/常驻)**:
|
||
|
||
| 组 | 层 | 大小 |
|
||
|---|---|---|
|
||
| 导航历史 `s_path`+`s_sel`+`s_depth` | 逻辑层 | 9 B |
|
||
| 每框滚动 `s_scroll[4]` | 逻辑层 | 8 B |
|
||
| 容器 `s_cont` | 显示层 | 4 B |
|
||
| 行数据 `s_row_node[d][i]` `[4][16]` | 显示层 | 64 B |
|
||
| 框体句柄 `s_frame_box[4]` | 显示层 | 16 B |
|
||
| 框几何缓存 `s_frame[4]`(`menu_frame_t`) | 显示层 | 48 B |
|
||
| 样式 `s_style_frame/scrollbar`(动态)+ flat 常量样式(Flash,零池占用 §9.5) | 显示层 | ~200 B |
|
||
| **小计** | | **~0.35 KB**(逻辑层 17 B + 显示层 ~0.33 KB) |
|
||
|
||
**LVGL 池(动态,`LV_MEM` 16 KB 预算内)**:行自绘无对象,池只占**屏上框体**(2~3 个 `lv_obj`)+ 根框装饰/状态行,
|
||
典型 ≈ **1~2 KB**,仍远小于 16 KB(池已于 2026-10-15 由 40 KB 缩到 16 KB,见 `docs/内存管理/RAM使用分析与优化方案.md` §4.1.1;真机开机实测 `menu build done: used=3192`)。
|
||
|
||
---
|
||
|
||
## 八、验证与实施陷阱
|
||
|
||
### 8.1 分级验证
|
||
|
||
| 环节 | 方法 |
|
||
|---|---|
|
||
| 模拟器无头 | `lvgl_sim.exe --screenshot shot.bin` / `--keys <seq> shot.bin`;I1 原始像素(160×160 = 3200 B)转 PNG(PIL)目检 |
|
||
| 对照参考图 | 7 状态(根页 + 6 个一级子框)逐一对照参考图 01~07;反色行/框几何用像素统计核验(偏差见 §8.2 注 5) |
|
||
| 编译 | `/keil-build`(ARMCC,0 错 0 警,强制) |
|
||
| 真机 | `/serial-boot-test`(Required 含 `UI port init: OK;UI menu init: OK`;屏幕内容仍需人工目检) |
|
||
|
||
### 8.2 陷阱与保真度说明
|
||
|
||
1. 本 LVGL v9.3 的 `lv_text_get_width(txt, length, font, letter_space)` **length=0 时返回 0**(不是"整串");必须传 `strlen`——否则框宽计算全 0(根框退化 8 px)。
|
||
2. 无头模式**不得**调 `lv_timer_handler()` / `lv_refr_now()`:I1 + `LV_DISPLAY_RENDER_MODE_FULL` 全屏刷新会进渲染死循环(截图因此走 `lv_snapshot`);`--keys` 注键是同步处理,无需驱动帧。
|
||
3. 选中行**保留 ▶**(随反色变黑):参考图 02/04 原图即"选中行箭头仍在、仅随反色变黑",勿实现成"选中隐箭头"(§4.4)。
|
||
4. 字体缺字形 = 方框:菜单树新增/改名的汉字必须同步加入字符集 `ui_cn_chars.txt`(当前 224 唯一字,2026-10-29 实测)并重生成字体(重跑 `extract_cn.py` + `make_font.py`),否则新字显示为方框(流程见 `UI_中文字体生成与配置.md`)。
|
||
5. **参考图保真度**:参考图为手工 mockup,4 个样本(子框顶位置)不满足单一公式;实现采用自洽规则(顶对齐/底对齐上移),实测与参考图偏差:顶位置 2/4/6/1 px(三遥/装置维护/通讯参数/记录查询),框宽 2/8/0/2 px(4字+箭头/4字/3字/2字+箭头)。两端极例(三遥、记录查询)精确吻合;统一规则优于逐屏特调,偏差范围内不影响识读与操作。
|
||
6. 装饰"浅 V"两腿线宽必须 3(水平线 2):1bpp 下细斜线边缘被阈值吃掉,腿不粗于水平线即消失;且依赖 `LV_DRAW_SW_COMPLEX=1`(见 `UI_设计架构.md` §1.4 约束 7)。
|
||
|
||
---
|
||
|
||
## 九、内容页(ui_content_page)
|
||
|
||
> **一句话职责**:菜单叶子节点进入后的**独立全屏页面**——装置信息(node 1)、交流量(node 3)、直流量(node 4)、用户密钥密码页(受保护叶子,表单/操作页)。与级联框同屏互斥,按需建毁。
|
||
> **文件**:`Drivers/BSP/UI/ui_content_page.c/h`(MCU / PC 模拟器双端共用,注册见 `UI_设计架构.md` §1.2/§1.4 约束 5)。
|
||
|
||
### 9.1 对象树与生命周期
|
||
|
||
```text
|
||
菜单屏(持久) 内容页屏幕(按需建毁)
|
||
screen_menu screen_page = lv_obj_create(NULL)
|
||
└─ cont 持久容器(焦点/键入口) └─ s_cont 全屏容器(flat + CLICK_FOCUSABLE,唯一键入口)
|
||
├─ 根框体 / 子框体… (行全部自绘,无子对象——与级联框同一"行=数据"策略)
|
||
```
|
||
|
||
| 动作 | 序列 |
|
||
|---|---|
|
||
| **开页**(菜单 ENTER 叶子 node 1/3/4) | `lv_obj_create(NULL)` 新建屏 → 建全屏容器(flat 常量样式 + 键回调 + 自绘回调)→ 入默认输入组并获焦(先于切屏,按键立即由本模块接管)→ `lv_screen_load` 切屏。菜单屏对象**保留不销毁** |
|
||
| **关页**(ESC/←) | 焦点先回菜单持久容器 → 切回菜单屏(原样还原)→ 删容器、再删屏。LVGL 池内存随即回落 |
|
||
|
||
- **导航状态不压层**:菜单 `s_depth/s_sel/s_scroll` 原样保留,返回即还原;内容页只记自身选中行 `s_sel`(每次进入从首行开始,不跨次保留)
|
||
- **单页互斥**:同一时刻至多一个内容页打开(`s_open` 哨兵防御)
|
||
|
||
### 9.2 页面定义表(Flash 常量,几何实测自参考图 10/20/21)
|
||
|
||
| 参数 | 装置信息 | 交流量 | 直流量 |
|
||
|---|---|---|---|
|
||
| `selectable` | 0(静态键值行) | 1(数据列表) | 1(数据列表) |
|
||
| 行数 / 每页 | 6 / 不分页 | 24 / 8(3 页) | 3 / 单页 |
|
||
| 首行槽顶 y / 行距 | 40 / 15 | 24 / 14 | 24 / 14 |
|
||
| 选中白带高 | 无 | 13(x=2..156) | 13(x=2..156) |
|
||
| 左列 x / 值列 x | 标签 6 / 86 | 名 7 / 101 | 名 7 / 101 |
|
||
| 副标题 | "馈线自动化终端" y=25 | — | — |
|
||
| 页脚(y=143,x=6) | 固定"第 10 页,共 1 页"(按参考图原样) | 动态"第 X 页,共 3 页" | 动态"第 1 页,共 1 页" |
|
||
|
||
- 标题 y=2(居中,浅 V 装饰区内;装饰几何与菜单根框一致:两腿 3px / 水平线 2px)
|
||
- **用户密钥页**(表单/操作页,无行槽):title_y=44、"浅 V"/页脚关(hide_deco/hide_footer)、key_intercept=1;自绘 3 个 14×14 数字框(间距 6px、行顶 y=78、首框左 x=53)+ 反显光标,详见 `内容页架构.md` §4.1
|
||
- 列表页窗口 = 选中行所在页(首可见行 = `(sel/每页数)×每页数`),行槽位置固定、数据随页切换
|
||
- 数据表为 Flash `const cp_row_t[]`(名+值字面量,不 strdup);交流量第 2/3 页当前为占位数据
|
||
|
||
### 9.3 键语义
|
||
|
||
| 键 | 行为 |
|
||
|---|---|
|
||
| ↑ / ↓ | 列表页移选(**首尾环绕、跨页连续**),窗口随页切换后重绘;信息页不响应 |
|
||
| ESC | 关页返回菜单(导航状态原样还原;恒不可拦截) |
|
||
| ← | `key_intercept=1` 时先交页面 key_cb(true = 已处理不关页,如密码页移光标);未置标志或未处理 → 照旧关页返回 |
|
||
|
||
### 9.4 公开 API
|
||
|
||
```c
|
||
bool ui_content_page_open(content_page_id_t id); /* 开页:建屏+切屏+焦点置页面容器;ID 无效/已有页打开 → false */
|
||
void ui_content_page_close(void); /* 关页:焦点回菜单容器 + 切回菜单屏 + 销毁页面 */
|
||
void ui_content_page_invalidate(void); /* 整页重绘(表单/操作页自身状态变化后调用) */
|
||
bool ui_content_page_is_open(void); /* 查询是否有内容页打开 */
|
||
```
|
||
|
||
- 节点→页映射在逻辑层 `ui_menu.c`(`menu_content_page_for(node)`):node 1 → `CONTENT_PAGE_DEVICE_INFO`、3 → `AC`、4 → `DC`;其余叶子无内容页(进空白终点框)。需密码叶子(行尾 ` #P`,如 node 8 参数;密码检查先于内容页判断,带内容页的需密码叶子同样先经密码页)先 arm + 开 `CONTENT_PAGE_PASSWORD`,确认一致后回调(携授予权限) **先关页再续做进入动作**(有内容页→开内容页携权限,页面经 `ui_content_page_is_editable()` 判只读/可编辑);无密码叶子直接开(默认只读)
|
||
|
||
### 9.5 内存预算(模拟器 x64 实测,16 KB 池)
|
||
|
||
| 时刻 | LVGL 池 used | 说明 |
|
||
|---|---|---|
|
||
| 菜单基线(根框建完) | 5368 B | 同 §1.5;2026-10-11 裁剪 LVGL 控件类后基线 5440→5368,本行旧值 4784 是更早的快照 |
|
||
| 内容页打开峰值 | +776~1192 B(增量口径) | 增量为实测值、与基线无关:**绝对水位随基线漂移,5560~5976 B 是老基线 4784 时代的快照,按 5368 基线的绝对峰值待重测**(回归脚本只钉基线与往返两个数) |
|
||
| 关页后回落 | **5448 B** | 一次性残留 **+80 B**(见下),非泄漏 |
|
||
|
||
**+80 B 一次性残留分解**(均为 LVGL 核心行为,与内容页模块无关;MCU 32 位下更小):
|
||
- +8 B:`disp->screens` 数组高水位——开页时第 5 个屏使 TLSF 块 32→40 B,关页缩回后块不归还(分配器行为)
|
||
- +72 B:`lv_screen_load()` 内部对菜单屏执行 `lv_obj_set_pos(act_scr,0,0)` → 局部 X/Y 样式各分配一个零初始化 `lv_style_t`+属性数组;每屏一次性,随菜单屏销毁才释放
|
||
|
||
**常驻样式零池占用**:flat 样式用常量样式(`LV_STYLE_CONST_INIT` + `lv_style_const_prop_t[]`,属性存 Flash)。动态 `lv_style_t` 首次 set_prop 会从池分配 `values_and_props` 且永不释放(x64 实测 88 B/个)——常驻样式禁用动态初始化(见 `UI_设计架构.md` §5.6)。
|