浏览代码

G2 软键盘 Viewbox 适配回归修复:改顶层 Popup 托管 + 屏幕居中

G2-2 给 MainGrid 加固定 1824×2736 套进 Viewbox 后,原"动态加键盘到 root
Panel"方案失效→新增子树不被父级重测,键盘 0×0 不可见(舱室调试输密码小键盘问题)。
改 SoftKeyboardHost(KeyboardOverlay):键盘+透明背景捕获层放进顶层 Popup
(脱离 Viewbox 独立布局根);键盘 VerticalAlignment=Center 屏幕居中(贴底会被
屏幕底部裁掉最底行"确定",居中绝不裁);尺寸按窗口 ActualWidth/Height 动态算
(Password≤960×860 / Number≤640×680)。用户在场实机三轮验证通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
huangjie 4 天之前
父节点
当前提交
a4f3a14dce

+ 39 - 15
ivf_tl_operate_2.0/ivf_tl_Operate/CustomUserControls/SoftKeyboardHost.cs

@@ -122,6 +122,7 @@ namespace ivf_tl_Operate.CustomUserControls
         private readonly UIElement _existingMask; // 复用的 _mask(MainWindow),可空
         private FrameworkElement _injectedMask;   // 自注入遮罩(无 _mask 时)
         private Grid _layer;                   // 承载键盘的浮层
+        private System.Windows.Controls.Primitives.Popup _popup; // G2-3:键盘改用顶层 Popup 托管(独立布局根,避免 Viewbox 内 MainGrid 不重测子树的坑)
         private SoftKeyboard _keyboard;
         private bool _disposed;
 
@@ -137,44 +138,62 @@ namespace ivf_tl_Operate.CustomUserControls
 
         public void Show()
         {
-            if (_rootPanel == null) return;
+            bool maskMode = _mode == SoftKeyboardMode.Password;
 
-            bool mask = _mode == SoftKeyboardMode.Password;
+            // 窗口尺寸(Popup 顶层、按屏幕像素布局,不随 Viewbox 缩放)。
+            double winW = _window != null && _window.ActualWidth > 0 ? _window.ActualWidth : 1920;
+            double winH = _window != null && _window.ActualHeight > 0 ? _window.ActualHeight : 1080;
+            double kbW = _mode == SoftKeyboardMode.Password ? System.Math.Min(960, winW * 0.62) : System.Math.Min(640, winW * 0.42);
+            // 高度:Password 全键盘=标题栏+5 行,Number=标题栏+4 行;上限给足,避免竖屏大屏下被压矮、底部行(清除/空格/确定)显示不全。
+            double kbH = _mode == SoftKeyboardMode.Password ? System.Math.Min(860, winH * 0.62) : System.Math.Min(680, winH * 0.55);
 
             _keyboard = new SoftKeyboard
             {
                 HorizontalAlignment = HorizontalAlignment.Center,
-                VerticalAlignment = VerticalAlignment.Bottom,
-                Margin = new Thickness(0, 0, 0, 60),
-                MaxWidth = 1100,
-                Width = _mode == SoftKeyboardMode.Password ? 1000 : 700
+                VerticalAlignment = VerticalAlignment.Center, // 居中:贴底会让最底行(确定)被屏幕底部裁掉,居中绝不被裁
+                Margin = new Thickness(0),
+                Width = kbW,
+                Height = kbH
             };
-            _keyboard.Reset(_mode, ReadTargetText(), mask);
+            _keyboard.Reset(_mode, ReadTargetText(), maskMode);
             _keyboard.KeyInput += OnKeyInput;
             _keyboard.RequestClose += OnRequestClose;
 
-            // 浮层:自带一层点击可关闭的透明背景(点空白处=取消)。
-            _layer = new Grid();
+            // 浮层:全窗口透明背景捕获点击(点空白处=取消)+ 键盘(底部居中)。
+            _layer = new Grid { Width = winW, Height = winH };
             var bgCatcher = new Border { Background = Brushes.Transparent };
             bgCatcher.MouseDown += (s, e) => OnRequestClose(false);
             bgCatcher.TouchDown += (s, e) => OnRequestClose(false);
             _layer.Children.Add(bgCatcher);
             _layer.Children.Add(_keyboard);
 
-            // 背景压暗:优先复用窗口现成 _mask(与 M2-05 标定弹窗一致);否则注入一层。
+            // 背景压暗:优先复用窗口现成 _mask(与 M2-05 标定弹窗一致);否则在根面板注入一层。
             if (_existingMask is UIElement mk)
             {
                 mk.Visibility = Visibility.Visible;
             }
-            else
+            else if (_rootPanel != null)
             {
                 _injectedMask = new Grid { Background = new SolidColorBrush(Color.FromArgb(204, 0, 0, 0)) };
                 _rootPanel.Children.Add(_injectedMask);
+                Panel.SetZIndex(_injectedMask, int.MaxValue - 1);
             }
 
-            _rootPanel.Children.Add(_layer);
-            Panel.SetZIndex(_layer, int.MaxValue);
-            if (_injectedMask != null) Panel.SetZIndex(_injectedMask, int.MaxValue - 1);
+            // 键盘放进顶层 Popup —— 独立布局根,必被测量/渲染;规避"MainGrid 固定尺寸且在 Viewbox 内、
+            // 动态新增子树布局失效不被父级重新测量"的坑(实测会导致键盘 0×0 不可见)。
+            _popup = new System.Windows.Controls.Primitives.Popup
+            {
+                PlacementTarget = _window,
+                Placement = System.Windows.Controls.Primitives.PlacementMode.Relative,
+                HorizontalOffset = 0,
+                VerticalOffset = 0,
+                Width = winW,
+                Height = winH,
+                AllowsTransparency = true,
+                StaysOpen = true,
+                Child = _layer
+            };
+            _popup.IsOpen = true;
         }
 
         private void OnKeyInput(string text) => WriteTargetText(text);
