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 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()
{
if (_rootPanel == null) return;
bool mask = _mode == SoftKeyboardMode.Password;
_keyboard = new SoftKeyboard
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Bottom,
Margin = new Thickness(0, 0, 0, 60),
MaxWidth = 1100,
Width = _mode == SoftKeyboardMode.Password ? 1000 : 700
};
_keyboard.Reset(_mode, ReadTargetText(), mask);
_keyboard.KeyInput += OnKeyInput;
_keyboard.RequestClose += OnRequestClose;
// 浮层:自带一层点击可关闭的透明背景(点空白处=取消)。
_layer = new Grid();
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
{
_injectedMask = new Grid { Background = new SolidColorBrush(Color.FromArgb(204, 0, 0, 0)) };
_rootPanel.Children.Add(_injectedMask);
}
_rootPanel.Children.Add(_layer);
Panel.SetZIndex(_layer, int.MaxValue);
if (_injectedMask != null) Panel.SetZIndex(_injectedMask, int.MaxValue - 1);
}
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 (_layer != null) _rootPanel?.Children.Remove(_layer);
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
}
}