using System.Windows.Markup; namespace Melskin.Layout; /// /// StackPanel 类继承自 Panel,用于在用户界面中以垂直或水平方式排列子元素。它支持设置子元素之间的间距、方向以及单个子项的填充方式。 /// [ContentProperty(nameof(Children))] public class StackPanel : Panel { /// /// 设置某子项附加属性,控制其是否填充到当前StackPanel所在的面板(控制单个子项的在面板填充方式) /// public static readonly DependencyProperty FillProperty = DependencyProperty.RegisterAttached( "Fill", typeof(FillType), typeof(StackPanel), new FrameworkPropertyMetadata( FillType.Auto, FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsParentArrange | FrameworkPropertyMetadataOptions.AffectsParentMeasure)); /// /// 定义 StackPanel 的排列方向,可以设置为水平或垂直。此属性影响子元素的布局方式。 /// public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register( nameof(Orientation), typeof(Orientation), typeof(StackPanel), new FrameworkPropertyMetadata( Orientation.Vertical, FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure)); /// /// 定义子元素之间的间距。此属性允许用户指定StackPanel中相邻子元素间的距离。 /// public static readonly DependencyProperty SpacingProperty = DependencyProperty.Register( nameof(Spacing), typeof(double), typeof(StackPanel), new FrameworkPropertyMetadata( 0.0, FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure)); static double CalculateTotalMarginToAdd(UIElementCollection children, double marginBetweenChildren) { //可见的子项数量 var visibleChildrenCount = children .OfType() .Count(x => x.Visibility != Visibility.Collapsed && GetFill(x) != FillType.Ignored); var marginMultiplier = Math.Max(visibleChildrenCount - 1, 0); var totalMarginToAdd = marginBetweenChildren * marginMultiplier; return totalMarginToAdd; } /// protected override Size ArrangeOverride(Size arrangeSize) { var children = InternalChildren; var totalChildrenCount = children.Count; double accumulatedLeft = 0; double accumulatedTop = 0; var isHorizontal = Orientation == Orientation.Horizontal; var spacing = Spacing; var totalMarginToAdd = CalculateTotalMarginToAdd(children, spacing); var allAutoSizedSum = 0.0; var countOfFillTypes = 0; foreach (var child in children.OfType()) { var fillType = GetFill(child); if (fillType != FillType.Auto) { if (child.Visibility != Visibility.Collapsed && fillType != FillType.Ignored) countOfFillTypes += 1; } else { var desiredSize = isHorizontal ? child.DesiredSize.Width : child.DesiredSize.Height; allAutoSizedSum += desiredSize; } } var remainingForFillTypes = isHorizontal ? Math.Max(0, arrangeSize.Width - allAutoSizedSum - totalMarginToAdd) : Math.Max(0, arrangeSize.Height - allAutoSizedSum - totalMarginToAdd); var fillTypeSize = remainingForFillTypes / countOfFillTypes; for (var i = 0; i < totalChildrenCount; ++i) { var child = children[i]; if (child == null) { continue; } var childDesiredSize = child.DesiredSize; var fillType = GetFill(child); var isCollapsed = child.Visibility == Visibility.Collapsed || fillType == FillType.Ignored; var isLastChild = i == totalChildrenCount - 1; var marginToAdd = isLastChild || isCollapsed ? 0 : spacing; var rcChild = new Rect( accumulatedLeft, accumulatedTop, Math.Max(0.0, arrangeSize.Width - accumulatedLeft), Math.Max(0.0, arrangeSize.Height - accumulatedTop)); if (isHorizontal) { rcChild.Width = fillType == FillType.Auto || isCollapsed ? childDesiredSize.Width : fillTypeSize; rcChild.Height = arrangeSize.Height; accumulatedLeft += rcChild.Width + marginToAdd; } else { rcChild.Width = arrangeSize.Width; rcChild.Height = fillType == FillType.Auto || isCollapsed ? childDesiredSize.Height : fillTypeSize; accumulatedTop += rcChild.Height + marginToAdd; } child.Arrange(rcChild); } return arrangeSize; } /// protected override Size MeasureOverride(Size constraint) { var children = InternalChildren; double parentWidth = 0; double parentHeight = 0; double accumulatedWidth = 0; double accumulatedHeight = 0; var isHorizontal = Orientation == Orientation.Horizontal; var totalMarginToAdd = CalculateTotalMarginToAdd(children, Spacing); for (var i = 0; i < children.Count; i++) { var child = children[i]; if (child == null) { continue; } // 先处理Auto的情况,再计算Fill的剩余空间 if (GetFill(child) != FillType.Auto) { continue; } // 子项约束为剩余大小; 即总大小减去前一个子项目消耗的大小。 var childConstraint = new Size( Math.Max(0.0, constraint.Width - accumulatedWidth), Math.Max(0.0, constraint.Height - accumulatedHeight)); // 计算子项 child.Measure(childConstraint); var childDesiredSize = child.DesiredSize; if (isHorizontal) { accumulatedWidth += childDesiredSize.Width; parentHeight = Math.Max(parentHeight, accumulatedHeight + childDesiredSize.Height); } else { parentWidth = Math.Max(parentWidth, accumulatedWidth + childDesiredSize.Width); accumulatedHeight += childDesiredSize.Height; } } // 在计算剩余空间之前,将所有边距添加到累积大小中,以便填充元素。 if (isHorizontal) { accumulatedWidth += totalMarginToAdd; } else { accumulatedHeight += totalMarginToAdd; } //Fill类型的总数量 var totalCountOfFillTypes = children .OfType() .Count(x => GetFill(x) == FillType.Fill && x.Visibility != Visibility.Collapsed); //可用的剩余空间 var availableSpaceRemaining = isHorizontal ? Math.Max(0, constraint.Width - accumulatedWidth) : Math.Max(0, constraint.Height - accumulatedHeight); var eachFillTypeSize = totalCountOfFillTypes > 0 ? availableSpaceRemaining / totalCountOfFillTypes : 0; for (var i = 0; i < children.Count; i++) { var child = children[i]; if (child == null) { continue; } // 处理所有的填充,给它们一部分剩余空间 if (GetFill(child) != FillType.Fill) { continue; } // 子项限制是剩余大小;即总大小减去前一个子项限制所消耗的大小。 var childConstraint = isHorizontal ? new Size(eachFillTypeSize, Math.Max(0.0, constraint.Height - accumulatedHeight)) : new Size(Math.Max(0.0, constraint.Width - accumulatedWidth), eachFillTypeSize); // 测量子项. child.Measure(childConstraint); var childDesiredSize = child.DesiredSize; if (isHorizontal) { accumulatedWidth += childDesiredSize.Width; parentHeight = Math.Max(parentHeight, accumulatedHeight + childDesiredSize.Height); } else { parentWidth = Math.Max(parentWidth, accumulatedWidth + childDesiredSize.Width); accumulatedHeight += childDesiredSize.Height; } } // 确保最终累积的大小反映在 parentSize 中。 parentWidth = Math.Max(parentWidth, accumulatedWidth); parentHeight = Math.Max(parentHeight, accumulatedHeight); var parent = new Size(parentWidth, parentHeight); return parent; } /// /// 获取指定元素的填充类型。 /// /// 要获取填充类型的依赖对象。 /// 返回指定元素的填充类型。 public static FillType GetFill(DependencyObject element) { return (FillType)element.GetValue(FillProperty); } /// /// 设置指定元素的填充方式属性。 /// /// 要设置填充方式的依赖对象。 /// 要设置的填充类型值,可以是Auto、Fill或Ignored。 public static void SetFill(DependencyObject element, FillType value) { element.SetValue(FillProperty, value); } /// /// 堆叠方向 /// public Orientation Orientation { get => (Orientation)GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); } /// /// 子项间距 /// public double Spacing { get => (double)GetValue(SpacingProperty); set => SetValue(SpacingProperty, value); } } /// /// /// public enum FillType { /// /// 实际大小填充 /// Auto, /// /// 填充剩余空间相当于Stretch /// Fill, /// /// 忽略当前项 /// Ignored }