/* * 模块名称:UI Page Password (用户密钥密码页) * 模块功能:3 位密码输入页(表单/操作页首个实例)——标题由框架自绘(无"浅 V"、 * 无页脚,跟随实机),本模块自绘 3 个数字框:预填 0、首框反显 = 光标位; * ↑↓调值(9↔0 环绕)、←→移光标(首↔末环绕)、ENTER 确认(修改密码→完成 * 回调授修改权限 / 查看密码→完成回调授只读权限 / 不一致→清空重输)、 * ESC 返回菜单(框架保证); * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:王建锋 * 创建日期:2026-09-09 * 修改记录: * 2026-09-09 王建锋 创建:表单/操作页首个实例(内容页架构.md §4.1; * 实机参考 mockups/密码页面实际机器的示例图片.jpg) * 2026-09-09 王建锋 改权限模型:arm 只载回调(不再载期望密码);页面比对 * ui_pwd_def.h 两个编译期密码(先查修改),一致时回调携 * 授予的权限(UI_PERM_MODIFY / UI_PERM_VIEW) * 2026-09-14 王建锋 输错提示:ENTER 不一致显示"密码输入错误",方向键清除后重输 * 2026-09-14 王建锋 修 lv_text_get_width 传 -1 死循环(本 LVGL 版 length≠0 时 * while(i #include "ui_page_password.h" #include "ui_pwd_def.h" /* 私有宏定义区 */ /* * 布局常量(设备像素;实机参考 mockups/密码页面实际机器的示例图片.jpg 实测): * 3 个数字框单行居中于屏中下部(框 14x14、间距 6、行顶 y=78、首框 x=53 → * 3 框 + 2 距 = 54px,屏中心 80);数字墨迹框内水平居中、垂直贴框顶 * (ASCII 字形顶距 +2 已计入字体度量) */ #define PW_DIGITS 3 /* 密码位数 */ #define PW_BOX_W 14 /* 数字框宽(px) */ #define PW_BOX_H 14 /* 数字框高(px) */ #define PW_BOX_GAP 6 /* 数字框间距(px) */ #define PW_BOX_Y 78 /* 数字框行顶 y */ #define PW_BOX_X0 53 /* 首个数字框左 x(居中) */ #define PW_TITLE_Y 44 /* 标题放置 y(中上;0 = 框架默认,实机明显低于其余各页的 y=2) */ #define PW_ERR_Y 104 /* "密码输入错误"提示行顶 y(数字框行之下) */ #define PW_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致;提示文本居中用) */ /* 静态变量定义区 */ /* 单位数字文本(Flash):数字 0..9 各 1 字符(text_static,不拷内存) */ static const char *const s_digit_str[10] = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" }; /* 页面运行状态(RAM,约 10 B):3 位值 + 光标位 + 错误标志 + 完成回调 */ static uint8_t s_digits[PW_DIGITS]; /* 3 位密码值(0..9) */ static uint8_t s_cur; /* 光标位 0..2(反显框) */ static uint8_t s_err; /* 错误提示标志(1 = 显示"密码输入错误",方向键清除) */ static void (*s_on_ok)(uint8_t); /* 完成回调(逻辑层预装,实参=授予权限;NULL = 一致时仅关页) */ /* 函数定义区 */ /* * 函数功能:判断两区域是否相交(等价 lv_area_intersect 非空判断;避免依赖 LVGL 私有头) * 入口参数:a - 区域 1 const lv_area_t * * b - 区域 2 const lv_area_t * * 返回值:true - 相交;false - 不相交 * 限定条件:无 * 函数说明:四边取 min/max 成交集,任一边为空即不相交(与 ui_content_page.c 同实现) */ static bool pw_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 * * box - 数字框区域 const lv_area_t * * digit - 数字值(0..9) uint32_t * color - 文本颜色 lv_color_t * 返回值:无 * 限定条件:数字框在重绘区内(调用方 culling) * 函数说明:文本为 Flash 字面量(不拷内存);ofs 相对区域原点 */ static void pw_draw_digit(lv_layer_t *layer, const lv_area_t *box, uint32_t digit, lv_color_t color) { lv_draw_label_dsc_t ld; lv_draw_label_dsc_init(&ld); ld.base.layer = layer; ld.text = s_digit_str[digit]; ld.text_static = 1; ld.color = color; ld.ofs_x = (PW_BOX_W - lv_text_get_width(s_digit_str[digit], 1, LV_FONT_DEFAULT, 0)) / 2; ld.ofs_y = 0; lv_draw_label(layer, &ld, box); } /* * 函数功能:页面自绘(3 个数字框:光标框反显,其余白底黑框) * 入口参数:layer - 绘制层 lv_layer_t * * clip - 重绘区 const lv_area_t * * 返回值:无 * 限定条件:页面打开(框架绘制链末尾调用) * 函数说明:标题/页脚由框架处理(每页 title_y、"浅 V"/页脚已关);本模块只绘数字框; * 1bpp 极性:white=暗、black=亮——光标框反显(white 底 black 字), * 其余框亮底暗字 + 1px 暗边 */ static void pw_draw_cb(lv_layer_t *layer, const lv_area_t *clip) { for (uint32_t i = 0u; i < PW_DIGITS; i++) { lv_area_t box; lv_draw_rect_dsc_t fd; box.x1 = (int32_t)(PW_BOX_X0 + i * (PW_BOX_W + PW_BOX_GAP)); box.x2 = (int32_t)(box.x1 + PW_BOX_W - 1u); box.y1 = PW_BOX_Y; box.y2 = (int32_t)(PW_BOX_Y + PW_BOX_H - 1u); if (!pw_areas_intersect(&box, clip)) { continue; /* 数字框不在重绘区:跳过(culling) */ } lv_draw_rect_dsc_init(&fd); fd.base.layer = layer; fd.bg_opa = LV_OPA_COVER; if (i == s_cur) { /* 光标框:反显(1bpp:white=暗底,black=亮字) */ fd.bg_color = lv_color_white(); } else { /* 非光标框:亮底暗字 + 1px 暗边(1bpp:black=亮底,white=暗) */ fd.bg_color = lv_color_black(); fd.border_color = lv_color_white(); fd.border_width = 1; } lv_draw_rect(layer, &fd, &box); pw_draw_digit(layer, &box, s_digits[i], (i == s_cur) ? lv_color_black() : lv_color_white()); } if (s_err != 0u) { /* "密码输入错误"提示(水平居中;1bpp:white=暗墨) */ lv_draw_label_dsc_t ld; /* 本 LVGL 版 lv_text_get_width:length=0 = 不测量(返回 0)、length≠0 时 * while(i 0) { lv_area_t err_box; err_box.x1 = (PW_SCREEN_W - w) / 2; err_box.x2 = (int32_t)(err_box.x1 + w - 1u); err_box.y1 = PW_ERR_Y; err_box.y2 = (int32_t)(err_box.y1 + 15u); /* 覆盖字体行高(13)+ 余量(同框架约定) */ if (pw_areas_intersect(&err_box, clip)) { lv_draw_label_dsc_init(&ld); ld.base.layer = layer; ld.text = "密码输入错误"; ld.text_static = 1; ld.color = lv_color_white(); ld.ofs_x = 0; ld.ofs_y = 0; lv_draw_label(layer, &ld, &err_box); } } } } /* * 函数功能:页面按键处理(表单页:键全由页面处理) * 入口参数:key - LVGL 键值 lv_key_t * 返回值:true - 页已处理(框架跳过内置语义);false - 未处理 * 限定条件:页面打开;经框架键分发调用(uiTask 上下文) * 函数说明: 1. ↑/↓:调整光标位数字(+1/-1,9↔0 环绕);错误提示态下先清除提示 * 2. ←/→:移动光标(首↔末环绕;← 键经框架拦截标志转入本函数);同上清提示 * 3. ENTER:修改密码 → 完成回调(授修改权限);查看密码 → 完成回调 * (授只读权限);不一致 → 清空重输 + 显示"密码输入错误" * 4. ESC 不经本函数(框架最优先恒关页);其余键值未处理 */ static bool pw_key_cb(lv_key_t key) { if (key != LV_KEY_ENTER && s_err != 0u) { s_err = 0u; /* 错误提示态:方向键先清除提示,再正常执行该键(重新输入) */ } switch (key) { case LV_KEY_UP: s_digits[s_cur] = (uint8_t)((s_digits[s_cur] + 1u) % 10u); /* 值 +1,9 环绕 0 */ break; case LV_KEY_DOWN: s_digits[s_cur] = (uint8_t)((s_digits[s_cur] + 9u) % 10u); /* 值 -1,0 环绕 9 */ break; case LV_KEY_LEFT: s_cur = (uint8_t)((s_cur + PW_DIGITS - 1u) % PW_DIGITS); /* 光标左移,首绕末 */ break; case LV_KEY_RIGHT: s_cur = (uint8_t)((s_cur + 1u) % PW_DIGITS); /* 光标右移,末绕首 */ break; case LV_KEY_ENTER: { uint16_t input = (uint16_t)((uint32_t)s_digits[0] * 100u + (uint32_t)s_digits[1] * 10u + (uint32_t)s_digits[2]); uint8_t perm; if (input == (uint16_t)UI_PWD_VALUE_MODIFY) { perm = (uint8_t)UI_PERM_MODIFY; /* 修改密码:授修改权限(先查,两值同设时授高权) */ } else if (input == (uint16_t)UI_PWD_VALUE_VIEW) { perm = (uint8_t)UI_PERM_VIEW; /* 查看密码:授只读权限 */ } else { /* 不一致:清空重输(3 框回 0、光标回首框)+ 显示错误提示 */ s_digits[0] = 0u; s_digits[1] = 0u; s_digits[2] = 0u; s_cur = 0u; s_err = 1u; break; } /* 一致:逻辑层接管"关页 + 按权限续进入"(回调内自行关页) */ if (s_on_ok != NULL) { s_on_ok(perm); } else { ui_content_page_close(); /* 防御:无回调 → 仅关页 */ } return true; /* 页面已关(或已切走),无需重绘 */ } default: return false; /* 未处理:落回框架内置语义 */ } ui_content_page_invalidate(); /* 状态已变:触发整页重绘 */ return true; } /* * 函数功能:装载完成回调(须在开页前调用) * 入口参数:on_ok - 完成回调(NULL = 一致时仅关页);一致后以授予的权限为实参 * 返回值:无 * 限定条件:由逻辑层在 uiTask 上下文、开页前调用 * 函数说明:密码页比对 ui_pwd_def.h 的两个编译期密码值(无需载值); * 每次进入重置为初始态(全 0、光标首框,与实机初始显示一致) */ void ui_page_password_arm(void (*on_ok)(uint8_t perm)) { s_on_ok = on_ok; s_digits[0] = 0u; s_digits[1] = 0u; s_digits[2] = 0u; s_cur = 0u; s_err = 0u; } /* * 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用): * 表单/操作页——selectable=0、无行数据,自绘内容 + 自有键; * 标题/装饰/页脚跟随实机(无"浅 V"、标题中上、无页脚); * key_intercept=1:← 键先由 key_cb 处理(移光标),ESC 恒不可拦截 */ const content_page_def_t ui_page_password_desc = { .title = "用户密钥", .title_y = PW_TITLE_Y, .hide_deco = 1u, .hide_footer = 1u, .selectable = 0u, .key_intercept = 1u, .draw_cb = pw_draw_cb, .key_cb = pw_key_cb, };