Custom WrapPanel Control với Hỗ trợ Đầu và Cuối cho Avalonia UI
Vì việc ràng buộc dữ liệu thông thường trong Avalonia khó đạt được hiệu ứng này, nên đã tự mình điều chỉnh phiên bản dựa trên mã nguồn gốc.
I. Hiệu ứng
Ví dụ sử dụng:
<ItemsControl ItemsSource="{Binding Tags}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<fff:WrapPanelEx>
<fff:WrapPanelEx.Head>
<TextBlock Background="LightGreen" Text="xin chào" />
</fff:WrapPanelEx.Head>
<fff:WrapPanelEx.Tail>
<TextBlock Background="LightGreen" Text="thế giới!" />
</fff:WrapPanelEx.Tail>
</fff:WrapPanelEx>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
II. Triển khai mã
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Utilities;
using System;
using static System.Math;
namespace FFFFControls
{
public enum CustomWrapPanelAlignment
{
/// <summary>
/// Các phần tử được sắp xếp sao cho phần tử đầu tiên trong mỗi cột/hàng chạm vào trên/trái của panel.
/// </summary>
Start,
/// <summary>
/// Các phần tử được sắp xếp sao cho mỗi cột/hàng được căn giữa theo chiều dọc/ngang trong panel.
/// </summary>
Center,
/// <summary>
/// Các phần tử được sắp xếp sao cho phần tử cuối cùng trong mỗi cột/hàng chạm vào dưới/phải của panel.
/// </summary>
End,
}
public class CustomWrapPanel : Panel, INavigableContainer
{
#region mã nguồn gốc
/// <summary>
/// Định nghĩa thuộc tính phụ thuộc <see cref="ItemSpacing"/>.
/// </summary>
public static readonly StyledProperty<double> ItemSpacingProperty =
AvaloniaProperty.Register<CustomWrapPanel, double>(nameof(ItemSpacing));
/// <summary>
/// Định nghĩa thuộc tính phụ thuộc <see cref="LineSpacing"/>.
/// </summary>
public static readonly StyledProperty<double> LineSpacingProperty =
AvaloniaProperty.Register<CustomWrapPanel, double>(nameof(LineSpacing));
/// <summary>
/// Định nghĩa thuộc tính <see cref="Orientation"/>.
/// </summary>
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<CustomWrapPanel, Orientation>(nameof(Orientation), defaultValue: Orientation.Horizontal);
/// <summary>
/// Định nghĩa thuộc tính <see cref="ItemsAlignment"/>.
/// </summary>
public static readonly StyledProperty<CustomWrapPanelAlignment> ItemsAlignmentProperty =
AvaloniaProperty.Register<CustomWrapPanel, CustomWrapPanelAlignment>(nameof(ItemsAlignment), defaultValue: CustomWrapPanelAlignment.Start);
/// <summary>
/// Định nghĩa thuộc tính <see cref="ItemWidth"/>.
/// </summary>
public static readonly StyledProperty<double> ItemWidthProperty =
AvaloniaProperty.Register<CustomWrapPanel, double>(nameof(ItemWidth), double.NaN);
/// <summary>
/// Định nghĩa thuộc tính <see cref="ItemHeight"/>.
/// </summary>
public static readonly StyledProperty<double> ItemHeightProperty =
AvaloniaProperty.Register<CustomWrapPanel, double>(nameof(ItemHeight), double.NaN);
/// <summary>
/// Khởi tạo các thành viên tĩnh của lớp <see cref="CustomWrapPanel"/>.
/// </summary>
static CustomWrapPanel()
{
AffectsMeasure<CustomWrapPanel>(ItemSpacingProperty, LineSpacingProperty, OrientationProperty, ItemWidthProperty, ItemHeightProperty, HeadProperty, TailProperty);
AffectsArrange<CustomWrapPanel>(ItemsAlignmentProperty);
HeadProperty.Changed.AddClassHandler<CustomWrapPanel>((x, e) => x.HeadChanged(e));
TailProperty.Changed.AddClassHandler<CustomWrapPanel>((x, e) => x.TailChanged(e));
}
/// <summary>
/// Lấy hoặc đặt khoảng cách giữa các dòng.
/// </summary>
public double ItemSpacing
{
get => GetValue(ItemSpacingProperty);
set => SetValue(ItemSpacingProperty, value);
}
/// <summary>
/// Lấy hoặc đặt khoảng cách giữa các phần tử.
/// </summary>
public double LineSpacing
{
get => GetValue(LineSpacingProperty);
set => SetValue(LineSpacingProperty, value);
}
/// <summary>
/// Lấy hoặc đặt hướng mà các điều khiển con sẽ được sắp xếp.
/// </summary>
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
/// <summary>
/// Lấy hoặc đặt căn chỉnh các phần tử trong CustomWrapPanel.
/// </summary>
public CustomWrapPanelAlignment ItemsAlignment
{
get => GetValue(ItemsAlignmentProperty);
set => SetValue(ItemsAlignmentProperty, value);
}
/// <summary>
/// Lấy hoặc đặt chiều rộng của tất cả các phần tử trong CustomWrapPanel.
/// </summary>
public double ItemWidth
{
get => GetValue(ItemWidthProperty);
set => SetValue(ItemWidthProperty, value);
}
/// <summary>
/// Lấy hoặc đặt chiều cao của tất cả các phần tử trong CustomWrapPanel.
/// </summary>
public double ItemHeight
{
get => GetValue(ItemHeightProperty);
set => SetValue(ItemHeightProperty, value);
}
/// <summary>
/// Lấy điều khiển tiếp theo theo hướng chỉ định.
/// </summary>
/// <param name="direction">Hướng di chuyển.</param>
/// <param name="from">Điều khiển bắt đầu di chuyển.</param>
/// <param name="wrap">Có bao quanh khi đến phần tử đầu tiên hoặc cuối cùng không.</param>
/// <returns>Điều khiển.</returns>
IInputElement? INavigableContainer.GetControl(NavigationDirection direction, IInputElement? from, bool wrap)
{
var panelOrientation = Orientation;
var childElements = GetExtendedChildren();
bool isHorizontal = panelOrientation == Orientation.Horizontal;
int currentIndex = from is not null ? Children.IndexOf((Control)from) : -1;
switch (direction)
{
case NavigationDirection.First:
currentIndex = 0;
break;
case NavigationDirection.Last:
currentIndex = childElements.Count - 1;
break;
case NavigationDirection.Next:
++currentIndex;
break;
case NavigationDirection.Previous:
--currentIndex;
break;
case NavigationDirection.Left:
currentIndex = isHorizontal ? currentIndex - 1 : -1;
break;
case NavigationDirection.Right:
currentIndex = isHorizontal ? currentIndex + 1 : -1;
break;
case NavigationDirection.Up:
currentIndex = isHorizontal ? -1 : currentIndex - 1;
break;
case NavigationDirection.Down:
currentIndex = isHorizontal ? -1 : currentIndex + 1;
break;
}
if (currentIndex >= 0 && currentIndex < childElements.Count)
{
return childElements[currentIndex];
}
else
{
return null;
}
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size constraint)
{
double elementWidth = ItemWidth;
double elementHeight = ItemHeight;
double elementSpacing = ItemSpacing;
double lineSpacing = LineSpacing;
var panelOrientation = Orientation;
var childElements = GetExtendedChildren();
var currentLineSize = new UVSize(panelOrientation);
var totalPanelSize = new UVSize(panelOrientation);
var uvConstraint = new UVSize(panelOrientation, constraint.Width, constraint.Height);
bool widthIsSet = !double.IsNaN(elementWidth);
bool heightIsSet = !double.IsNaN(elementHeight);
bool elementExists = false;
bool lineExists = false;
var childConstraint = new Size(
widthIsSet ? elementWidth : constraint.Width,
heightIsSet ? elementHeight : constraint.Height);
for (int i = 0, count = childElements.Count; i < count; ++i)
{
var child = childElements[i];
// Flow truyền ràng buộc của chính nó cho các phần tử con
child.Measure(childConstraint);
// Đây là kích thước của phần tử con không gian UV
UVSize childSize = new UVSize(panelOrientation,
widthIsSet ? elementWidth : child.DesiredSize.Width,
heightIsSet ? elementHeight : child.DesiredSize.Height);
var nextSpacing = elementExists && child.IsVisible ? elementSpacing : 0;
if (MathUtilities.GreaterThan(currentLineSize.U + childSize.U + nextSpacing, uvConstraint.U)) // Cần chuyển sang dòng khác
{
totalPanelSize.U = Max(currentLineSize.U, totalPanelSize.U);
totalPanelSize.V += currentLineSize.V + (lineExists ? lineSpacing : 0);
currentLineSize = childSize;
elementExists = child.IsVisible;
lineExists = true;
}
else // Tiếp tục tích lũy một dòng
{
currentLineSize.U += childSize.U + nextSpacing;
currentLineSize.V = Max(childSize.V, currentLineSize.V);
elementExists |= child.IsVisible; // giữ true
}
}
// Kích thước dòng cuối cùng, nếu có, nên được thêm vào
totalPanelSize.U = Max(currentLineSize.U, totalPanelSize.U);
totalPanelSize.V += currentLineSize.V + (lineExists ? lineSpacing : 0);
// Đi từ không gian UV sang không gian W/H
return new Size(totalPanelSize.Width, totalPanelSize.Height);
}
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
double elementWidth = ItemWidth;
double elementHeight = ItemHeight;
double elementSpacing = ItemSpacing;
double lineSpacing = LineSpacing;
var panelOrientation = Orientation;
bool isHorizontal = panelOrientation == Orientation.Horizontal;
var childElements = GetExtendedChildren();
int firstInLine = 0;
double accumulatedV = 0;
double itemU = isHorizontal ? elementWidth : elementHeight;
var currentLineSize = new UVSize(panelOrientation);
var uvFinalSize = new UVSize(panelOrientation, finalSize.Width, finalSize.Height);
bool widthIsSet = !double.IsNaN(elementWidth);
bool heightIsSet = !double.IsNaN(elementHeight);
bool elementExists = false;
bool lineExists = false;
for (int i = 0; i < childElements.Count; ++i)
{
var child = childElements[i];
var childSize = new UVSize(panelOrientation,
widthIsSet ? elementWidth : child.DesiredSize.Width,
heightIsSet ? elementHeight : child.DesiredSize.Height);
var nextSpacing = elementExists && child.IsVisible ? elementSpacing : 0;
if (MathUtilities.GreaterThan(currentLineSize.U + childSize.U + nextSpacing, uvFinalSize.U)) // Cần chuyển sang dòng khác
{
accumulatedV += lineExists ? lineSpacing : 0; // thêm khoảng cách để sắp xếp dòng đầu tiên
ArrangeLine(currentLineSize.V, firstInLine, i);
accumulatedV += currentLineSize.V; // thêm chiều cao của dòng vừa sắp xếp
currentLineSize = childSize;
firstInLine = i;
elementExists = child.IsVisible;
lineExists = true;
}
else // Tiếp tục tích lũy một dòng
{
currentLineSize.U += childSize.U + nextSpacing;
currentLineSize.V = Max(childSize.V, currentLineSize.V);
elementExists |= child.IsVisible; // giữ true
}
}
// Sắp xếp dòng cuối cùng, nếu có
if (firstInLine < childElements.Count)
{
accumulatedV += lineExists ? lineSpacing : 0; // thêm khoảng cách để sắp xếp dòng đầu tiên
ArrangeLine(currentLineSize.V, firstInLine, childElements.Count);
}
return finalSize;
void ArrangeLine(double lineV, int start, int end)
{
bool useItemU = isHorizontal ? widthIsSet : heightIsSet;
double u = 0;
if (ItemsAlignment != CustomWrapPanelAlignment.Start)
{
double totalU = -elementSpacing;
for (int i = start; i < end; ++i)
{
totalU += GetChildU(i) + (!childElements[i].IsVisible ? 0 : elementSpacing);
}
u = ItemsAlignment switch
{
CustomWrapPanelAlignment.Center => (uvFinalSize.U - totalU) / 2,
CustomWrapPanelAlignment.End => uvFinalSize.U - totalU,
CustomWrapPanelAlignment.Start => 0,
_ => throw new ArgumentOutOfRangeException(nameof(ItemsAlignment), ItemsAlignment, null),
};
}
for (int i = start; i < end; ++i)
{
double layoutSlotU = GetChildU(i);
childElements[i].Arrange(isHorizontal ? new(u, accumulatedV, layoutSlotU, lineV) : new(accumulatedV, u, lineV, layoutSlotU));
u += layoutSlotU + (!childElements[i].IsVisible ? 0 : elementSpacing);
}
return;
double GetChildU(int i) => useItemU ? itemU :
isHorizontal ? childElements[i].DesiredSize.Width : childElements[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 tùy chỉnh
/// <summary>
/// Định nghĩa thuộc tính kiểu Head
/// </summary>
public static readonly StyledProperty<object> HeadProperty =
AvaloniaProperty.Register<CustomWrapPanel, object>(nameof(Head));
/// <summary>
/// Lấy hoặc đặt thuộc tính Head. Thuộc tính kiểu này
/// chỉ định ....
/// </summary>
public object Head
{
get => this.GetValue(HeadProperty);
set => SetValue(HeadProperty, value);
}
/// <summary>
/// Định nghĩa thuộc tính kiểu Tail
/// </summary>
public static readonly StyledProperty<object> TailProperty =
AvaloniaProperty.Register<CustomWrapPanel, object>(nameof(Tail));
/// <summary>
/// Lấy hoặc đặt thuộc tính Tail. Thuộc tính kiểu này
/// chỉ định ....
/// </summary>
public object Tail
{
get => this.GetValue(TailProperty);
set => SetValue(TailProperty, value);
}
/// <summary>
/// Được gọi khi thuộc tính <see cref="Child"/> thay đổi.
/// </summary>
/// <param name="e">Các đối tượng sự kiện.</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>
/// Được gọi khi thuộc tính <see cref="Child"/> thay đổi.
/// </summary>
/// <param name="e">Các đối tượng sự kiện.</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 GetExtendedChildren()
{
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
}
}