龙腾猫跃 4 месяцев назад
Родитель
Сommit
282a779997
1 измененных файлов с 81 добавлено и 27 удалено
  1. 81 27
      Wpf/Controls/ModernTooltipService.cs

+ 81 - 27
Wpf/Controls/ModernTooltipService.cs

@@ -1,4 +1,5 @@
 // 此文件主体由 AI 生成,应作为独立模块,尽量减少与其他内容的耦合。
+// 使用 ModernTooltipService 接管后,TooltipService 仅有 IsEnabled、ShowOnDisabled、InitialShowDelay 仍然有效,其他属性不再生效。
 
 using System.Windows;
 using System.Windows.Controls;
@@ -7,14 +8,15 @@ using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Animation;
 using System.Windows.Media.Effects;
+using System.Windows.Media.Media3D;
 using System.Windows.Threading;
 
 namespace MeloongCore.Wpf;
 public static class ModernTooltipService {
 
-    // 控制是否在控件上启用现代化 Tooltip;默认为 true。
-    public static void SetIsEnabled(DependencyObject element, bool value) => element.SetValue(IsEnabledProperty, value);
-    public static bool GetIsEnabled(DependencyObject element) => (bool) element.GetValue(IsEnabledProperty);
+    // 控制是否使用现代化 Tooltip 渲染;默认为 true。是否允许显示 Tooltip 仍由 ToolTipService.IsEnabled 控制。
+    public static void SetUseModernTooltip(DependencyObject element, bool value) => element.SetValue(UseModernTooltipProperty, value);
+    public static bool GetUseModernTooltip(DependencyObject element) => (bool) element.GetValue(UseModernTooltipProperty);
 
     // 控制 Tooltip 是否跟随鼠标移动;默认为 true。
     public static void SetFollowMouse(DependencyObject element, bool value) => element.SetValue(FollowMouseProperty, value);
@@ -44,9 +46,7 @@ public static class ModernTooltipService {
             if (sender is FrameworkElement owner && ReferenceEquals(owner, currentOwner)) Close(false);
         }), true);
 
-        EventManager.RegisterClassHandler(typeof(FrameworkElement), ToolTipService.ToolTipOpeningEvent, new ToolTipEventHandler(static (sender, e) => {
-            if (sender is FrameworkElement owner && GetIsEnabled(owner) && TryGetToolTip(owner, out _, out _)) e.Handled = true;
-        }), true);
+        EventManager.RegisterClassHandler(typeof(FrameworkElement), ToolTipService.ToolTipOpeningEvent, new ToolTipEventHandler(OnToolTipOpening), true);
 
         // Loaded 负责常规路径;PreviewMouseDown 兜底处理 Init 晚于控件 Loaded 的情况。
         EventManager.RegisterClassHandler(typeof(ComboBox), FrameworkElement.LoadedEvent, new RoutedEventHandler(static (sender, _) => HookComboBoxDropDown(sender as ComboBox)), true);
