Files
STM32F4-Base/docs/UI设计/菜单页面设计.md
T
wandering1 52976bdec3 refactor(编号): 批 4 陷阱号统一为 TRAP-<模块码>-<nn>
本批是 ② 形态迁移清的第一族(用户裁定顺序:② 里的 Trap 一族先做)

统一了什么(数字不变、只补模块码,§7.1)——改名工具 23 处 / 11 文件
- Trap 01~07 → TRAP-GD5F-01~07:属主文档 7 个标题行从此成为定义位
- 陷阱 1 → TRAP-RTC-01(SD2506 属 RTC 域,§4.2)、见陷阱 01 → TRAP-GD5F-01
- Trap 21 / Trap 19 → TRAP-NET-21 / TRAP-NET-19(引用的是活登记处
  CH395F_Trap_Records.md,号本身不变)
- 波及 Drivers/BSP/GD5F2GQ5UE/*(4 处注释)与 test/board/*(2 处)→ 已重编

三处判读后**刻意不改**(写进 §8.3,不让人当成漏网)
- docs/环境与部署/并行任务工作树方案.md 的 wip/trap22-fix 是 git 分支名,不是编号;
  这类非编号字面量要靠类型处理,与位号 T1、IEEE 负序 S2、gitlint 规则 ID 同一批
- docs/UI设计/菜单页面设计.md 变更记录行的「陷阱 3」按 §8.1 冻结不改写
- docs/硬件平台/V0.0、V0.1 网表报告里 6 处 Trap05 语义确指 TRAP-NET-05,但该目录
  是门禁与改名工具共同的排除语料 → 不越界动笔,盲区登记进注册表 TRAP 族 cursor_note

两处不是改名、是改错话(映射表类工具的典型失效:字面对了、意思错了)
- scripts/README.md 原写「Trap 19 从未存在」;改名后字面变成 TRAP-NET-19,而它是
  **真实条目**(SET_MAC 必须在 INIT_CH395 之前)→ 那句话成了假话,改用站得住的例子
- docs/编号规范.md 同一处事故叙述补准确说法:当年裸形不在册 ≠ 这个坑不存在
- 菜单页面设计.md 的「见 §8.2 陷阱 1」本就不是 TRAP-* 族的号(指本文小节第 1 条),
  改写成「见本文 §8.2 第 1 条(lv_text_get_width 的 length=0 陷阱)」

关闸第一次真拦人:咬的是自己家的审计表
- scripts/rename_maps/batch3-trap.json 里必然写着 Trap 21 / 陷阱 1 等旧形态,
  被当成 6 类「台账外新账」→ 提交被拦(拦得对,但表不是语料)
- 第 4 次同一类自循环(前三次:别名表、注册表、债务台账)→ 整个 rename_maps/
  目录入 excluded_paths;这条规律写进 §8.4:**登记号的表一律不得当引用语料**

实测变化
- 错误级 659 → 632(−27);② 空格/中文形 Trap 残余 2 处(都是上面刻意保留的)
- ③ 悬空错误 44 → 42:TRAP-GD5F-01/05/06 三处历史悬空引用自动落地为已定义
- 编号字面量 8277 → 8312(识别面没有缩水,这是改名最容易踩的坑);
  可判定比例 82.92% → 82.99%(基线 82.6%);⑦ 定义无引用 100 → 103(如 TRAP-GD5F-02
  只有定义位无人引用,如实挂 warn 不压掉)
- 台账 --prune-backlog 收掉已还清 42 条,剩 298 条(只减不增);--strict 通过

另修:core.autocrlf=true 把 SD2506_Trap_Records.md 检出成 CRLF(41 处)→ 还原 LF,
该文件 diff 只剩 1 行实质改动(仓库权威行尾是 LF,见 .gitattributes)

验证(全绿)
- @build:0 错误 0 警告(Program Size Code=213356 RO=18580 RW=1184 ZI=131480)
- ci_local.py:L0 + L0.5 全过(16 runner / 1104 断言)
- check_doc_links.py --all:143 篇文档 0 失效、0 待确认
- 改动文件字节卫生逐项与 HEAD 对比:BOM / CR / U+FFFD 异常 0

审计轨迹与文档
- 映射表 scripts/rename_maps/batch3-trap.json(含作用域切分理由与刻意不改清单)
- 别名 11 条全部带 scope;docs/编号规范.md §7.1 行状态、§8.3 批 4 行 + 说明段、
  §8.4 自循环第 4 条、变更记录
2026-10-21 12:32:13 +08:00

69 KiB
Raw Blame History

菜单页面设计

用途:定义 DTU 主菜单的内容(42 节点层级树)、视觉规范、三层代码架构(数据层 / 显示层 / 逻辑层),以及叶子节点进入后的内容页模块(§九)。 范围:菜单层级(节点/父子/层级/进出导航)+ 三个已实现的内容页(装置信息/交流量/直流量,§九);其余叶子的内容页后续按同模式扩展。 来源:用户提供的主菜单截图 7 屏(根页 + 实时数据/参数定值/三遥设置/装置维护/通讯参数/记录查询各一级子页 + 参数定值·整定二级)。 导航模型:树形逐级进入——ENTER/→ 进入子页,ESC/← 返回上一级;最多 4 级(L0~L3)。

阅读指南:

你想… 读
搞懂哪段代码干什么、三层怎么配合 §一 架构总览(职责速览 / 调用矩阵 / 一次按键的旅程 / 初始化时序 / 层间不变量),再按需深入 §三 / §五 / §六
深入某一层 §三 数据层 / §五 显示层 / §六 逻辑层(各章自包含:职责边界 + 字段 + 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 三层职责速览

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[42] 父子索引表 + 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 按键协作:一次按键的旅程

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(),三层按此顺序配合:

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 打印)——@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 内存监控

二、菜单内容:42 节点层级树

2.1 层级总览

主菜单为 4 级树:

层级 含义 例子
L0 根 主界面,7 个一级条目 主界面
L1 一级 根页的 7 个条目 装置信息、实时数据、参数定值、三遥设置、装置维护、通讯参数、记录查询
L2 二级 一级子菜单的子项 交流量、整定、遥测死区、时钟设置、通讯设置、SOE记录 …
L3 三级 仅"参数定值 → 整定/查看"下的子项 参数、定值、控制字、软压板

节点分两类(决定导航行为):

类型 标识 行为
菜单节点 行尾 ▶ 箭头 ENTER 进入 → 展开下一级子页
叶子节点 无箭头 ENTER 进入 → 有内容页(node 1/3/4,§九)则切内容屏;否则进空白终点框

根页默认选中第 1 项"装置信息"(整行反色:白带、黑字)。

2.2 完整层级树

主界面 (L0)
├─ 装置信息                          [叶子]
├─ 实时数据 ▶                        [菜单]
│   ├─ 交流量                        [叶子]
│   ├─ 直流量                        [叶子]
│   └─ 遥信量                        [叶子]
├─ 参数定值 ▶                        [菜单]
│   ├─ 整定 ▶                        [菜单]
│   │   ├─ 参数                      [叶子]
│   │   ├─ 定值                      [叶子]
│   │   ├─ 控制字                    [叶子]
│   │   └─ 软压板                    [叶子]
│   └─ 查看 ▶                        [菜单]
│       ├─ 参数                      [叶子]
│       ├─ 定值                      [叶子]
│       ├─ 控制字                    [叶子]
│       └─ 软压板                    [叶子]
├─ 三遥设置 ▶                        [菜单]
│   ├─ 遥测死区                      [叶子]
│   ├─ 遥测系数                      [叶子]
│   ├─ 遥信类型                      [叶子]
│   ├─ 遥信防抖                      [叶子]
│   └─ 双点遥信                      [叶子]
├─ 装置维护 ▶                        [菜单]
│   ├─ 时钟设置                      [叶子]
│   ├─ 强制复归                      [叶子]
│   ├─ 手动录波                      [叶子]
│   └─ 清除记录                      [叶子]
├─ 通讯参数 ▶                        [菜单]
│   ├─ 通讯设置                      [叶子]
│   ├─ 网口设置                      [叶子]
│   └─ SNTP设置                      [叶子]
└─ 记录查询 ▶                        [菜单]
    ├─ SOE记录                       [叶子]
    ├─ 事故记录                      [叶子]
    ├─ 操作记录                      [叶子]
    ├─ 保护告警                      [叶子]
    ├─ 保护启动                      [叶子]
    ├─ 遥控记录                      [叶子]
    ├─ 自检记录                      [叶子]
    ├─ 运行记录                      [叶子]
    └─ 运行报告                      [叶子]

统计:菜单节点 9 个(含根),叶子节点 33 个,共 42 个节点;最大深度 4 级。

2.3 节点定义表(逻辑层级定义)

扁平表,与 §3.3 生成物 g_menu_tab[] 一一对应:# = 数组下标,父节点 = 父菜单的 #,子项数 = 直接子项个数(叶子为 0)。

# 名称 层级 类型 父节点 子项数
0 主界面 L0 菜单(根) — 7
1 装置信息 L1 叶子 0 0
2 实时数据 L1 菜单 0 3
3 交流量 L2 叶子 2 0
4 直流量 L2 叶子 2 0
5 遥信量 L2 叶子 2 0
6 参数定值 L1 菜单 0 2
7 整定 L2 菜单 6 4
8 参数 L3 叶子 7 0
9 定值 L3 叶子 7 0
10 控制字 L3 叶子 7 0
11 软压板 L3 叶子 7 0
12 查看 L2 菜单 6 4
13 参数 L3 叶子 12 0
14 定值 L3 叶子 12 0
15 控制字 L3 叶子 12 0
16 软压板 L3 叶子 12 0
17 三遥设置 L1 菜单 0 5
18 遥测死区 L2 叶子 17 0
19 遥测系数 L2 叶子 17 0
20 遥信类型 L2 叶子 17 0
21 遥信防抖 L2 叶子 17 0
22 双点遥信 L2 叶子 17 0
23 装置维护 L1 菜单 0 4
24 时钟设置 L2 叶子 23 0
25 强制复归 L2 叶子 23 0
26 手动录波 L2 叶子 23 0
27 清除记录 L2 叶子 23 0
28 通讯参数 L1 菜单 0 3
29 通讯设置 L2 叶子 28 0
30 网口设置 L2 叶子 28 0
31 SNTP设置 L2 叶子 28 0
32 记录查询 L1 菜单 0 9
33 SOE记录 L2 叶子 32 0
34 事故记录 L2 叶子 32 0
35 操作记录 L2 叶子 32 0
36 保护告警 L2 叶子 32 0
37 保护启动 L2 叶子 32 0
38 遥控记录 L2 叶子 32 0
39 自检记录 L2 叶子 32 0
40 运行记录 L2 叶子 32 0
41 运行报告 L2 叶子 32 0

三、数据层:ui_menu_tab(生成物,只读)

一句话职责:提供"有什么条目"——一张扁平父子索引表(42 节点,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 数据结构定义

#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 完整 42 节点表(按 §2.3 节点定义表落地)

产物 ui_menu_tab.c(§3.6 生成):const ui_menu_node_t g_menu_tab[UI_MENU_TAB_COUNT],内容与下表一致。

const ui_menu_node_t g_menu_tab[UI_MENU_TAB_COUNT] = {
    /* parent    name            下标 */
    { MENU_ROOT, "主界面" },      /*   0  L0 菜单(根) */
    { 0,         "装置信息" },    /*   1  L1 叶子 */
    { 0,         "实时数据" },    /*   2  L1 菜单 */
    { 2,         "交流量" },      /*   3  L2 叶子 */
    { 2,         "直流量" },      /*   4  L2 叶子 */
    { 2,         "遥信量" },      /*   5  L2 叶子 */
    { 0,         "参数定值" },    /*   6  L1 菜单 */
    { 6,         "整定" },        /*   7  L2 菜单 */
    { 7,         "参数" },        /*   8  L3 叶子 */
    { 7,         "定值" },        /*   9  L3 叶子 */
    { 7,         "控制字" },      /*  10  L3 叶子 */
    { 7,         "软压板" },      /*  11  L3 叶子 */
    { 6,         "查看" },        /*  12  L2 菜单 */
    { 12,        "参数" },        /*  13  L3 叶子 */
    { 12,        "定值" },        /*  14  L3 叶子 */
    { 12,        "控制字" },      /*  15  L3 叶子 */
    { 12,        "软压板" },      /*  16  L3 叶子 */
    { 0,         "三遥设置" },    /*  17  L1 菜单 */
    { 17,        "遥测死区" },    /*  18  L2 叶子 */
    { 17,        "遥测系数" },    /*  19  L2 叶子 */
    { 17,        "遥信类型" },    /*  20  L2 叶子 */
    { 17,        "遥信防抖" },    /*  21  L2 叶子 */
    { 17,        "双点遥信" },    /*  22  L2 叶子 */
    { 0,         "装置维护" },    /*  23  L1 菜单 */
    { 23,        "时钟设置" },    /*  24  L2 叶子 */
    { 23,        "强制复归" },    /*  25  L2 叶子 */
    { 23,        "手动录波" },    /*  26  L2 叶子 */
    { 23,        "清除记录" },    /*  27  L2 叶子 */
    { 0,         "通讯参数" },    /*  28  L1 菜单 */
    { 28,        "通讯设置" },    /*  29  L2 叶子 */
    { 28,        "网口设置" },    /*  30  L2 叶子 */
    { 28,        "SNTP设置" },    /*  31  L2 叶子 */
    { 0,         "记录查询" },    /*  32  L1 菜单 */
    { 32,        "SOE记录" },     /*  33  L2 叶子 */
    { 32,        "事故记录" },    /*  34  L2 叶子 */
    { 32,        "操作记录" },    /*  35  L2 叶子 */
    { 32,        "保护告警" },    /*  36  L2 叶子 */
    { 32,        "保护启动" },    /*  37  L2 叶子 */
    { 32,        "遥控记录" },    /*  38  L2 叶子 */
    { 32,        "自检记录" },    /*  39  L2 叶子 */
    { 32,        "运行记录" },    /*  40  L2 叶子 */
    { 32,        "运行报告" },    /*  41  L2 叶子 */
};