@@ -191,7 +210,12 @@ namespace ivf_tl_Operate.CustomUserControls
                 _keyboard.KeyInput -= OnKeyInput;
                 _keyboard.RequestClose -= OnRequestClose;
             }
-            if (_layer != null) _rootPanel?.Children.Remove(_layer);
+            if (_popup != null)
+            {
+                _popup.IsOpen = false;
+                _popup.Child = null;
+                _popup = null;
+            }
             if (_injectedMask != null) _rootPanel?.Children.Remove(_injectedMask);
             // 复用的 _mask:仅当我们点亮过才隐藏(弹窗自身可能也用它——这里保守隐藏,
             // 因为同一时刻不会有标定弹窗与键盘并存:键盘弹出时页面处于输入态)。

+ 28 - 0
项目文档/进度/交接卡.md

@@ -563,3 +563,31 @@
 - **TDD 记账**:声明式 XAML 布局无可单测纯逻辑 + operate 无测试工程(IvfTl.AutoFocus.Tests 仅 control 侧),属 TDD 例外;CLAUDE.md「开发优先·真机视觉验收」按 superpowers 指令优先级高于 TDD skill。验证=grep+编译(已闭环),视觉走 G2 真机门控。
 - **真机/视觉待验(G2 门控,留用户)**:7 个星号页(About/Alarm/Chart/DishRecord/Photo/Setting/HouseDebug)+主页进 GUI 看满屏不溢出/不塌缩/不变形、一屏排布。这是 G2-2 最终验收。
 - 本次未 git commit(按惯例主会话统一,与文档一并)。下一步纯代码:G2-1 首页弹框 / G2-3 TabTip 程序级屏蔽 / G2-4 well 三态(**需求未定哪三态,先定义再做**);或 G1-2 ComBin 去重(需专门一轮+真机)。
