2025-08-20 12:10:13 +08:00
|
|
|
|
using System.Windows.Markup;
|
2025-07-31 20:12:24 +08:00
|
|
|
|
|
2026-01-02 17:30:41 +08:00
|
|
|
|
namespace Melskin.Layout;
|
2025-07-31 20:12:24 +08:00
|
|
|
|
|
|
|
|
|
|
/// <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));
|
|
|
|
|
|
|
2025-08-20 12:10:13 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 定义 StackPanel 的排列方向,可以设置为水平或垂直。此属性影响子元素的布局方式。
|
|
|
|
|
|
/// </summary>
|
2025-07-31 20:12:24 +08:00
|
|
|
|
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
|
|
|
|
|
|
nameof(Orientation),
|
|
|
|
|
|
typeof(Orientation),
|
|
|
|
|
|
typeof(StackPanel),
|
|
|
|
|
|
new FrameworkPropertyMetadata(
|
|
|
|
|
|
Orientation.Vertical,
|
|
|
|
|
|
FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));
|
|
|
|
|
|
|
2025-08-20 12:10:13 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 定义子元素之间的间距。此属性允许用户指定StackPanel中相邻子元素间的距离。
|
|
|
|
|
|
/// </summary>
|
2025-07-31 20:12:24 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-20 12:10:13 +08:00
|
|
|
|
/// <inheritdoc />
|
2025-07-31 20:12:24 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-20 12:10:13 +08:00
|
|
|
|
/// <inheritdoc />
|
2025-07-31 20:12:24 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-20 12:10:13 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取指定元素的填充类型。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="element">要获取填充类型的依赖对象。</param>
|
|
|
|
|
|
/// <returns>返回指定元素的填充类型。</returns>
|
|
|
|
|
|
public static FillType GetFill(DependencyObject element)
|
|
|
|
|
|
{
|
|
|
|
|
|
return (FillType)element.GetValue(FillProperty); }
|
2025-07-31 20:12:24 +08:00
|
|
|
|
|
2025-08-20 12:10:13 +08:00
|
|
|
|
/// <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); }
|
2025-07-31 20:12:24 +08:00
|
|
|
|
|
|
|
|
|
|
/// <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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-12 23:08:54 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
///
|
|
|
|
|
|
/// </summary>
|
2025-07-31 20:12:24 +08:00
|
|
|
|
public enum FillType
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
2025-08-12 23:08:54 +08:00
|
|
|
|
/// 实际大小填充
|
2025-07-31 20:12:24 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
Auto,
|
|
|
|
|
|
/// <summary>
|
2025-08-12 23:08:54 +08:00
|
|
|
|
/// 填充剩余空间相当于Stretch
|
2025-07-31 20:12:24 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
Fill,
|
|
|
|
|
|
/// <summary>
|
2025-08-12 23:08:54 +08:00
|
|
|
|
/// 忽略当前项
|
2025-07-31 20:12:24 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
Ignored
|
|
|
|
|
|
}
|