@@ -56,8 +56,8 @@ public static class ModernTooltipService {
     #region 附加属性与运行状态
 
     // 附加属性
-    public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached(
-        "IsEnabled", typeof(bool), typeof(ModernTooltipService), new PropertyMetadata(true));
+    public static readonly DependencyProperty UseModernTooltipProperty = DependencyProperty.RegisterAttached(
+        "UseModernTooltip", typeof(bool), typeof(ModernTooltipService), new PropertyMetadata(true));
     public static readonly DependencyProperty FollowMouseProperty = DependencyProperty.RegisterAttached(
         "FollowMouse", typeof(bool), typeof(ModernTooltipService), new PropertyMetadata(true));
     private static readonly DependencyProperty IsComboBoxDropDownHookedProperty = DependencyProperty.RegisterAttached(
@@ -65,9 +65,11 @@ public static class ModernTooltipService {
 
     private static readonly DispatcherTimer openTimer = new();
     private static readonly DispatcherTimer leaveWatchTimer = new() { Interval = TimeSpan.FromMilliseconds(50) };
+    private const int leaveWatchMissLimit = 2;
 
     private static bool initialized;
     private static int animationToken;
+    private static int leaveWatchMissCount;
     private static Point lastMousePoint;
     private static Popup? popup;
     private static Border? popupCard;
@@ -88,14 +90,21 @@ public static class ModernTooltipService {
                 return;
             }
 
-            if (TryGetCursorPoint(currentOwner, out var point)) {
-                lastMousePoint = point;
-                if (GetFollowMouse(currentOwner) && popup is { IsOpen: true }) UpdatePosition(currentOwner, point);
-                if (currentOwner.IsMouseOver) leaveWatchTimer.Stop();
+            if (!TryGetMousePoint(currentOwner, out var point)) {
+                Close(true);
+                return;
+            }
+
+            lastMousePoint = point;
+            if (GetFollowMouse(currentOwner) && popup is { IsOpen: true }) UpdatePosition(currentOwner, point);
+
+            if (IsMouseRelatedToOwner(currentOwner)) {
+                leaveWatchMissCount = 0;
+                if (currentOwner.IsEnabled) leaveWatchTimer.Stop();
                 return;
             }
 
-            Close(true);
+            if (++leaveWatchMissCount >= leaveWatchMissLimit) Close(true);
         };
     }
 
@@ -104,7 +113,7 @@ public static class ModernTooltipService {
     #region 全局鼠标事件
 
     private static void OnMouseEnter(object sender, MouseEventArgs e) {
-        if (sender is not FrameworkElement owner || !GetIsEnabled(owner) || !TryGetToolTip(owner, out _, out _)) return;
+        if (sender is not FrameworkElement owner || !CanShowModernTooltip(owner) || !TryGetToolTip(owner, out _, out _)) return;
         if (ReferenceEquals(currentOwner, owner) && popup is { IsOpen: true }) {
             leaveWatchTimer.Stop();
             return;
@@ -142,34 +151,79 @@ public static class ModernTooltipService {
         owner.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => {
             if (!ReferenceEquals(owner, currentOwner)) return;
 
-            if (TryGetCursorPoint(owner, out _)) {
-                leaveWatchTimer.Start();
+            if (!TryGetMousePoint(owner, out var point)) {
+                Close(true);
                 return;
             }
 
-            Close(true);
+            lastMousePoint = point;
+            if (GetFollowMouse(owner) && popup is { IsOpen: true }) UpdatePosition(owner, point);
+
+            if (IsMouseRelatedToOwner(owner)) {
+                leaveWatchMissCount = 0;
+                leaveWatchTimer.Stop();
+                return;
+            }
+
+            leaveWatchMissCount = 0;
+            leaveWatchTimer.Start();
         }));
     }
 
-    // 确认鼠标位置
-    private static bool TryGetCursorPoint(FrameworkElement owner, out Point point) {
+    // 坐标仍在控件内只说明需要复核;最终是否保留还要看 WPF 命中或捕获是否属于该控件树。
+    private static bool TryGetMousePoint(FrameworkElement owner, out Point point) {
         point = default;
-        if (!owner.IsLoaded || !owner.IsVisible || !GetCursorPos(out var cursor)) return false;
+        if (!owner.IsLoaded || !owner.IsVisible) return false;
 
-        point = owner.PointFromScreen(new Point(cursor.X, cursor.Y));
+        point = Mouse.GetPosition(owner);
         return point.X >= 0 && point.Y >= 0 && point.X <= owner.ActualWidth && point.Y <= owner.ActualHeight;
     }
-    [DllImport("user32.dll")] private static extern bool GetCursorPos(out CursorPoint point);
-    [StructLayout(LayoutKind.Sequential)]
-    private struct CursorPoint {
-        public int X;
-        public int Y;
+
+    private static bool IsMouseRelatedToOwner(FrameworkElement owner) {
+        if (owner.IsMouseOver) return true;
+        if (!owner.IsEnabled && ToolTipService.GetShowOnDisabled(owner) && TryGetMousePoint(owner, out _)) return true;
+        if (IsInOwnerTree(Mouse.DirectlyOver as DependencyObject, owner)) return true;
+        return IsInOwnerTree(Mouse.Captured as DependencyObject, owner);
+    }
+
+    private static bool IsInOwnerTree(DependencyObject? element, DependencyObject owner) {
+        while (element is not null) {
+            if (ReferenceEquals(element, owner)) return true;
+
+            var visualParent = element is Visual or Visual3D ? VisualTreeHelper.GetParent(element) : null;
+            element = visualParent ?? LogicalTreeHelper.GetParent(element);
+        }
+
+        return false;
     }
 
     #endregion
 
     #region Tooltip 生命周期
 
+    private static bool CanShowModernTooltip(FrameworkElement owner) {
+        return GetUseModernTooltip(owner) && ToolTipService.GetIsEnabled(owner) && (owner.IsEnabled || ToolTipService.GetShowOnDisabled(owner));
+    }
+
+    private static void OnToolTipOpening(object sender, ToolTipEventArgs e) {
+        if (sender is not FrameworkElement owner || !CanShowModernTooltip(owner) || !TryGetToolTip(owner, out _, out _)) return;
+
+        e.Handled = true;
+        if (owner.IsEnabled) return;
+
+        if (!ReferenceEquals(currentOwner, owner)) HideImmediately();
+
+        currentOwner = owner;
+        openTimer.Stop();
+        leaveWatchTimer.Stop();
+
+        lastMousePoint = Mouse.GetPosition(owner);
+        Show(owner, lastMousePoint);
+
+        leaveWatchMissCount = 0;
+        leaveWatchTimer.Start();
+    }
+
     private static bool TryGetToolTip(FrameworkElement owner, out object? content, out ToolTip? sourceToolTip) {
         var raw = owner.ToolTip;
         sourceToolTip = raw as ToolTip;
@@ -178,7 +232,7 @@ public static class ModernTooltipService {
     }
 
     private static void Show(FrameworkElement owner, Point point) {
-        if (!ReferenceEquals(owner, currentOwner) || !TryGetToolTip(owner, out var content, out var sourceToolTip)) return;
+        if (!ReferenceEquals(owner, currentOwner) || !CanShowModernTooltip(owner) || !TryGetToolTip(owner, out var content, out var sourceToolTip)) return;
 
         EnsurePopup();