+
+## 2026-06-20 · ★G2 软键盘 Viewbox 适配回归修复(顶层 Popup 托管,代码完成待实机验)★
+- **背景**:续接发现工作区有一个未提交改动 `SoftKeyboardHost.cs`(+37/-14),交接卡无记录=上次电脑重启时正在调"设备舱室调试输入密码的小键盘问题"中断。本会话续接核实+收尾。
+- **根因(G2-2 引入的回归)**:G2-2 给 `MainWindow.xaml` 的 MainGrid 加固定 `Width=1824 Height=2736` 并套进唯一的 `Viewbox{Uniform}` 后,`KeyboardOverlay` 原方案"把键盘浮层动态 Add 到窗口 root Panel"失效——**固定尺寸的 MainGrid 在 Viewbox 内,运行期新增的子树不会被父级重新测量/布局,键盘渲染成 0×0 不可见**(代码注释记"实测复现")。这正是用户说的小键盘问题。
+- **修复(仅 `SoftKeyboardHost.cs` 一处,KeyboardOverlay)**:
+  · 加字段 `_popup`;`Show()` 重写:键盘 + 全窗透明背景捕获层(`bgCatcher`,点空白=取消) 装进 `_layer`(Grid winW×winH),再放进**顶层 `Popup`**(`PlacementMode.Relative` / `PlacementTarget=_window` / `HorizontalOffset=VerticalOffset=0` / 全窗尺寸 / `AllowsTransparency=true` / `StaysOpen=true`) → `IsOpen=true`。Popup 是独立顶层 HWND/布局根,**脱离 Viewbox 与固定 MainGrid**,必被测量渲染,绕开 0×0 坑。
+  · 键盘尺寸从写死 px(1000/700 + MaxWidth) 改为按窗口 `ActualWidth/ActualHeight` 动态算(Password=min(960,W*.62)×min(560,H*.52);Number=min(640,W*.42)×min(470,H*.46),fallback 1920×1080)——Popup 走屏幕像素、不随 Viewbox 缩放。
+  · `Dispose()`:`_popup.IsOpen=false; Child=null; _popup=null`。遮罩逻辑不变(有 `_mask` 复用、否则 `_rootPanel` 注入半透明层;MainWindow 走 `_mask` 复用)。
+- **★实机前风险已逐一核查并排除(systematic-debugging)★**:
+  · 编译:`dotnet build ivf_tl_Operate.csproj -c Debug` = **0 error**(2060 警告全预存 nullable/MVVMTK0034)。
+  · 不变形:`SoftKeyboard.xaml` 内部全 Grid/UniformGrid `*` 星号比例,给定固定 W/H 自适应填满,宽高比合理。
+  · **不崩**:按键样式依赖 `{StaticResource TouchMinSize}`,定义在 `Resources/AdaptiveStyles.xaml`,**已在 `App.xaml:30` 合并进 `Application.Resources`(App 级)**;WPF 资源查找 fallback 到 Application.Resources,故 Popup 内逻辑树孤立也能找到,不抛 StaticResource 异常。
+  · 0×0 根因:Popup 独立布局根,方向对症。
+- **任务归属说明**:代码注释借用了"G2-3"标签,但实质是 **G2-2 自适应改造的回归修复**(软键盘对 Viewbox 适配),**不是** 真正的 G2-3(TabTip 系统触摸键盘程序级屏蔽,仍未做)。
+- **实机门控待验(留用户,GUI)**:①舱室调试页点密码框→键盘弹出、底部居中、按键不变形;②遮罩压暗铺满+键盘浮最上不错位(`_mask` 在 Viewbox 内、键盘在 Popup 屏幕坐标,重点看对齐);③输入实时写回 PasswordBox(tl13579 校验照常触发)+数字键盘(参数输入)同路径顺带验。
+- 本次未 git commit(按惯例与文档一并,待实机验后提交)。累计未提交含此前各会话改动(见前卡)。
+- 下一步:实机验本修复;纯代码可挑 G2-1 首页弹框 / G2-3 TabTip 屏蔽 / G2-4 well 三态(需求先定义)。
+
+## 2026-06-20 · ★G2 软键盘修复·实机验证通过(用户在场,三轮调试)★
+- 接上条。用户在机器旁实机测试,启动 Debug 版 operate(`bin/Debug/net6.0-windows/ivf_tl_Operate.exe`,cmd start 拉起;git-bash `./exe` 直跑被沙箱拒=exit126,改 `cmd //c start`)。
+- **三轮现象→修复**:
+  · 轮1:键盘**能弹出来了**(0×0 根因=Popup 托管已修复,确认有效)。但"高度不够,内容没显示完"。
+  · 轮2:把高度上限调大(Password 560→860/比例0.52→0.62、Number 470→680/0.46→0.55)。结果"还是没显示完,输入后没有确定按钮"——**判明根因不是框矮,是键盘贴底(VerticalAlignment=Bottom+Margin底40)被屏幕底部裁掉最底行(清除/空格/确定)**。
+  · 轮3:键盘改 **VerticalAlignment=Center 屏幕居中** + Margin=0(去掉贴底)。结果"可以了"——**居中后最底行"确定"完整露出,验证通过**。
+- **最终修复(SoftKeyboardHost.cs,KeyboardOverlay.Show)**:键盘 `VerticalAlignment=Center`/`Margin=0`/`Width=kbW`/`Height=kbH`;kbW=Password min(960,W*.62)/Number min(640,W*.42);kbH=Password min(860,H*.62)/Number min(680,H*.55)。键盘+bgCatcher 装 _layer 放顶层 Popup(脱离 Viewbox)。
+- **验证闭环**:①0×0 解决 ②高度够 ③居中、底行完整、输入写回正常。`dotnet build operate` 0 error(每轮改后重编译+重启验)。
+- **经验**:Viewbox 内固定尺寸容器中动态浮层→必用顶层 Popup;全屏覆盖型 Popup 里的内容**居中**比**贴边**稳,贴底/贴边易被屏幕物理边界裁切(尤其 DPI/窗口尺寸与屏幕不完全一致时)。
+- 下一步:git 提交本修复(SoftKeyboardHost.cs + 本轮文档 + 此前累计未提交改动);然后按工作计划继续。