UI_MENU_TAB_COUNT(=42)与 MENU_ROOT 在 ui_menu_tab.h 定义。

3.4 查询 API(逻辑层 / 显示层共用)

4 个查询 API 统一模块前缀 ui_menu_tab_*(与逻辑层 menu_* 函数名区分),参数 uint8_t(菜单上限 255 项),随表生成在 ui_menu_tab.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):

/* 显示层 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] 完整 42 节点表

重新生成:

python tools/menu_gen/gen_menu.py

产物 ui_menu_tab.c/.h 含 42 节点(9 菜单 + 33 叶子,最深 L3);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 改菜单流程

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(生成) 42 节点扁平父子表 ≈ 42×8 B ≈ 336 B
名字字面量 同上 42 个 const char[] ≈ 400 B
UI_MENU_TAB_COUNT / MENU_ROOT ui_menu_tab.h 节点数 42 / 根哨兵 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——再照数建框体、注册行数据、应用选中/滚动。四步各配一格图(下图 ①~④):

级联框几何计算四步:① 行剖面定框宽 ② X 贴父框右缘 ③ Y 顶对齐选中行 ④ Y 溢出上移

① 框宽 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() 的实现即此五式):

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):

显示"节点 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 下标):主界面 → 参数定值 → 整定 → 参数,再一级级退回

【根页   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]=参数定值

导航状态 = 一条"当前节点"链(= 从根到当前框的路径)

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 返回:关最右框(毁框体+行数据)、焦点回父框、选中态保持(不跳)
焦点 / 框头 焦点 = 最右(最深)框;无逐框页头/返回按钮(框即紧凑列表,返回靠 ←);仅根框带主界面装饰

进入/返回示例(路径,→/← = 进/退动作方向):

主界面 → 参数定值 → 整定 → 参数
   L0      L1       L2     L3
返回:主界面 ← 参数定值 ← 整定 ← 参数

分发(menu_key_handle(d, key),d = 焦点所在层):

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 预算内):行自绘无对象,池只占屏上框体(23 个 lv_obj)+ 根框装饰/状态行, 典型 ≈ **12 KB**,仍远小于 16 KB(池已于 2026-10-15 由 40 KB 缩到 16 KB,见 docs/内存管理/RAM使用分析与优化方案.md §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)
编译 @build(ARMCC,0 错 0 警,强制)
真机 @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(当前 122 唯一字)并重生成字体(重跑 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 对象树与生命周期

菜单屏(持久)                          内容页屏幕(按需建毁)
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

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 说明
菜单基线(根框建完) 4784 B 同 §1.5
内容页打开峰值 5560~5976 B +776~1192 B:屏对象+容器+事件列表+局部样式+screens 数组扩容(x64 指针 8B,MCU 32 位下更小)
关页后回落 4864 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)。


十、变更记录

日期 变更
2026-09-03 初始版本:依据用户 7 屏主菜单截图定义完整 4 级逻辑层级(42 节点)+ 导航逻辑 + 与现有 g_menu_tab 差异对照
2026-09-03 按范围调整:移除"内容页类型"相关内容,聚焦菜单层级(节点/父子/层级/导航);新增 子项数 列;标注"查看"子项为推测待确认
2026-09-03 查看(#12)子项经用户确认与"整定"同构(参数/定值/控制字/软压板),由推测转为正式定义,移除全部 ⚠ 标记与待确认节
2026-09-03 新增 §六"菜单树数据结构":选型扁平"父子索引表"(邻接表/关系表模型,对比指针树/首子-兄弟/堆式),给出 ui_menu_node_t + 完整 42 节点表 + 导航 API(menu_get_children/menu_has_children/menu_get_name)+ 启动自检 + 声明式生成器 + 与现有 g_menu_tab 迁移映射
2026-09-03 声明式生成器落地(§6.6 由"可选"转"已实现"):新增 ui_menu_tree.txt(缩进即层级源)+ tools/menu_gen/gen_menu.py(同 make_font.py 模式),生成 Drivers/BSP/UI/ui_menu_tab.c/.h(42 节点:9 菜单 + 33 叶子,最深 L3);已跑通并校验 parent 下标/连通性/子项数全部正确
2026-09-04 §四 新增"导航走查":用真实 g_menu_tab 下标逐步演示"移/进/退"如何由 parent==当前节点 一次过滤驱动(当前页=过滤、进=压页/终点、退=弹页、导航态=当前节点链),并修正根页模型为"主界面 node 0 的子项"(§6.2/6.7 同步,去掉"虚拟根页"表述)
2026-09-04 新增 §七"导航运行时数据结构(完整清单)":静态层(g_menu_tab 只读)+ 运行时层(导航历史 s_path+s_sel+s_depth=13 B、持久容器 s_cont[4]、单套活跃行 s_row*[]、页头/装饰、样式);"存 vs 算"区分、操作×字段读写表、内存预算、与现有 s_pages[4] 收敛对照
2026-09-04 改名 menu_is_menu → menu_has_children(原名"menu 是不是 menu"含糊;新名=该节点有无子项);§四 路径/代码映射由旧 s_pages[] 统一为 §七 的 s_path[]/s_depth/s_sel[]/s_row*[] 模型,消除 §四/§七 不一致
2026-09-04 改名 menu_children → menu_get_children、menu_name → menu_get_name(裸名词改"动词+对象",API 统一 has_/get_ 风格);§6.4 建页片段改用 s_path/s_depth/s_cont 现算模型(条目不落地)
2026-09-04 §6.4 补"签名约定"说明:node/out 取 uint16_t 因与 parent(须装 0xFFFF 根哨兵)同域、非容量所需(42 下标 8 位够);max=出参缓冲容量(防越界 + 对齐单页行数,今日 9≤16 不触发截断)
2026-09-04 索引位宽 16→8 位:MENU_ROOT 由 0xFFFF 降为 0xFF,parent/node/out/s_path[] 均改 uint8_t(菜单上限 255 项,远够);路径历史 13→9 B、省 ~20 B RAM,Flash 不变(结构体因指针仍 8 B/项);双护栏=生成器 节点数>255 报错 + 头文件 C99 断言 menu_tab_count_check。生成器已改并重生成 ui_menu_tab.c/.h,MSVC /W4 编译 + 导航测试(根 7 / 整定 4 / 查看 4 / 记录查询 9)通过;同步 §四/§6.2/§6.4/§6.5/§七。取代上一条"签名约定(uint16_t)"决定
2026-09-04 修正 §四"进入/返回示例":返回行节点顺序原与进入行相反(参数在左)致 ← 被读成"前进"方向;改为与进入行同序(主界面 L0 在左…参数 L3 在右)+ ←,并补"怎么读"注(→/← = 进/退动作方向;返回 = 从最右 参数(L3) 沿 ← 一级级退回最左 主界面(L0))
2026-09-04 子页面显示模型定稿(横向级联子框):§四 新增 ⑧"显示模型"——横向树,每级一框在上一级右侧、顶边对齐上级"选中行"行顶;水平右锚定(最深框钉右缘、父框超宽滚出左、行按屏上/外建毁)+ 框宽文字自适应 + 无动画;垂直每框独立(顶边=上级选中行行顶、框高按内容、仅底边>160 才上移贴屏底、比整屏高则 ↑/↓ 滚);焦点=最右框(↑/↓ 移选/滚、→/ENTER 进、←/ESC 关最右框)。§七 同步:行对象按层 s_row[d][i] 仅屏上框建、去掉逐框页头(s_header/s_back_btn/s_title)、新增每框滚动 s_scroll[](8 B)、"存 vs 算"增 滚动=存/位置=算、持久态 9→17 B、内存预算(静态 ~0.75→1.3 KB、LVGL 池=屏上 23 框)、对照表更新
2026-09-04 §四⑧ 按参考图 02~07 像素实测校准(设备 px,误差 ≤3px):层级树/命名/箭头/选中态全部与图一致 ✓;显示模型两处修正——水平由"右锚定(钉屏幕右缘)"改为"子框左缘=父框右缘(共享边框列、零间隙)+ 宽自适应"(实测各子框均贴父框右缘 x≈67/106,最深框止于 x≈113~127 而非屏右缘);垂直溢出由"上移使底边贴屏底"改为"上移使子框底边对齐父选中行行底"(实测三遥 top=26/装置维护 top=54/通讯参数 top=82/记录查询 top=12 均吻合,且可叠到屏顶状态行区、顶部不钳制)。本菜单最深路径 ≈155<160 不会溢出;>160 行为与"根页框宽 99 vs 68"列为待确认。§7.2/§7.3/§7.4 "右锚定"表述同步
2026-09-04 实现落地,本文档与代码同步:三层拆分实现(ui_menu_tab 生成物 + 新增 ui_menu_view 显示层、ui_menu 瘦身为纯状态机,§8.1);§四⑦/§6.4/§6.5/§6.6/§6.7 函数与 API 名同步为实际 ui_menu_tab_*/ui_menu_view_*/menu_open|menu_back|menu_key_handle;§五"待实现"→"已实现";§七 修正为实际字段(容器从不切 HIDDEN、s_style_frame、新增框几何缓存 s_frame[],§7.2 G);§四⑧ 实测值区分"参考图/实现"两套(实现:根框 66、子框 42/46/54/6,顶 28/58/88/13,偏差入 §8.8 注 5),>160 溢出"待确认"落定为实现行为"屏缘裁切";新增 §八 实现架构与关键实施细节(布局常量/几何公式/对象模型/键路径/初始化时序/分级验证/6 条陷阱与保真度说明);验证:模拟器 7 状态对照参考图 01~07 + @build 0 错 0 警
2026-09-07 全文结构重排(理清三层架构与协作):旧版三层描述分散在 §六(数据结构)/§七(运行时数据)/§八(实现架构) 三处、§四 又混装导航逻辑与显示模型,读者难以看清各层职责与层间配合;重排为十章——一 架构总览(1.1 为什么拆三层 / 1.2 职责速览+依赖图 / 1.3 调用矩阵 / 1.4 一次按键的旅程 / 1.5 初始化时序 / 1.6 层间不变量)→ 二 菜单内容 → 三 数据层(含 3.7 改菜单流程)→ 四 显示规范(自 §四⑧ 抽出,含 4.5 参考图偏差)→ 五 显示层(5.1 职责边界 / 5.5 公开 API 8 接口表)→ 六 逻辑层(6.2 17 B 状态 / 6.3 导航规则含完整走查 / 6.4 键语义与分发)→ 七 层间配合细节(7.1 逐键分层动作 / 7.2 框生命周期 / 7.3 存vs算 / 7.4 读写矩阵 / 7.5 内存预算带层归属)→ 八 验证与陷阱 → 九 迁移历史(结构/运行时/内容三表收拢)→ 十 变更记录;修正归属错误:s_scroll[] 实为逻辑层 ui_menu.c 字段(17 B 导航状态之一,见 ui_menu.c),旧 §7.2 误划给显示层,新文 §6.2/§7.5 已归正;全部交叉引用同步到新章号;技术内容零删减
2026-09-07 §1.2/§1.4/§1.5 三幅架构图由 ASCII 转 Mermaid(1 张 flowchart + 2 张 sequenceDiagram,初始化时序含表自检 alt/else 分支);键源措辞统一为“远端键”(与 UI_设计架构.md“远端客户端”一致:MCU 端按键经 TCP 7003 远端客户端 → ui_net_sink → keypad);职责表/调用矩阵/“两个要点”文字保留
2026-09-07 正文现状化:删除正文过渡性历史叙述(“设计阶段拟名…/实现采用…/见变更记录”等,旧文见 git 历史),各节改写为现状描述;移除 §九“迁移历史”整章(旧→新对照保留于本变更记录),变更记录重编号为 §九;§8 去日期注记,陷阱 3/4 改写为现状约束
2026-09-07 §4.1 总览图由 ASCII 框线图升级为 SVG 精确几何图(新增 docs/images/级联框总览.svg,按 §5.2 常量 / §5.3 公式逐 px 绘制,含根框装饰 / 状态行 / 选中行反色 / 共享边框列标注 + ①~⑦ 位置标注);ASCII 版删除(旧图见 git 历史)
2026-09-07 删除 §4.5“参考图与实现偏差”表(偏差数据已在 §8.2 注 5“参考图保真度”完整记载);正文两处引用(§四 引言、§8.1 验证表)改指 §8.2 注 5
2026-09-07 §5.1 职责边界表去掉“不做”列、改为列表(负向约束已由章首“绝不做”行与 §1.6 层间不变量表承载,无信息损失)
2026-09-07 §6.1 职责边界表同样去掉“不做”列、改为列表(负向约束由章首“绝不做”行、§1.3 调用矩阵与 §1.6 层间不变量表承载,无信息损失)
2026-09-08 §5.3 几何计算重写为图文并茂:新增 docs/images/级联框几何计算.svg 四格图(① 行剖面定框宽 ② X 共享边框列 ③ Y 顶对齐 ④ Y 溢出上移,含虚线幽灵框 + 超屏红区示意),正文改为四步叙述 + 实测框宽表 + 公式汇总(五式内容不变)
2026-09-08 menu_frame_t 瘦身:built bool→uint8_t(与系统存储类型一致);x/y/w/h int32_t→int16_t(值域 0..160 / ≤242,16 位够;y 需有符号——钳制前可为负);s_frame[4] 96 B→48 B,省 48 B RAM(§5.4 对象模型表 / §7.5 内存预算表同步)
2026-09-08 行 label 指针合并:删 s_row_text / s_row_arrow 两数组(省 512 B);文本/箭头颜色改在行容器 s_row 上设置、子 label 继承(LVGL text_color 为可继承属性,lv_style.c 属性标志表证实);§5.4 对象模型表 / §7.5 内存预算表同步(768 B→256 B)
2026-09-08 行对象 → 自绘重写(方案1,省 LVGL 池"每行对象机制税"):行不再是 LVGL 对象——行数据改 s_row_node[4][16](uint8_t 节点下标,256 B→64 B),框体 LV_EVENT_DRAW_MAIN 自绘(选中行白带 lv_draw_rect + 每行 lv_draw_label 文本/箭头,按滚动偏移枚举可见行、重绘区外行 culling);框内滚动改 LV_EVENT_GET_SELF_SIZE 虚报内容高 + 手动跟随滚动(view_scroll_to_sel,替代 lv_obj_scroll_to_view);删 s_style_selected、view_add_row 与 flex 堆行;frame_build/destroy/refresh 改行数据注册/清空/重绘。§5.4/§5.6/§6.2/§7.1~7.5 同步。验证:@build 0 错 0 警 + 代码规范 PASS + 模拟器 8 状态像素回归(白带/边框/根框不变性 + 3 组往返态逐像素一致)全 PASS
2026-09-08 4 层容器→单一持久容器 + 根框装饰/状态行自绘(RAM 收敛):焦点/键唯一入口 = 持久容器(框深度由逻辑层 s_depth 决定,键回调去掉 user_data=depth,container/focus 接口去参);根框装饰(浅 V)与状态行改容器 DRAW_MAIN 自绘(3 段 lv_draw_line + 1 行 lv_draw_label,删 3 lv_line + 1 label + 3 容器对象);静态显示层 ~0.5→~0.4 KB。验证:@build 0 错 0 警 + 代码规范 PASS + 模拟器 8 状态像素回归全 PASS(装饰/状态行像素级一致);LVGL 池实测 根框 7168→4872 B、最深四级级联 8488→6128 B(省 ~2.3 KB)
2026-09-09 新增 §九"内容页(ui_content_page)":装置信息/交流量/直流量三叶子页落地(独立屏按需建毁 + 行自绘,几何实测自参考图 10/20/21);节点→页映射入逻辑层 menu_content_page_for(node 1/3/4),§2.1 叶子行为同步;flat 样式改常量样式(LV_STYLE_CONST_INIT 零池分配,动态常驻样式 x64 实测 88 B/个永久占用);关页后一次性残留 +80 B 分解入 §9.5(screens 数组高水位 +8、lv_screen_load 菜单屏局部 X/Y 样式 +72,均 LVGL 核心行为非泄漏)。验证:@build 0 错 0 警 + 代码规范 PASS + 模拟器三页对照参考图几何一致 + 菜单 8 状态像素回归全 PASS
2026-09-09 用户密钥密码页落地(首个表单/操作页,§九 / 内容页架构.md §4.1):框架描述符新增 title_y/hide_deco/hide_footer/key_intercept + 新 API ui_content_page_invalidate;受保护叶子路由(arm→开密码页,确认一致后回调先关页再续做进入动作);§9.2/§9.3/§9.4 同步。验证:回归扩至 7 页 + 往返水位全 PASS、五门禁全绿
2026-09-09 菜单显示层 flat 样式同法转常量样式(§5.6/§7.5/§9.5):LVGL 池基线 4872→4784 B、最深四级级联 6128→6040 B、内容页峰值 56486064→55605976 B(x64 省 88 B);LV_STYLE_CONST_INIT 生成外部链接符号,内容页同名者改名 cp_style_flat。验证:@build 0 错 0 警 + 代码规范 PASS + 菜单 8 状态像素回归全 PASS + 三页几何不变
2026-09-09 权限模型(§3.7/§3.8/§9.4 同步):菜单表行尾注记 #P(需密码);g_menu_tab pwd 列改 0/1 布尔(API ui_menu_tab_has_pwd,表项仍 8 B/项);两个编译期密码(查看 000 / 修改 123)+ ui_perm_t 权限枚举在 ui_pwd_def.h(用户定案:密码仅编译期写入,不落 Flash、无运行期设置页)——密码页输查看密码授只读权限、输修改密码授修改权限(只有修改权限进入后才能改参数);逻辑层密码检查提前到内容页判断之前(带内容页的需密码叶子同样先经密码页,修复路由缺口);无密码叶子直接进、默认只读。验证:五门禁全绿(菜单 8 态 + 内容页 7 页像素一致、池 4784/4864 B 不变、Keil 0 错 0 警、代码规范 PASS)