写的页面架构
This commit is contained in:
@@ -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 不变) |
|
||||
|
||||
Binary file not shown.
@@ -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 项设计全部确认、无遗留待确认——具备实施条件(框架扩展 + 新页面模块 + 每页保护标志/密码) |
|
||||
Reference in New Issue
Block a user