Avalonia 支持头和尾显示的 WrapPanel 控件
Avalonia 支持头和尾显示的 WrapPanel 控件
因为普通的 Avalonia 数据绑定很难实现这样的效果,于是就自己在源代码的基础上魔改了一个版本。
一、效果

使用例:
<ItemsControl ItemsSource="{Binding Tags}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<fff:WrapPanelEx>
<fff:WrapPanelEx.Head>
<TextBlock Background="LightGreen" Text="hello" />
</fff:WrapPanelEx.Head>
<fff:WrapPanelEx.Tail>
<TextBlock Background="LightGreen" Text="world!" />
</fff:WrapPanelEx.Tail>
</fff:WrapPanelEx>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
二、代码
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Utilities;
using System;
using static System.Math;
namespace FFFFControls
{
public enum WrapPanelItemsAlignment
{
/// <summary>
/// Items are laid out so the first one in each column/row touches the top/left of the panel.
/// </summary>
Start,
/// <summary>
/// Items are laid out so that each column/row is centred vertically/horizontally within the panel.
/// </summary>
Center,
/// <summary>
/// Items are laid out so the last one in each column/row touches the bottom/right of the panel.
/// </summary>
End,
}
public class WrapPanelEx : Panel, INavigableContainer
{
#region src
/// <summary>
/// Defines the <see cref="ItemSpacing"/> dependency property.
/// </summary>
public static readonly StyledProperty<double> ItemSpacingProperty =
AvaloniaProperty.Register<WrapPanelEx, double>(nameof(ItemSpacing));
/// <summary>
/// Defines the <see cref="LineSpacing"/> dependency property.
/// </summary>
public static readonly StyledProperty<double> LineSpacingProperty =
AvaloniaProperty.Register<WrapPanelEx, double>(nameof(LineSpacing));
/// <summary>
/// Defines the <see cref="Orientation"/> property.
/// </summary>
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<WrapPanelEx, Orientation>(nameof(Orientation), defaultValue: Orientation.Horizontal);
/// <summary>
/// Defines the <see cref="ItemsAlignment"/> property.
/// </summary>
public static readonly StyledProperty<WrapPanelItemsAlignment> ItemsAlignmentProperty =
AvaloniaProperty.Register<WrapPanelEx, WrapPanelItemsAlignment>(nameof(ItemsAlignment), defaultValue: WrapPanelItemsAlignment.Start);
/// <summary>
/// Defines the <see cref="ItemWidth"/> property.
/// </summary>
public static readonly StyledProperty<double> ItemWidthProperty =
AvaloniaProperty.Register<WrapPanelEx, double>(nameof(ItemWidth), double.NaN);
/// <summary>
/// Defines the <see cref="ItemHeight"/> property.
/// </summary>
public static readonly StyledProperty<double> ItemHeightProperty =
AvaloniaProperty.Register<WrapPanelEx, double>(nameof(ItemHeight), double.NaN);
/// <summary>
/// Initializes static members of the <see cref="WrapPanelEx"/> class.
/// </summary>
static WrapPanelEx()
{
AffectsMeasure<WrapPanelEx>(ItemSpacingProperty, LineSpacingProperty, OrientationProperty, ItemWidthProperty, ItemHeightProperty, HeadProperty, TailProperty);
AffectsArrange<WrapPanelEx>(ItemsAlignmentProperty);
HeadProperty.Changed.AddClassHandler<WrapPanelEx>((x, e) => x.HeadChanged(e));
TailProperty.Changed.AddClassHandler<WrapPanelEx>((x, e) => x.TailChanged(e));
}
/// <summary>
/// Gets or sets the spacing between lines.
/// </summary>
public double ItemSpacing
{
get => GetValue(ItemSpacingProperty);
set => SetValue(ItemSpacingProperty, value);
}
/// <summary>
/// Gets or sets the spacing between items.
/// </summary>
public double LineSpacing
{
get => GetValue(LineSpacingProperty);
set => SetValue(LineSpacingProperty, value);
}
/// <summary>
/// Gets or sets the orientation in which child controls will be laid out.
/// </summary>
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
/// <summary>
/// Gets or sets the alignment of items in the WrapPanel.
/// </summary>
public WrapPanelItemsAlignment ItemsAlignment
{
get => GetValue(ItemsAlignmentProperty);
set => SetValue(ItemsAlignmentProperty, value);
}
/// <summary>
/// Gets or sets the width of all items in the WrapPanel.
/// </summary>
public double ItemWidth
{
get => GetValue(ItemWidthProperty);
set => SetValue(ItemWidthProperty, value);
}
/// <summary>
/// Gets or sets the height of all items in the WrapPanel.
/// </summary>
public double ItemHeight
{
get => GetValue(ItemHeightProperty);
set => SetValue(ItemHeightProperty, value);
}
/// <summary>
/// Gets the next control in the specified direction.
/// </summary>
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <param name="wrap">Whether to wrap around when the first or last item is reached.</param>
/// <returns>The control.</returns>
IInputElement? INavigableContainer.GetControl(NavigationDirection direction, IInputElement? from, bool wrap)
{
var orientation = Orientation;
var children = GetExChildren(); //Children;
bool horiz = orientation == Orientation.Horizontal;
int index = from is not null ? Children.IndexOf((Control)from) : -1;
switch (direction)
{
case NavigationDirection.First:
index = 0;
break;
case NavigationDirection.Last:
index = children.Count - 1;
break;
case NavigationDirection.Next:
++index;
break;
case NavigationDirection.Previous:
--index;
break;
case NavigationDirection.Left:
index = horiz ? index - 1 : -1;
break;
case NavigationDirection.Right:
index = horiz ? index + 1 : -1;
break;
case NavigationDirection.Up:
index = horiz ? -1 : index - 1;
break;
case NavigationDirection.Down:
index = horiz ? -1 : index + 1;
break;
}
if (index >= 0 && index < children.Count)
{
return children[index];
}
else
{
return null;
}
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size constraint)
{
double itemWidth = ItemWidth;
double itemHeight = ItemHeight;
double itemSpacing = ItemSpacing;
double lineSpacing = LineSpacing;
var orientation = Orientation;
var children = GetExChildren(); // Children;
var curLineSize = new UVSize(orientation);
var panelSize = new UVSize(orientation);
var uvConstraint = new UVSize(orientation, constraint.Width, constraint.Height);
bool itemWidthSet = !double.IsNaN(itemWidth);
bool itemHeightSet = !double.IsNaN(itemHeight);
bool itemExists = false;
bool lineExists = false;
var childConstraint = new Size(
itemWidthSet ? itemWidth : constraint.Width,
itemHeightSet ? itemHeight : constraint.Height);
for (int i = 0, count = children.Count; i < count; ++i)
{
var child = children[i];
// Flow passes its own constraint to children
child.Measure(childConstraint);
// This is the size of the child in UV space
UVSize childSize = new UVSize(orientation,
itemWidthSet ? itemWidth : child.DesiredSize.Width,
itemHeightSet ? itemHeight : child.DesiredSize.Height);
var nextSpacing = itemExists && child.IsVisible ? itemSpacing : 0;
if (MathUtilities.GreaterThan(curLineSize.U + childSize.U + nextSpacing, uvConstraint.U)) // Need to switch to another line
{
panelSize.U = Max(curLineSize.U, panelSize.U);
panelSize.V += curLineSize.V + (lineExists ? lineSpacing : 0);
curLineSize = childSize;
itemExists = child.IsVisible;
lineExists = true;
}
else // Continue to accumulate a line
{
curLineSize.U += childSize.U + nextSpacing;
curLineSize.V = Max(childSize.V, curLineSize.V);
itemExists |= child.IsVisible; // keep true
}
}
// The last line size, if any should be added
panelSize.U = Max(curLineSize.U, panelSize.U);
panelSize.V += curLineSize.V + (lineExists ? lineSpacing : 0);
// Go from UV space to W/H space
return new Size(panelSize.Width, panelSize.Height);
}
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
double itemWidth = ItemWidth;
double itemHeight = ItemHeight;
double itemSpacing = ItemSpacing;
double lineSpacing = LineSpacing;
var orientation = Orientation;
bool isHorizontal = orientation == Orientation.Horizontal;
var children = GetExChildren();//; Children;
int firstInLine = 0;
double accumulatedV = 0;
double itemU = isHorizontal ? itemWidth : itemHeight;
var curLineSize = new UVSize(orientation);
var uvFinalSize = new UVSize(orientation, finalSize.Width, finalSize.Height);
bool itemWidthSet = !double.IsNaN(itemWidth);
bool itemHeightSet = !double.IsNaN(itemHeight);
bool itemExists = false;
bool lineExists = false;
for (int i = 0; i < children.Count; ++i)
{
var child = children[i];
var childSize = new UVSize(orientation,
itemWidthSet ? itemWidth : child.DesiredSize.Width,
itemHeightSet ? itemHeight : child.DesiredSize.Height);
var nextSpacing = itemExists && child.IsVisible ? itemSpacing : 0;
if (MathUtilities.GreaterThan(curLineSize.U + childSize.U + nextSpacing, uvFinalSize.U)) // Need to switch to another line
{
accumulatedV += lineExists ? lineSpacing : 0; // add spacing to arrange line first
ArrangeLine(curLineSize.V, firstInLine, i);
accumulatedV += curLineSize.V; // add the height of the line just arranged
curLineSize = childSize;
firstInLine = i;
itemExists = child.IsVisible;
lineExists = true;
}
else // Continue to accumulate a line
{
curLineSize.U += childSize.U + nextSpacing;
curLineSize.V = Max(childSize.V, curLineSize.V);
itemExists |= child.IsVisible; // keep true
}
}
// Arrange the last line, if any
if (firstInLine < children.Count)
{
accumulatedV += lineExists ? lineSpacing : 0; // add spacing to arrange line first
ArrangeLine(curLineSize.V, firstInLine, children.Count);
}
return finalSize;
void ArrangeLine(double lineV, int start, int end)
{
bool useItemU = isHorizontal ? itemWidthSet : itemHeightSet;
double u = 0;
if (ItemsAlignment != WrapPanelItemsAlignment.Start)
{
double totalU = -itemSpacing;
for (int i = start; i < end; ++i)
{
totalU += GetChildU(i) + (!children[i].IsVisible ? 0 : itemSpacing);
}
u = ItemsAlignment switch
{
WrapPanelItemsAlignment.Center => (uvFinalSize.U - totalU) / 2,
WrapPanelItemsAlignment.End => uvFinalSize.U - totalU,
WrapPanelItemsAlignment.Start => 0,
_ => throw new ArgumentOutOfRangeException(nameof(ItemsAlignment), ItemsAlignment, null),
};
}
for (int i = start; i < end; ++i)
{
double layoutSlotU = GetChildU(i);
children[i].Arrange(isHorizontal ? new(u, accumulatedV, layoutSlotU, lineV) : new(accumulatedV, u, lineV, layoutSlotU));
u += layoutSlotU + (!children[i].IsVisible ? 0 : itemSpacing);
}
return;
double GetChildU(int i) => useItemU ? itemU :
isHorizontal ? children[i].DesiredSize.Width : children[i].DesiredSize.Height;
}
}
private struct UVSize
{
internal UVSize(Orientation orientation, double width, double height)
{
U = V = 0d;
_orientation = orientation;
Width = width;
Height = height;
}
internal UVSize(Orientation orientation)
{
U = V = 0d;
_orientation = orientation;
}
internal double U;
internal double V;
private Orientation _orientation;
internal double Width
{
get => _orientation == Orientation.Horizontal ? U : V;
set { if (_orientation == Orientation.Horizontal) U = value; else V = value; }
}
internal double Height
{
get => _orientation == Orientation.Horizontal ? V : U;
set { if (_orientation == Orientation.Horizontal) V = value; else U = value; }
}
}
#endregion
#region customs
/// <summary>
/// Head StyledProperty definition
/// </summary>
public static readonly StyledProperty<object> HeadProperty =
AvaloniaProperty.Register<WrapPanelEx, object>(nameof(Head));
/// <summary>
/// Gets or sets the Head property. This StyledProperty
/// indicates ....
/// </summary>
public object Head
{
get => this.GetValue(HeadProperty);
set => SetValue(HeadProperty, value);
}
/// <summary>
/// Tail StyledProperty definition
/// </summary>
public static readonly StyledProperty<object> TailProperty =
AvaloniaProperty.Register<WrapPanelEx, object>(nameof(Tail));
/// <summary>
/// Gets or sets the Tail property. This StyledProperty
/// indicates ....
/// </summary>
public object Tail
{
get => this.GetValue(TailProperty);
set => SetValue(TailProperty, value);
}
/// <summary>
/// Called when the <see cref="Child"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private void HeadChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldChild = (Control?)e.OldValue;
var newChild = (Control?)e.NewValue;
if (oldChild != null)
{
((ISetLogicalParent)oldChild).SetParent(null);
LogicalChildren.Clear();
VisualChildren.Remove(oldChild);
}
if (newChild != null)
{
((ISetLogicalParent)newChild).SetParent(this);
VisualChildren.Add(newChild);
LogicalChildren.Add(newChild);
}
}
/// <summary>
/// Called when the <see cref="Child"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private void TailChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldChild = (Control?)e.OldValue;
var newChild = (Control?)e.NewValue;
if (oldChild != null)
{
((ISetLogicalParent)oldChild).SetParent(null);
LogicalChildren.Clear();
VisualChildren.Remove(oldChild);
}
if (newChild != null)
{
((ISetLogicalParent)newChild).SetParent(this);
VisualChildren.Add(newChild);
LogicalChildren.Add(newChild);
}
}
private Controls GetExChildren()
{
var children = Children;
var controls = new Controls();
if (Head is Control headControl)
controls.Add(headControl);
controls.AddRange(children);
if (Tail is Control tailControl)
controls.Add(tailControl);
return controls;
}
#endregion
}
}

浙公网安备 33010602011771号