From 020278d4e3de063f711b7e8d224ec9a591feaf24 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Wed, 9 Sep 2026 17:15:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=99=E7=9A=84=E9=A1=B5=E9=9D=A2=E6=9E=B6?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .opencode/skills/font-regen/SKILL.md | 32 ++ .opencode/skills/new-source-file/SKILL.md | 39 ++ .opencode/skills/ui-verify/SKILL.md | 45 ++ Drivers/BSP/UI/ui_content_page.c | 285 ++++++------ Drivers/BSP/UI/ui_content_page.h | 54 ++- Drivers/BSP/UI/ui_deco.c | 81 ++++ Drivers/BSP/UI/ui_deco.h | 29 ++ Drivers/BSP/UI/ui_menu_view.c | 41 +- Drivers/BSP/UI/ui_page_ac.c | 59 +++ Drivers/BSP/UI/ui_page_ac.h | 23 + Drivers/BSP/UI/ui_page_dc.c | 37 ++ Drivers/BSP/UI/ui_page_dc.h | 23 + Drivers/BSP/UI/ui_page_device_info.c | 45 ++ Drivers/BSP/UI/ui_page_device_info.h | 22 + MDK-ARM/STM32F407-Demo.uvprojx | 20 + docs/UI 设计/UI_设计架构.md | 5 +- .../mockups/密码页面实际机器的示例图片.jpg | Bin 0 -> 66895 bytes docs/UI 设计/内容页架构.md | 433 ++++++++++++++++++ simulator/CMakeLists.txt | 4 + 19 files changed, 1088 insertions(+), 189 deletions(-) create mode 100644 .opencode/skills/font-regen/SKILL.md create mode 100644 .opencode/skills/new-source-file/SKILL.md create mode 100644 .opencode/skills/ui-verify/SKILL.md create mode 100644 Drivers/BSP/UI/ui_deco.c create mode 100644 Drivers/BSP/UI/ui_deco.h create mode 100644 Drivers/BSP/UI/ui_page_ac.c create mode 100644 Drivers/BSP/UI/ui_page_ac.h create mode 100644 Drivers/BSP/UI/ui_page_dc.c create mode 100644 Drivers/BSP/UI/ui_page_dc.h create mode 100644 Drivers/BSP/UI/ui_page_device_info.c create mode 100644 Drivers/BSP/UI/ui_page_device_info.h create mode 100644 docs/UI 设计/mockups/密码页面实际机器的示例图片.jpg create mode 100644 docs/UI 设计/内容页架构.md diff --git a/.opencode/skills/font-regen/SKILL.md b/.opencode/skills/font-regen/SKILL.md new file mode 100644 index 00000000..e1078abb --- /dev/null +++ b/.opencode/skills/font-regen/SKILL.md @@ -0,0 +1,32 @@ +--- +name: font-regen +description: 重生成中文子集字体 ui_font_cn_12(extract_cn 提取 → make_font 生成 → diff 检查,固定 4 步,.venv 解释器)。Use when UI 代码新增中文文案、用户报告屏幕缺字/空白块、或提交任何含新汉字的 UI 字符串之前。 +--- + +# 中文字体重生成 + +本工程**唯一字体** `ui_font_cn_12`(宋体 12px 1bpp 子集,MCU/模拟器双端共用)。**缺字渲染为空白**——新增汉字必须走以下固定顺序,不得跳过或调换。详细原理见 `docs/UI 设计/UI_中文字体生成与配置.md`。 + +## 固定顺序(仓库根目录执行) + +```powershell +# 1. 代码落库(新界面字符串已写入 ui_menu.c / ui_content_page.c 等 UI 源码) +# 2. 提取字符集(纯标准库,任意 python3 均可) +python tools/font_gen/extract_cn.py # 扫描 UI 源码字符串 → 仓库根 ui_cn_chars.txt +# 3. 生成字体(需 fontTools,一律用 .venv;宋体 .ttc 的 face0 抽取与 ▶ 箭头补回均自动完成,幂等) +.venv\Scripts\python.exe tools/font_gen/make_font.py +# 变体:--bpp 2(宋体细笔画发虚时)/ --font C:/path/to/font.ttf(换字体) +# 4. diff 检查:新增字在位、旧字不变、生成物 Opts 行字符集与预期一致 +``` + +## 提交清单 + +`ui_cn_chars.txt`(仓库根)+ `Drivers/BSP/UI/ui_font_cn_12.c` **随代码一起提交**,缺一个都会让另一端/下次构建字体与字集不一致。 + +## 坑 + +- 不要用 PATH 默认 `python`(msys64,无 fonttools)跑 make_font.py;`.venv` 缺失时按 AGENTS.md「Python 环境」重建 +- `requirements.txt` 必须**纯 ASCII**(Windows 下 pip 按 GBK 解码,中文注释 → UnicodeDecodeError) +- 生僻字(宋体可能无字形)→ **写码前先手动验证字形**,避免"代码写完才发现是空白" +- 生成成功后跑 `ui-verify` skill 全回归;黄金参考因字形变化而 FAIL 属预期 → `check_content_pages.py --update` 刷新 +- 字数变化时同步更新文档中的字符集计数(当前 97 汉字 + ASCII 0x20-0x7E,共 193 字形):`UI_中文字体生成与配置.md`、`UI_界面制作流程.md` C3、`UI_设计架构.md`、`simulator/README.md` 已知坑 #4 diff --git a/.opencode/skills/new-source-file/SKILL.md b/.opencode/skills/new-source-file/SKILL.md new file mode 100644 index 00000000..c0159e9a --- /dev/null +++ b/.opencode/skills/new-source-file/SKILL.md @@ -0,0 +1,39 @@ +--- +name: new-source-file +description: 新增 .c 源文件的双端注册:MDK-ARM/STM32F407-Demo.uvprojx 与 simulator/CMakeLists.txt 均为显式文件列表,新文件必须手动加进两处再双端构建验证。Use when 新建 App/、Drivers/BSP/ 下的 .c 文件,或构建/运行报 undefined reference、"改了代码没生效" 而怀疑文件漏注册。 +--- + +# 新文件双端注册 + +本工程 Keil 工程与 PC 模拟器都使用**显式文件列表**(无通配扫描)——新 `.c` 文件不手动注册就不会被编译;只注册一端 = 另一端**静默**缺文件(不一定报错,功能悄悄不生效)。 + +## 注册步骤 + +1. **Keil MDK**:编辑 `MDK-ARM/STM32F407-Demo.uvprojx`,在对应 `` 的 `` 中按相邻条目格式插入(`..\` 开头反斜杠路径): + +```xml + + ui_new_module.c + 1 + ..\Drivers\BSP\UI\ui_new_module.c + +``` + + 现有组:`App`、`Drivers/BSP`、`Drivers/BSP/UI`、`Test` 等;新文件放语义对应的组。 + +2. **模拟器**(仅当文件需 PC 端编译,如 `Drivers/BSP/UI/*.c`):在 `simulator/CMakeLists.txt` 的 `set(UI_SOURCES ...)` 列表加一行 `${UI_DIR}/ui_new_module.c`。非 UI 模块文件通常不需模拟器注册。 + +3. **双端构建验证**(两端都必须过): + +```powershell +cmake --build simulator/build --config Debug # 0 警告 +& "MDK-ARM\build.bat" # BUILD_OK,0 错 0 警 +``` + +## 注意 + +- **禁止**把源文件复制进 `simulator/`——两端共用 `Drivers/BSP/` 原件(UI_界面制作流程.md C7) +- 只注册 `.c`(含 `.s`);`.h` 靠 include 路径找到,不加工程 +- 漏注册的典型症状:一端编译失败(undefined reference)或一端行为悄悄停留在旧逻辑 +- 注册完成后跑回归:UI 文件 → `ui-verify` skill;其他模块 → `@test`(`test/run_boot_test.ps1`) +- 改过 `CMakeLists.txt` 后需重新 `cmake -B build` 配置 diff --git a/.opencode/skills/ui-verify/SKILL.md b/.opencode/skills/ui-verify/SKILL.md new file mode 100644 index 00000000..81eb4e01 --- /dev/null +++ b/.opencode/skills/ui-verify/SKILL.md @@ -0,0 +1,45 @@ +--- +name: ui-verify +description: UI 改动后的回归验证:5 步门禁(模拟器构建、菜单像素回归、内容页黄金参考、Keil 构建、代码规范)。Use whenever code under Drivers/BSP/UI, 字体 ui_font_cn_12.c / ui_cn_chars.txt、菜单/内容页被修改,或用户要求"验证 UI / 跑 UI 回归 / UI 改动检查"。 +--- + +# UI 回归验证 + +UI 相关改动完成后必须跑完 5 个门禁再提交。完整说明见 `docs/UI 设计/UI_自动化测试流程.md`。 + +## 步骤(仓库根目录执行,python 一律用 .venv) + +```powershell +cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警告) +.venv\Scripts\python.exe simulator/tests/check_menu_rows.py # 1) 菜单 8 状态像素回归 + 池基线门禁 +.venv\Scripts\python.exe simulator/tests/check_content_pages.py # 2) 内容页黄金参考回归 + 往返池门禁 +& "MDK-ARM\build.bat" # 3) Keil 构建(BUILD_OK,0 错 0 警) +.venv\Scripts\python.exe scripts/check_coding_standard.py # 4) 代码规范检查(PASS) +``` + +退出码:0=PASS / 1=断言失败 / 2=环境错误(exe 缺失等,先按 AGENTS.md 重建对应环境再重跑)。 + +## 失败处置决策 + +| 现象 | 判定 | 处置 | +|---|---|---| +| 内容页 FAIL,且**有意**改了该页视觉 | 预期变化 | 先肉眼确认新效果正确(窗口模式或 `i1_to_png.py` 出图对照 mockup),再 `.venv\Scripts\python.exe simulator/tests/check_content_pages.py --update` 刷参考,`git diff` 检查 ref/ 变化后随代码提交 | +| 内容页 FAIL,但**未**有意改视觉 | bug(缺字/几何漂移/样式回归) | 修代码;**禁止**刷参考掩盖 | +| 池水位门禁失败但像素一致 | 结构/样式变更(常量样式转换、增删框体等) | 确认新值合理(残留分解见 `菜单页面设计.md` §9.5)→ 更新脚本常量 + 在流程文档变更记录登记 | +| 菜单回归失败 | 结构断言(白带占比/边框/往返一致) | 按脚本打印的未过断言清单逐项修代码,无参考文件可刷 | + +## 池水位门禁常量(在脚本头部) + +| 常量 | 脚本 | 当前值 | 含义 | +|---|---|---|---| +| `MENU_POOL_BASELINE` | check_menu_rows.py | 4784 B | 菜单根框建完的 LVGL 池 used(x64 模拟器) | +| `POOL_AFTER_ROUNDTRIP` | check_content_pages.py | 4864 B | 开页-关页往返后 = 基线 + 80 B 一次性残留 | + +**禁止为过门禁盲目改常量**——无法解释的漂移 = 内存回归 bug。 + +## 关联注意 + +- `.venv` 缺失/换机:`& 'C:\ProgramData\anaconda3\python.exe' -m venv .venv; & '.venv\Scripts\python.exe' -m pip install -r requirements.txt` +- 新增 UI `.c` 文件先走 `new-source-file` skill 双端注册,否则一端静默缺文件 +- 字体重生成后全页大面积像素差异属预期:确认字形正确后 `--update` 刷参考 +- 涉及新界面/大改时本 skill 是 P3 门禁,完整管线(P0~P5)见 `docs/UI 设计/UI_界面制作流程.md` diff --git a/Drivers/BSP/UI/ui_content_page.c b/Drivers/BSP/UI/ui_content_page.c index 9838f999..6fa95393 100644 --- a/Drivers/BSP/UI/ui_content_page.c +++ b/Drivers/BSP/UI/ui_content_page.c @@ -1,13 +1,21 @@ /* - * 模块名称:UI Content Page (内容页面显示层) - * 模块功能:菜单叶子节点的内容页(装置信息/交流量/直流量)——独立 LVGL 屏幕, - * 全屏容器 DRAW_MAIN 自绘全部内容(浅 V 装饰 + 居中标题 + 行数据 + - * 页脚页码),无子对象;进入时切屏并获焦、ESC 返回菜单屏并还原焦点 + * 模块名称:UI Content Page (内容页框架) + * 模块功能:菜单叶子节点进入的内容页(独立全屏页面)框架——全屏容器 + * 自绘全部内容(浅 V 装饰 + 居中标题 + 行数据 + 页脚页码),无子对象; + * 只持机制:屏/容器按需建毁、焦点、按键分发、描述符驱动通用绘制; + * 不持任何页面数据——各页数据/几何/钩子由 ui_page_xxx 模块经 + * content_page_def_t 接入描述符表 s_defs[](架构见 内容页架构.md); + * 进入时切屏并获焦、ESC 返回菜单屏并还原焦点; + * 不持有导航状态——由逻辑层 ui_menu.c 在 ENTER 叶子节点时驱动 * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:王建锋 * 创建日期:2026-09-09 * 修改记录: * 2026-09-09 王建锋 创建:装置信息/交流量/直流量三页(参考图 10/20/21 实测几何) + * 2026-09-09 王建锋 "浅 V"装饰几何提取至共享 ui_deco 模块(不再本地重复定义) + * 2026-09-09 王建锋 拆分为"框架 + 页面插件"模型(内容页架构.md):三页数据移入 + * ui_page_xxx 模块,本文件只留机制 + 描述符表;绘制/按键改 + * 按描述符驱动(静态表/refresh + 4 钩子),行为零变化 */ /* 头文件包含区 */ @@ -15,8 +23,16 @@ #include "ui_content_page.h" +#include "ui_page_device_info.h" + +#include "ui_page_ac.h" + +#include "ui_page_dc.h" + #include "ui_menu_view.h" +#include "ui_deco.h" + #include "lvgl.h" #define DBG_TAG "[UI_CONTENT_PAGE]" @@ -27,8 +43,6 @@ /* * 布局常量(设备像素;参考图 10/20/21 实测,3x 放大图缩回 160x160 测量): * 标题 y=2(CJK 字形顶距 +1 → 墨迹自 y=3 起,与参考图一致)、页脚 y=143; - * 信息页行:首行 y=40、行距 15、标签列 x=6 / 值列 x=86(无选中带); - * 列表页行:槽顶 = 24+14i、选中白带 x=2..156 高 13、名 x=7 / 值 x=101; * 字形顶距(CJK +1 / ASCII +2)已计入放置 y,墨迹与参考图对齐 */ #define CP_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致) */ @@ -37,106 +51,18 @@ #define CP_TITLE_Y 2 /* 标题放置 y(居中;墨迹自 y=3 起) */ #define CP_PAGE_LINE_Y 143 /* 页脚"第 X 页,共 Y 页"放置 y(x=6 左对齐) */ -#define CP_DECO_LEG_W 3 /* "浅 V"两腿线宽(与菜单根框一致:1bpp 斜线须加粗) */ -#define CP_DECO_HORIZ_W 2 /* "浅 V"水平线宽 */ - -/* 私有类型定义区 */ - -/* 行数据:左列名 + 右列值(Flash 字面量,不 strdup) */ -typedef struct { - const char *name; - const char *value; -} cp_row_t; - -/* - * 页面定义(Flash 常量表):标题/行几何/数据表/页脚参数; - * selectable=1 为可移选分页的数据列表页,0 为静态信息页 - */ -typedef struct { - const char *title; /* 标题(浅 V 区居中) */ - uint8_t selectable; /* 1 = 数据列表页(↑↓移选/分页);0 = 信息页(静态行) */ - int32_t top_y; /* 首行槽顶 y */ - int32_t pitch; /* 行距(相邻行槽顶间距) */ - int32_t band_h; /* 选中白带高(0 = 无选中带) */ - int32_t text_dy; /* 文本放置相对行槽顶的垂直偏移 */ - int32_t left_x; /* 左列 x(信息页=标签、列表页=名) */ - int32_t right_x; /* 右列 x(值) */ - uint8_t rows_per_page; /* 每页可见行数(0 = 不分页) */ - uint16_t total_rows; /* 数据总行数 */ - const cp_row_t *rows; /* 数据表(Flash) */ - const char *subtitle; /* 副标题(居中;NULL = 无,仅信息页有) */ - int32_t subtitle_y; /* 副标题放置 y(-1 = 无) */ - uint8_t fixed_cur_page; /* 页脚"第 X 页"固定显示值(信息页按参考图原样) */ - uint8_t fixed_total_pages; /* 页脚"共 Y 页"固定显示值 */ -} cp_def_t; +#define CP_ROW_BUF_LEN 32 /* 动态行栈缓冲长度(同帧同步光栅化;超出行带即被裁切) */ /* 静态变量定义区 */ -/* "浅 V"装饰点集与线宽:几何与菜单根框一致(ui_menu_view.c s_deco_*),culling 区同范围 */ -static const lv_point_precise_t s_deco_leg_l[2] = { {7, 7}, {16, 17} }; /* 左腿 */ -static const lv_point_precise_t s_deco_hoz[2] = { {16, 17}, {145, 17} }; /* 水平线:接两腿底端 */ -static const lv_point_precise_t s_deco_leg_r[2] = { {158, 7}, {145, 17} }; /* 右腿 */ -static const lv_point_precise_t *const s_deco_segs[3] = {s_deco_leg_l, s_deco_leg_r, s_deco_hoz}; -static const int32_t s_deco_seg_w[3] = {CP_DECO_LEG_W, CP_DECO_LEG_W, CP_DECO_HORIZ_W}; - -static const lv_area_t s_deco_band = {0, 0, CP_SCREEN_W - 1, 19}; /* 浅 V culling 区 */ - -/* 装置信息行(参考图 10;标签含全角冒号,值列固定 x=86) */ -static const cp_row_t s_info_rows[] = { - {"终端类型:", "F30"}, - {"终端型号:", "F30"}, - {"软件版本:", "SV0.010"}, - {"硬件版本:", "HW0.010"}, - {"软件校验:", "4454"}, - {"程序日期:", "2024.08.27"}, -}; - -/* 交流量行(参考图 20 = 第 1 页原样;第 2/3 页为占位数据,可替换) */ -static const cp_row_t s_ac_rows[] = { - {"Ia1", "0.000"}, /* 第 1 页(参考图 20) */ - {"Ib1", "0.137"}, - {"Ic1", "0.274"}, - {"Ia2", "0.411"}, - {"Ib2", "0.548"}, - {"Ic2", "0.685"}, - {"Ua1", "0.82"}, - {"Ub1", "0.96"}, - {"Uc1", "1.03"}, /* 第 2 页(占位数据) */ - {"Uab", "1.17"}, - {"Ubc", "1.24"}, - {"Uca", "1.31"}, - {"Hz", "50.00"}, - {"P", "1.42"}, - {"Q", "0.87"}, - {"S", "1.63"}, - {"PF", "0.95"}, /* 第 3 页(占位数据) */ - {"Iavg","0.35"}, - {"Uavg","0.87"}, - {"Fmax","50.02"}, - {"Imax","1.24"}, - {"Pmax","2.10"}, - {"Qmax","1.05"}, - {"Smax","2.45"}, -}; - -/* 直流量行(参考图 21) */ -static const cp_row_t s_dc_rows[] = { - {"ZL01", "3.01"}, - {"ZL02", "3.15"}, - {"Pa1", "3.288"}, -}; - -/* 页面定义表(顺序与 content_page_id_t 一致) */ -static const cp_def_t s_defs[CONTENT_PAGE_COUNT] = { - /* 装置信息:静态键值行 + 副标题;页脚按参考图原样"第 10 页,共 1 页" */ - {"装置信息", 0u, 40, 15, 0, 0, 6, 86, 0u, (uint16_t)6, s_info_rows, - "馈线自动化终端", 25, 10u, 1u}, - /* 交流量:24 项 / 8 项每页(3 页);text_dy=0:ASCII 字形顶距 +2,墨迹落槽顶+2(与参考图一致) */ - {"交流量", 1u, 24, 14, 13, 0, 7, 101, 8u, (uint16_t)24, s_ac_rows, - NULL, -1, 0u, 0u}, - /* 直流量:3 项单页 */ - {"直流量", 1u, 24, 14, 13, 0, 7, 101, 8u, (uint16_t)3, s_dc_rows, - NULL, -1, 0u, 0u}, +/* + * 描述符表:页 ID → 页面模块描述符(每页一行;新增页 = 枚举值 + 此处一行, + * 框架机制不变,见 内容页架构.md §七) + */ +static const content_page_def_t *const s_defs[CONTENT_PAGE_COUNT] = { + [CONTENT_PAGE_DEVICE_INFO] = &ui_page_device_info_desc, + [CONTENT_PAGE_AC] = &ui_page_ac_desc, + [CONTENT_PAGE_DC] = &ui_page_dc_desc, }; static content_page_id_t s_open = CONTENT_PAGE_COUNT; /* 当前打开的页(COUNT = 未打开) */ @@ -181,6 +107,20 @@ static bool cp_areas_intersect(const lv_area_t *a, const lv_area_t *b) { return (x1 <= x2 && y1 <= y2); } +/* + * 函数功能:取页面总行数(静态页 = 描述符 row_count,动态页 = count_cb) + * 入口参数:d - 页面描述符 const content_page_def_t * + * 返回值:总行数 uint16_t + * 限定条件:d 非空 + * 函数说明:行数唯一入口;绘制(窗口/页脚)与按键(移选取模)均经此取 + */ +static uint16_t cp_row_total(const content_page_def_t *d) { + if (d->count_cb != NULL) { + return d->count_cb(); + } + return d->row_count; +} + /* * 函数功能:绘制居中单行文本(标题/副标题) * 入口参数:layer - 绘制层 lv_layer_t * @@ -211,34 +151,25 @@ static void cp_draw_centered_text(lv_layer_t *layer, const char *text, int32_t y } /* - * 函数功能:页面自绘(浅 V 装饰 + 标题/副标题 + 行数据 + 页脚页码) + * 函数功能:页面自绘(浅 V 装饰 + 标题/副标题 + 行数据 + 页脚页码 + 页面钩子) * 入口参数:e - 事件指针 lv_event_t * * 返回值:无 * 限定条件:事件为 LV_EVENT_DRAW_MAIN,页面已打开 - * 函数说明: 1. 浅 V 装饰几何与菜单根框一致(3 段线逐段画) - * 2. 列表页窗口 = 选中行所在页(首可见行 = (sel/每页数)*每页数), + * 函数说明: 1. 浅 V 装饰:共享 ui_deco 模块(与菜单根框同几何) + * 2. 行数:静态页取 row_count,动态页取 count_cb(); + * 列表页窗口 = 选中行所在页(首可见行 = (sel/每页数)*每页数), * 行槽位置固定、数据随页切换;信息页全量静态绘制 - * 3. 选中行整行白带(x=2..156)+ 黑字,非选中白字 - * 4. 各区块按 culling 区跳过不在重绘区的部分 + * 3. 行文本:静态页直绘 Flash 表(不拷内存);动态页经 refresh() + * 逐行取入栈缓冲(同帧同步光栅化,安全) + * 4. 选中行整行白带(x=2..156)+ 黑字,非选中白字 + * 5. 各区块按 culling 区跳过不在重绘区的部分;末尾调 draw_cb(页自定义内容) */ static void cp_draw_cb(lv_event_t *e) { - const cp_def_t *d = &s_defs[s_open]; + const content_page_def_t *d = s_defs[s_open]; lv_layer_t *layer = lv_event_get_layer(e); - /* "浅 V"装饰:3 段线(与菜单根框同几何) */ - if (cp_areas_intersect(&s_deco_band, &layer->_clip_area)) { - for (uint8_t i = 0; i < 3; i++) { - lv_draw_line_dsc_t ld; - - lv_draw_line_dsc_init(&ld); - ld.base.layer = layer; - ld.color = lv_color_white(); - ld.width = s_deco_seg_w[i]; - ld.p1 = s_deco_segs[i][0]; - ld.p2 = s_deco_segs[i][1]; - lv_draw_line(layer, &ld); - } - } + /* "浅 V"装饰:共享 ui_deco 模块(与菜单根框同几何) */ + ui_deco_draw(layer, &layer->_clip_area); /* 标题(居中) */ { @@ -267,17 +198,22 @@ static void cp_draw_cb(lv_event_t *e) { } /* 行数据:列表页窗口跟随选中行所在页,信息页全量 */ + int32_t nrows = (int32_t)cp_row_total(d); int32_t first = 0; - int32_t cnt = (int32_t)d->total_rows; + int32_t cnt = nrows; - if (d->selectable && d->rows_per_page > 0u) { + if (d->selectable != 0u && d->rows_per_page > 0u) { first = (int32_t)(s_sel / d->rows_per_page) * d->rows_per_page; /* 窗口顶行 */ - cnt = (int32_t)d->total_rows - first; + cnt = nrows - first; if (cnt > (int32_t)d->rows_per_page) { cnt = (int32_t)d->rows_per_page; /* 末页不足整页时截断 */ } } + bool dynamic = (d->refresh != NULL); + char dyn_name[CP_ROW_BUF_LEN]; /* 动态行栈缓冲(同帧同步光栅化) */ + char dyn_value[CP_ROW_BUF_LEN]; + for (int32_t i = 0; i < cnt; i++) { int32_t row_idx = first + i; /* 数据下标(跨页连续) */ int32_t slot_y = d->top_y + i * d->pitch; /* 行槽顶(窗口内位置固定) */ @@ -310,15 +246,28 @@ static void cp_draw_cb(lv_event_t *e) { lv_draw_rect(layer, &fd, &band); } - /* 左列(标签/名)与右列(值):固定列 x,垂直偏移 text_dy */ + /* 行文本:静态 = Flash 表(不拷内存);动态 = refresh 逐行取(栈缓冲) */ + const char *name; + const char *value; + + if (dynamic) { + d->refresh((uint16_t)row_idx, dyn_name, dyn_value); + name = dyn_name; + value = dyn_value; + } + else { + name = d->rows[row_idx].name; + value = d->rows[row_idx].value; + } + { lv_draw_label_dsc_t ld; /* ofs_x/ofs_y 相对 coords 原点(strip.y1=slot_y),勿再叠加 slot_y */ lv_draw_label_dsc_init(&ld); ld.base.layer = layer; - ld.text = d->rows[row_idx].name; - ld.text_static = 1; /* Flash 字符串:不拷内存 */ + ld.text = name; + ld.text_static = (uint32_t)(!dynamic); /* 静态不拷;动态缓冲同帧内有效 */ ld.color = color; /* 选中黑 / 非选中白 */ ld.ofs_x = d->left_x; ld.ofs_y = d->text_dy; @@ -326,8 +275,8 @@ static void cp_draw_cb(lv_event_t *e) { lv_draw_label_dsc_init(&ld); ld.base.layer = layer; - ld.text = d->rows[row_idx].value; - ld.text_static = 1; + ld.text = value; + ld.text_static = (uint32_t)(!dynamic); ld.color = color; ld.ofs_x = d->right_x; ld.ofs_y = d->text_dy; @@ -339,16 +288,16 @@ static void cp_draw_cb(lv_event_t *e) { { char buf[32]; uint8_t cur; - uint8_t total; + uint8_t pages; lv_area_t t; - if (d->selectable) { + if (d->selectable != 0u) { cur = (uint8_t)(s_sel / d->rows_per_page + 1u); - total = (uint8_t)((d->total_rows + d->rows_per_page - 1u) / d->rows_per_page); + pages = (uint8_t)((nrows + (int32_t)d->rows_per_page - 1) / d->rows_per_page); } else { cur = d->fixed_cur_page; - total = d->fixed_total_pages; + pages = d->fixed_total_pages; } t.x1 = 0; @@ -357,7 +306,7 @@ static void cp_draw_cb(lv_event_t *e) { t.y2 = CP_PAGE_LINE_Y + 15; if (cp_areas_intersect(&t, &layer->_clip_area)) { /* lv_snprintf 而非 snprintf:MSVC 对 printf 族做格式串扫描,中文字面量会误报 C4819 */ - lv_snprintf(buf, sizeof(buf), "第 %u 页,共 %u 页", (unsigned)cur, (unsigned)total); + lv_snprintf(buf, sizeof(buf), "第 %u 页,共 %u 页", (unsigned)cur, (unsigned)pages); lv_draw_label_dsc_t ld; @@ -371,38 +320,64 @@ static void cp_draw_cb(lv_event_t *e) { lv_draw_label(layer, &ld, &t); } } + + /* 页面钩子:自定义内容(表单/操作页;在内置区块之后绘制) */ + if (d->draw_cb != NULL) { + d->draw_cb(layer, &layer->_clip_area); + } } /* - * 函数功能:页面按键事件入口(↑↓移选 / ESC·←返回) + * 函数功能:页面按键事件入口(↑↓移选 / ESC·←返回 / 页面钩子) * 入口参数:e - 事件指针 lv_event_t * * 返回值:无 * 限定条件:容器已加入默认输入组并获焦 - * 函数说明:1. ↑/↓:列表页移选(首尾环绕,跨页连续),窗口随页切换后重绘;信息页不响应 - * 2. ESC/←:关闭页面返回菜单(导航状态未压层,原样还原) + * 函数说明: 1. ESC/←:恒关页返回菜单(框架保证,页面钩子不可拦截) + * 2. 其余键:key_cb 钩子先调(返回 true = 页已处理,框架跳过内置) + * 3. 内置:↑/↓ 列表页移选(首尾环绕,跨页连续),窗口随页切换后重绘; + * 信息页不响应;动态行数可能缩小,选中行越界时重置回首行 */ static void cp_key_cb(lv_event_t *e) { - const cp_def_t *d = &s_defs[s_open]; + const content_page_def_t *d = s_defs[s_open]; lv_key_t key = (lv_key_t)lv_event_get_key(e); + /* ESC/←:关闭页面返回菜单屏(焦点/导航状态还原) */ + if (key == LV_KEY_ESC || key == LV_KEY_LEFT) { + ui_content_page_close(); + return; + } + + /* 页面钩子先调:true = 页已处理,框架跳过内置语义 */ + if (d->key_cb != NULL && d->key_cb(key)) { + return; + } + switch (key) { - case LV_KEY_UP: - if (d->selectable && d->total_rows > 0u) { + case LV_KEY_UP: { + uint16_t total = cp_row_total(d); + + if (d->selectable != 0u && total > 0u) { + if (s_sel >= total) { + s_sel = 0u; /* 动态行数可能缩小:选中行越界重置回首行 */ + } /* 上移、首尾环绕:先加 total 再减 1 保证无符号下不为负 */ - s_sel = (uint16_t)((s_sel + d->total_rows - 1u) % d->total_rows); + s_sel = (uint16_t)((s_sel + total - 1u) % total); lv_obj_invalidate(s_cont); /* 重绘(选中带/窗口/页码可能变化) */ } break; - case LV_KEY_DOWN: - if (d->selectable && d->total_rows > 0u) { - s_sel = (uint16_t)((s_sel + 1u) % d->total_rows); /* 下移、越尾绕回首行 */ + } + case LV_KEY_DOWN: { + uint16_t total = cp_row_total(d); + + if (d->selectable != 0u && total > 0u) { + if (s_sel >= total) { + s_sel = 0u; /* 动态行数可能缩小:选中行越界重置回首行 */ + } + s_sel = (uint16_t)((s_sel + 1u) % total); /* 下移、越尾绕回首行 */ lv_obj_invalidate(s_cont); } break; - case LV_KEY_ESC: - case LV_KEY_LEFT: - ui_content_page_close(); /* 返回菜单屏(焦点/导航状态还原) */ - break; + } default: break; /* 其余键值忽略 */ } @@ -411,7 +386,7 @@ static void cp_key_cb(lv_event_t *e) { /* * 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器) * 入口参数:id - 内容页 ID content_page_id_t - * 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/对象创建失败) + * 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/无描述符/对象创建失败) * 限定条件:ui_menu_init() 已完成(默认输入组与菜单屏存在) * 函数说明:1. lv_obj_create(NULL) 新建独立屏幕 + 全屏容器(自绘载体,无子对象) * 2. 容器入默认输入组并获焦后再切屏——按键立即由本模块接管 @@ -422,6 +397,12 @@ bool ui_content_page_open(content_page_id_t id) { return false; /* ID 无效或已有页面打开(防御) */ } + const content_page_def_t *d = s_defs[id]; + if (d == NULL) { + DBG_ERROR("page open: id=%u no descriptor (enum/table drift)", (unsigned)id); + return false; /* 枚举/描述符表漂移:拒开并留痕,不崩溃 */ + } + s_scr = lv_obj_create(NULL); /* parent=NULL → 新建屏幕 */ if (s_scr == NULL) { return false; /* LVGL 池耗尽 */ @@ -451,7 +432,7 @@ bool ui_content_page_open(content_page_id_t id) { } lv_screen_load(s_scr); /* 切到内容页(菜单屏对象保留、不销毁) */ - DBG_INFO("page open: id=%u rows=%u", (unsigned)id, (unsigned)s_defs[id].total_rows); + DBG_INFO("page open: id=%u rows=%u", (unsigned)id, (unsigned)cp_row_total(d)); return true; } diff --git a/Drivers/BSP/UI/ui_content_page.h b/Drivers/BSP/UI/ui_content_page.h index 3e1ad85c..7c55fb5d 100644 --- a/Drivers/BSP/UI/ui_content_page.h +++ b/Drivers/BSP/UI/ui_content_page.h @@ -1,14 +1,21 @@ /* - * 模块名称:UI Content Page (内容页面显示层) - * 模块功能:菜单叶子节点的内容页(装置信息/交流量/直流量)——独立 LVGL 屏幕, - * 全屏容器自绘全部内容(浅 V 装饰 + 居中标题 + 行数据 + 页脚页码), - * 无子对象;进入时切屏并获焦、ESC 返回菜单屏并还原焦点; + * 模块名称:UI Content Page (内容页框架) + * 模块功能:菜单叶子节点进入的内容页(独立全屏页面)框架—— + * 屏/容器按需建毁生命周期 + 焦点 + 切屏、按键分发(内置 ↑↓/ESC + key_cb 钩子)、 + * 描述符驱动通用绘制(浅 V + 标题/副标题 + 行 + 选中带 + 页脚 + draw_cb); + * 不持任何页面数据:各页数据/几何/钩子由 ui_page_xxx 模块经 content_page_def_t + * 接入框架描述符表(架构见 内容页架构.md); + * 进入时切屏并获焦、ESC 返回菜单屏并还原焦点; * 不持有导航状态——由逻辑层 ui_menu.c 在 ENTER 叶子节点时驱动 * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:王建锋 * 创建日期:2026-09-09 * 修改记录: * 2026-09-09 王建锋 创建:装置信息/交流量/直流量三页(参考图 10/20/21) + * 2026-09-09 王建锋 "浅 V"装饰几何提取至共享 ui_deco 模块(不再本地重复定义) + * 2026-09-09 王建锋 拆分为"框架 + 页面插件"模型(内容页架构.md):三页数据移入 + * ui_page_xxx 模块;本文件只留描述类型与 API;描述符新增 + * 4 个扩展钩子(refresh/count_cb/draw_cb/key_cb) */ #ifndef UI_CONTENT_PAGE_H #define UI_CONTENT_PAGE_H @@ -16,6 +23,8 @@ #include #include +#include "lvgl.h" + /* * 内容页 ID:与菜单叶子节点一一对应(映射见 ui_menu.c menu_content_page_for); * CONTENT_PAGE_COUNT 兼作"无内容页/未打开"哨兵值 @@ -27,13 +36,46 @@ typedef enum { CONTENT_PAGE_COUNT /* 哨兵:无效 ID / 未打开 */ } content_page_id_t; +/* 行数据:左列名 + 右列值(Flash 字面量,不 strdup) */ +typedef struct { + const char *name; + const char *value; +} cp_row_t; + +/* + * 页面描述符:一页 = 一个 Flash const 结构(几何 + 数据源 + 可选钩子) + * 由各 ui_page_xxx 页面模块提供,框架描述符表 s_defs[] 按页 ID 引用 + */ +typedef struct { + const char *title; /* 标题(浅 V 区居中) */ + const char *subtitle; /* 副标题;NULL = 无(仅信息页有) */ + int32_t subtitle_y; /* 副标题放置 y(仅 subtitle 非空时有效) */ + uint8_t selectable; /* 1 = 列表页(↑↓移选+分页);0 = 信息页(静态行) */ + int32_t top_y; /* 首行槽顶 y */ + int32_t pitch; /* 行距(相邻行槽顶间距) */ + int32_t band_h; /* 选中白带高(0 = 无选中带) */ + int32_t text_dy; /* 文本放置相对行槽顶的垂直偏移 */ + int32_t left_x; /* 左列 x(信息页=标签、列表页=名) */ + int32_t right_x; /* 右列 x(值) */ + uint8_t rows_per_page; /* 每页可见行数(0 = 不分页) */ + uint16_t row_count; /* 总行数(静态页;动态页用 count_cb) */ + const cp_row_t *rows; /* 静态数据表(Flash);NULL = 动态页 */ + uint8_t fixed_cur_page; /* 信息页页脚"第 X 页"固定显示值 */ + uint8_t fixed_total_pages; /* 信息页页脚"共 Y 页"固定显示值 */ + /* —— 扩展钩子(全可选;NULL = 纯内置行为;均运行在 uiTask 上下文)—— */ + void (*refresh)(uint16_t row, char *name, char *value); /* 动态行:逐行取数(名/值) */ + uint16_t (*count_cb)(void); /* 动态行数(记录页) */ + void (*draw_cb)(lv_layer_t *layer, const lv_area_t *clip); /* 表单/操作页:自定义内容 */ + bool (*key_cb)(lv_key_t key); /* 页面自有键;true = 页已处理 */ +} content_page_def_t; + /* * 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器) * 入口参数:id - 内容页 ID content_page_id_t - * 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/对象创建失败) + * 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/无描述符/对象创建失败) * 限定条件:ui_menu_init() 已完成(默认输入组与菜单屏存在) * 函数说明:1. 新建独立屏幕,全屏容器自绘全部内容(无子对象,池开销极小) - * 2. 容器入默认输入组并获焦——按键由本模块处理(↑↓移选/ESC返回) + * 2. 容器入默认输入组并获焦——按键由本模块处理(↑↓移选/ESC返回/页面钩子) * 3. 导航状态不压层:菜单 s_depth/s_sel 原样保留,ESC 返回即还原 */ bool ui_content_page_open(content_page_id_t id); diff --git a/Drivers/BSP/UI/ui_deco.c b/Drivers/BSP/UI/ui_deco.c new file mode 100644 index 00000000..d8d78039 --- /dev/null +++ b/Drivers/BSP/UI/ui_deco.c @@ -0,0 +1,81 @@ +/* + * 模块名称:UI Deco (顶部装饰层) + * 模块功能:顶部"浅 V"装饰(左腿/水平线/右腿 3 段线)共享自绘—— + * 菜单根框与内容页共用的几何点集、线宽与 culling 区在本模块唯一定义, + * 各页面经 ui_deco_draw 调用绘制(无 LVGL 对象,DRAW_MAIN 自绘) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:提取 ui_menu_view.c / ui_content_page.c 重复的"浅 V"几何 + */ + +/* 头文件包含区 */ +#include + +#include "ui_deco.h" + +/* 私有宏定义区 */ + +#define DECO_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致) */ +#define DECO_LEG_W 3 /* "浅 V"两腿线宽(1bpp 下斜线边缘易被阈值吃掉,需加粗) */ +#define DECO_HORIZ_W 2 /* "浅 V"水平线宽 */ + +/* 静态变量定义区 */ + +static const lv_point_precise_t s_leg_l[2] = { {7, 7}, {16, 17} }; /* 左腿:自顶端斜下 */ +static const lv_point_precise_t s_hoz[2] = { {16, 17}, {145, 17} }; /* 水平线:接两腿底端 */ +static const lv_point_precise_t s_leg_r[2] = { {158, 7}, {145, 17} }; /* 右腿 */ + +/* 绘制次序 = 原对象创建序(左腿、右腿、水平线),重叠像素与原渲染一致 */ +static const lv_point_precise_t *const s_segs[3] = {s_leg_l, s_leg_r, s_hoz}; +static const int32_t s_seg_w[3] = {DECO_LEG_W, DECO_LEG_W, DECO_HORIZ_W}; + +static const lv_area_t s_band = {0, 0, DECO_SCREEN_W - 1, 19}; /* "浅 V" culling 区(与原线段对象范围一致) */ + +/* 函数定义区 */ + +/* + * 函数功能:判断两区域是否相交(等价 lv_area_intersect 非空判断;避免依赖 LVGL 私有头) + * 入口参数:a - 区域 1 const lv_area_t * + * b - 区域 2 const lv_area_t * + * 返回值:true - 相交;false - 不相交 + * 限定条件:无 + * 函数说明:四边取 min/max 成交集,任一边为空即不相交(与 ui_menu_view.c 同实现) + */ +static bool deco_areas_intersect(const lv_area_t *a, const lv_area_t *b) { + int32_t x1 = (a->x1 > b->x1) ? a->x1 : b->x1; + int32_t y1 = (a->y1 > b->y1) ? a->y1 : b->y1; + int32_t x2 = (a->x2 < b->x2) ? a->x2 : b->x2; + int32_t y2 = (a->y2 < b->y2) ? a->y2 : b->y2; + + return (x1 <= x2 && y1 <= y2); +} + +/* + * 函数功能:绘制顶部"浅 V"装饰(3 段线,线宽各异,白色) + * 入口参数:layer - 绘制层 lv_layer_t * + * clip - 重绘裁剪区 const lv_area_t * + * 返回值:无 + * 限定条件:layer/clip 有效(调用方自 LV_EVENT_DRAW_MAIN 事件取得) + * 函数说明:1. 几何共享:两腿 3px 粗、水平线 2px 细(1bpp 斜线边缘易被阈值吃掉,腿须加粗) + * 2. 绘制次序固定(左腿、右腿、水平线),重叠像素与原渲染一致 + * 3. 重绘区未覆盖 culling 区(屏顶 20px 带)则整体跳过 + */ +void ui_deco_draw(lv_layer_t *layer, const lv_area_t *clip) { + if (!deco_areas_intersect(&s_band, clip)) { + return; + } + + for (uint8_t i = 0; i < 3; i++) { + lv_draw_line_dsc_t ld; + + lv_draw_line_dsc_init(&ld); + ld.base.layer = layer; + ld.color = lv_color_white(); + ld.width = s_seg_w[i]; + ld.p1 = s_segs[i][0]; + ld.p2 = s_segs[i][1]; + lv_draw_line(layer, &ld); + } +} diff --git a/Drivers/BSP/UI/ui_deco.h b/Drivers/BSP/UI/ui_deco.h new file mode 100644 index 00000000..328e1a6a --- /dev/null +++ b/Drivers/BSP/UI/ui_deco.h @@ -0,0 +1,29 @@ +/* + * 模块名称:UI Deco (顶部装饰层) + * 模块功能:顶部"浅 V"装饰(左腿/水平线/右腿 3 段线)共享自绘接口—— + * 菜单根框(ui_menu_view)与内容页(ui_content_page)共用同一几何 + * 与绘制次序;重绘区未覆盖 culling 区则整体跳过 + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:提取 ui_menu_view.c / ui_content_page.c 重复的"浅 V"几何 + */ +#ifndef UI_DECO_H +#define UI_DECO_H + +#include "lvgl.h" + +/* + * 函数功能:绘制顶部"浅 V"装饰(3 段线,线宽各异,白色) + * 入口参数:layer - 绘制层 lv_layer_t * + * clip - 重绘裁剪区 const lv_area_t * + * 返回值:无 + * 限定条件:layer/clip 有效(调用方自 LV_EVENT_DRAW_MAIN 事件取得) + * 函数说明:1. 几何共享:两腿 3px 粗、水平线 2px 细(1bpp 斜线边缘易被阈值吃掉,腿须加粗) + * 2. 绘制次序固定(左腿、右腿、水平线),重叠像素与原渲染一致 + * 3. 重绘区未覆盖 culling 区(屏顶 20px 带)则整体跳过 + */ +void ui_deco_draw(lv_layer_t *layer, const lv_area_t *clip); + +#endif /* UI_DECO_H */ diff --git a/Drivers/BSP/UI/ui_menu_view.c b/Drivers/BSP/UI/ui_menu_view.c index 1d294467..d03e644e 100644 --- a/Drivers/BSP/UI/ui_menu_view.c +++ b/Drivers/BSP/UI/ui_menu_view.c @@ -19,6 +19,7 @@ * 2026-09-08 王建锋 4 层容器→1 个持久容器(焦点/键唯一入口;帧深度由逻辑层 * s_depth 决定,键回调 user_data=depth 冗余);根框装饰 * (浅 V)+状态行改容器自绘(删 3 lv_line + 1 label 对象) + * 2026-09-09 王建锋 "浅 V"装饰几何提取至共享 ui_deco 模块(不再本地重复定义) */ /* 头文件包含区 */ @@ -28,6 +29,8 @@ #include "ui_menu_tab.h" +#include "ui_deco.h" + #include "lvgl.h" #define DBG_TAG "[UI_MENU_VIEW]" @@ -52,8 +55,6 @@ #define MENU_ROOT_BOX_Y 44 /* 根框顶 */ #define MENU_ROOT_STATUS_X 17 /* 状态行左 */ #define MENU_ROOT_STATUS_Y 20 /* 状态行顶 */ -#define MENU_DECO_LEG_W 3 /* 顶部"浅 V"两腿线宽(1bpp 下斜线边缘易被阈值吃掉,需加粗) */ -#define MENU_DECO_HORIZ_W 2 /* 顶部"浅 V"水平线宽 */ /* 私有类型定义区 */ @@ -542,45 +543,25 @@ lv_obj_t *ui_menu_view_container(void) { } /* - * 根框装饰(浅 V)点集与自绘区域:容器 DRAW_MAIN 自绘(无对象); - * culling 区域覆盖原 lv_line 对象范围((0,0) 处 160×20)与状态行文本区 + * 根框装饰(浅 V):几何/绘制/culling 由共享 ui_deco 模块承载(无对象,容器自绘); + * 状态行 culling 区为本模块私有(文本约 104px 宽 + 余量) */ -static const lv_point_precise_t s_deco_leg_l[2] = { {7, 7}, {16, 17} }; /* 左腿:自顶端斜下 */ -static const lv_point_precise_t s_deco_hoz[2] = { {16, 17}, {145, 17} }; /* 水平线:接两腿底端 */ -static const lv_point_precise_t s_deco_leg_r[2] = { {158, 7}, {145, 17} }; /* 右腿 */ -static const lv_point_precise_t *const s_deco_segs[3] = {s_deco_leg_l, s_deco_leg_r, s_deco_hoz}; -static const int32_t s_deco_seg_w[3] = {MENU_DECO_LEG_W, MENU_DECO_LEG_W, MENU_DECO_HORIZ_W}; - -static const lv_area_t s_deco_band = {0, 0, MENU_SCREEN_W - 1, 19}; /* 浅 V culling 区(与原线段对象范围一致) */ -static const lv_area_t s_status_area = {MENU_ROOT_STATUS_X, MENU_ROOT_STATUS_Y, 130, 33}; /* 状态行 culling 区(文本约 104px 宽 + 余量) */ +static const lv_area_t s_status_area = {MENU_ROOT_STATUS_X, MENU_ROOT_STATUS_Y, 130, 33}; /* 状态行 culling 区 */ /* * 函数功能:自绘根框装饰 + 状态行(容器 DRAW_MAIN;替代旧 4 个 LVGL 对象) * 入口参数:e - 事件指针 lv_event_t * * 返回值:无 * 限定条件:事件为 LV_EVENT_DRAW_MAIN,容器已建 - * 函数说明: 1. 浅 V:两腿 3px 粗、水平线 2px 细(1bpp 斜线边缘易被阈值吃掉,腿须比水平线粗) - * 2. 绘制次序 = 旧对象创建序(左腿、右腿、水平线),重叠像素与旧渲染一致 - * 3. 状态行:静态字符串、白色,与旧 label 同字体同位置 - * 4. 重绘区不覆盖对应 culling 区则跳过(culling) + * 函数说明: 1. 浅 V:共享 ui_deco 模块(两腿 3px / 水平线 2px,绘制次序左腿、右腿、水平线) + * 2. 状态行:静态字符串、白色,与旧 label 同字体同位置 + * 3. 重绘区不覆盖对应 culling 区则跳过(culling) */ static void view_deco_draw_cb(lv_event_t *e) { lv_layer_t *layer = lv_event_get_layer(e); - /* "浅 V"装饰:3 段线(线宽各异,逐段画) */ - if (areas_intersect(&s_deco_band, &layer->_clip_area)) { - for (uint8_t i = 0; i < 3; i++) { - lv_draw_line_dsc_t ld; - - lv_draw_line_dsc_init(&ld); - ld.base.layer = layer; - ld.color = lv_color_white(); - ld.width = s_deco_seg_w[i]; - ld.p1 = s_deco_segs[i][0]; - ld.p2 = s_deco_segs[i][1]; - lv_draw_line(layer, &ld); - } - } + /* "浅 V"装饰:共享 ui_deco 模块(几何/绘制次序/culling 均在其内) */ + ui_deco_draw(layer, &layer->_clip_area); /* 状态行:当前模式(静态文本) */ if (areas_intersect(&s_status_area, &layer->_clip_area)) { diff --git a/Drivers/BSP/UI/ui_page_ac.c b/Drivers/BSP/UI/ui_page_ac.c new file mode 100644 index 00000000..f9c37d5a --- /dev/null +++ b/Drivers/BSP/UI/ui_page_ac.c @@ -0,0 +1,59 @@ +/* + * 模块名称:UI Page AC (交流量内容页) + * 模块功能:交流量叶子节点的内容页数据——数据列表(24 项 / 8 项每页,共 3 页); + * 行槽顶 = 24+14i、选中白带 x=2..156 高 13、名 x=7 / 值 x=101(参考图 20 实测); + * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:自 ui_content_page 单体拆出(数据原样迁移,零改动) + */ + +/* 头文件包含区 */ +#include "ui_page_ac.h" + +/* 静态变量定义区 */ + +/* 交流量行(参考图 20 = 第 1 页原样;第 2/3 页为占位数据,可替换) */ +static const cp_row_t s_rows[] = { + {"Ia1", "0.000"}, /* 第 1 页(参考图 20) */ + {"Ib1", "0.137"}, + {"Ic1", "0.274"}, + {"Ia2", "0.411"}, + {"Ib2", "0.548"}, + {"Ic2", "0.685"}, + {"Ua1", "0.82"}, + {"Ub1", "0.96"}, + {"Uc1", "1.03"}, /* 第 2 页(占位数据) */ + {"Uab", "1.17"}, + {"Ubc", "1.24"}, + {"Uca", "1.31"}, + {"Hz", "50.00"}, + {"P", "1.42"}, + {"Q", "0.87"}, + {"S", "1.63"}, + {"PF", "0.95"}, /* 第 3 页(占位数据) */ + {"Iavg","0.35"}, + {"Uavg","0.87"}, + {"Fmax","50.02"}, + {"Imax","1.24"}, + {"Pmax","2.10"}, + {"Qmax","1.05"}, + {"Smax","2.45"}, +}; + +/* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用;列表页,钩子全 NULL) */ +const content_page_def_t ui_page_ac_desc = { + .title = "交流量", + .selectable = 1u, + .top_y = 24, + .pitch = 14, + .band_h = 13, + .text_dy = 0, + .left_x = 7, + .right_x = 101, + .rows_per_page = 8u, + .row_count = 24, + .rows = s_rows, +}; diff --git a/Drivers/BSP/UI/ui_page_ac.h b/Drivers/BSP/UI/ui_page_ac.h new file mode 100644 index 00000000..f91a7ea8 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_ac.h @@ -0,0 +1,23 @@ +/* + * 模块名称:UI Page AC (交流量内容页) + * 模块功能:交流量叶子节点的内容页数据——数据列表(24 项 / 8 项每页,共 3 页; + * 第 1 页参考图 20 原样,第 2/3 页占位数据),可移选分页 + 选中白带; + * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:自 ui_content_page 单体拆出(数据原样迁移,零改动) + */ +#ifndef UI_PAGE_AC_H +#define UI_PAGE_AC_H + +#include "ui_content_page.h" + +/* + * 页面描述符(Flash const):交流量页 = CONTENT_PAGE_AC; + * 接入框架描述符表 s_defs[](定义于 ui_page_ac.c) + */ +extern const content_page_def_t ui_page_ac_desc; + +#endif /* UI_PAGE_AC_H */ diff --git a/Drivers/BSP/UI/ui_page_dc.c b/Drivers/BSP/UI/ui_page_dc.c new file mode 100644 index 00000000..b157f06d --- /dev/null +++ b/Drivers/BSP/UI/ui_page_dc.c @@ -0,0 +1,37 @@ +/* + * 模块名称:UI Page DC (直流量内容页) + * 模块功能:直流量叶子节点的内容页数据——数据列表(3 项单页); + * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:自 ui_content_page 单体拆出(数据原样迁移,零改动) + */ + +/* 头文件包含区 */ +#include "ui_page_dc.h" + +/* 静态变量定义区 */ + +/* 直流量行(参考图 21) */ +static const cp_row_t s_rows[] = { + {"ZL01", "3.01"}, + {"ZL02", "3.15"}, + {"Pa1", "3.288"}, +}; + +/* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用;列表页单页,钩子全 NULL) */ +const content_page_def_t ui_page_dc_desc = { + .title = "直流量", + .selectable = 1u, + .top_y = 24, + .pitch = 14, + .band_h = 13, + .text_dy = 0, + .left_x = 7, + .right_x = 101, + .rows_per_page = 8u, + .row_count = 3, + .rows = s_rows, +}; diff --git a/Drivers/BSP/UI/ui_page_dc.h b/Drivers/BSP/UI/ui_page_dc.h new file mode 100644 index 00000000..3eb29ae5 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_dc.h @@ -0,0 +1,23 @@ +/* + * 模块名称:UI Page DC (直流量内容页) + * 模块功能:直流量叶子节点的内容页数据——数据列表(3 项单页); + * 行几何与交流量页一致(参考图 21 实测); + * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:自 ui_content_page 单体拆出(数据原样迁移,零改动) + */ +#ifndef UI_PAGE_DC_H +#define UI_PAGE_DC_H + +#include "ui_content_page.h" + +/* + * 页面描述符(Flash const):直流量页 = CONTENT_PAGE_DC; + * 接入框架描述符表 s_defs[](定义于 ui_page_dc.c) + */ +extern const content_page_def_t ui_page_dc_desc; + +#endif /* UI_PAGE_DC_H */ diff --git a/Drivers/BSP/UI/ui_page_device_info.c b/Drivers/BSP/UI/ui_page_device_info.c new file mode 100644 index 00000000..61804629 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_device_info.c @@ -0,0 +1,45 @@ +/* + * 模块名称:UI Page Device Info (装置信息内容页) + * 模块功能:装置信息叶子节点的内容页数据——静态键值行(参考图 10,值列固定 x=86) + * + 副标题,单页,页脚按参考图原样"第 10 页,共 1 页"; + * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:自 ui_content_page 单体拆出(数据原样迁移,零改动) + */ + +/* 头文件包含区 */ +#include "ui_page_device_info.h" + +/* 静态变量定义区 */ + +/* 装置信息行(参考图 10;标签含全角冒号,值列固定 x=86) */ +static const cp_row_t s_rows[] = { + {"终端类型:", "F30"}, + {"终端型号:", "F30"}, + {"软件版本:", "SV0.010"}, + {"硬件版本:", "HW0.010"}, + {"软件校验:", "4454"}, + {"程序日期:", "2024.08.27"}, +}; + +/* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用;静态信息页,钩子全 NULL) */ +const content_page_def_t ui_page_device_info_desc = { + .title = "装置信息", + .subtitle = "馈线自动化终端", + .subtitle_y = 25, + .selectable = 0u, + .top_y = 40, + .pitch = 15, + .band_h = 0, + .text_dy = 0, + .left_x = 6, + .right_x = 86, + .rows_per_page = 0u, + .row_count = 6, + .rows = s_rows, + .fixed_cur_page = 10u, + .fixed_total_pages = 1u, +}; diff --git a/Drivers/BSP/UI/ui_page_device_info.h b/Drivers/BSP/UI/ui_page_device_info.h new file mode 100644 index 00000000..4850a366 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_device_info.h @@ -0,0 +1,22 @@ +/* + * 模块名称:UI Page Device Info (装置信息内容页) + * 模块功能:装置信息叶子节点的内容页数据(静态键值行 + 副标题,单页)—— + * 向框架 ui_content_page 提供页面描述符(数据/几何/钩子,零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:自 ui_content_page 单体拆出(数据原样迁移,零改动) + */ +#ifndef UI_PAGE_DEVICE_INFO_H +#define UI_PAGE_DEVICE_INFO_H + +#include "ui_content_page.h" + +/* + * 页面描述符(Flash const):装置信息页 = CONTENT_PAGE_DEVICE_INFO; + * 接入框架描述符表 s_defs[](定义于 ui_page_device_info.c) + */ +extern const content_page_def_t ui_page_device_info_desc; + +#endif /* UI_PAGE_DEVICE_INFO_H */ diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index c448dcc6..a5100423 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -3128,6 +3128,26 @@ 1 ..\Drivers\BSP\UI\ui_content_page.c + + ui_deco.c + 1 + ..\Drivers\BSP\UI\ui_deco.c + + + ui_page_device_info.c + 1 + ..\Drivers\BSP\UI\ui_page_device_info.c + + + ui_page_ac.c + 1 + ..\Drivers\BSP\UI\ui_page_ac.c + + + ui_page_dc.c + 1 + ..\Drivers\BSP\UI\ui_page_dc.c + ui_menu_tab.c 1 diff --git a/docs/UI 设计/UI_设计架构.md b/docs/UI 设计/UI_设计架构.md index 7eb72716..421d8dd6 100644 --- a/docs/UI 设计/UI_设计架构.md +++ b/docs/UI 设计/UI_设计架构.md @@ -19,7 +19,7 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓 | `Drivers/BSP/UI/ui_menu.c/h` | **共用** | 菜单导航逻辑层:持有导航状态(路径/选中/深度/滚动,共 17 B)+ 键语义,单向驱动显示层;不持 LVGL 对象 | | `Drivers/BSP/UI/ui_menu_view.c/h` | **共用** | 菜单显示层:横向级联框渲染(右贴靠/自适应宽/顶底对齐),持有全部 LVGL 对象与样式、无导航状态 | | `Drivers/BSP/UI/ui_menu_tab.c/h` | **共用** | 菜单数据层:`g_menu_tab[]` 父子索引表 + 查询 API(生成物,勿手改) | -| `Drivers/BSP/UI/ui_content_page.c/h` | **共用** | 内容页模块:装置信息/交流量/直流量三页(独立屏按需建毁,自绘行,零常驻池占用) | +| `Drivers/BSP/UI/ui_content_page.c/h` | **共用** | 内容页框架(机制 + 描述符表);装置信息/交流量/直流量三页由 `ui_page_xxx` 模块接入(独立屏按需建毁,自绘行,零常驻池占用);多页面"框架+页面插件"架构见 `内容页架构.md` | | `Drivers/BSP/UI/ui_net_sink.c/h` | MCU | RemoDispBus 服务端(收键 + 回送帧缓冲) | | `Drivers/BSP/UI/ui_font_cn_12.c` | **共用** | 中文 1bpp 子集字体(`lv_font_conv` 生成物) | | `App/task/ui_task.c/h` | MCU | UI 任务:等 NET_READY → 初始化 → 10ms 刷新循环 | @@ -193,6 +193,7 @@ ENTER/→ 进子框: - **导航状态不压层**:菜单 `s_depth/s_sel/s_scroll` 原样保留,内容页只记自身选中行 `s_sel`(每次进入从首行开始) - **池占用实测**(模拟器 x64,40KB 池):开页峰值 +776 B(屏+容器+事件列表+局部样式),关页后回落;一次性残留 **+80 B**(screens 数组高水位 +8、`lv_screen_load` 给菜单屏设 X/Y 局部样式 +72,均为 LVGL 核心行为、非本模块泄漏) - **flat 样式用常量样式**(`LV_STYLE_CONST_INIT`):属性存静态数组、零池分配;动态 `lv_style_t` 首次 set_prop 会从池分配 `values_and_props` 且永不释放(x64 实测 88 B/个,MCU 同量级),常驻样式一律用常量样式 +- **多页面架构(已定稿并拆分,2026-09-09)**:待实现 30 个叶子页;框架(本模块)+ 每页一文件(`ui_page_xxx`,描述符为 extern const 对象)+ 4 个扩展钩子(refresh/count_cb/draw_cb/key_cb),见 `内容页架构.md`;架构第一步(共享"浅 V"装饰 `ui_deco`)与拆分本身均于 2026-09-09 落地,纯重构回归全绿(像素一致、池 4784/4864 B 不变) ## 五、设计决策与考量(为什么这样做) @@ -255,3 +256,5 @@ ENTER/→ 进子框: | 2026-09-08 | 菜单 RAM 收敛两连:行对象→框体自绘(行降为数据,§4.4/§5.5);4 层容器→单一持久容器(焦点恒在其上,框深度由逻辑层 `s_depth` 决定,§4.3/§5.4)+ 根框装饰/状态行容器自绘(无对象);LVGL 池实测 根框 4872 B / 最深四级级联 6128 B | | 2026-09-09 | 内容页模块 `ui_content_page`(§4.7):装置信息/交流量/直流量三叶子页,独立屏按需建毁 + 行自绘;flat 样式改常量样式(`LV_STYLE_CONST_INIT`,零池分配,§5.6);字体 78→97 汉字(193 字形) | | 2026-09-09 | 菜单显示层 flat 样式同法转常量样式(§5.6):LVGL 池基线 4872→4784 B、最深四级级联 6128→6040 B(x64 省 88 B,MCU 同量级);`LV_STYLE_CONST_INIT` 生成外部链接符号,内容页同名者改名 `cp_style_flat` 避免两文件冲突 | +| 2026-09-09 | 内容页多页面架构定稿(`内容页架构.md`):框架+页面插件模型(每页一文件 `ui_page_xxx` + 4 个扩展钩子)、30 个待实现叶子页清单、新增页面六步流程;同日"浅 V"装饰提取为共享 `ui_deco` 模块(架构第一步,菜单根框/内容页共用,像素回归验证一致) | +| 2026-09-09 | 内容页拆分执行(`内容页架构.md` §九 变更记录):`ui_content_page` 提纯为纯框架(描述符表 + NULL 防护 + 描述符驱动绘制/按键、4 个扩展钩子);三页数据拆出 `ui_page_device_info` / `ui_page_ac` / `ui_page_dc`(extern const 描述符对象);双端注册;Keil 0 错 0 警 + 模拟器 0 警告 + `ui-verify` 五门禁全绿(像素一致、池 4784/4864 B 不变) | diff --git a/docs/UI 设计/mockups/密码页面实际机器的示例图片.jpg b/docs/UI 设计/mockups/密码页面实际机器的示例图片.jpg new file mode 100644 index 0000000000000000000000000000000000000000..829318aedbbd142e67d76837944a14e5b897109c GIT binary patch literal 66895 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%0Ado7^QkK z%tGQLA$$@u!2cu;WEZ;#k|sbkJZs%i_ny<38H3$GBj>xmkLboJT`z0A+O3ip*VY`3 z@1&;a5owY>GWkzJ&ywzw}vd7ThPpDH~7Ef*I`g`T1^kDADVRmlPp~hu?mjo z5LRzhN0A_;S{@1oXSxUHrKy_`UJq<{le`wv%d6pN)a8|CqC5yQ0Ee98l~WDv@Ie@D zksl!}3@_+9CXHA#KRG-q8hlR(j);qu;)O;H5QTse*J1QV`|RRQ;7UfQEIAZ*WzQFF zbwK?QV8JU8ta>dWqk?-A6uJJ6c_i}jp|0HUU?m#76sP4OoPmjqNiOBzLX&ZpXfZ?ENAGff1;yLV~^R?y#23!U)8VXouZ@ z5fc%H3*g^`{(ao7xqoR?Li;eLo3{Rm0wj145`>Tq)%ZSZ?Ll1V&BfX%$@>rz6QMu7 z2!F8y(4%+VU(p@X$|n>Mv^s)_yAjTM>F242y>+H*{vualKfk*P0WAr`@jy+d{A_== zyqZjDBqE9oQYlD8E$Q4uR?o}9!BYO*0MFL5`H z7-L+^I*Zl_IVW7%2<7nGP>n~R;pmE-xvlKOX{;uHAnc47gaE%2kj`jfpeM3-8Mlfrla6ChvrFFxBJ%!51RKZ>D6WRcOf*gy?uweybr#%k9i2Q_Z4@TbYD6_&b(Ca?U{ zCjZ5`E{w^v`f;>t6%v)Yysb7u!TdXk+mgfd(XjN>y1e8wkPmh)Q(d78=qFYQdUu=n z0eWTA#8}mCAx9dqs`{a6iI5}jGz?%JzKhLG>4(EbWZv_Fx{!;NYU7z|w}5a1>5W=t-X@6Fyw;~Z#-kLqA;kLCf714akg zZvh(gU&BGf0x(sBnnE?5I!faRL%4Gx-ZgWLQE~{ZDZ*6q#%0h5=>At2IR^*+GzeTEBO z1e$=kSnojqa{CRTDa3TQ%tB5Bun@cbon~kp+@XX}2(LnGAcQ@2XF)yi>f_G6`3`uQ zBWOq&p#<_9Y=u;Z-l&_jw~2v{Hw@BK^K79h!W)_}myTZ87;{MMKXs#y&QnDDS_ATA z@~}xJ{@o_OpzOkQ7&FC zroM8OXnS{-f^*hjoL5>dffEi}Poo!dqk?lCqQX`9Z|JpSHZK@;c%AirB2cD9^w3g= zZ%M33L;IPFok@JN9$~w-`jO4bbtdEpG5nCZN7X74kA^_5G*9X3i+gzPF!B*-v&-~Z zJ$Aa;L&q34Ngp}*kaH5?g#Nh-_}&MvXr&YMKnp0utXCjfDM&-I4=l$9z*4YM8cu|p z(E^3iO*aiS`PFH)6m2i6&2 zn^Y3NG+k0^YDE=u9;3qM3R4NhJW=x?i0F|?kd`z?mxZJ8EeIrF7Mz7dpeNqu5t<{z z?+&GI<%uo=tobD4NNNstqYGb|2CyySHc){NcrKnj1ZLX~?$yEcLKC;xrNYDEv=+I^ zXLVOR!&`VLoEk^(x`EsLCW5Q%ghR??7Gsp2Qc6Sf6q8eVM^pliS=ZNfEibu=s~cJ( zq*d1hRJ6@8L5H54$q_mNI&|EXrsdW8ToeB%X7VHJ4Pv?C+{EUVyfbPPWggi&%878* zE8uc{6#Z!Dt@varW~!aIsLyG0`5_`if7UNvi7tc-yNFpD%=#1q$;hX#gzLhiF6eQk z>%{GwISQh3hv<`_Zi-batY^gj)bd~pZsC+mFdn6F9;ELWCc6N-BaaLsovl|OjoT(S zB0E5|qA7d66@iHMXE-)eA1Gxlh9G7x2U)6IN;I=#bS>hIKtsmg-&qMYyuDC{%uCe* zV}mQ$p{JcGrmXcTonlE(}tkSBwR&Wny$iks1xb>a*Rn~iRV;BG~XH)fJbSfUZ} z!;K-j2T9vWHi%jTE7631pG&en5?OmL#dSit5nRG2z;UcbC)I{xh5V%0dptUSH1YSc<;_c~bFII8oofY8NmQ^+{J(VU2?9P?L%>$Y>U>l#- z6(*yXn5xz%-H+?k^=;WOVOJC7S*(oLP*;(5N&83y-T@EA{UA`NzWNwf;4TCMAHz=A zu#bX?>444&4_Kclmw3(9Cd9e~D|j}_&-#6BWNk!}(S}*PA$4iQTZm#Q#%8 zVg*;v@JV<;ZntSI0c^?r<4~JxGp;#6e_sg zwqurU?Yh35m9ehO-G*f*{!Pu)*1R|q&nP@*A$^%tgG;F+*9c9NYP~#Rs#_v_)y$F7 zS-OE0#i<+0IjI;D_8_HIl26qVY@*R3`l!3GNRl|nOQizpmyB9o z5{nbKKOzW(I0*oM)#7<a{zCH&gA z+bZ%oUVSHFF|N=Si3GR z!gPZY8?(1E+HU0%KXQ$h8=aeVzc6c<*u5&HV<-C|<5r72Tj3H`j$>2~Fk;NLaB=VZ8w@+AEqi&u?D z6vRgQM*rPqx}e{a1LYt2!9{0WGtL~{`WONI(`&|gqt6z*Qau9Yrs+BFHhXij`i*{M zlFk;I#>QPU#+Ii+uXFneU8~|hR!aJ>iZB0ud{)Kg4f;#3S$-J5aH}F?wQ|XUSAP(% z%C66i=MUw6TA_XS;Or&Bqvp%A&uPx(w0YTl=?uY<tzaOD0D84`tfptgU0wL0jFs zU&ek(Z7goR^hV$;DxT3keD#55;S1JG1NG;aXtjezZK};d3&Hpz3TZuOSeNNM^ z=?|rEciR6<B0`ZC{@I0Sha{u!&Bq%gSWQlGv1*ZPHwyqUzG44 z2+Sxjh&`2?RjdL*`gWo*i5%Px(1v@qiVZe}Qp&T;;FR=M2+hzFX|RO80Zc%hZ!;Fcn;3C(3} zK!Ae}uQkOH0u%_%g*qEl?~rnG6U+%|z-CT_Hy|pU@-yp^0ftYg99c-jE>EBa8$mH~ z+4^8FghQ!lvysT34P^3zh&Zq%33GP4+wfpONDSC9oFRI<5TW5WZyl4hK6PvtC>4B*?&&$Ly0tbJqH7zM)_0tl#7 z%^oq{q2Hzf!MDXbl4nY(ID2z?>mL>%4^Y^{-D1`SS?eC^p`nA!)#sQA0!m+IJ10CZ z8IWCStb9vsqh7rre46vY_S*aN+A4(^-)xg_OaH2^DQxE@`_c_RJf`0)INMKjrXcp@ z^9KQYmp;5XQDA9&rn&UXC5s_q?&t#Nwp;1%;#kDhXE(ZO0W-kGPi9sbkxrZcl)-H-Fs6F;q2erL&jU_Q3= zA=@}+Ykfo~`TErSyWiv)ei+=q@v_|8DtAw=~7PjG_?cZ%%MQd`l zg|(H3k?)tX{&YS5;8yxNe9&g)tQ~>m#n-lIj}1+;Vni?gQI}HJbfHCkiGCYGBfL z(5}ys4WULc#soG)-rS}yw}M_<3VyLq>CBACEAI!HA8T{TYXhNoUuI8nv+}$)PsQK6Cs7+kG|8nPfvV_0~zXXcV9G zsj>CzeVta$&`0&XmFZjKcfU zA>6n1YF)A-l%CUj!{Gh~)#Hy8--ejY_od^E_S+tYwAf?B!p*!c2Bwd=cT4hqM{(M_ z$KsKp+Nu}RM>dej_A7~pQ90?@aaj2!TQ6jt)2&xXnl2?`8I;0F1f_)2$|i~?OoN~B z5I-xCn;^oz{~?VDMHwL%Az*PB5`&}x8jy`Zi0|gYIFpBUlFe%B>p!>E>8gyKwJGDC zyv+dpT*nBJ-Q#?N9l}CNr*d~2F1U7MwzUC#0#+(%HQ1?q)B_}51wwHe^cktrka-F# z*QIi$avxf4cjQU@JJR8GVA}AIa4t_`!hXz#wdxqH{iY|aQngTG0kb-lBTtk;fO^vg zol+WPtELz&LnvBEkt7{eQKr}2?iE%`ttrc;DuDsyQo{VU&>bmFl%GYsOZbyFBvn5} zjI&KAnA^P(is=DL2p-||uC8Q?=Ek_PN*cAgo0DCAowBS)=Hh#xISA~Pre>-iCc=~N zf<86kjbx322I&IPaQVcbdXgwW+k&<+^C_hvtMq7!sbT~YObn6|!ka{Ul_x@dp^`AH zuAf3d{DStF<7Lc(-k0RvN;2z0E?u(2ExyO}_ePa^U>hgYlaE10Qd!{BitXKl9PVPS@iPeKd|C+;=8a{x}%nYzTK=aNH`prMmUuT++#=3xI*_$+Hvy^A^{iwg{p%n`W5QN6%P!es2rG_W-f~rFWS=G#f z2AMnGuX8;T(IJ)XWtEKp?4^=!s=7=clZG)yvT4>Yf`v>jr3_^ql|Y?2ypCC|%0eRR zN)`6^M?wNX*zHtf7k=wvToAd*qRpsM0*lmTjkst%(Y_c30i)%IQy~Cbk7vjweG&>% zTJudajwoFS$ZDaNKCZ2Bvpi8sg&=d%P!=+tBWrvN%^EhNBmw?XYqgG68WsGYA(gQzAI!251hamcE9pb( zH+;{E(jFgj62C>cvI+%E2nmPK>KL9(Mek-f@W}v%E#KZ`yxl>ZJPFA|NFvc->q$mx?pt@$m(DEM zHY$9kA>#Gdc!A92tM#)k1m9Sy-I%|i=_Q|Ef9qGkPn%tOw(5gZV%^H0w5zOVrl6pf zl{(pV)t#eDQ8sXdhYC4INxrUAJ%Y>$SZVbvy|0Ki%_n5T8j6gJeue_}K)=BSm^{Y} z6t`?W3MML-s+qG}o7Xh45x5hY^`3N1q1=V~mqKfDk*Nzase{BpXZDKviOIv1z6&Kz zyMI&@PfvM?v)A8|w}Vnq7lTVfkZX{361%v{49(mX7L(%V&7E<616!3yZw>OqRmBy0R-t4HtStd=4K}uJ1S#a62w-* z+p;yaoQri@^Dd0sbd9yMu*Bh69*etc%%q3{l}IX`;21T zLcXy>7O!PcRbUauvSjgnIhjg!%G;6cqIQrcp|E_Wkwvs#5>BQN=?50Ee^LuaLxbsH zG*V%0)>5WsZ4~r(cFA9?8)odMAZd9g3e=dtHp5+c7#0fZdWOs=yF&j`me)yJRY7{0 zY;b=4n0^r>Pc=@j`#L4Y#aR%a=TOTAPZD1%4yqfYpuR3D*5xn@dmp_uYqOl`C>^?j zThMas;}_Jfg4Vi89&fhz<=v945^s9aP_x41aY{Vsm?LI9{ncJxebCT?Kbwb5d1=!5 z)~n;Q+oyl)x$C{x&eU3#c3QT7UY(cz)yc5>A-q=}Ht`KBs>A*~V62OY%yYcNG1W|A za<|P|^k(0~VcTZM`9emdX!}qFE&FzzynNmf8SxUY*>yc&_S>q@z6q`Wj_Lj7E}gM* z{ER!vUGj$&=Hp^*!@if6b#J_KU#j~`?qEh6%@K1yTs-H{5bh(op-H^d)T&DA@8TAu zJb3-i^wKx_Is z?rh~3wc?LOCo5@}#Co4Hd1b1G_}!9aWS;d4U3A^E(BF@q^}4pt!sT-=2z|{JM&ImR z+a!F?@{zzmu`}*+DknkhrHGR8fcm&LeP(zl$#f0{xCBTd#K>OYTmtvM|C*sV8&+oyvJ&b6`G#E@-K1tjm+} z#oId##uS2n07xkbw8Kjv3{)fzYD`MW@ru%BL_{|6A+v^vpyIzOvCtN{qaGzU{31yS z?v-ywDbk*AIYeOsicMlFbR2d)14EaEj5CuTA{`-)HE1Mc?o6UVtGeXnfg@M9_Du{hE(rIt__utnd6s_Q9hSonj_xd^KN