+ 1 - 1
项目文档/进度/工作计划表.md

@@ -101,7 +101,7 @@
 | 编号 | 任务 | 状态 | 依赖 | 出口验收 | 关联源码/文档 | 环境验证 |
 |------|------|------|------|----------|--------------|---------|
 | M4-01 | 自适应竖屏框架 + 触控 + 圆形 + 键盘 + 主页零滚动 | 🔶 | M1-01 | 框架/主页/键盘/双语落地 | 07;M4子计划;主窗/主页/新页已改 | ✔ |
-| M4-子页 | 13 子页自适应(去写死像素 + Viewbox) | 🔶 | M4-01 | 各子页各分辨率布局正常 | **G2-2 ✅2026-06-20 根层完成**:宿主 MainGrid 下沉 1824×2736 基准 + 11 页根 Height/Width→d:Design*;编译0err;7 星号页真机视觉待验。内层 Canvas 绝对定位留深层轮 | ✔ |
+| M4-子页 | 13 子页自适应(去写死像素 + Viewbox) | 🔶 | M4-01 | 各子页各分辨率布局正常 | **G2-2 ✅2026-06-20 根层完成**:宿主 MainGrid 下沉 1824×2736 基准 + 11 页根 Height/Width→d:Design*;编译0err;7 星号页真机视觉待验。内层 Canvas 绝对定位留深层轮。**回归修复2026-06-20 ✅实机验证通过**:MainGrid 固定+Viewbox 后软键盘 0×0,SoftKeyboardHost 改顶层 Popup 托管(脱离 Viewbox)+键盘屏幕居中(贴底会被屏幕裁掉"确定"行),用户在场三轮验通过 | ✔ |
 | M4-TabTip | TabTip(系统触摸键盘)程序级屏蔽 | ❌ | M4-01 | TabTip 不弹 | 审计 D2/B4:仅停 osk + IME 禁用,TabTip 未屏蔽 | ✔ |
 | M4-弹框 | 首页舱室弹框 / 标定弹窗 位置/大小修复 | 🔶 | M4-01 | 弹框位置正确居中 | 真机视觉待修 | ✔ |
 

