写的页面架构

This commit is contained in:
2026-09-09 17:15:04 +08:00
parent 24c2f57555
commit 856b6f6426
19 changed files with 1091 additions and 189 deletions
+32
View File
@@ -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
+39
View File
@@ -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`,在对应 `<GroupName>` 的 `<Files>` 中按相邻条目格式插入(`..\` 开头反斜杠路径):
```xml
<File>
<FileName>ui_new_module.c</FileName>
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\UI\ui_new_module.c</FilePath>
</File>
```
现有组:`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` 配置
+45
View File
@@ -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`
+133 -152
View File
@@ -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;
}
+48 -6
View File
@@ -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 <stdint.h>
#include <stdbool.h>
#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);
+81
View File
@@ -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 <stdbool.h>
#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);
}
}
+29
View File
@@ -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 */
+11 -30
View File
@@ -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)) {
+59
View File
@@ -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,
};
+23
View File
@@ -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 */
+37
View File
@@ -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,
};
+23
View File
@@ -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 */
+45
View File
@@ -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,
};
+22
View File
@@ -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 */
+20
View File
@@ -3128,6 +3128,26 @@
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\UI\ui_content_page.c</FilePath>
</File>
<File>
<FileName>ui_deco.c</FileName>
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\UI\ui_deco.c</FilePath>
</File>
<File>
<FileName>ui_page_device_info.c</FileName>
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\UI\ui_page_device_info.c</FilePath>
</File>
<File>
<FileName>ui_page_ac.c</FileName>
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\UI\ui_page_ac.c</FilePath>
</File>
<File>
<FileName>ui_page_dc.c</FileName>
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\UI\ui_page_dc.c</FilePath>
</File>
<File>
<FileName>ui_menu_tab.c</FileName>
<FileType>1</FileType>
+4 -1
View File
@@ -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 不变) |
+433
View File
@@ -0,0 +1,433 @@
# 内容页架构
> **用途**:说明内容页体系在代码中**实际如何工作**(框架机制 / 页面插件 / 数据流 / 键与绘制行为 / 内存),
> 并给出新增页面的标准步骤。全文与当前代码一一对应;页面像素几何(行槽/选中带/页脚实测值)
> 见 `菜单页面设计.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 刷新<br/>(均在 uiTask 上下文,10ms 周期)"]) --> M["逻辑层 ui_menu.c<br/>导航状态机(17 B)+ 键语义<br/>menu_content_page_for:叶子节点 → 页 ID"]
subgraph FW["框架 ui_content_page.c/h —— 只有机制,零页面数据"]
S[("描述符表 s_defs[]<br/>页 ID → const content_page_def_t *<br/>(几何 + 数据源 + 4 钩子)")]
L1["屏/容器生命周期<br/>按需建毁 · 焦点 · 切屏"]
L2["按键分发<br/>内置 ↑↓/ESC + key_cb 钩子"]
L3["通用绘制(LV_EVENT_DRAW_MAIN)<br/>浅 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<br/>装置信息(静态信息页)"]
P2["ui_page_ac<br/>交流量(列表页)"]
P3["ui_page_dc<br/>直流量(列表页)"]
PN["ui_page_xxx<br/>扩展点:每新增页一文件"]
end
P1 -- "const 描述符(Flash)" --> S
P2 -- "const 描述符(Flash)" --> S
P3 -- "const 描述符(Flash)" --> S
PN -- "const 描述符(Flash)" --> S
L3 -- "ui_deco_draw" --> D["ui_deco(共享)<br/>顶部“浅 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()<br/>关页返回菜单(框架保证,页面钩子不可拦截)"]
B -- 否 --> D{"key_cb 钩子非空<br/>且返回 true ?"}
D -- 是 --> E["页面已处理(表单/操作页自有键)<br/>框架跳过内置语义"]
D -- 否 --> F{"↑ / ↓ ?"}
F -- 是 --> G{"selectable=1 且<br/>总行数 > 0 ?"}
G -- 是 --> H["移选(首尾环绕,跨页连续)<br/>选中行越界 → 重置首行(防动态行数缩小)<br/>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)<br/>② 标题(居中,y=2) ⓢ 副标题(居中,y=subtitle_y;仅非空页)"]
B --> C["④ 行区块<br/>列表页:窗口 = 选中行所在页(首可见行 = (sel/每页数)×每页数)<br/>信息页:全量;行槽位置固定,数据随页切换"]
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)<br/>→ 左/右两列 lv_draw_label(选中黑字,其余白字)"]
D --> E["⑤ 页脚“第 X 页,共 Y 页”(y=143,x=6)<br/>列表页:随选中行现算;信息页: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 项设计全部确认、无遗留待确认——具备实施条件(框架扩展 + 新页面模块 + 每页保护标志/密码) |
+4
View File
@@ -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
)