Files
HandyControl/src/Shared/HandyControl_Shared/Controls/Panel/RelativePanel.cs
T
2020-07-11 22:17:16 +08:00

680 lines
28 KiB
C#

//reference doc : https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.Controls.RelativePanel
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using HandyControl.Data;
using HandyControl.Tools.Extension;
namespace HandyControl.Controls
{
public class RelativePanel : Panel
{
private readonly Graph _childGraph;
public RelativePanel() => _childGraph = new Graph();
#region Panel alignment
public static readonly DependencyProperty AlignLeftWithPanelProperty = DependencyProperty.RegisterAttached(
"AlignLeftWithPanel", typeof(bool), typeof(RelativePanel), new FrameworkPropertyMetadata(ValueBoxes.FalseBox, FrameworkPropertyMetadataOptions.AffectsRender));
public static void SetAlignLeftWithPanel(DependencyObject element, bool value)
=> element.SetValue(AlignLeftWithPanelProperty, ValueBoxes.BooleanBox(value));
public static bool GetAlignLeftWithPanel(DependencyObject element)
=> (bool) element.GetValue(AlignLeftWithPanelProperty);
public static readonly DependencyProperty AlignTopWithPanelProperty = DependencyProperty.RegisterAttached(
"AlignTopWithPanel", typeof(bool), typeof(RelativePanel), new FrameworkPropertyMetadata(ValueBoxes.FalseBox, FrameworkPropertyMetadataOptions.AffectsRender));
public static void SetAlignTopWithPanel(DependencyObject element, bool value)
=> element.SetValue(AlignTopWithPanelProperty, ValueBoxes.BooleanBox(value));
public static bool GetAlignTopWithPanel(DependencyObject element)
=> (bool) element.GetValue(AlignTopWithPanelProperty);
public static readonly DependencyProperty AlignRightWithPanelProperty = DependencyProperty.RegisterAttached(
"AlignRightWithPanel", typeof(bool), typeof(RelativePanel), new FrameworkPropertyMetadata(ValueBoxes.FalseBox, FrameworkPropertyMetadataOptions.AffectsRender));
public static void SetAlignRightWithPanel(DependencyObject element, bool value)
=> element.SetValue(AlignRightWithPanelProperty, ValueBoxes.BooleanBox(value));
public static bool GetAlignRightWithPanel(DependencyObject element)
=> (bool) element.GetValue(AlignRightWithPanelProperty);
public static readonly DependencyProperty AlignBottomWithPanelProperty = DependencyProperty.RegisterAttached(
"AlignBottomWithPanel", typeof(bool), typeof(RelativePanel), new FrameworkPropertyMetadata(ValueBoxes.FalseBox, FrameworkPropertyMetadataOptions.AffectsRender));
public static void SetAlignBottomWithPanel(DependencyObject element, bool value)
=> element.SetValue(AlignBottomWithPanelProperty, ValueBoxes.BooleanBox(value));
public static bool GetAlignBottomWithPanel(DependencyObject element)
=> (bool) element.GetValue(AlignBottomWithPanelProperty);
#endregion
#region Sibling alignment
public static readonly DependencyProperty AlignLeftWithProperty = DependencyProperty.RegisterAttached(
"AlignLeftWith", typeof(UIElement), typeof(RelativePanel), new FrameworkPropertyMetadata(default(UIElement), FrameworkPropertyMetadataOptions.AffectsRender));
public static void SetAlignLeftWith(DependencyObject element, UIElement value)
=> element.SetValue(AlignLeftWithProperty, value);
[TypeConverter(typeof(NameReferenceConverter))]
public static UIElement GetAlignLeftWith(DependencyObject element)
=> (UIElement) element.GetValue(AlignLeftWithProperty);
public static readonly DependencyProperty AlignTopWithProperty = DependencyProperty.RegisterAttached(
"AlignTopWith", typeof(UIElement), typeof(RelativePanel), new FrameworkPropertyMetadata(default(UIElement), FrameworkPropertyMetadataOptions.AffectsRender));
public static void SetAlignTopWith(DependencyObject element, UIElement value)
=> element.SetValue(AlignTopWithProperty, value);
[TypeConverter(typeof(NameReferenceConverter))]
public static UIElement GetAlignTopWith(DependencyObject element)
=> (UIElement) element.GetValue(AlignTopWithProperty);
public static readonly DependencyProperty AlignRightWithProperty = DependencyProperty.RegisterAttached(
"AlignRightWith", typeof(UIElement), typeof(RelativePanel), new FrameworkPropertyMetadata(default(UIElement), FrameworkPropertyMetadataOptions.AffectsRender));
public static void SetAlignRightWith(DependencyObject element, UIElement value)
=> element.SetValue(AlignRightWithProperty, value);
[TypeConverter(typeof(NameReferenceConverter))]
public static UIElement GetAlignRightWith(DependencyObject element)
=> (UIElement) element.GetValue(AlignRightWithProperty);
public static readonly DependencyProperty AlignBottomWithProperty = DependencyProperty.RegisterAttached(
"AlignBottomWith", typeof(UIElement), typeof(RelativePanel), new FrameworkPropertyMetadata(default(UIElement), FrameworkPropertyMetadataOptions.AffectsRender));
public static void SetAlignBottomWith(DependencyObject element, UIElement value)
=> element.SetValue(AlignBottomWithProperty, value);
[TypeConverter(typeof(NameReferenceConverter))]
public static UIElement GetAlignBottomWith(DependencyObject element)
=> (UIElement) element.GetValue(AlignBottomWithProperty);
#endregion
#region Sibling positional
public static readonly DependencyProperty LeftOfProperty = DependencyProperty.RegisterAttached(
"LeftOf", typeof(UIElement), typeof(RelativePanel), new FrameworkPropertyMetadata(default(UIElement), FrameworkPropertyMetadataOptions.AffectsRender));
public static void SetLeftOf(DependencyObject element, UIElement value)
=> element.SetValue(LeftOfProperty, value);
[TypeConverter(typeof(NameReferenceConverter))]
public static UIElement GetLeftOf(DependencyObject element)
=> (UIElement)element.GetValue(LeftOfProperty);
public static readonly DependencyProperty AboveProperty = DependencyProperty.RegisterAttached(
"Above", typeof(UIElement), typeof(RelativePanel), new FrameworkPropertyMetadata(default(UIElement), FrameworkPropertyMetadataOptions.AffectsRender));
public static void SetAbove(DependencyObject element, UIElement value)
=> element.SetValue(AboveProperty, value);
[TypeConverter(typeof(NameReferenceConverter))]
public static UIElement GetAbove(DependencyObject element)
=> (UIElement)element.GetValue(AboveProperty);
public static readonly DependencyProperty RightOfProperty = DependencyProperty.RegisterAttached(
"RightOf", typeof(UIElement), typeof(RelativePanel), new FrameworkPropertyMetadata(default(UIElement), FrameworkPropertyMetadataOptions.AffectsRender));
public static void SetRightOf(DependencyObject element, UIElement value)
=> element.SetValue(RightOfProperty, value);
[TypeConverter(typeof(NameReferenceConverter))]
public static UIElement GetRightOf(DependencyObject element)
=> (UIElement)element.GetValue(RightOfProperty);
public static readonly DependencyProperty BelowProperty = DependencyProperty.RegisterAttached(
"Below", typeof(UIElement), typeof(RelativePanel), new FrameworkPropertyMetadata(default(UIElement), FrameworkPropertyMetadataOptions.AffectsRender));
public static void SetBelow(DependencyObject element, UIElement value)
=> element.SetValue(BelowProperty, value);
[TypeConverter(typeof(NameReferenceConverter))]
public static UIElement GetBelow(DependencyObject element)
=> (UIElement) element.GetValue(BelowProperty);
#endregion
#region Center alignment
public static readonly DependencyProperty AlignHorizontalCenterWithPanelProperty = DependencyProperty.RegisterAttached(
"AlignHorizontalCenterWithPanel", typeof(bool), typeof(RelativePanel), new FrameworkPropertyMetadata(ValueBoxes.FalseBox, FrameworkPropertyMetadataOptions.AffectsRender));
public static void SetAlignHorizontalCenterWithPanel(DependencyObject element, bool value)
=> element.SetValue(AlignHorizontalCenterWithPanelProperty, ValueBoxes.BooleanBox(value));
public static bool GetAlignHorizontalCenterWithPanel(DependencyObject element)
=> (bool)element.GetValue(AlignHorizontalCenterWithPanelProperty);
public static readonly DependencyProperty AlignVerticalCenterWithPanelProperty = DependencyProperty.RegisterAttached(
"AlignVerticalCenterWithPanel", typeof(bool), typeof(RelativePanel), new FrameworkPropertyMetadata(ValueBoxes.FalseBox, FrameworkPropertyMetadataOptions.AffectsRender));
public static void SetAlignVerticalCenterWithPanel(DependencyObject element, bool value)
=> element.SetValue(AlignVerticalCenterWithPanelProperty, ValueBoxes.BooleanBox(value));
public static bool GetAlignVerticalCenterWithPanel(DependencyObject element)
=> (bool)element.GetValue(AlignVerticalCenterWithPanelProperty);
public static readonly DependencyProperty AlignHorizontalCenterWithProperty = DependencyProperty.RegisterAttached(
"AlignHorizontalCenterWith", typeof(UIElement), typeof(RelativePanel), new FrameworkPropertyMetadata(default(UIElement), FrameworkPropertyMetadataOptions.AffectsRender));
public static void SetAlignHorizontalCenterWith(DependencyObject element, UIElement value)
=> element.SetValue(AlignHorizontalCenterWithProperty, value);
[TypeConverter(typeof(NameReferenceConverter))]
public static UIElement GetAlignHorizontalCenterWith(DependencyObject element)
=> (UIElement)element.GetValue(AlignHorizontalCenterWithProperty);
public static readonly DependencyProperty AlignVerticalCenterWithProperty = DependencyProperty.RegisterAttached(
"AlignVerticalCenterWith", typeof(UIElement), typeof(RelativePanel), new FrameworkPropertyMetadata(default(UIElement), FrameworkPropertyMetadataOptions.AffectsRender));
public static void SetAlignVerticalCenterWith(DependencyObject element, UIElement value)
=> element.SetValue(AlignVerticalCenterWithProperty, value);
[TypeConverter(typeof(NameReferenceConverter))]
public static UIElement GetAlignVerticalCenterWith(DependencyObject element)
=> (UIElement)element.GetValue(AlignVerticalCenterWithProperty);
#endregion
protected override Size MeasureOverride(Size availableSize)
{
#region Calc DesiredSize
_childGraph.Clear();
foreach (UIElement child in InternalChildren)
{
if (child == null) continue;
var node = _childGraph.AddNode(child);
node.AlignLeftWithNode = _childGraph.AddLink(node, GetAlignLeftWith(child));
node.AlignTopWithNode = _childGraph.AddLink(node, GetAlignTopWith(child));
node.AlignRightWithNode = _childGraph.AddLink(node, GetAlignRightWith(child));
node.AlignBottomWithNode = _childGraph.AddLink(node, GetAlignBottomWith(child));
node.LeftOfNode = _childGraph.AddLink(node, GetLeftOf(child));
node.AboveNode = _childGraph.AddLink(node, GetAbove(child));
node.RightOfNode = _childGraph.AddLink(node, GetRightOf(child));
node.BelowNode = _childGraph.AddLink(node, GetBelow(child));
node.AlignHorizontalCenterWith = _childGraph.AddLink(node, GetAlignHorizontalCenterWith(child));
node.AlignVerticalCenterWith = _childGraph.AddLink(node, GetAlignVerticalCenterWith(child));
}
_childGraph.Measure(availableSize);
#endregion
#region Calc AvailableSize
_childGraph.Reset();
var boundingSize = _childGraph.GetBoundingSize(Width.IsNaN(), Height.IsNaN());
_childGraph.Reset();
_childGraph.Measure(boundingSize);
return boundingSize;
#endregion
}
protected override Size ArrangeOverride(Size arrangeSize)
{
_childGraph.GetNodes().Do(node => node.Arrange(arrangeSize));
return arrangeSize;
}
private class GraphNode
{
public bool Measured { get; set; }
public UIElement Element { get; }
private bool HorizontalOffsetFlag { get; set; }
private bool VerticalOffsetFlag { get; set; }
private Size BoundingSize { get; set; }
public Size OriginDesiredSize { get; set; }
public double Left { get; set; } = double.NaN;
public double Top { get; set; } = double.NaN;
public double Right { get; set; } = double.NaN;
public double Bottom { get; set; } = double.NaN;
public HashSet<GraphNode> OutgoingNodes { get; }
public GraphNode AlignLeftWithNode { get; set; }
public GraphNode AlignTopWithNode { get; set; }
public GraphNode AlignRightWithNode { get; set; }
public GraphNode AlignBottomWithNode { get; set; }
public GraphNode LeftOfNode { get; set; }
public GraphNode AboveNode { get; set; }
public GraphNode RightOfNode { get; set; }
public GraphNode BelowNode { get; set; }
public GraphNode AlignHorizontalCenterWith { get; set; }
public GraphNode AlignVerticalCenterWith { get; set; }
public GraphNode(UIElement element)
{
OutgoingNodes = new HashSet<GraphNode>();
Element = element;
}
public void Arrange(Size arrangeSize) => Element.Arrange(new Rect(Left, Top, Math.Max(arrangeSize.Width - Left - Right, 0), Math.Max(arrangeSize.Height - Top - Bottom, 0)));
public void Reset()
{
Left = double.NaN;
Top = double.NaN;
Right = double.NaN;
Bottom = double.NaN;
Measured = false;
}
public Size GetBoundingSize()
{
if (Measured) return BoundingSize;
if (!OutgoingNodes.Any())
{
BoundingSize = Element.DesiredSize;
Measured = true;
}
else
{
BoundingSize = GetBoundingSize(this, Element.DesiredSize, OutgoingNodes);
Measured = true;
}
return BoundingSize;
}
private static Size GetBoundingSize(GraphNode prevNode, Size prevSize, IEnumerable<GraphNode> nodes)
{
foreach (var node in nodes)
{
if (node.Measured || !node.OutgoingNodes.Any())
{
if (prevNode.LeftOfNode != null && prevNode.LeftOfNode == node ||
prevNode.RightOfNode != null && prevNode.RightOfNode == node)
{
prevSize.Width += node.BoundingSize.Width;
if (GetAlignHorizontalCenterWithPanel(node.Element) || node.HorizontalOffsetFlag)
{
prevSize.Width += prevNode.OriginDesiredSize.Width;
prevNode.HorizontalOffsetFlag = true;
}
if (node.VerticalOffsetFlag)
{
prevNode.VerticalOffsetFlag = true;
}
}
if (prevNode.AboveNode != null && prevNode.AboveNode == node ||
prevNode.BelowNode != null && prevNode.BelowNode == node)
{
prevSize.Height += node.BoundingSize.Height;
if (GetAlignVerticalCenterWithPanel(node.Element) || node.VerticalOffsetFlag)
{
prevSize.Height += prevNode.OriginDesiredSize.Height;
prevNode.VerticalOffsetFlag = true;
}
if (node.HorizontalOffsetFlag)
{
prevNode.HorizontalOffsetFlag = true;
}
}
}
else
{
return GetBoundingSize(node, prevSize, node.OutgoingNodes);
}
}
return prevSize;
}
}
private class Graph
{
private readonly Dictionary<DependencyObject, GraphNode> _nodeDic;
private Size AvailableSize { get; set; }
public Graph() => _nodeDic = new Dictionary<DependencyObject, GraphNode>();
public IEnumerable<GraphNode> GetNodes() => _nodeDic.Values;
public void Clear()
{
AvailableSize = new Size();
_nodeDic.Clear();
}
public void Reset() => _nodeDic.Values.Do(node => node.Reset());
public GraphNode AddLink(GraphNode from, UIElement to)
{
if (to == null) return null;
GraphNode nodeTo;
if (_nodeDic.ContainsKey(to))
{
nodeTo = _nodeDic[to];
}
else
{
nodeTo = new GraphNode(to);
_nodeDic[to] = nodeTo;
}
from.OutgoingNodes.Add(nodeTo);
return nodeTo;
}
public GraphNode AddNode(UIElement value)
{
if (!_nodeDic.ContainsKey(value))
{
var node = new GraphNode(value);
_nodeDic.Add(value, node);
return node;
}
return _nodeDic[value];
}
public void Measure(Size availableSize)
{
AvailableSize = availableSize;
Measure(_nodeDic.Values, null);
}
private void Measure(IEnumerable<GraphNode> nodes, HashSet<DependencyObject> set)
{
set ??= new HashSet<DependencyObject>();
foreach (var node in nodes)
{
/*
* 该节点无任何依赖,所以从这里开始计算元素位置。
* 因为无任何依赖,所以忽略同级元素
*/
if (!node.Measured && !node.OutgoingNodes.Any())
{
MeasureChild(node);
continue;
}
// 判断依赖元素是否全部排列完毕
if (node.OutgoingNodes.All(item => item.Measured))
{
MeasureChild(node);
continue;
}
// 判断是否有循环
if (!set.Add(node.Element)) throw new Exception("RelativePanel error: Circular dependency detected. Layout could not complete.");
// 没有循环,且有依赖,则继续往下
Measure(node.OutgoingNodes, set);
if (!node.Measured)
{
MeasureChild(node);
}
}
}
private void MeasureChild(GraphNode node)
{
var child = node.Element;
child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
node.OriginDesiredSize = child.DesiredSize;
var alignLeftWithPanel = GetAlignLeftWithPanel(child);
var alignTopWithPanel = GetAlignTopWithPanel(child);
var alignRightWithPanel = GetAlignRightWithPanel(child);
var alignBottomWithPanel = GetAlignBottomWithPanel(child);
#region Panel alignment
if (alignLeftWithPanel) node.Left = 0;
if (alignTopWithPanel) node.Top = 0;
if (alignRightWithPanel) node.Right = 0;
if (alignBottomWithPanel) node.Bottom = 0;
#endregion
#region Sibling alignment
if (node.AlignLeftWithNode != null)
{
node.Left = node.Left.IsNaN() ? node.AlignLeftWithNode.Left : node.AlignLeftWithNode.Left * 0.5;
}
if (node.AlignTopWithNode != null)
{
node.Top = node.Top.IsNaN() ? node.AlignTopWithNode.Top : node.AlignTopWithNode.Top * 0.5;
}
if (node.AlignRightWithNode != null)
{
node.Right = node.Right.IsNaN()
? node.AlignRightWithNode.Right
: node.AlignRightWithNode.Right * 0.5;
}
if (node.AlignBottomWithNode != null)
{
node.Bottom = node.Bottom.IsNaN()
? node.AlignBottomWithNode.Bottom
: node.AlignBottomWithNode.Bottom * 0.5;
}
#endregion
#region Measure
var availableHeight = AvailableSize.Height - node.Top - node.Bottom;
if (availableHeight.IsNaN())
{
availableHeight = AvailableSize.Height;
if (!node.Top.IsNaN() && node.Bottom.IsNaN())
{
availableHeight -= node.Top;
}
else if (node.Top.IsNaN() && !node.Bottom.IsNaN())
{
availableHeight -= node.Bottom;
}
}
var availableWidth = AvailableSize.Width - node.Left - node.Right;
if (availableWidth.IsNaN())
{
availableWidth = AvailableSize.Width;
if (!node.Left.IsNaN() && node.Right.IsNaN())
{
availableWidth -= node.Left;
}
else if (node.Left.IsNaN() && !node.Right.IsNaN())
{
availableWidth -= node.Right;
}
}
child.Measure(new Size(Math.Max(availableWidth, 0), Math.Max(availableHeight, 0)));
var childSize = child.DesiredSize;
#endregion
#region Sibling positional
if (node.LeftOfNode != null && node.Left.IsNaN())
{
node.Left = node.LeftOfNode.Left - childSize.Width;
}
if (node.AboveNode != null && node.Top.IsNaN())
{
node.Top = node.AboveNode.Top - childSize.Height;
}
if (node.RightOfNode != null)
{
if (node.Right.IsNaN())
{
node.Right = node.RightOfNode.Right - childSize.Width;
}
if (node.Left.IsNaN())
{
node.Left = AvailableSize.Width - node.RightOfNode.Right;
}
}
if (node.BelowNode != null)
{
if (node.Bottom.IsNaN())
{
node.Bottom = node.BelowNode.Bottom - childSize.Height;
}
if (node.Top.IsNaN())
{
node.Top = AvailableSize.Height - node.BelowNode.Bottom;
}
}
#endregion
#region Sibling-center alignment
if (node.AlignHorizontalCenterWith != null)
{
var halfWidthLeft = (AvailableSize.Width + node.AlignHorizontalCenterWith.Left - node.AlignHorizontalCenterWith.Right - childSize.Width) * 0.5;
var halfWidthRight = (AvailableSize.Width - node.AlignHorizontalCenterWith.Left + node.AlignHorizontalCenterWith.Right - childSize.Width) * 0.5;
if (node.Left.IsNaN()) node.Left = halfWidthLeft;
else node.Left = (node.Left + halfWidthLeft) * 0.5;
if (node.Right.IsNaN()) node.Right = halfWidthRight;
else node.Right = (node.Right + halfWidthRight) * 0.5;
}
if (node.AlignVerticalCenterWith != null)
{
var halfHeightTop = (AvailableSize.Height + node.AlignVerticalCenterWith.Top - node.AlignVerticalCenterWith.Bottom - childSize.Height) * 0.5;
var halfHeightBottom = (AvailableSize.Height - node.AlignVerticalCenterWith.Top + node.AlignVerticalCenterWith.Bottom - childSize.Height) * 0.5;
if (node.Top.IsNaN()) node.Top = halfHeightTop;
else node.Top = (node.Top + halfHeightTop) * 0.5;
if (node.Bottom.IsNaN()) node.Bottom = halfHeightBottom;
else node.Bottom = (node.Bottom + halfHeightBottom) * 0.5;
}
#endregion
#region Panel-center alignment
if (GetAlignHorizontalCenterWithPanel(child))
{
var halfSubWidth = (AvailableSize.Width - childSize.Width) * 0.5;
if (node.Left.IsNaN()) node.Left = halfSubWidth;
else node.Left = (node.Left + halfSubWidth) * 0.5;
if (node.Right.IsNaN()) node.Right = halfSubWidth;
else node.Right = (node.Right + halfSubWidth) * 0.5;
}
if (GetAlignVerticalCenterWithPanel(child))
{
var halfSubHeight = (AvailableSize.Height - childSize.Height) * 0.5;
if (node.Top.IsNaN()) node.Top = halfSubHeight;
else node.Top = (node.Top + halfSubHeight) * 0.5;
if (node.Bottom.IsNaN()) node.Bottom = halfSubHeight;
else node.Bottom = (node.Bottom + halfSubHeight) * 0.5;
}
#endregion
if (node.Left.IsNaN())
{
if (!node.Right.IsNaN())
node.Left = AvailableSize.Width - node.Right - childSize.Width;
else
{
node.Left = 0;
node.Right = AvailableSize.Width - childSize.Width;
}
}
else if (!node.Left.IsNaN() && node.Right.IsNaN())
{
node.Right = AvailableSize.Width - node.Left - childSize.Width;
}
if (node.Top.IsNaN())
{
if (!node.Bottom.IsNaN())
node.Top = AvailableSize.Height - node.Bottom - childSize.Height;
else
{
node.Top = 0;
node.Bottom = AvailableSize.Height - childSize.Height;
}
}
else if (!node.Top.IsNaN() && node.Bottom.IsNaN())
{
node.Bottom = AvailableSize.Height - node.Top - childSize.Height;
}
node.Measured = true;
}
public Size GetBoundingSize(bool calcWidth, bool calcHeight)
{
var boundingSize = new Size();
foreach (var node in _nodeDic.Values)
{
var size = node.GetBoundingSize();
boundingSize.Width = Math.Max(boundingSize.Width, size.Width);
boundingSize.Height = Math.Max(boundingSize.Height, size.Height);
}
boundingSize.Width = calcWidth ? boundingSize.Width : AvailableSize.Width;
boundingSize.Height = calcHeight ? boundingSize.Height : AvailableSize.Height;
return boundingSize;
}
}
}
}