Files
STM32F4-Base/Drivers/BSP/UI/ui_content_page.c
T
2026-09-09 13:18:06 +08:00

498 lines
20 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:UI Content Page (内容页面显示层)
* 模块功能:菜单叶子节点的内容页(装置信息/交流量/直流量)——独立 LVGL 屏幕,
* 全屏容器 DRAW_MAIN 自绘全部内容(浅 V 装饰 + 居中标题 + 行数据 +
* 页脚页码),无子对象;进入时切屏并获焦、ESC 返回菜单屏并还原焦点
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-09-09
* 修改记录:
* 2026-09-09 王建锋 创建:装置信息/交流量/直流量三页(参考图 10/20/21 实测几何)
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_content_page.h"
#include "ui_menu_view.h"
#include "lvgl.h"
#define DBG_TAG "[UI_CONTENT_PAGE]"
#include "dbg_log.h"
/* 私有宏定义区 */
/*
* 布局常量(设备像素;参考图 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 / 模拟器一致) */
#define CP_SCREEN_H 160 /* 屏幕高(px) */
#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;
/* 静态变量定义区 */
/* "浅 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},
};
static content_page_id_t s_open = CONTENT_PAGE_COUNT; /* 当前打开的页(COUNT = 未打开) */
static uint16_t s_sel; /* 列表页选中行(全局下标,跨页连续) */
static lv_obj_t *s_scr; /* 页面屏幕(进入时建、返回时毁) */
static lv_obj_t *s_cont; /* 页面全屏容器(焦点/键入口 + 自绘载体) */
/* flat:透明、无边框/圆角/内边距——剥离 mono 主题给普通 lv_obj 附加的 card 样式(同 ui_menu_view.c) */
/* 用常量样式(LV_STYLE_CONST_INIT):属性存静态数组,零 LV_MEM 池分配;
* 动态样式首次 set_prop 会从池分配 values_and_props 且永不释放(x64 实测 88B/个) */
static const lv_style_const_prop_t cp_flat_props[] = {
{ .prop = LV_STYLE_BG_OPA, .value = { .num = LV_OPA_TRANSP } },
{ .prop = LV_STYLE_BORDER_WIDTH, .value = { .num = 0 } },
{ .prop = LV_STYLE_RADIUS, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_LEFT, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_RIGHT, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_TOP, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_BOTTOM, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_ROW, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_COLUMN, .value = { .num = 0 } },
LV_STYLE_CONST_PROPS_END
};
LV_STYLE_CONST_INIT(cp_style_flat, cp_flat_props);
/* 函数定义区 */
/*
* 函数功能:判断两区域是否相交(等价 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 cp_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);
}
/*
* 函数功能:绘制居中单行文本(标题/副标题)
* 入口参数:layer - 绘制层 lv_layer_t *
* text - 文本(Flash) const char *
* y - 放置 y int32_t
* 返回值:无
* 限定条件:text 非空;重绘区已含该行区域(调用方 culling)
* 函数说明:x = (屏宽 - 文本实际步进)/2 居中;白色、静态字符串不拷内存
*/
static void cp_draw_centered_text(lv_layer_t *layer, const char *text, int32_t y) {
lv_area_t area;
lv_draw_label_dsc_t ld;
area.x1 = 0;
area.x2 = CP_SCREEN_W - 1;
area.y1 = y;
area.y2 = y + 15; /* 覆盖字体行高(13)+ 余量 */
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = text;
ld.text_static = 1; /* Flash 字符串:不拷内存 */
ld.color = lv_color_white();
/* 本 LVGL 版 lv_text_get_width 的 length=0 表示"不测量",必须传 strlen */
ld.ofs_x = (CP_SCREEN_W - lv_text_get_width(text, (uint32_t)strlen(text), LV_FONT_DEFAULT, 0)) / 2;
ld.ofs_y = 0;
lv_draw_label(layer, &ld, &area);
}
/*
* 函数功能:页面自绘(浅 V 装饰 + 标题/副标题 + 行数据 + 页脚页码)
* 入口参数:e - 事件指针 lv_event_t *
* 返回值:无
* 限定条件:事件为 LV_EVENT_DRAW_MAIN,页面已打开
* 函数说明: 1. 浅 V 装饰几何与菜单根框一致(3 段线逐段画)
* 2. 列表页窗口 = 选中行所在页(首可见行 = (sel/每页数)*每页数),
* 行槽位置固定、数据随页切换;信息页全量静态绘制
* 3. 选中行整行白带(x=2..156)+ 黑字,非选中白字
* 4. 各区块按 culling 区跳过不在重绘区的部分
*/
static void cp_draw_cb(lv_event_t *e) {
const cp_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);
}
}
/* 标题(居中) */
{
lv_area_t t;
t.x1 = 0;
t.x2 = CP_SCREEN_W - 1;
t.y1 = CP_TITLE_Y;
t.y2 = CP_TITLE_Y + 15;
if (cp_areas_intersect(&t, &layer->_clip_area)) {
cp_draw_centered_text(layer, d->title, CP_TITLE_Y);
}
}
/* 副标题(仅信息页) */
if (d->subtitle != NULL) {
lv_area_t t;
t.x1 = 0;
t.x2 = CP_SCREEN_W - 1;
t.y1 = d->subtitle_y;
t.y2 = d->subtitle_y + 15;
if (cp_areas_intersect(&t, &layer->_clip_area)) {
cp_draw_centered_text(layer, d->subtitle, d->subtitle_y);
}
}
/* 行数据:列表页窗口跟随选中行所在页,信息页全量 */
int32_t first = 0;
int32_t cnt = (int32_t)d->total_rows;
if (d->selectable && 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;
if (cnt > (int32_t)d->rows_per_page) {
cnt = (int32_t)d->rows_per_page; /* 末页不足整页时截断 */
}
}
for (int32_t i = 0; i < cnt; i++) {
int32_t row_idx = first + i; /* 数据下标(跨页连续) */
int32_t slot_y = d->top_y + i * d->pitch; /* 行槽顶(窗口内位置固定) */
bool is_sel = (d->selectable != 0u) && (row_idx == (int32_t)s_sel);
lv_color_t color = is_sel ? lv_color_black() : lv_color_white();
lv_area_t strip;
/* 行条带(整宽)不在重绘区:跳过(culling) */
strip.x1 = 0;
strip.x2 = CP_SCREEN_W - 1;
strip.y1 = slot_y;
strip.y2 = slot_y + d->pitch - 1;
if (!cp_areas_intersect(&strip, &layer->_clip_area)) {
continue;
}
/* 选中行:整行白带(x=2..156,高 band_h) */
if (is_sel && d->band_h > 0) {
lv_draw_rect_dsc_t fd;
lv_area_t band;
band.x1 = 2;
band.x2 = CP_SCREEN_W - 3; /* x=156(与参考图实测一致) */
band.y1 = slot_y;
band.y2 = slot_y + d->band_h - 1;
lv_draw_rect_dsc_init(&fd);
fd.base.layer = layer;
fd.bg_color = lv_color_white();
fd.bg_opa = LV_OPA_COVER;
lv_draw_rect(layer, &fd, &band);
}
/* 左列(标签/名)与右列(值):固定列 x,垂直偏移 text_dy */
{
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.color = color; /* 选中黑 / 非选中白 */
ld.ofs_x = d->left_x;
ld.ofs_y = d->text_dy;
lv_draw_label(layer, &ld, &strip);
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = d->rows[row_idx].value;
ld.text_static = 1;
ld.color = color;
ld.ofs_x = d->right_x;
ld.ofs_y = d->text_dy;
lv_draw_label(layer, &ld, &strip);
}
}
/* 页脚"第 X 页,共 Y 页":列表页随选中行现算,信息页按参考图固定值 */
{
char buf[32];
uint8_t cur;
uint8_t total;
lv_area_t t;
if (d->selectable) {
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);
}
else {
cur = d->fixed_cur_page;
total = d->fixed_total_pages;
}
t.x1 = 0;
t.x2 = CP_SCREEN_W - 1;
t.y1 = CP_PAGE_LINE_Y;
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_draw_label_dsc_t ld;
/* ofs 相对 coords 原点(t.y1=CP_PAGE_LINE_Y),垂直偏移取 0 */
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = buf; /* 栈缓冲:本页码每次现算 */
ld.color = lv_color_white();
ld.ofs_x = 6;
ld.ofs_y = 0;
lv_draw_label(layer, &ld, &t);
}
}
}
/*
* 函数功能:页面按键事件入口(↑↓移选 / ESC·←返回)
* 入口参数:e - 事件指针 lv_event_t *
* 返回值:无
* 限定条件:容器已加入默认输入组并获焦
* 函数说明:1. ↑/↓:列表页移选(首尾环绕,跨页连续),窗口随页切换后重绘;信息页不响应
* 2. ESC/←:关闭页面返回菜单(导航状态未压层,原样还原)
*/
static void cp_key_cb(lv_event_t *e) {
const cp_def_t *d = &s_defs[s_open];
lv_key_t key = (lv_key_t)lv_event_get_key(e);
switch (key) {
case LV_KEY_UP:
if (d->selectable && d->total_rows > 0u) {
/* 上移、首尾环绕:先加 total 再减 1 保证无符号下不为负 */
s_sel = (uint16_t)((s_sel + d->total_rows - 1u) % d->total_rows);
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); /* 下移、越尾绕回首行 */
lv_obj_invalidate(s_cont);
}
break;
case LV_KEY_ESC:
case LV_KEY_LEFT:
ui_content_page_close(); /* 返回菜单屏(焦点/导航状态还原) */
break;
default:
break; /* 其余键值忽略 */
}
}
/*
* 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器)
* 入口参数:id - 内容页 ID content_page_id_t
* 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/对象创建失败)
* 限定条件:ui_menu_init() 已完成(默认输入组与菜单屏存在)
* 函数说明:1. lv_obj_create(NULL) 新建独立屏幕 + 全屏容器(自绘载体,无子对象)
* 2. 容器入默认输入组并获焦后再切屏——按键立即由本模块接管
* 3. 导航状态不压层:菜单 s_depth/s_sel 原样保留,ESC 返回即还原
*/
bool ui_content_page_open(content_page_id_t id) {
if (id >= CONTENT_PAGE_COUNT || s_open != CONTENT_PAGE_COUNT) {
return false; /* ID 无效或已有页面打开(防御) */
}
s_scr = lv_obj_create(NULL); /* parent=NULL → 新建屏幕 */
if (s_scr == NULL) {
return false; /* LVGL 池耗尽 */
}
s_cont = lv_obj_create(s_scr);
if (s_cont == NULL) {
lv_obj_delete(s_scr); /* 回滚:屏已建、容器失败 */
s_scr = NULL;
return false;
}
lv_obj_add_flag(s_cont, LV_OBJ_FLAG_IGNORE_LAYOUT); /* 绝对定位,不参与父级布局 */
lv_obj_set_size(s_cont, CP_SCREEN_W, CP_SCREEN_H); /* 全屏覆盖 */
lv_obj_add_style(s_cont, &cp_style_flat, 0); /* 透明、无边框:剥离主题 card */
lv_obj_add_flag(s_cont, LV_OBJ_FLAG_CLICK_FOCUSABLE); /* 可获焦:键事件入口 */
lv_obj_add_event_cb(s_cont, cp_key_cb, LV_EVENT_KEY, NULL);
lv_obj_add_event_cb(s_cont, cp_draw_cb, LV_EVENT_DRAW_MAIN, NULL);
s_open = id;
s_sel = 0u; /* 每次进入从首行开始(页面按需建毁,不跨次保留) */
/* 焦点/键入口:入默认输入组并获焦(先于切屏,保证按键立即由本模块接管) */
lv_group_t *g = lv_group_get_default();
if (g != NULL) {
lv_group_add_obj(g, s_cont);
lv_group_focus_obj(s_cont);
}
lv_screen_load(s_scr); /* 切到内容页(菜单屏对象保留、不销毁) */
DBG_INFO("page open: id=%u rows=%u", (unsigned)id, (unsigned)s_defs[id].total_rows);
return true;
}
/*
* 函数功能:关闭当前内容页(焦点回菜单容器 + 切回菜单屏 + 销毁页面)
* 入口参数:无
* 返回值:无
* 限定条件:ui_content_page_is_open() 为 true
* 函数说明:1. 先移焦到菜单容器再删屏——防输入组焦点悬空(删除自动移出组)
* 2. 切回菜单屏后销毁页面屏幕,LVGL 池内存随即回落
*/
void ui_content_page_close(void) {
if (s_open == CONTENT_PAGE_COUNT || s_cont == NULL) {
return; /* 未打开(防御) */
}
lv_obj_t *menu_cont = ui_menu_view_container();
if (menu_cont != NULL) {
lv_group_focus_obj(menu_cont); /* 焦点先回菜单容器 */
lv_screen_load(lv_obj_get_screen(menu_cont)); /* 切回菜单屏(对象未动,原样还原) */
}
DBG_INFO("page close: id=%u", (unsigned)s_open);
lv_obj_delete(s_cont); /* 先删容器 */
lv_obj_delete(s_scr); /* 再删屏 */
s_scr = NULL;
s_cont = NULL;
s_open = CONTENT_PAGE_COUNT;
s_sel = 0u;
}
/*
* 函数功能:查询是否有内容页打开
* 入口参数:无
* 返回值:true - 有页面打开;false - 无
* 限定条件:无
* 函数说明:供逻辑层/调试判断当前处于菜单屏还是内容页
*/
bool ui_content_page_is_open(void) {
return (s_open != CONTENT_PAGE_COUNT);
}