feat: Add ElementGroup control, styles and demo.

This commit is contained in:
NaBian committed 2026-06-13 18:42:59 +08:00
1 parent 700abba273
commit 2ac7dfaf96
6 files changed
+289

No files matched your search

@@ -0,0 +1,45 @@
<UserControl x:Class="HandyControlDemo.UserControl.ElementGroupDemo"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:langs="clr-namespace:HandyControlDemo.Properties.Langs"
Background="{DynamicResource RegionBrush}">
<TransitioningContentControl>
<hc:UniformSpacingPanel Margin="32" VerticalAlignment="Center" Orientation="Vertical" Spacing="16">
<hc:ElementGroup Orientation="Horizontal" Layout="Stack">
<Button HorizontalAlignment="Stretch" Width="NaN" Content="{x:Static langs:Lang.Button}"/>
<Button HorizontalAlignment="Stretch" Width="NaN" Content="{x:Static langs:Lang.Button}"/>
<Button HorizontalAlignment="Stretch" Width="NaN" Content="{x:Static langs:Lang.Button}"/>
<Button HorizontalAlignment="Stretch" Width="NaN" Content="{x:Static langs:Lang.Button}"/>
</hc:ElementGroup>
<hc:ElementGroup Orientation="Horizontal" MaxWidth="500">
<ToggleButton HorizontalAlignment="Stretch" Width="NaN" Content="{x:Static langs:Lang.ToggleButton}"/>
<Button HorizontalAlignment="Stretch" Width="NaN" Content="{x:Static langs:Lang.Button}"/>
<ToggleButton HorizontalAlignment="Stretch" Width="NaN" Content="{x:Static langs:Lang.ToggleButton}"/>
<Button HorizontalAlignment="Stretch" Width="NaN" Content="{x:Static langs:Lang.Button}"/>
</hc:ElementGroup>
<hc:ElementGroup Orientation="Horizontal" Layout="Stack">
<Border Theme="{StaticResource BorderRegion}" Padding="6,0">
<Path Data="{StaticResource ClockGeometry}" Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource BorderBrush}"/>
</Border>
<TextBox MinWidth="200"/>
<Button Content="{x:Static langs:Lang.Button}"/>
<ToggleButton Content="{x:Static langs:Lang.ToggleButton}"/>
</hc:ElementGroup>
<hc:ElementGroup>
<hc:ElementGroup.ItemsPanel>
<ItemsPanelTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
</Grid>
</ItemsPanelTemplate>
</hc:ElementGroup.ItemsPanel>
<TextBox HorizontalAlignment="Stretch"/>
<Button Grid.Column="1" Width="100" Content="{x:Static langs:Lang.Button}" HorizontalAlignment="Right" Theme="{StaticResource ButtonPrimary}"/>
</hc:ElementGroup>
</hc:UniformSpacingPanel>
</TransitioningContentControl>
</UserControl>
@@ -0,0 +1,9 @@
namespace HandyControlDemo.UserControl;
public partial class ElementGroupDemo : Avalonia.Controls.UserControl
{
public ElementGroupDemo()
{
InitializeComponent();
}
}
@@ -0,0 +1,170 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Layout;
using HandyControl.Data;
using HandyControl.Tools;
namespace HandyControl.Controls;
public class ElementGroup : ItemsControl
{
private static readonly Dictionary<Orientation, Dictionary<ChildLocation, CornerRadius>> CornerRadiusDict;
public static readonly StyledProperty<Orientation> OrientationProperty =
StackPanel.OrientationProperty.AddOwner<ElementGroup>(
new StyledPropertyMetadata<Orientation>(defaultValue: Orientation.Horizontal));
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
public static readonly StyledProperty<LinearLayout> LayoutProperty =
AvaloniaProperty.Register<ElementGroup, LinearLayout>(nameof(Layout), defaultValue: LinearLayout.Uniform);
public LinearLayout Layout
{
get => GetValue(LayoutProperty);
set => SetValue(LayoutProperty, value);
}
static ElementGroup()
{
AffectsMeasure<ElementGroup>(OrientationProperty, LayoutProperty);
var defaultCornerRadius = ResourceHelper.GetResource<CornerRadius>("DefaultCornerRadius");
CornerRadiusDict = new Dictionary<Orientation, Dictionary<ChildLocation, CornerRadius>>
{
[Orientation.Horizontal] = new()
{
[ChildLocation.Single] = defaultCornerRadius,
[ChildLocation.First] = new CornerRadius(defaultCornerRadius.TopLeft, 0, 0, defaultCornerRadius.BottomLeft),
[ChildLocation.Middle] = new CornerRadius(),
[ChildLocation.Last] = new CornerRadius(0, defaultCornerRadius.TopRight, defaultCornerRadius.BottomRight, 0)
},
[Orientation.Vertical] = new()
{
[ChildLocation.Single] = defaultCornerRadius,
[ChildLocation.First] = new CornerRadius(defaultCornerRadius.TopLeft, defaultCornerRadius.TopRight, 0, 0),
[ChildLocation.Middle] = new CornerRadius(),
[ChildLocation.Last] = new CornerRadius(0, 0, defaultCornerRadius.BottomRight, defaultCornerRadius.BottomLeft)
}
};
}
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
{
base.PrepareContainerForItemOverride(container, item, index);
container.GotFocus += ElementAdded_GotFocus;
container.LostFocus += ElementAdded_LostFocus;
container.PropertyChanged += Container_PropertyChanged;
UpdateChildrenCornerRadius();
}
protected override void ClearContainerForItemOverride(Control container)
{
base.ClearContainerForItemOverride(container);
container.GotFocus -= ElementAdded_GotFocus;
container.LostFocus -= ElementAdded_LostFocus;
container.PropertyChanged -= Container_PropertyChanged;
UpdateChildrenCornerRadius();
}
private void Container_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == IsVisibleProperty)
{
UpdateChildrenCornerRadius();
}
}
private void UpdateChildrenCornerRadius()
{
var visibleChildren = GetVisibleChildren();
var count = visibleChildren.Count;
if (count == 0)
{
return;
}
var orientation = Orientation;
if (count == 1)
{
UpdateChildCornerRadius(visibleChildren[0], CornerRadiusDict[orientation][ChildLocation.Single]);
}
else
{
UpdateChildCornerRadius(visibleChildren[0], CornerRadiusDict[orientation][ChildLocation.First]);
var childMargin = orientation == Orientation.Horizontal
? new Thickness(-1, 0, 0, 0)
: new Thickness(0, -1, 0, 0);
for (var childIndex = 1; childIndex < count; childIndex++)
{
var child = visibleChildren[childIndex];
child.SetCurrentValue(MarginProperty, childMargin);
UpdateChildCornerRadius(child, CornerRadiusDict[orientation][ChildLocation.Middle]);
}
var lastChild = visibleChildren[count - 1];
lastChild.SetCurrentValue(MarginProperty, childMargin);
UpdateChildCornerRadius(lastChild, CornerRadiusDict[orientation][ChildLocation.Last]);
}
}
private List<Control> GetVisibleChildren()
{
return Items.OfType<Control>().Where(element => element.IsVisible).ToList();
}
private static void UpdateChildCornerRadius(AvaloniaObject child, CornerRadius cornerRadius)
{
if (child is Border border)
{
border.SetCurrentValue(Border.CornerRadiusProperty, cornerRadius);
}
else
{
BorderElement.SetCornerRadius(child, cornerRadius);
}
}
private void ElementAdded_LostFocus(object? sender, RoutedEventArgs e) => ResetElementZIndex(e.Source);
private void ElementAdded_GotFocus(object? sender, RoutedEventArgs e) => MaximizeElementZIndex(e.Source);
private static void ResetElementZIndex(object? element)
{
if (element is Control control)
{
control.ZIndex = 0;
}
}
private static void MaximizeElementZIndex(object? element)
{
if (element is Control control)
{
control.ZIndex = int.MaxValue;
}
}
private enum ChildLocation
{
Single,
First,
Middle,
Last
}
}
@@ -0,0 +1,8 @@
namespace HandyControl.Data;
public enum LinearLayout
{
Stack,
Uniform
}
@@ -0,0 +1,56 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal"
xmlns:hc="clr-namespace:HandyControl.Controls">
<ItemsPanelTemplate x:Key="UniformHorizontalItemsPanelTemplate">
<UniformGrid Rows="1" />
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="UniformVerticalItemsPanelTemplate">
<UniformGrid Columns="1" />
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="StackHorizontalItemsPanelTemplate">
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="StackVerticalItemsPanelTemplate">
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
<ControlTheme x:Key="ElementGroupBaseStyle"
TargetType="hc:ElementGroup">
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="Layout" Value="Uniform" />
<Setter Property="Focusable" Value="False" />
<Setter Property="ItemsPanel" Value="{StaticResource UniformHorizontalItemsPanelTemplate}" />
<Setter Property="Template">
<ControlTemplate TargetType="hc:ElementGroup">
<Border Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<ItemsPresenter Name="PART_ItemsPresenter"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^[Layout=Uniform][Orientation=Vertical]">
<Setter Property="ItemsPanel" Value="{StaticResource UniformVerticalItemsPanelTemplate}" />
</Style>
<Style Selector="^[Layout=Stack][Orientation=Horizontal]">
<Setter Property="ItemsPanel" Value="{StaticResource StackHorizontalItemsPanelTemplate}" />
</Style>
<Style Selector="^[Layout=Stack][Orientation=Vertical]">
<Setter Property="ItemsPanel" Value="{StaticResource StackVerticalItemsPanelTemplate}" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type hc:ElementGroup}"
BasedOn="{StaticResource ElementGroupBaseStyle}"
TargetType="hc:ElementGroup" />
</ResourceDictionary>
@@ -58,6 +58,7 @@
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/SelectableTextBlock.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/HeaderedContentControl.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Card.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/ElementGroup.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Styles.Resources>