Files
Future/Future.Contract/Adorners/FutureDrawer.cs

367 lines
16 KiB
C#
Raw Normal View History

2025-08-30 17:19:57 +08:00
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Future.Contract
{
/// <summary>
/// [Content]属性就是抽屉中要呈现的视图
/// <MyNamespace:FutureDrawer/>
/// </summary>
[TemplatePart(Name = "TitleBorder", Type = typeof(Border))]//执行透明度动画
[TemplatePart(Name = "ContentView", Type = typeof(Border))]//执行Margin动画
[TemplatePart(Name = "CloseHandle", Type = typeof(Button))]//执行透明度动化
[TemplatePart(Name = "DrawerRoot", Type = typeof(DockPanel))]//处理窗口最大化时的边距
public class FutureDrawer : ContentControl
{
static FutureDrawer()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(FutureDrawer), new FrameworkPropertyMetadata(typeof(FutureDrawer)));
}
#region
private const string TitleBorder = "TitleBorder";
private const string ContentView = "ContentView";
private const string CloseButton = "CloseHandle";
private const string DrawerRoot = "DrawerRoot";
private Border mContentView;
private Button mCloseButton;
private DockPanel mDrawerRoot;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
mContentView = GetTemplateChild(ContentView) as Border;
mCloseButton = GetTemplateChild(CloseButton) as Button;
mDrawerRoot = GetTemplateChild(DrawerRoot) as DockPanel;
if (mCloseButton != null) mCloseButton.Click += (s, e) =>
{
if(this.ApplyCloseDrawer != null && !this.ApplyCloseDrawer(this.Sign)) return;
else IsOpen = false;
};
this.Loaded += (s, e) => IsOpen = true;
}
public bool IsOpen
{
get { return (bool)GetValue(IsOpenProperty); }
set { SetValue(IsOpenProperty, value); }
}
public static readonly DependencyProperty IsOpenProperty =
DependencyProperty.Register("IsOpen", typeof(bool), typeof(FutureDrawer), new PropertyMetadata(false, (s, e) =>
{
var drawer = s as FutureDrawer;
if (e.NewValue is bool b && b)
{
drawer?.StartAnimationIn();
}
else
{
drawer?.StartAnimationOut();
}
}));
private async void StartAnimationIn(float seconds = 0.3f)
{
var sb = new Storyboard();
var offset = mDrawerRoot?.ActualWidth ?? 0;
var animation = new ThicknessAnimation
{
Duration = new Duration(TimeSpan.FromSeconds(seconds)),
From = new Thickness(-offset, 0, offset, 0),
To = new Thickness(0)
};
Storyboard.SetTargetProperty(animation, new PropertyPath("Margin"));
sb.Children.Add(animation);
sb.Begin(mDrawerRoot);
await Task.Delay((int)(seconds * 1000));
}
private async void StartAnimationOut(float seconds = 0.3f)
{
var sb = new Storyboard();
var offset = mDrawerRoot?.ActualWidth ?? 0;
var animation = new ThicknessAnimation
{
Duration = new Duration(TimeSpan.FromSeconds(seconds)),
From = new Thickness(0),
To = new Thickness(-offset, 0, offset, 0),
};
Storyboard.SetTargetProperty(animation, new PropertyPath("Margin"));
sb.Children.Add(animation);
sb.Begin(mDrawerRoot);
await Task.Delay((int)(seconds * 1000));
this.DrawerClosed?.Invoke();
}
#endregion
public Dock Dock
{
get { return (Dock)GetValue(DockProperty); }
set { SetValue(DockProperty, value); }
}
// Using a DependencyProperty as the backing store for Dock. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DockProperty =
DependencyProperty.Register("Dock", typeof(Dock), typeof(FutureDrawer), new PropertyMetadata(Dock.Right));
public double TitleHeight
{
get { return (double)GetValue(TitleHeightProperty); }
set { SetValue(TitleHeightProperty, value); }
}
// Using a DependencyProperty as the backing store for TitleHeight. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TitleHeightProperty =
DependencyProperty.Register("TitleHeight", typeof(double), typeof(FutureDrawer), new PropertyMetadata(40.0));
public double TitleIndent
{
get { return (double)GetValue(TitleIndentProperty); }
set { SetValue(TitleIndentProperty, value); }
}
// Using a DependencyProperty as the backing store for TitleIndent. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TitleIndentProperty =
DependencyProperty.Register("TitleIndent", typeof(double), typeof(FutureDrawer), new PropertyMetadata(0.0));
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
// Using a DependencyProperty as the backing store for Title. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(string), typeof(FutureDrawer), new PropertyMetadata(""));
public int? Sign
{
get { return (int?)GetValue(SignProperty); }
set { SetValue(SignProperty, value); }
}
// Using a DependencyProperty as the backing store for Sign. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SignProperty =
DependencyProperty.Register("Sign", typeof(int?), typeof(FutureDrawer), new PropertyMetadata(0));
/// <summary>
/// 当抽屉准备关闭时发出的申请
/// </summary>
public event Func<int?, bool> ApplyCloseDrawer;
/// <summary>
/// 抽屉关闭后发出通知的委托
/// </summary>
public Action DrawerClosed;
/// <summary>
/// 默认构造:用来使用设计器构造抽屉模板
/// </summary>
public FutureDrawer() { }
public void Close()
{
this.StartAnimationOut();
}
/// <summary>
/// 内部构造:用来复刻当前抽屉模板创建新的抽屉来显示内容
/// </summary>
/// <param name="view">要呈现的视图</param>
/// <param name="title">抽屉的标题</param>
/// <param name="callBackSign">用于CallBack的识别码</param>
/// <param name="applyCloseCallBack">抽屉要关闭时的回调通知</param>
private FutureDrawer(UIElement view, string title, int? callBackSign = null, Func<int?, bool> applyCloseCallBack = null)
{
this.Content = view;
this.Title = title;
this.Sign = Sign;
this.ApplyCloseDrawer = applyCloseCallBack;
}
/// <summary>
/// 克隆当前抽屉的所有属性
/// </summary>
/// <returns></returns>
private FutureDrawer Clone(UIElement view, string title, int? callBackSign = null, Func<int?, bool> applyCloseCallBack = null)
{
FutureDrawer result = new FutureDrawer(view, title, callBackSign, applyCloseCallBack);
result.Dock = this.Dock;
result.TitleHeight = this.TitleHeight;
result.TitleIndent = this.TitleIndent;
result.Background = this.Background;
result.Foreground = this.Foreground;
result.Margin = this.Margin;
result.HorizontalAlignment = this.HorizontalAlignment;
result.VerticalAlignment = this.VerticalAlignment;
result.HorizontalContentAlignment = this.HorizontalContentAlignment;
result.VerticalContentAlignment = this.VerticalContentAlignment;
result.FontSize = this.FontSize;
return result;
}
private Window GetWindow(FrameworkElement origin)
{
if (origin == null) return null;
while (origin != null && origin is not Window) origin = origin.Parent as FrameworkElement;
return origin as Window;
}
/// <summary>
/// 抽屉使用的装饰器,如果存在则证明抽屉曾经打开过,不需要重复创建
/// </summary>
private DrawerAdorner _adorner;
/// <summary>
/// 将当前抽屉显示在目标UIElement【target】中
/// </summary>
/// <param name="target">要显示当前抽屉的UIElement元素</param>
public void ShowInTarget(FrameworkElement target, object content = null)
{
this.Content = content ?? this.Content;
Window window = GetWindow(target);
if (window != null)
{
if (window.WindowState == WindowState.Maximized) this.Padding = new Thickness(8);
window.StateChanged += (s, e) =>
{
Window w = s as Window;
if (w != null)
{
if (w.WindowState == WindowState.Maximized)
this.Padding = new Thickness(8);
else this.Padding = new Thickness(0);
}
};
}
//TODO:这里需要处理多个抽屉叠加的效果
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(target);
_adorner = _adorner ?? new DrawerAdorner(adornerLayer) { Child = this };
this.DrawerClosed = () => adornerLayer.Remove(_adorner);
adornerLayer.Add(_adorner);
}
/// <summary>
/// 在参照点【origin】所在的视觉树向上查找Window对象如果找到则在其可视范围内显示抽屉
/// </summary>
/// <param name="origin">参照点</param>
public void ShowInWindow(FrameworkElement origin, object content = null)
{
FrameworkElement target = origin;
while (target != null && !(target is Window)) target = target.Parent as FrameworkElement;
if (target is Window) this.ShowInTarget(target, content);
}
/// <summary>
/// 在参照点【origin】所在的视觉树内名称为【targetName】名称的UIElement可视范围内显示抽屉
/// </summary>
/// <param name="origin">参照点</param>
/// <param name="targetName">目标UIElement的名称</param>
public void ShowInNamedView(FrameworkElement origin, string targetName, object content = null)
{
FrameworkElement target = origin;
while (target != null && target.Parent != null && target.Name != targetName) target = target.Parent as FrameworkElement;
if (target != null && target.Name != targetName) target = target.FindName(targetName) as FrameworkElement;
if (target is UIElement && target.Name == targetName) this.ShowInTarget(target, content);
}
/// <summary>
/// 以当前抽屉为模板复制一个新的抽屉在目标UIElement的可视范围内显示抽屉
/// </summary>
/// <param name="view">抽屉中承载的内容</param>
/// <param name="target">承载抽屉的UI元素</param>
/// <param name="title">抽屉的标题</param>
/// <param name="callBackSign">请求关闭回调时传入的识别码</param>
/// <param name="applyCloseCallBack">抽屉请求关闭时的回调</param>
public FutureDrawer ShowNewInTarget(UIElement view, FrameworkElement target, string title, int? callBackSign = null, Func<int?, bool> applyCloseCallBack = null)
{
FutureDrawer drawer = this.Clone(view, title, callBackSign, applyCloseCallBack);
drawer.ShowInTarget(target);
return drawer;
}
/// <summary>
/// 以当前抽屉为模板复制一个新的抽屉以参照原点【origin】为起点沿着视觉树向上查找到的第一个Window对象在其可视范围内显示抽屉
/// </summary>
/// <param name="view">抽屉中承载的内容</param>
/// <param name="origin">查找的参照点</param>
/// <param name="title">抽屉的标题</param>
/// <param name="callBackSign">请求关闭回调时传入的识别码</param>
/// <param name="applyCloseCallBack">抽屉请求关闭时的回调</param>
public void ShowNewInWindow(UIElement view, FrameworkElement origin, string title, int? callBackSign = null, Func<int?, bool> applyCloseCallBack = null)
{
FutureDrawer drawer = this.Clone(view, title, callBackSign, applyCloseCallBack);
drawer?.ShowInWindow(origin);
}
/// <summary>
/// 以当前抽屉为模板复制一个新的抽屉在参照原点【origin】的视觉树内查找【target】名称的UIElement元素在其可视范围内显示抽屉
/// </summary>
/// <param name="view">抽屉中承载的内容</param>
/// <param name="origin">查找的参照点</param>
/// <param name="targetName">要查找的目标UIElement的名称</param>
/// <param name="title">抽屉的标题</param>
/// <param name="callBackSign">请求关闭回调时传入的识别码</param>
/// <param name="applyCloseCallBack">抽屉请求关闭时的回调</param>
public void ShowNewInNamedView(UIElement view, FrameworkElement origin, string targetName, string title, int? callBackSign = null, Func<int?, bool> applyCloseCallBack = null)
{
FutureDrawer drawer = this.Clone(view, title, callBackSign, applyCloseCallBack);
drawer?.ShowInNamedView(origin, targetName);
}
#region
/// <summary>
/// 专为抽屉使用的装饰器类
/// </summary>
private class DrawerAdorner : Adorner
{
/// <summary>
/// Adorner本身没有Child属性所以这里需要自己加一个
/// </summary>
private UIElement mChild;
public DrawerAdorner(UIElement adornedElement) : base(adornedElement) { }
public UIElement Child
{
get => mChild;
set
{
if (value == null) RemoveVisualChild(mChild);
else AddVisualChild(value);
mChild = value;
}
}
//重写VisualChildrenCount 表示此控件只有一个子控件
protected override int VisualChildrenCount => 1;
//控件计算大小的时候,我们在装饰层上添加的控件也计算一下大小
protected override Size ArrangeOverride(Size finalSize)
{
mChild?.Arrange(new Rect(finalSize));
return finalSize;
}
//重写GetVisualChild,返回我们添加的控件
protected override Visual GetVisualChild(int index)
{
if (index == 0 && mChild != null) return mChild;
return base.GetVisualChild(index);
}
}
#endregion
}
}