UniformSpacingPanel是HandyControl非常好用的一个容器,具备自动换行、等间距等特性,我一直用它来作为控制栏、属性编辑窗口的容器。
刚看了下HandyControl官网甚至没有文档…UniformSpacingPanel真的是一个非常好用的容器,可以像StackPanel一样设置元素向某一个方向排列,并且通过设置Spacing 设置元素间的间距,同时还支持自动换行,当元素的时候可以通过这个控件实现容器自适应宽高变化。
但是HandyControl中并不支持Avalonia,所以尝试按照其源码改写了一个简单的Avalonia版本,暂时没有添加ItemWidth 、ItemHeight等属性 基本满足个人的使用需求,供大家参考。
下面是代码片段,需要注意的是在WPF中是通过InternalChildren获得UIElement类型的子对象。Avalonia中我采用Control代替,不确定是否合适,目前没有问题,可以根据实际情况自行修改。
using Avalonia.Layout;
namespace Infrastructure.Avalonia;
public class UniformSpacingPanel : Panel
{
public enum Wrapping
{
None,
Wrap
}
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<UniformSpacingPanel, Orientation>(nameof(Orientation), Orientation.Horizontal);
public static readonly StyledProperty<Wrapping> ChildWrappingProperty =
AvaloniaProperty.Register<UniformSpacingPanel, Wrapping>(nameof(ChildWrapping), Wrapping.Wrap);
public static readonly StyledProperty<double> SpacingProperty =
AvaloniaProperty.Register<UniformSpacingPanel, double>(nameof(Spacing), 0);
public static readonly StyledProperty<double> HorizontalSpacingProperty =
AvaloniaProperty.Register<UniformSpacingPanel, double>(nameof(HorizontalSpacing), 0);
public static readonly StyledProperty<double> VerticalSpacingProperty =
AvaloniaProperty.Register<UniformSpacingPanel, double>(nameof(VerticalSpacing), 0);
public static readonly StyledProperty<double> ItemWidthProperty =
AvaloniaProperty.Register<UniformSpacingPanel, double>(nameof(ItemWidth), double.NaN);
public static readonly StyledProperty<double> ItemHeightProperty =
AvaloniaProperty.Register<UniformSpacingPanel, double>(nameof(ItemHeight), double.NaN);
public static readonly StyledProperty<HorizontalAlignment> ItemHorizontalAlignmentProperty =
AvaloniaProperty.Register<UniformSpacingPanel, HorizontalAlignment>(nameof(ItemHorizontalAlignment),
HorizontalAlignment.Stretch);
public static readonly StyledProperty<VerticalAlignment> ItemVerticalAlignmentProperty =
AvaloniaProperty.Register<UniformSpacingPanel, VerticalAlignment>(nameof(ItemVerticalAlignment),
VerticalAlignment.Stretch);
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
public Wrapping ChildWrapping
{
get => GetValue(ChildWrappingProperty);
set => SetValue(ChildWrappingProperty, value);
}
public double Spacing
{
get => GetValue(SpacingProperty);
set => SetValue(SpacingProperty, value);
}
public double HorizontalSpacing
{
get => GetValue(HorizontalSpacingProperty);
set => SetValue(HorizontalSpacingProperty, value);
}
public double VerticalSpacing
{
get => GetValue(VerticalSpacingProperty);
set => SetValue(VerticalSpacingProperty, value);
}
public double ItemWidth
{
get => GetValue(ItemWidthProperty);
set => SetValue(ItemWidthProperty, value);
}
public double ItemHeight
{
get => GetValue(ItemHeightProperty);
set => SetValue(ItemHeightProperty, value);
}
public HorizontalAlignment ItemHorizontalAlignment
{
get => GetValue(ItemHorizontalAlignmentProperty);
set => SetValue(ItemHorizontalAlignmentProperty, value);
}
public VerticalAlignment ItemVerticalAlignment
{
get => GetValue(ItemVerticalAlignmentProperty);
set => SetValue(ItemVerticalAlignmentProperty, value);
}
protected override Size MeasureOverride(Size availableSize)
{
var uvConstraint = new PanelUvSize(Orientation, availableSize);
var panelSize = new PanelUvSize(Orientation);
double spacing = Spacing;
bool firstChild = true;
foreach (Control child in Children)
{
child.Measure(availableSize);
var sz = new PanelUvSize(Orientation, child.DesiredSize);
if (Orientation == Orientation.Horizontal)
{
double nextU = firstChild ? sz.U : sz.U + spacing; // 考虑Spacing
if (panelSize.U + nextU > uvConstraint.U) // 检查是否有足够空间放下当前子元素
{
panelSize.V += panelSize.U > 0 ? sz.V : 0; // 开始新行
panelSize.U = sz.U;
firstChild = true; // 重置首元素标志
}
else
{
panelSize.U += nextU;
panelSize.V = Math.Max(panelSize.V, sz.V);
firstChild = false;
}
}
else
{
double nextV = firstChild ? sz.V : sz.V + spacing; // 考虑Spacing
if (panelSize.V + nextV > uvConstraint.V) // 检查是否有足够空间放下当前子元素
{
panelSize.U += panelSize.V > 0 ? sz.U : 0; // 开始新列
panelSize.V = sz.V;
firstChild = true; // 重置首元素标志
}
else
{
panelSize.V += nextV;
panelSize.U = Math.Max(panelSize.U, sz.U);
firstChild = false;
}
}
}
return new Size(panelSize.Width, panelSize.Height);
}
protected override Size ArrangeOverride(Size finalSize)
{
var origin = new Point(0, 0);
var lineStart = 0;
var uvFinalSize = new PanelUvSize(Orientation, finalSize);
var currentLine = new PanelUvSize(Orientation);
double accumulatedSize = 0; // 用于追踪当前行或列的累计尺寸,包括间隔
for (int i = 0; i < Children.Count; i++)
{
var child = Children[i] as Control;
var sz = new PanelUvSize(Orientation, child.DesiredSize);
double nextSize = (Orientation == Orientation.Horizontal ? sz.U : sz.V);
// 确定是否需要添加间隔
if (i > lineStart) {
nextSize += Spacing; // 如果不是行或列的第一个元素,则添加间隔
}
// 检查是否需要换行或换列
if ((Orientation == Orientation.Horizontal && accumulatedSize + nextSize > uvFinalSize.U) ||
(Orientation == Orientation.Vertical && accumulatedSize + nextSize > uvFinalSize.V))
{
ArrangeLine(lineStart, i, origin.X, origin.Y, currentLine);
origin = Orientation == Orientation.Horizontal ? new Point(0, origin.Y + currentLine.V) : new Point(origin.X + currentLine.U, 0);
currentLine = new PanelUvSize(Orientation); // 重置当前行或列尺寸
lineStart = i; // 更新行或列的开始索引
accumulatedSize = 0; // 重置累计尺寸
i--; // 重新考虑当前元素,因为它需要放到新的行或列
continue;
}
// 更新当前行或列的尺寸
accumulatedSize += nextSize;
if (Orientation == Orientation.Horizontal) {
currentLine.U = accumulatedSize;
currentLine.V = Math.Max(currentLine.V, sz.V);
} else {
currentLine.V = accumulatedSize;
currentLine.U = Math.Max(currentLine.U, sz.U);
}
}
// 处理最后一行或列
ArrangeLine(lineStart, Children.Count, origin.X, origin.Y, currentLine);
return finalSize;
}
private void ArrangeLine(int start, int end, double originX, double originY, PanelUvSize currentLine)
{
double pos = 0;
for (int i = start; i < end; i++)
{
var child = Children[i] as Control;
double childSize = Orientation == Orientation.Horizontal ? child.DesiredSize.Width : child.DesiredSize.Height;
if (i != start) pos += Spacing;
if (Orientation == Orientation.Horizontal) {
child.Arrange(new Rect(originX + pos, originY, child.DesiredSize.Width, currentLine.V));
pos += child.DesiredSize.Width;
} else {
child.Arrange(new Rect(originX, originY + pos, currentLine.U, child.DesiredSize.Height));
pos += child.DesiredSize.Height;
}
}
}
}
using Avalonia.Layout;
namespace Infrastructure.Avalonia;
internal struct PanelUvSize
{
private readonly Orientation _orientation;
public Size ScreenSize => new(U, V);
public double U { get; set; }
public double V { get; set; }
public double Width
{
get => _orientation == Orientation.Horizontal ? U : V;
set
{
if (_orientation == Orientation.Horizontal)
{
U = value;
}
else
{
V = value;
}
}
}
public double Height
{
get => _orientation == Orientation.Horizontal ? V : U;
set
{
if (_orientation == Orientation.Horizontal)
{
V = value;
}
else
{
U = value;
}
}
}
public PanelUvSize(Orientation orientation, double width, double height)
{
U = V = 0d;
_orientation = orientation;
Width = width;
Height = height;
}
public PanelUvSize(Orientation orientation, Size size)
{
U = V = 0d;
_orientation = orientation;
Width = size.Width;
Height = size.Height;
}
public PanelUvSize(Orientation orientation)
{
U = V = 0d;
_orientation = orientation;
}
}
<TabItem Header="UniformSpacingPanel">
<TabItem.Styles>
<Style Selector="Button">
<Setter Property="Width" Value="100"></Setter>
</Style>
<Style Selector="Border">
<Setter Property="BorderBrush" Value="Gray"></Setter>
<Setter Property="BorderThickness" Value="2"></Setter>
</Style>
</TabItem.Styles>
<StackPanel Orientation="Horizontal">
<Border Width="500">
<avalonia:UniformSpacingPanel Spacing="10" ChildWrapping="Wrap" Orientation="Horizontal">
<Button>1</Button>
<Button>2</Button>
<Button>3</Button>
<Button>4</Button>
<Button>5</Button>
<RadioButton>6</RadioButton>
<CheckBox>7</CheckBox>
</avalonia:UniformSpacingPanel>
</Border>
<Border Height="500" Width="300">
<avalonia:UniformSpacingPanel Spacing="10" ChildWrapping="Wrap" Orientation="Horizontal">
<Button>1</Button>
<Button>2</Button>
<Button>3</Button>
<Button>4</Button>
<Button>5</Button>
<RadioButton>6</RadioButton>
<CheckBox>7</CheckBox>
</avalonia:UniformSpacingPanel>
</Border>
<Border Height="150">
<avalonia:UniformSpacingPanel Spacing="10" ChildWrapping="Wrap" Orientation="Vertical">
<Button>1</Button>
<Button>2</Button>
<Button>3</Button>
<Button>4</Button>
<Button>5</Button>
<RadioButton>6</RadioButton>
<CheckBox>7</CheckBox>
</avalonia:UniformSpacingPanel>
</Border>
</StackPanel>
</TabItem>