//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(false); var calcWidth = Width.IsNaN() && HorizontalAlignment != HorizontalAlignment.Stretch; var calcHeight = Height.IsNaN() && VerticalAlignment != VerticalAlignment.Stretch; var boundingSize = _childGraph.GetBoundingSize(calcWidth, calcHeight); _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 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(); 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(bool clearPos) { if (clearPos) { Left = double.NaN; Top = double.NaN; Right = double.NaN; Bottom = double.NaN; } Measured = false; } public Size GetBoundingSize() { if (Left < 0 || Top < 0) return default; 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 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 _nodeDic; private Size AvailableSize { get; set; } public Graph() => _nodeDic = new Dictionary(); public IEnumerable GetNodes() => _nodeDic.Values; public void Clear() { AvailableSize = new Size(); _nodeDic.Clear(); } public void Reset(bool clearPos = true) => _nodeDic.Values.Do(node => node.Reset(clearPos)); 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 = EnsureValidSize(availableSize); Measure(_nodeDic.Values, null); } private static Size EnsureValidSize(Size size) { var width = double.IsInfinity(size.Width) ? 0 : size.Width; var height = double.IsInfinity(size.Height) ? 0 : size.Height; return new Size(width, height); } private void Measure(IEnumerable nodes, HashSet set) { set ??= new HashSet(); 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; } } }