using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace ivf_tl_Operate.CustomUserControls { /// /// M4-04-2 · 内置软键盘弹出宿主 + 绑定附加属性。 /// /// 用法(XAML,仅声明、不改任何业务/校验逻辑): /// /// <PasswordBox cu:SoftKeyboardHost.Mode="Password" ... /> /// <TextBox cu:SoftKeyboardHost.Mode="Number" ... /> /// /// 输入框获焦/触控/点击时弹内置 ,点「确定」「×」或点遮罩收起。 /// 键入实时写回目标控件(TextBox.Text / PasswordBox.Password),其余 PasswordChanged/ /// TextChanged/MouseUp 校验事件照常触发,故 tl13579 校验、参数校验语义完全不变。 /// /// /// 弹出层挂在目标所属 Window 的最外层容器上: /// 若该 Window 暴露名为 "_mask" 的遮罩(MainWindow,与 M2-05 标定弹窗共用)则复用其遮罩背景; /// 否则(LoginWindow 等独立窗口)注入一层自带半透明遮罩。两种情况都不依赖具体页面。 /// /// public static class SoftKeyboardHost { /// 当前已弹出的键盘宿主层(同一时刻至多一个)。 private static KeyboardOverlay _current; #region 附加属性 Mode public static readonly DependencyProperty ModeProperty = DependencyProperty.RegisterAttached( "Mode", typeof(SoftKeyboardMode?), typeof(SoftKeyboardHost), new PropertyMetadata(null, OnModeChanged)); public static void SetMode(DependencyObject element, SoftKeyboardMode? value) => element.SetValue(ModeProperty, value); public static SoftKeyboardMode? GetMode(DependencyObject element) => (SoftKeyboardMode?)element.GetValue(ModeProperty); private static void OnModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is Control c)) return; // 先摘除(防重复挂载),再按需挂上。 c.GotKeyboardFocus -= Target_TriggerOpen; c.PreviewMouseLeftButtonDown -= Target_PreviewMouseDown; c.TouchDown -= Target_TouchDown; if (e.NewValue is SoftKeyboardMode) { c.GotKeyboardFocus += Target_TriggerOpen; c.PreviewMouseLeftButtonDown += Target_PreviewMouseDown; c.TouchDown += Target_TouchDown; } } #endregion #region 触发弹出 private static void Target_PreviewMouseDown(object sender, MouseButtonEventArgs e) { // 已为当前目标弹出则不重复;否则弹出(不抢焦点行为,仅打开键盘)。 if (sender is Control c) Open(c); } private static void Target_TouchDown(object sender, TouchEventArgs e) { if (sender is Control c) Open(c); } private static void Target_TriggerOpen(object sender, KeyboardFocusChangedEventArgs e) { if (sender is Control c) Open(c); } #endregion /// /// 为目标输入框弹出软键盘。重复目标的弹出请求会被忽略,避免抖动。 /// public static void Open(Control target) { var mode = GetMode(target); if (mode == null) return; // 同一目标已开则忽略。 if (_current != null && ReferenceEquals(_current.Target, target)) return; Close(false); var window = Window.GetWindow(target); if (window == null) return; var overlay = new KeyboardOverlay(window, target, mode.Value); overlay.Closed += () => { if (ReferenceEquals(_current, overlay)) _current = null; }; _current = overlay; overlay.Show(); } /// 收起当前键盘(若有)。 public static void Close(bool submit) { _current?.Dispose(submit); _current = null; } } /// /// 单次键盘弹出的宿主层:遮罩 + 浮层 + SoftKeyboard,挂在目标 Window 的根 Panel 上。 /// internal sealed class KeyboardOverlay { public Control Target { get; } public event Action Closed; private readonly Window _window; private readonly SoftKeyboardMode _mode; private readonly Panel _rootPanel; // 注入遮罩/浮层的容器(窗口根 Panel) 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; public KeyboardOverlay(Window window, Control target, SoftKeyboardMode mode) { _window = window; Target = target; _mode = mode; _rootPanel = FindRootPanel(window); _existingMask = FindMask(window); } public void Show() { bool maskMode = _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.Center, // 居中:贴底会让最底行(确定)被屏幕底部裁掉,居中绝不被裁 Margin = new Thickness(0), Width = kbW, Height = kbH }; _keyboard.Reset(_mode, ReadTargetText(), maskMode); _keyboard.KeyInput += OnKeyInput; _keyboard.RequestClose += OnRequestClose; // 浮层:全窗口透明背景捕获点击(点空白处=取消)+ 键盘(底部居中)。 _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 标定弹窗一致);否则在根面板注入一层。 if (_existingMask is UIElement mk) { mk.Visibility = Visibility.Visible; } 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); } // 键盘放进顶层 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); private void OnRequestClose(bool submit) => Dispose(submit); public void Dispose(bool submit) { if (_disposed) return; _disposed = true; if (_keyboard != null) { _keyboard.KeyInput -= OnKeyInput; _keyboard.RequestClose -= OnRequestClose; } if (_popup != null) { _popup.IsOpen = false; _popup.Child = null; _popup = null; } if (_injectedMask != null) _rootPanel?.Children.Remove(_injectedMask); // 复用的 _mask:仅当我们点亮过才隐藏(弹窗自身可能也用它——这里保守隐藏, // 因为同一时刻不会有标定弹窗与键盘并存:键盘弹出时页面处于输入态)。 if (_existingMask is UIElement mk) mk.Visibility = Visibility.Hidden; Closed?.Invoke(); } #region 目标读写(兼容 TextBox / PasswordBox) private string ReadTargetText() { switch (Target) { case PasswordBox pb: return pb.Password; case TextBox tb: return tb.Text; default: return string.Empty; } } private void WriteTargetText(string text) { switch (Target) { case PasswordBox pb: pb.Password = text; // 触发 PasswordChanged(背景/校验钩子照常) break; case TextBox tb: tb.Text = text; // 触发 TextChanged tb.CaretIndex = tb.Text.Length; break; } } #endregion #region 容器查找 private static Panel FindRootPanel(Window w) { // 窗口内容若已是 Panel 直接用;若是 Viewbox/Border 等装饰则向下找首个 Panel; // 找不到则不弹(极端情况,保守)。 DependencyObject node = w.Content as DependencyObject; return FindFirstPanel(node); } private static Panel FindFirstPanel(DependencyObject node) { if (node == null) return null; if (node is Panel p) return p; int count = VisualTreeHelperChildCountSafe(node); for (int i = 0; i < count; i++) { var child = System.Windows.Media.VisualTreeHelper.GetChild(node, i); var found = FindFirstPanel(child); if (found != null) return found; } // 逻辑树兜底(Visual 树未生成时) if (node is ContentControl cc && cc.Content is DependencyObject ccChild) return FindFirstPanel(ccChild); if (node is Decorator dec && dec.Child is DependencyObject decChild) return FindFirstPanel(decChild); return null; } private static int VisualTreeHelperChildCountSafe(DependencyObject node) { try { return System.Windows.Media.VisualTreeHelper.GetChildrenCount(node); } catch { return 0; } } /// 查找窗口中名为 "_mask" 的遮罩元素(MainWindow 暴露,与标定弹窗共用)。 private static UIElement FindMask(Window w) { return w.FindName("_mask") as UIElement; } #endregion } }