Files
ShrlAlgoToolkit/Melskin/Layout/StackPanel.cs
2026-02-17 22:17:13 +08:00

299 lines
10 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.Windows.Markup;
namespace Melskin.Layout;
/// <summary>
/// StackPanel 类继承自 Panel用于在用户界面中以垂直或水平方式排列子元素。它支持设置子元素之间的间距、方向以及单个子项的填充方式。
/// </summary>
[ContentProperty(nameof(Children))]
public class StackPanel : Panel
{
/// <summary>
/// 设置某子项附加属性控制其是否填充到当前StackPanel所在的面板控制单个子项的在面板填充方式
/// </summary>
public static readonly DependencyProperty FillProperty = DependencyProperty.RegisterAttached(
"Fill",
typeof(FillType),
typeof(StackPanel),
new FrameworkPropertyMetadata(
FillType.Auto,
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsParentArrange |
FrameworkPropertyMetadataOptions.AffectsParentMeasure));
/// <summary>
/// 定义 StackPanel 的排列方向,可以设置为水平或垂直。此属性影响子元素的布局方式。
/// </summary>
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
nameof(Orientation),
typeof(Orientation),
typeof(StackPanel),
new FrameworkPropertyMetadata(
Orientation.Vertical,
FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));
/// <summary>
/// 定义子元素之间的间距。此属性允许用户指定StackPanel中相邻子元素间的距离。
/// </summary>
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<UIElement>()
.Count(x => x.Visibility != Visibility.Collapsed && GetFill(x) != FillType.Ignored);
var marginMultiplier = Math.Max(visibleChildrenCount - 1, 0);
var totalMarginToAdd = marginBetweenChildren * marginMultiplier;
return totalMarginToAdd;
}
/// <inheritdoc />
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<UIElement>())
{
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;
}
/// <inheritdoc />
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<UIElement>()
.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;
}
/// <summary>
/// 获取指定元素的填充类型。
/// </summary>
/// <param name="element">要获取填充类型的依赖对象。</param>
/// <returns>返回指定元素的填充类型。</returns>
public static FillType GetFill(DependencyObject element)
{
return (FillType)element.GetValue(FillProperty); }
/// <summary>
/// 设置指定元素的填充方式属性。
/// </summary>
/// <param name="element">要设置填充方式的依赖对象。</param>
/// <param name="value">要设置的填充类型值可以是Auto、Fill或Ignored。</param>
public static void SetFill(DependencyObject element, FillType value)
{
element.SetValue(FillProperty, value); }
/// <summary>
/// 堆叠方向
/// </summary>
public Orientation Orientation
{
get => (Orientation)GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
/// <summary>
/// 子项间距
/// </summary>
public double Spacing
{
get => (double)GetValue(SpacingProperty);
set => SetValue(SpacingProperty, value);
}
}
/// <summary>
///
/// </summary>
public enum FillType
{
/// <summary>
/// 实际大小填充
/// </summary>
Auto,
/// <summary>
/// 填充剩余空间相当于Stretch
/// </summary>
Fill,
/// <summary>
/// 忽略当前项
/// </summary>
Ignored
}