Files
Shrlalgo.RvKits/Melskin/Assists/BehaviorAssist.cs
2026-02-17 22:17:13 +08:00

266 lines
11 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media.Animation;
using Melskin.Controls;
using Melskin.Extensions;
namespace Melskin.Assists
{
/// <summary>
/// 控件行为的附加属性类
/// </summary>
public class BehaviorAssist
{
#region Popup
/// <summary>
/// 表示是否使 Popup 控件模拟原生平台的弹出行为。当设置为 true 时Popup 将会在用户点击外部区域或窗口失去焦点时自动关闭,从而提供更接近于原生应用程序的用户体验。
/// </summary>
public static readonly DependencyProperty SimulateNativeBehaviorProperty =
DependencyProperty.RegisterAttached(
"SimulateNativeBehavior", typeof(bool), typeof(BehaviorAssist),
new PropertyMetadata(false, OnSimulateNativeBehaviorChanged));
/// <summary>
///
/// </summary>
/// <param name="element"></param>
/// <param name="value"></param>
public static void SetSimulateNativeBehavior(DependencyObject element, bool value) => element.SetValue(SimulateNativeBehaviorProperty, value);
/// <summary>
///
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
[AttachedPropertyBrowsableForType(typeof(ColorPicker))]
[AttachedPropertyBrowsableForType(typeof(AutoComplete))]
[AttachedPropertyBrowsableForType(typeof(Cascader))]
[AttachedPropertyBrowsableForType(typeof(ComboBox))]
[AttachedPropertyBrowsableForType(typeof(DatePicker))]
[AttachedPropertyBrowsableForType(typeof(MultiComboBox))]
[AttachedPropertyBrowsableForType(typeof(TimePicker))]
public static bool GetSimulateNativeBehavior(DependencyObject element) => (bool)element.GetValue(SimulateNativeBehaviorProperty);
// 私有状态属性的定义保持不变
private static readonly DependencyProperty PreviewMouseDownHandlerProperty =
DependencyProperty.RegisterAttached("PreviewMouseDownHandler", typeof(MouseButtonEventHandler), typeof(BehaviorAssist));
private static readonly DependencyProperty WindowEventHandlerProperty =
DependencyProperty.RegisterAttached("WindowEventHandler", typeof(EventHandler), typeof(BehaviorAssist));
private static readonly DependencyProperty PreviewMouseWheelHandlerProperty =
DependencyProperty.RegisterAttached("PreviewMouseWheelHandler", typeof(MouseWheelEventHandler), typeof(BehaviorAssist));
private static void OnSimulateNativeBehaviorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not Popup popup) return;
if ((bool)e.NewValue)
{
popup.Opened += OnPopupOpened;
popup.Closed += OnPopupClosed;
}
else
{
popup.Opened -= OnPopupOpened;
popup.Closed -= OnPopupClosed;
}
}
private static void OnPopupOpened(object? sender, EventArgs e)
{
if (sender is not Popup popup || !GetSimulateNativeBehavior(popup)) return;
var window = Window.GetWindow(popup);
if (window == null) return;
// 【防御性编程】在添加任何处理器之前,先执行一次清理,防止因竞争条件导致的泄漏。
CleanupHandlers(popup, window);
// --- 添加自动关闭逻辑 ---
EventHandler windowEventHandler = (_, _) =>
{
popup.Dispatcher.BeginInvoke(new Action(() => { popup.IsOpen = false; }));
};
popup.SetValue(WindowEventHandlerProperty, windowEventHandler);
window.Deactivated += windowEventHandler;
window.LocationChanged += windowEventHandler;
// --- 添加鼠标点击外部逻辑 ---
MouseButtonEventHandler mouseDownHandler = (_, _) =>
{
if (popup.IsMouseOver || popup.PlacementTarget is { IsMouseOver: true })
{
return;
}
popup.IsOpen = false;
};
popup.SetValue(PreviewMouseDownHandlerProperty, mouseDownHandler);
window.AddHandler(UIElement.PreviewMouseDownEvent, mouseDownHandler, true);
// --- 添加鼠标滚轮拦截逻辑 ---
MouseWheelEventHandler mouseWheelHandler = (_, args) =>
{
if (!popup.IsMouseOver)
{
args.Handled = true;
}
};
popup.SetValue(PreviewMouseWheelHandlerProperty, mouseWheelHandler);
window.AddHandler(UIElement.PreviewMouseWheelEvent, mouseWheelHandler, true);
}
private static void OnPopupClosed(object? sender, EventArgs e)
{
if (sender is not Popup popup || !GetSimulateNativeBehavior(popup)) return;
var window = Window.GetWindow(popup);
// 窗口可能在Popup关闭时已经不存在了
if (window != null)
{
CleanupHandlers(popup, window);
}
}
/// <summary>
/// 【新增】一个集中的清理方法,确保所有附加的处理器都被移除。
/// </summary>
private static void CleanupHandlers(Popup popup, Window window)
{
if (popup.GetValue(WindowEventHandlerProperty) is EventHandler windowEventHandler)
{
window.Deactivated -= windowEventHandler;
window.LocationChanged -= windowEventHandler;
popup.ClearValue(WindowEventHandlerProperty);
}
if (popup.GetValue(PreviewMouseDownHandlerProperty) is MouseButtonEventHandler mouseDownHandler)
{
window.RemoveHandler(UIElement.PreviewMouseDownEvent, mouseDownHandler);
popup.ClearValue(PreviewMouseDownHandlerProperty);
}
if (popup.GetValue(PreviewMouseWheelHandlerProperty) is MouseWheelEventHandler mouseWheelHandler)
{
window.RemoveHandler(UIElement.PreviewMouseWheelEvent, mouseWheelHandler);
popup.ClearValue(PreviewMouseWheelHandlerProperty);
}
}
#endregion
#region ListBox
/// <summary>
/// 获取指定对象的滑动动画启用状态。
/// </summary>
/// <param name="obj">要获取属性值的对象。</param>
/// <returns>如果为 true则表示启用了滑动动画否则为 false。</returns>
[AttachedPropertyBrowsableForType(typeof(ListBox))]
public static bool GetEnableSlideAnimation(DependencyObject obj)
{
return (bool)obj.GetValue(EnableSlideAnimationProperty);
}
/// <summary>
/// 设置指定对象的滑动动画启用状态。
/// </summary>
/// <param name="obj">要设置属性值的对象。</param>
/// <param name="value">一个布尔值,表示是否启用滑动动画。如果为 true则启用滑动动画否则不启用。</param>
public static void SetEnableSlideAnimation(DependencyObject obj, bool value)
{
obj.SetValue(EnableSlideAnimationProperty, value);
}
/// <summary>
/// 用于控制 ListBox 控件是否启用滑动动画效果的依赖属性。通过设置此属性,可以为 ListBox 添加平滑的滑动过渡效果,从而增强用户体验。
/// </summary>
public static readonly DependencyProperty EnableSlideAnimationProperty =
DependencyProperty.RegisterAttached("EnableSlideAnimation", typeof(bool), typeof(BehaviorAssist), new PropertyMetadata(false, OnEnableSlideAnimationChanged));
private static void OnEnableSlideAnimationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not ListBox listBox) return;
if ((bool)e.NewValue)
{
listBox.Loaded += ListBox_Loaded;
listBox.SelectionChanged += ListBox_SelectionChanged;
}
else
{
listBox.Loaded -= ListBox_Loaded;
listBox.SelectionChanged -= ListBox_SelectionChanged;
}
}
private static void ListBox_Loaded(object sender, RoutedEventArgs e)
{
if (sender is ListBox listBox && listBox.IsVisible)
{
UpdateIndicator(listBox, false);
}
}
private static void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is ListBox { IsLoaded: true } listBox)
{
UpdateIndicator(listBox, true);
}
}
private static void UpdateIndicator(ListBox? listBox, bool useAnimation)
{
if (listBox?.SelectedItem == null) return;
var indicator = listBox.Template.FindName("PART_SelectionIndicator", listBox) as FrameworkElement;
var itemsPresenter = listBox.FindVisualChild<ItemsPresenter>();
if (indicator == null || itemsPresenter == null) return;
var selectedItemUI = listBox.ItemContainerGenerator.ContainerFromItem(listBox.SelectedItem) as FrameworkElement;
if (selectedItemUI == null) return;
var position = selectedItemUI.TransformToAncestor(itemsPresenter).Transform(new Point(0, 0));
indicator.Width = selectedItemUI.ActualWidth;
indicator.Height = selectedItemUI.ActualHeight;
var transform = indicator.RenderTransform as TranslateTransform;
// 如果 transform 为 null或者它是一个被冻结的只读实例
// 那么我们就需要创建一个新的、可写的实例来替换它。
if (transform == null || transform.IsFrozen)
{
transform = new TranslateTransform();
indicator.RenderTransform = transform;
}
if (useAnimation)
{
var animationX = new DoubleAnimation(position.X, new Duration(TimeSpan.FromMilliseconds(300)))
{
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
};
var animationY = new DoubleAnimation(position.Y, new Duration(TimeSpan.FromMilliseconds(300)))
{
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
};
transform.BeginAnimation(TranslateTransform.XProperty, animationX);
transform.BeginAnimation(TranslateTransform.YProperty, animationY);
}
else
{
// 现在 transform 保证是可写的,所以这里不会再抛出异常
transform.X = position.X;
transform.Y = position.Y;
}
}
#endregion
}
}