瀏覽代碼

初步添加 ModernToolTipService

龙腾猫跃 4 月之前
父節點
當前提交
0208b6c78f
共有 2 個文件被更改,包括 264 次插入 和 0 次删除
  1. 263 0
      Wpf/Controls/ModernTooltip.cs
  2. 1 0
      Wpf/MeloongCore.Wpf.csproj

+ 263 - 0
Wpf/Controls/ModernTooltip.cs

@@ -0,0 +1,263 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+using System.Windows.Media.Effects;
+using System.Windows.Threading;
+namespace MeloongCore.Wpf;
+
+public static class ModernToolTipService {
+    public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached(
+        "IsEnabled", 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 TimeSpan animationTime = TimeSpan.FromMilliseconds(70);
+    private static readonly int shadowRadius = 18;
+    private static readonly float shadowOpacity = 0.15f;
+    private static readonly Thickness contentMargin = new(12, 11, 12, 8.5);
+    private static readonly Brush backgroundBrush = new SolidColorBrush(Colors.White);
+    private static readonly Brush borderBrush = new SolidColorBrush(Color.FromRgb(0xD6, 0xD6, 0xD6));
+    private static readonly Brush foregroundBrush = new SolidColorBrush(Color.FromRgb(0x52, 0x52, 0x52));
+    private static readonly DispatcherTimer openTimer = new();
+
+    private static bool initialized;
+    private static int animationToken;
+    private static Point lastMousePoint;
+    private static Popup? popup;
+    private static Border? popupCard;
+    private static ScaleTransform? popupScale;
+    private static FrameworkElement? currentOwner;
+    private static ToolTip? borrowedToolTip;
+    private static object? borrowedContent;
+
+    static ModernToolTipService() {
+        openTimer.Tick += (_, _) => {
+            openTimer.Stop();
+            if (currentOwner is not null) Show(currentOwner, lastMousePoint);
+        };
+    }
+
+    /// <summary>
+    /// 全局启用现代 Tooltip。启用后会接管所有 <see cref="FrameworkElement.ToolTip"/>。
+    /// </summary>
+    public static void Initialize() {
+        if (initialized) return;
+        initialized = true;
+
+        EventManager.RegisterClassHandler(typeof(FrameworkElement), UIElement.MouseEnterEvent, new MouseEventHandler(OnMouseEnter), true);
+        EventManager.RegisterClassHandler(typeof(FrameworkElement), UIElement.MouseMoveEvent, new MouseEventHandler(OnMouseMove), true);
+        EventManager.RegisterClassHandler(typeof(FrameworkElement), UIElement.MouseLeaveEvent, new MouseEventHandler(OnMouseLeave), true);
+        EventManager.RegisterClassHandler(typeof(FrameworkElement), FrameworkElement.UnloadedEvent, new RoutedEventHandler(OnUnloaded), true);
+        EventManager.RegisterClassHandler(typeof(FrameworkElement), ToolTipService.ToolTipOpeningEvent, new ToolTipEventHandler(OnNativeToolTipOpening), true);
+    }
+
+    public static void SetIsEnabled(DependencyObject element, bool value) => element.SetValue(IsEnabledProperty, value);
+
+    public static bool GetIsEnabled(DependencyObject element) => (bool) element.GetValue(IsEnabledProperty);
+
+    public static void SetFollowMouse(DependencyObject element, bool value) => element.SetValue(FollowMouseProperty, value);
+
+    public static bool GetFollowMouse(DependencyObject element) => (bool) element.GetValue(FollowMouseProperty);
+
+    private static void OnMouseEnter(object sender, MouseEventArgs e) {
+        if (sender is not FrameworkElement owner || !GetIsEnabled(owner) || GetToolTipContent(owner) is null) return;
+        if (ReferenceEquals(currentOwner, owner) && popup is { IsOpen: true }) return;
+
+        if (!ReferenceEquals(currentOwner, owner)) HideImmediately();
+
+        currentOwner = owner;
+        lastMousePoint = e.GetPosition(owner);
+        openTimer.Stop();
+
+        var delay = Math.Max(0, ToolTipService.GetInitialShowDelay(owner));
+        if (delay == 0) {
+            Show(owner, lastMousePoint);
+        } else {
+            openTimer.Interval = TimeSpan.FromMilliseconds(delay);
+            openTimer.Start();
+        }
+    }
+
+    private static void OnMouseMove(object sender, MouseEventArgs e) {
+        if (sender is not FrameworkElement owner || !ReferenceEquals(owner, currentOwner)) return;
+
+        lastMousePoint = e.GetPosition(owner);
+        if (GetFollowMouse(owner) && popup is { IsOpen: true }) UpdatePosition(owner, lastMousePoint);
+    }
+
+    private static void OnMouseLeave(object sender, MouseEventArgs e) {
+        if (sender is FrameworkElement owner && ReferenceEquals(owner, currentOwner)) Close(true);
+    }
+
+    private static void OnUnloaded(object sender, RoutedEventArgs e) {
+        if (sender is FrameworkElement owner && ReferenceEquals(owner, currentOwner)) Close(false);
+    }
+
+    private static void OnNativeToolTipOpening(object sender, ToolTipEventArgs e) {
+        if (sender is FrameworkElement owner && GetIsEnabled(owner) && GetToolTipContent(owner) is not null) e.Handled = true;
+    }
+
+    private static object? GetToolTipContent(FrameworkElement owner) {
+        var raw = owner.ToolTip;
+        var content = raw is ToolTip toolTip ? toolTip.Content : raw;
+        return content is string text && text.Length == 0 ? null : content;
+    }
+
+    private static void Show(FrameworkElement owner, Point point) {
+        if (!ReferenceEquals(owner, currentOwner)) return;
+
+        var raw = owner.ToolTip;
+        var sourceToolTip = raw as ToolTip;
+        var content = sourceToolTip?.Content ?? raw;
+        if (content is null) return;
+        if (content is string text && text.Length == 0) return;
+
+        EnsurePopup();
+
+        popup!.PlacementTarget = owner;
+        popupCard!.DataContext = sourceToolTip?.DataContext ?? owner.DataContext;
+        popupCard.FlowDirection = owner.FlowDirection;
+        UpdatePosition(owner, point);
+
+        popupCard.Child = null;
+        RestoreBorrowedContent();
+
+        if (sourceToolTip is not null && content is DependencyObject contentObject && ReferenceEquals(LogicalTreeHelper.GetParent(contentObject), sourceToolTip)) {
+            borrowedToolTip = sourceToolTip;
+            borrowedContent = content;
+            sourceToolTip.Content = null;
+        }
+
+        popupCard.Child = content is string stringContent && sourceToolTip?.ContentTemplate is null && sourceToolTip?.ContentTemplateSelector is null
+            ? new TextBlock {
+                Text = stringContent,
+                TextWrapping = TextWrapping.Wrap,
+                Foreground = foregroundBrush,
+                Margin = contentMargin,
+                FontSize = 12.5,
+                LineHeight = 17,
+                MaxWidth = 676
+            }
+            : new ContentPresenter {
+                Content = content,
+                ContentTemplate = sourceToolTip?.ContentTemplate,
+                ContentTemplateSelector = sourceToolTip?.ContentTemplateSelector,
+                ContentStringFormat = sourceToolTip?.ContentStringFormat,
+                Margin = contentMargin,
+                MaxWidth = 676
+            };
+
+        animationToken++;
+        popupCard.Opacity = 0;
+        popupScale!.ScaleX = 0.97;
+        popupScale.ScaleY = 0.97;
+        popup.IsOpen = true;
+        Animate(1, 1, null);
+    }
+
+    private static void Close(bool animated) {
+        openTimer.Stop();
+        currentOwner = null;
+
+        if (popup is null || popupCard is null) return;
+        if (!popup.IsOpen || !animated) {
+            HideImmediately();
+            return;
+        }
+
+        var token = ++animationToken;
+        Animate(0, 0.97, (_, _) => {
+            if (token != animationToken) return;
+            HideImmediately();
+        });
+    }
+
+    private static void HideImmediately() {
+        animationToken++;
+
+        if (popup is not null) popup.IsOpen = false;
+        if (popupCard is not null) {
+            popupCard.BeginAnimation(UIElement.OpacityProperty, null);
+            popupCard.Child = null;
+        }
+
+        if (popupScale is not null) {
+            popupScale.BeginAnimation(ScaleTransform.ScaleXProperty, null);
+            popupScale.BeginAnimation(ScaleTransform.ScaleYProperty, null);
+        }
+
+        RestoreBorrowedContent();
+    }
+
+    private static void RestoreBorrowedContent() {
+        if (borrowedToolTip is null) return;
+
+        borrowedToolTip.Content = borrowedContent;
+        borrowedToolTip = null;
+        borrowedContent = null;
+    }
+
+    private static void EnsurePopup() {
+        if (popup is not null) return;
+
+        popupScale = new ScaleTransform(0.97, 0.97);
+        popupCard = new Border {
+            Background = backgroundBrush,
+            BorderBrush = borderBrush,
+            BorderThickness = new Thickness(1),
+            CornerRadius = new CornerRadius(8),
+            MaxWidth = 700,
+            SnapsToDevicePixels = true,
+            UseLayoutRounding = true,
+            RenderTransform = popupScale,
+            RenderTransformOrigin = new Point(0, 0),
+            Effect = new DropShadowEffect {
+                Opacity = shadowOpacity,
+                BlurRadius = shadowRadius,
+                ShadowDepth = 0,
+                Color = Colors.Black
+            }
+        };
+
+        var root = new Grid {
+            Margin = new Thickness(shadowRadius + 1),
+            SnapsToDevicePixels = true,
+            UseLayoutRounding = true
+        };
+        root.Children.Add(popupCard);
+
+        popup = new Popup {
+            AllowsTransparency = true,
+            IsHitTestVisible = false,
+            StaysOpen = true,
+            PopupAnimation = PopupAnimation.None,
+            Placement = PlacementMode.Relative,
+            Child = root
+        };
+    }
+
+    private static void UpdatePosition(FrameworkElement owner, Point point) {
+        popup!.PlacementTarget = owner;
+        popup.HorizontalOffset = Math.Round(point.X + 25);
+        popup.VerticalOffset = Math.Round(point.Y + 20);
+    }
+
+    private static void Animate(double opacity, double scale, EventHandler? completed) {
+        var opacityAnimation = new DoubleAnimation(opacity, new Duration(animationTime)) {
+            EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }
+        };
+        if (completed is not null) opacityAnimation.Completed += completed;
+
+        var scaleAnimation = new DoubleAnimation(scale, new Duration(animationTime)) {
+            EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }
+        };
+
+        popupCard!.BeginAnimation(UIElement.OpacityProperty, opacityAnimation, HandoffBehavior.SnapshotAndReplace);
+        popupScale!.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimation, HandoffBehavior.SnapshotAndReplace);
+        popupScale.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimation, HandoffBehavior.SnapshotAndReplace);
+    }
+}

+ 1 - 0
Wpf/MeloongCore.Wpf.csproj

@@ -50,6 +50,7 @@
 		<Reference Include="System.Windows.Forms" />
 		<Reference Include="PresentationCore" />
 		<Reference Include="PresentationFramework" />
+		<Reference Include="System.Xaml" />
 	</ItemGroup>
 
 	<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">