3&ESOpS|x; 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 **用途**:说明内容页体系在代码中**实际如何工作**(框架机制 / 页面插件 / 数据流 / 键与绘制行为 / 内存), +> 并给出新增页面的标准步骤。全文与当前代码一一对应;页面像素几何(行槽/选中带/页脚实测值) +> 见 `菜单页面设计.md` §九。 +> **配套文档**:`UI_设计架构.md`(§1.2 文件地图 / §4.7 内容页 / §5.6 常量样式)、`菜单页面设计.md`(§九 内容页)、 +> `UI_界面制作流程.md`(参考图→代码管线)、`UI_自动化测试流程.md`(回归门禁与黄金参考)。 + +--- + +## 一、什么是内容页 + +菜单**叶子节点** ENTER 后进入的**独立全屏页面**:新建一块 LVGL 屏幕,全部内容(装饰 + 标题 + 行 + 页脚) +由框架一次性自绘(**无子对象**),ESC 返回菜单屏。当前已接 3 页: + +| 页面 | 菜单节点 | 形态 | 数据 | 描述符对象 | +|---|---|---|---|---| +| 装置信息 | node 1 | 静态信息页(6 行 + 副标题,不分页) | Flash 表 | `ui_page_device_info_desc` | +| 交流量 | node 3 | 列表页(24 项,8 项/页,共 3 页) | Flash 表(第 1 页实测,2/3 页占位) | `ui_page_ac_desc` | +| 直流量 | node 4 | 列表页(3 项,单页) | Flash 表 | `ui_page_dc_desc` | + +设计核心是 **"框架 + 页面插件"**:框架 `ui_content_page` 只持**机制**(屏/容器生命周期、焦点、 +按键分发、通用绘制),每页的**数据/几何/钩子**由独立 `ui_page_xxx` 模块经描述符 `content_page_def_t` +提供;两者只以描述符相遇——框架不持任何页面数据,页面模块不碰任何 LVGL 对象。 + +### 1.1 系统总览 + +```mermaid +flowchart TD + K(["按键 / LVGL 刷新
(均在 uiTask 上下文,10ms 周期)"]) --> M["逻辑层 ui_menu.c
导航状态机(17 B)+ 键语义
menu_content_page_for:叶子节点 → 页 ID"] + + subgraph FW["框架 ui_content_page.c/h —— 只有机制,零页面数据"] + S[("描述符表 s_defs[]
页 ID → const content_page_def_t *
(几何 + 数据源 + 4 钩子)")] + L1["屏/容器生命周期
按需建毁 · 焦点 · 切屏"] + L2["按键分发
内置 ↑↓/ESC + key_cb 钩子"] + L3["通用绘制(LV_EVENT_DRAW_MAIN)
浅 V → 标题/副标题 → 行 → 选中带 → 页脚 → draw_cb"] + end + + M -- "ENTER:ui_content_page_open(id)" --> S + S -- "描述符驱动" --> L1 + S -- "描述符驱动" --> L2 + S -- "描述符驱动" --> L3 + L2 -- "ESC/←:close()" --> L1 + + subgraph PG["页面插件 —— 数据(Flash)+ 几何 + 钩子实现,零 LVGL 对象"] + P1["ui_page_device_info
装置信息(静态信息页)"] + P2["ui_page_ac
交流量(列表页)"] + P3["ui_page_dc
直流量(列表页)"] + PN["ui_page_xxx
扩展点:每新增页一文件"] + end + + P1 -- "const 描述符(Flash)" --> S + P2 -- "const 描述符(Flash)" --> S + P3 -- "const 描述符(Flash)" --> S + PN -- "const 描述符(Flash)" --> S + + L3 -- "ui_deco_draw" --> D["ui_deco(共享)
顶部“浅 V”装饰"] + L3 -- "动态行:refresh / count_cb" --> PG + L2 -- "页面自有键:key_cb" --> PG +``` + +读图要点: + +- **数据只向下一层流**:按键 → 逻辑层 → 框架;页面插件不主动调任何人,只被动提供描述符与钩子 +- **框架 ↔ 页面只以描述符相遇**:框架不持页面数据,页面不碰 LVGL 对象 +- 反向调用(`refresh/count_cb/draw_cb/key_cb`)是**框架回调页面**,方向与依赖方向相反 +- 菜单根框与内容页共用同一"浅 V"装饰(`ui_deco` 模块,几何唯一定义处) + +## 二、系统组成 + +| 文件 | 角色 | 一句话职责 | +|---|---|---| +| `ui_content_page.c/h` | **框架** | 屏/容器生命周期 + 按键分发 + 描述符驱动通用绘制 + 描述符表;公开 `content_page_id_t` / `content_page_def_t` / `cp_row_t` 与 3 个 API | +| `ui_page_device_info.c/h` | 页面插件 | 装置信息页:6 行 Flash 表 + 描述符对象 `ui_page_device_info_desc` | +| `ui_page_ac.c/h` | 页面插件 | 交流量页:24 行 Flash 表 + 描述符对象 `ui_page_ac_desc` | +| `ui_page_dc.c/h` | 页面插件 | 直流量页:3 行 Flash 表 + 描述符对象 `ui_page_dc_desc` | +| `ui_deco.c/h` | 共享 | 顶部"浅 V"装饰(3 段线 + culling),菜单根框/内容页共用 | +| `ui_menu.c` | 逻辑层 | `menu_content_page_for` 叶子节点 → 页 ID 映射;ENTER 叶子时调 `ui_content_page_open` | +| `ui_menu_view.c` | 逻辑层依赖 | `ui_content_page_close` 经 `ui_menu_view_container()` 把焦点切回菜单持久容器 | + +依赖方向:`ui_menu(逻辑)→ ui_content_page(框架)→ ui_page_xxx(页面数据)`; +页面模块只依赖框架头文件(取 `content_page_def_t` / `cp_row_t` 类型),**不反向调用任何人**。 + +## 三、框架机制(ui_content_page.c) + +### 3.1 公开 API + +| API | 作用 | 关键行为(代码实际) | +|---|---|---| +| `bool ui_content_page_open(id)` | 开页 | ① 查 `s_defs[id]`:NULL → 打 `ERROR` 日志并返回 false(枚举/表漂移防护)② `lv_obj_create(NULL)` 建屏 + 全屏容器(flat 常量样式 + `IGNORE_LAYOUT` + `CLICK_FOCUSABLE`)③ 容器**先**入默认输入组并获焦 ⓸ `lv_screen_load` 切屏 | +| `void ui_content_page_close()` | 关页 | ① 焦点**先**回菜单持久容器(防输入组焦点悬空)② 切回菜单屏(菜单屏对象未动,原样还原)③ 再删容器+屏,池内存回落 | +| `bool ui_content_page_is_open()` | 查询 | `s_open != CONTENT_PAGE_COUNT` | + +框架持有的运行状态(全部 4 个变量,约 20 B): + +```c +static content_page_id_t s_open; /* 当前打开的页(CONTENT_PAGE_COUNT = 未打开) */ +static uint16_t s_sel; /* 列表页选中行(全局下标,跨页连续;每次进入清零) */ +static lv_obj_t *s_scr; /* 页面屏幕(进入时建、返回时毁) */ +static lv_obj_t *s_cont; /* 页面全屏容器(焦点/键入口 + 自绘载体) */ +``` + +### 3.2 页面生命周期:一次 ENTER → 一次 ESC + +```mermaid +sequenceDiagram + participant U as uiTask(按键 / 10ms 刷新) + participant M as 逻辑层 ui_menu.c + participant F as 框架 ui_content_page + participant P as 页面模块 ui_page_xxx + participant D as ui_deco(共享装饰) + + Note over M,F: 菜单屏:焦点 = 菜单持久容器,按键归逻辑层 + U->>M: ENTER(LV_EVENT_KEY → menu_key_cb) + M->>M: menu_content_page_for(node):叶子节点 → 页 ID + M->>F: ui_content_page_open(id) + F->>F: s_defs[id] 查描述符(NULL → 返回 false + 错误日志) + F->>F: 建屏 + 全屏容器(flat 常量样式)→ 入输入组获焦 → 切屏 + Note over F: 内容页:焦点 = 页面容器,按键归框架键分发 + F->>D: DRAW_MAIN:ui_deco_draw(浅 V) + F->>F: 标题/副标题(居中) + F->>P: refresh(row, name, value)(动态页逐行取数;同帧同步光栅化,栈缓冲安全) + F->>P: count_cb(动态行数) + F->>F: 行 + 选中白带 + 页脚页码 + F->>P: draw_cb(表单/操作页:自定义内容) + U->>F: ESC/←(LV_EVENT_KEY → 框架键分发) + F->>F: close():焦点先回菜单持久容器 → 切回菜单屏 → 再销毁页面屏 + Note over M: 菜单导航状态(s_depth/s_sel/s_scroll)原样保留,返回即还原 +``` + +要点: + +- **导航状态不压层**:进内容页不动菜单 `s_depth/s_sel/s_scroll`,ESC 返回即原样还原 +- 菜单屏对象**保留不销毁**(`lv_screen_load` 来回切,屏上对象原样还原) +- 页面按需建毁:任一时刻至多一个"屏 + 容器"存在 +- 焦点/键入口在切屏**之前**接线(先入组获焦,后 `lv_screen_load`),按键立即由框架接管 + +### 3.3 描述符表与页面注册 + +```c +/* ui_content_page.c(实际代码) */ +static const content_page_def_t *const s_defs[CONTENT_PAGE_COUNT] = { + [CONTENT_PAGE_DEVICE_INFO] = &ui_page_device_info_desc, + [CONTENT_PAGE_AC] = &ui_page_ac_desc, + [CONTENT_PAGE_DC] = &ui_page_dc_desc, +}; +``` + +- 每页一行:`页 ID → &描述符对象`;绘制/按键逻辑一律经 `s_defs[s_open]` 取当前页描述符 +- **描述符是 extern const 对象**(头文件声明、页面模块 `.c` 定义),框架表直接存其**地址** + > ⚠️ 为什么不用"访问器函数"建表:ARMCC v5 禁止用函数名初始化对象指针数组(error #144, + > 拆分实施时实测命中)。extern const 对象模式与菜单数据层 `g_menu_tab[]` 一致,且天然落 Flash。 +- **NULL 防护**:`open()` 时查表为 NULL(枚举加了值但表漏加行)→ 拒开 + `ERROR` 日志,不崩溃 + +### 3.4 按键分发(cp_key_cb) + +```mermaid +flowchart TD + A(["按键(焦点容器 → LV_EVENT_KEY → cp_key_cb)"]) --> B{"ESC / ← ?"} + B -- 是 --> C["ui_content_page_close()
关页返回菜单(框架保证,页面钩子不可拦截)"] + B -- 否 --> D{"key_cb 钩子非空
且返回 true ?"} + D -- 是 --> E["页面已处理(表单/操作页自有键)
框架跳过内置语义"] + D -- 否 --> F{"↑ / ↓ ?"} + F -- 是 --> G{"selectable=1 且
总行数 > 0 ?"} + G -- 是 --> H["移选(首尾环绕,跨页连续)
选中行越界 → 重置首行(防动态行数缩小)
lv_obj_invalidate 触发重绘"] + G -- 否 --> I["不响应(信息页 / 空表)"] + F -- 否 --> J["其余键值:忽略"] +``` + +行为细节(与代码一致): + +- **ESC/← 恒返回**:最先判断,`key_cb` 无法拦截(框架保证页面可退出) +- **key_cb 先于内置 ↑↓**:返回 `true` = 页已处理,框架不再执行内置移选 +- **↑↓ 移选**:`(sel + total ∓ 1) % total` 首尾环绕;`s_sel` 是全局下标,跨页连续 +- **越界防护**:`s_sel >= total` 时先重置首行(为动态行数可能缩小而设;静态页恒不触发) +- 移选只改 `s_sel` + `lv_obj_invalidate(s_cont)` 触发重绘,**不重建任何对象** + +### 3.5 通用绘制链(cp_draw_cb,LV_EVENT_DRAW_MAIN) + +```mermaid +flowchart TD + A(["LV_EVENT_DRAW_MAIN → cp_draw_cb"]) --> B["① 浅 V 装饰:ui_deco_draw(共享模块,内置 culling)
② 标题(居中,y=2) ⓢ 副标题(居中,y=subtitle_y;仅非空页)"] + B --> C["④ 行区块
列表页:窗口 = 选中行所在页(首可见行 = (sel/每页数)×每页数)
信息页:全量;行槽位置固定,数据随页切换"] + C --> C1{"行数据来源"} + C1 -- "静态:rows 表非空" --> C2["直绘 Flash 表(text_static=1,不拷内存)"] + C1 -- "动态:refresh 非空" --> C3["refresh() 逐行取入栈缓冲(32 B×2,同帧同步光栅化)"] + C2 --> D + C3 --> D["逐行:culling 判交 → 选中行白带(x=2..156,高 band_h)
→ 左/右两列 lv_draw_label(选中黑字,其余白字)"] + D --> E["⑤ 页脚“第 X 页,共 Y 页”(y=143,x=6)
列表页:随选中行现算;信息页:fixed_cur/total 固定值"] + E --> F{"draw_cb 非空 ?"} + F -- 是 --> G["⑥ 页面自定义内容(表单/操作页,在内置区块之后)"] + F -- 否 --> H(["绘制完成"]) + G --> H +``` + +行为细节(与代码一致): + +- **行窗口**:`first = (s_sel / rows_per_page) * rows_per_page`,`cnt` 截断到 `rows_per_page` + (末页不足整页);信息页(`selectable=0`)全量绘制 +- **culling**:每个区块(标题/副标题/行条带/页脚)先判"重绘区 ∩ 该区块 ≠ ∅"(`cp_areas_intersect`, + 与 LVGL `lv_area_intersect` 等价的本地实现,避免依赖私有头),不交则整块跳过 +- **选中带**:`band_h > 0` 且该行选中 → 整行白带 `x=2..156`(参考图实测),文字随之黑字 +- **文本**:静态行 `text_static=1`(Flash 字符串不拷贝);动态行与页脚页码用栈缓冲 + (`lv_draw_label` 在同一刷新周期内**同步**光栅化,指针仅需调用期间有效) +- **页脚**:`lv_snprintf` 现算(MSVC 对 `snprintf` 中文格式串误报 C4819,故用 LVGL 自带 `lv_snprintf`) +- 容器样式为**常量样式**(`LV_STYLE_CONST_INIT`,零 LVGL 池分配),见 `UI_设计架构.md` §5.6 + +## 四、页面描述符(content_page_def_t) + +```c +/* ui_content_page.h(实际代码) */ +typedef struct { + const char *title; /* 标题(浅 V 区居中) */ + const char *subtitle; /* 副标题;NULL = 无(仅信息页有) */ + int32_t subtitle_y; /* 副标题放置 y(仅 subtitle 非空时有效) */ + uint8_t selectable; /* 1 = 列表页(↑↓移选+分页);0 = 信息页(静态行) */ + int32_t top_y; /* 首行槽顶 y */ + int32_t pitch; /* 行距(相邻行槽顶间距) */ + int32_t band_h; /* 选中白带高(0 = 无选中带) */ + int32_t text_dy; /* 文本放置相对行槽顶的垂直偏移 */ + int32_t left_x; /* 左列 x(信息页=标签、列表页=名) */ + int32_t right_x; /* 右列 x(值) */ + uint8_t rows_per_page; /* 每页可见行数(0 = 不分页) */ + uint16_t row_count; /* 总行数(静态页;动态页用 count_cb) */ + const cp_row_t *rows; /* 静态数据表(Flash);NULL = 动态页 */ + uint8_t fixed_cur_page; /* 信息页页脚"第 X 页"固定显示值 */ + uint8_t fixed_total_pages; /* 信息页页脚"共 Y 页"固定显示值 */ + /* —— 扩展钩子(全可选;NULL = 纯内置行为;均运行在 uiTask 上下文)—— */ + void (*refresh)(uint16_t row, char *name, char *value); /* 动态行:逐行取数(名/值) */ + uint16_t (*count_cb)(void); /* 动态行数(记录页) */ + void (*draw_cb)(lv_layer_t *layer, const lv_area_t *clip); /* 表单/操作页:自定义内容 */ + bool (*key_cb)(lv_key_t key); /* 页面自有键;true = 页已处理 */ +} content_page_def_t; +``` + +字段一览(含当前 3 页实际值): + +| 字段 | 类型 | 作用 | 装置信息 | 交流量 | 直流量 | +|---|---|---|---|---|---| +| `title` | `const char *` | 标题(浅 V 区居中) | "装置信息" | "交流量" | "直流量" | +| `subtitle` | `const char *` | 副标题;NULL = 无 | "馈线自动化终端" | NULL | NULL | +| `subtitle_y` | `int32_t` | 副标题放置 y | 25 | — | — | +| `selectable` | `uint8_t` | 1 = 列表页(↑↓+分页);0 = 信息页 | 0 | 1 | 1 | +| `top_y` | `int32_t` | 首行槽顶 y | 40 | 24 | 24 | +| `pitch` | `int32_t` | 行距 | 15 | 14 | 14 | +| `band_h` | `int32_t` | 选中白带高(0 = 无) | 0 | 13 | 13 | +| `text_dy` | `int32_t` | 文本垂直偏移 | 0 | 0 | 0 | +| `left_x` / `right_x` | `int32_t` | 左列(标签/名)/ 右列(值)x | 6 / 86 | 7 / 101 | 7 / 101 | +| `rows_per_page` | `uint8_t` | 每页行数(0 = 不分页) | 0 | 8 | 8 | +| `row_count` | `uint16_t` | 总行数(静态页) | 6 | 24 | 3 | +| `rows` | `const cp_row_t *` | 静态数据表(Flash) | 6 行 | 24 行 | 3 行 | +| `fixed_cur_page` / `fixed_total_pages` | `uint8_t` | 信息页页脚固定值 | 10 / 1 | — | — | +| `refresh` / `count_cb` / `draw_cb` / `key_cb` | 函数指针 | 扩展钩子 | 全 NULL | 全 NULL | 全 NULL | + +扩展钩子(机制已实现,当前 3 页均未使用): + +| 钩子 | 签名 | 框架调用时机(uiTask 上下文) | 用途 | +|---|---|---|---| +| `refresh` | `void (uint16_t row, char *name, char *value)` | 绘制行区块时,每行一次、`lv_draw_label` 之前 | 动态行:逐行取名/值(遥信量、记录页等) | +| `count_cb` | `uint16_t (void)` | 绘制(行窗口/页脚)与按键(移选取模)经 `cp_row_total()` 取行数 | 动态行数(记录查询类) | +| `draw_cb` | `void (lv_layer_t *layer, const lv_area_t *clip)` | 绘制链末尾(内置区块之后) | 表单/操作页自定义内容 | +| `key_cb` | `bool (lv_key_t key)` | 键分发时(ESC 之后、内置 ↑↓ 之前) | 页面自有键(ENTER 确认等);返回 true = 页已处理 | + +### 4.1 表单/操作页首个应用:密码页(待实现) + +实机(淮源电力 F30)已有"用户密钥"页:进入某些子叶面前须先输入密码(实机照片见 +`mockups/密码页面实际机器的示例图片.jpg`)。它是本项目**第一个表单/操作页**,落地形态即 +`draw_cb` + `key_cb` 组合。 + +**功能规则(已确认)** + +- 密码共 **3 位**;子页(叶子)可配置"是否需要密码页",每个受保护页可设**专属密码** +- 受保护叶子 ENTER:先开密码页;输入与专属密码一致后按确认键进入目标页;ESC 随时返回菜单 +- 键语义(全部由页面 `key_cb` 处理;`selectable=0`,框架内置 ↑↓ 移选不适用): + +| 键 | 行为 | +|---|---| +| ↑ / ↓ | 调整光标位数字值(0–9,环绕:9↔0) | +| ← / → | 移动光标(反显框)左/右(环绕:首框↔末框) | +| ENTER | 确认:一致 → 关密码页并打开目标页;不一致 → 3 框清空回 `0`、光标回首框,重输(已确认) | +| ESC | 返回菜单(框架保证,不可拦截) | + +**实机视觉(照片;✅ 已确认按此实现)** + +- 标题"用户密钥"居中、位于屏中上部(约屏高 1/4 处);**无**"浅 V"装饰、**无**页脚 +- 3 个方框单行居中(约屏高 1/2 处),均**预填 `0`**:首框**反显(黑底白字)= 光标位**, + 其余 2 框白底黑字、细边框 + +**框架映射(描述符配置)** + +| 字段 | 值 | +|---|---| +| `selectable` | 0(无内置移选) | +| `rows` / `row_count` | NULL / 0 | +| `draw_cb` | 非空:绘制标题 + 3 个数字框(光标框反显) | +| `key_cb` | 非空:↑↓ / ←→ / ENTER 按上表 | +| `title` | "用户密钥"(y≈45 中上、"浅 V"关 —— 跟随实机;需下方 ⚠️ 扩展 2) | + +**⚠️ 框架扩展需求(落地前必须完成)** + +1. **← 键拦截(✅ 已确认:描述符拦截开关)**:当前 `cp_key_cb` 将 `LV_KEY_LEFT` 与 ESC 同等对待:在调用 `key_cb` + **之前**即无条件关页返回(ui_content_page.c:345,注释明示"页面钩子不可拦截")——密码页的光标 + ←/→ 移动会被框架吞掉。方案:描述符增加拦截标志(如 `uint8_t key_intercept`),置位时 `LV_KEY_LEFT` + **先**交 `key_cb`,返回 false 才执行内置返回;`LV_KEY_ESC` **不受**拦截("页面恒可退出"的框架 + 保证不放松)。现有 3 页不置标志,行为完全不变 +2. **每页标题 y / "浅 V" 开关(✅ 需要,视觉已确认跟随实机)**:绘制链当前固定标题 `CP_TITLE_Y=2` + 且恒绘"浅 V";实机密码页无装饰、标题在中上部 → 描述符增加标题 y 与装饰开关字段 + +**设计确认进度** + +| # | 事项 | 状态 | +|---|---|---| +| 1 | 密码不一致行为 | ✅ 已确认:清空重输(3 框回 `0`、光标回首框) | +| 2 | 每页专属密码的存放与设置 | ✅ 已确认:配置页可设、存 Flash 配置区(配置页本身亦是后续表单/操作页) | +| 3 | 视觉风格 | ✅ 已确认:跟随实机照片(无"浅 V"、标题中上)→ 需 ⚠️ 扩展 2 | +| 4 | ← 键冲突解法 | ✅ 已确认:方案 A 描述符拦截开关(`key_intercept`);`LV_KEY_LEFT` 先交页面钩子,ESC 恒不可拦截,现有 3 页不置开关、零影响 | +| 5 | 边界环绕行为 | ✅ 已确认:都环绕(数字 9↔0,光标首框↔末框) | + +## 五、页面模块(ui_page_xxx) + +每页一个 `.c/h` 文件对,只含**数据(Flash)+ 描述符对象**,零 LVGL 对象、零机制代码。 +以装置信息页为例(`ui_page_device_info.c` 实际代码节选): + +```c +/* 头文件包含区 */ +#include "ui_page_device_info.h" /* 内含 ui_content_page.h,并声明描述符对象 */ + +/* 静态变量定义区 */ + +static const cp_row_t s_rows[] = { + {"终端类型:", "F30"}, + {"终端型号:", "F30"}, + {"软件版本:", "SV0.010"}, + /* … 共 6 行(参考图 10) */ + {"程序日期:", "2024.08.27"}, +}; + +/* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用) */ +const content_page_def_t ui_page_device_info_desc = { + .title = "装置信息", + .subtitle = "馈线自动化终端", + .subtitle_y = 25, + .selectable = 0u, + .top_y = 40, + .pitch = 15, + .band_h = 0, + .text_dy = 0, + .left_x = 6, + .right_x = 86, + .rows_per_page = 0u, + .row_count = 6, + .rows = s_rows, + .fixed_cur_page = 10u, + .fixed_total_pages = 1u, +}; +``` + +```c +/* ui_page_device_info.h(对外只有两个声明) */ +#include "ui_content_page.h" + +extern const content_page_def_t ui_page_device_info_desc; /* 描述符(Flash const) */ +``` + +- 初始化器按**声明顺序**写指定域(ARMCC v5 = C99 要求顺序一致);省略的钩子字段零初始化为 NULL +- 行表 `s_rows[]` 是模块私有(`static`),只有描述符对象是外部链接 +- 动态页的差异:`rows = NULL`,改在描述符里给 `refresh`(逐行取数),行数变化再给 `count_cb` +- 文件编码:`Drivers/BSP/UI/` 下为 **UTF-8 带 BOM**(中文字面量依赖 BOM 让 MSVC 认 UTF-8, + 见 AGENTS.md 编码铁律) + +## 六、执行上下文与内存 + +**执行上下文** + +- 框架全部回调(绘制 `cp_draw_cb`、键 `cp_key_cb`、4 个页面钩子)运行在 **uiTask 上下文** + (LVGL 刷新与按键都走 `ui_task_func` 主循环,10 ms 周期) +- `refresh` 取到的动态文本在同一刷新周期内经 `lv_draw_label` **同步**光栅化 → 32 B 栈缓冲即安全 +- 跨任务数据(ADC/网络/RTC/存储)沿用既有约定:生产者任务写快照(原子/队列), + UI 任务在钩子里读快照;框架与页面模块**不加锁** + +**内存(实测,40 KB LVGL 池)** + +| 项 | 值 | 说明 | +|---|---|---| +| 框架常驻 RAM | ~20 B | `s_open/s_sel/s_scr/s_cont` 4 变量 | +| 开页峰值 | +776 B | 屏 + 容器 + 事件列表 + 常量样式引用 | +| 关页回落 | 基线 +80 B 一次性残留 | screens 数组高水位 +8、`lv_screen_load` 菜单屏局部 X/Y 样式 +72(LVGL 核心行为,非泄漏;分解见 `菜单页面设计.md` §9.5) | +| 池水位门禁 | 4784 B / 4864 B | 菜单基线 / 开页-关页往返(回归脚本常量) | +| 描述符/行表/标题 | Flash const | 不进 LVGL 池;每页增量 = 描述符 ~40 B + 行表 + 代码 | +| 动态行栈缓冲 | 2×32 B | 仅绘制期存在 | + +## 七、新增页面(标准步骤) + +| # | 动作 | 改哪里 | +|---|---|---| +| 1 | `content_page_id_t` 加一个枚举值 | `ui_content_page.h` | +| 2 | 新建 `ui_page_xxx.c/h`:行表(或回调)+ `extern const content_page_def_t ui_page_xxx_desc`(头文件声明 + `.c` 定义) | `Drivers/BSP/UI/` | +| 3 | 描述符表加一行 `[新 ID] = &ui_page_xxx_desc` | `ui_content_page.c` `s_defs[]` | +| 4 | 叶子节点 → 页 ID 映射加一个 case | `ui_menu.c` `menu_content_page_for` | +| 5 | 双端注册(显式文件列表,无通配) | `MDK-ARM/*.uvprojx`(Drivers/BSP/UI 组)+ `simulator/CMakeLists.txt`(`UI_SOURCES`) | +| 6 | 回归验证 | `ui-verify` 五门禁;新页在 `check_content_pages.py` 登记黄金参考;含新汉字先重生成字体 | + +按页面形态选描述符配置: + +| 页面形态 | 配置要点 | +|---|---| +| 静态信息页 | `selectable=0`、`rows`=Flash 表、`fixed_cur_page/fixed_total_pages` 定页脚 | +| 静态列表页 | `selectable=1`、`rows_per_page` 分页、`rows`=Flash 表(钩子全 NULL) | +| 动态数据页 | `rows=NULL` + `refresh`(逐行取数);行数随数据变再加 `count_cb` | +| 表单/操作页 | `selectable=0`、`rows=NULL`、`draw_cb` 自绘内容 + `key_cb` 自有键(ESC 返回仍由框架保证;首个实例=密码页,见 §4.1) | + +## 八、验证与回归 + +- **门禁**(`ui-verify` skill / `UI_自动化测试流程.md`): + 0) 模拟器构建 0 警告 → 1) 菜单 8 状态像素回归 + 池基线 → 2) 3 内容页黄金参考逐字节比对 + + 开页-关页往返水位 → 3) Keil 构建 0 错 0 警 → 4) 代码规范检查 +- **门禁常量**(脚本头部):`MENU_POOL_BASELINE = 4784 B`、`POOL_AFTER_ROUNDTRIP = 4864 B` +- **失败处置**:像素不一致 = 代码 bug(修代码,**禁止**刷参考掩盖);**有意**改视觉 → + 先肉眼确认新效果正确,再 `check_content_pages.py --update` 刷参考并随代码提交 +- 新增页面后同步在 `check_content_pages.py` 增加该页的按键序列与参考图 + +## 九、变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-09-09 | 架构定稿:"框架 + 页面插件"模型,新增页面六步流程 | +| 2026-09-09 | 总览图由 ASCII 框线升级为 Mermaid(结构 flowchart + 生命周期 sequenceDiagram) | +| 2026-09-09 | 拆分执行:`ui_content_page` 提纯为框架(描述符表 + NULL 防护 + 描述符驱动绘制/按键、4 扩展钩子);三页拆出 `ui_page_device_info` / `ui_page_ac` / `ui_page_dc`(extern const 描述符对象);双端注册;五门禁全绿(像素一致、池 4784/4864 B 不变);描述符定稿 extern const 对象模式(ARMCC #144:函数名不能入对象指针表) | +| 2026-09-09 | 按实际代码重写(本版):章节重组为"总览 → 组成 → 框架机制 → 描述符 → 页面模块 → 上下文与内存 → 新增页面 → 验证";去掉规划性页面清单与拆分前执行清单;图扩充为 4 张 Mermaid(系统总览 / 页面生命周期 / 按键分发 / 绘制链);全部内容逐项核对当前代码 | +| 2026-09-09 | 密码页(首个表单/操作页,待实现)设计落档(§4.1):3 位密码、每页专属密码、每页"是否需要密码页"配置、↑↓ 调值 / ←→ 移光标 / ENTER 确认进入;实机视觉参考 `mockups/密码页面实际机器的示例图片.jpg`;同时发现框架冲突:`cp_key_cb` 对 `LV_KEY_LEFT` 先于钩子拦截(ui_content_page.c:345),落地前需描述符拦截标志扩展(ESC 恒不可拦截);4 项待确认(错误行为 / 密码存放 / 环绕行为 / 视觉风格) | +| 2026-09-09 | 密码页设计确认 3 项(§4.1):不一致 = 清空重输;每页专属密码 = 配置页可设 + 存 Flash 配置区;视觉 = 跟随实机照片(由此确定绘制链扩展需求:每页标题 y / "浅 V" 开关);← 键拦截方案与环绕行为仍待确认 | +| 2026-09-09 | 密码页 ← 键冲突解法确认(§4.1):方案 A 描述符拦截开关(`key_intercept`:`LV_KEY_LEFT` 先交页面钩子、ESC 恒不可拦截、现有 3 页零影响);框架扩展清单定稿(拦截标志 + 每页标题 y / "浅 V" 开关),仅剩环绕行为待确认 | +| 2026-09-09 | 密码页设计定稿(§4.1):边界行为 = 都环绕(数字 9↔0、光标首框↔末框);5 项设计全部确认、无遗留待确认——具备实施条件(框架扩展 + 新页面模块 + 每页保护标志/密码) | diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index fd3d1005..3a09ae20 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -70,6 +70,10 @@ set(UI_SOURCES ${UI_DIR}/ui_menu.c ${UI_DIR}/ui_menu_view.c ${UI_DIR}/ui_content_page.c + ${UI_DIR}/ui_deco.c + ${UI_DIR}/ui_page_device_info.c + ${UI_DIR}/ui_page_ac.c + ${UI_DIR}/ui_page_dc.c ${UI_DIR}/ui_menu_tab.c ${UI_DIR}/ui_font_cn_12.c )