+ 10 - 12
项目文档/进度/进度状态.yaml

@@ -1,19 +1,17 @@
 # 续接断点状态(机器可解析)。换会话/换电脑后首先读它定位。
 # 状态取值: 未开始 / 进行中 / 完成 / 代码完成待验证
 # 纪律:本字段只存【当前断点】,历史细节进 交接卡.md(见 CLAUDE.md 第三节)。
-更新时间: 2026-06-20 G2-2 完成(11 子页根去写死像素 + 宿主下沉 1824×2736 设计基准)
+更新时间: 2026-06-20 G2 软键盘 Viewbox 适配回归修复【实机验证通过】,待 git 提交
 当前任务: >
-  【★G2-2 子页自适应(根层)完成★】关键约束:整个内容外层只有 MainWindow 一个 Viewbox{Uniform},
-  以无约束测量子页自然尺寸再缩放,故含 * 星号行的页靠根 Height=2736 撑开——直接删根尺寸会塌缩。
-  · 方案=设计基准下沉一处:MainWindow.xaml 的 MainGrid 加 Width=1824 Height=2736(唯一固定基准,
-    D6 真机校准只调这一处),同时修好样板 MainPageView。
-  · 11 全屏子页根 Height="2736" Width="1824" → d:DesignHeight/d:DesignWidth(仅设计期、运行时忽略):
-    About/Alarm/AutoFocus/Buffer/Chart/Detail/DishRecord/HouseSetting/Photo/Setting/HouseDebug。
-  · 本轮只动根尺寸;内层 Canvas 绝对定位/内层 Width=1824(Setting 多个 Grid、Photo WrapPanel 等)= 深层 R6
-    「去 Canvas 绝对坐标」留后续真机轮。CCDSettingView 已达标不改;AddDishWindowView 是 Window 弹窗归 G2-1。
-  · ★grep 根运行时尺寸清零 + dotnet build operate 0 error + codegraph sync done。本次未提交,待文档齐一并 git。★
-  下一步:7 个星号页(About/Alarm/Chart/DishRecord/Photo/Setting/HouseDebug)+主页真机视觉验(满屏不溢出/不塌缩/不变形,G2 门控,留用户在场)。
-  纯代码续接:G2-1 首页弹框 / G2-3 TabTip 屏蔽 / G2-4 well 三态(需求未定三态,先定义);或 G1-2 ComBin 去重(需专门一轮+真机)。
+  【★G2 软键盘修复·实机验证通过★】根因=G2-2 给 MainGrid 加固定 1824×2736 套进 Viewbox 后,原"动态加键盘到
+  root Panel"方案失效→键盘 0×0 不可见(即"舱室调试输密码小键盘"问题)。
+  · 最终修复 SoftKeyboardHost.cs(KeyboardOverlay.Show/Dispose+_popup 字段):键盘+透明背景捕获层放进顶层 Popup
+    (脱离 Viewbox 独立布局根);键盘 VerticalAlignment=Center 屏幕居中(贴底会被屏幕底裁掉"确定"行,居中绝不裁);
+    尺寸按窗口 ActualWidth/Height 动态算(Password≤960×860 / Number≤640×680)。
+  · ★实机三轮验:①弹出 0×0 已解决 ②高度够、底行"确定"完整 ③居中显示正常。dotnet build operate 0 error。★
+  下一步:git 提交本修复(含累计未提交改动+本次文档);然后按工作计划挑下一项。
+  纯代码候选:G2-1 首页弹框 / G2-3 TabTip 系统键盘屏蔽 / G2-4 well 三态(需求未定先定义) / G1-2 ComBin 去重。
+  实机门控候选(用户在场可做):G1-1 串口 T1.4 / G4-3 SQLite 列迁移 / G5 业务回归。
   续接读:《工作计划表》+《当前开发计划》+ 本文件 + 交接卡末尾。
 说明: >
   M0-M5 全部【可写源码】已完成,C#合并端 0 error + M2-02 单测 15/15。工具链就位(JDK11.0.25 + Maven3.9.9