From 2ac7dfaf96ac887e9e860bdfeb261c1953e30602 Mon Sep 17 00:00:00 2001
From: NaBian <836904362@qq.com>
Date: Sat, 13 Jun 2026 18:42:59 +0800
Subject: [PATCH] feat: Add ElementGroup control, styles and demo.
---
.../Controls/ElementGroupDemo.axaml | 45 +++++
.../Controls/ElementGroupDemo.axaml.cs | 9 +
.../Controls/Panel/ElementGroup.cs | 170 ++++++++++++++++++
.../Data/Enum/LinearLayout.cs | 8 +
.../Themes/Styles/ElementGroup.axaml | 56 ++++++
.../Themes/Styles/MergedStyles.axaml | 1 +
6 files changed, 289 insertions(+)
create mode 100644 src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ElementGroupDemo.axaml
create mode 100644 src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ElementGroupDemo.axaml.cs
create mode 100644 src/Avalonia/HandyControl_Avalonia/Controls/Panel/ElementGroup.cs
create mode 100644 src/Avalonia/HandyControl_Avalonia/Data/Enum/LinearLayout.cs
create mode 100644 src/Avalonia/HandyControl_Avalonia/Themes/Styles/ElementGroup.axaml
diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ElementGroupDemo.axaml b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ElementGroupDemo.axaml
new file mode 100644
index 00000000..ce175252
--- /dev/null
+++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ElementGroupDemo.axaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ElementGroupDemo.axaml.cs b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ElementGroupDemo.axaml.cs
new file mode 100644
index 00000000..1212b6ba
--- /dev/null
+++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ElementGroupDemo.axaml.cs
@@ -0,0 +1,9 @@
+namespace HandyControlDemo.UserControl;
+
+public partial class ElementGroupDemo : Avalonia.Controls.UserControl
+{
+ public ElementGroupDemo()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/Avalonia/HandyControl_Avalonia/Controls/Panel/ElementGroup.cs b/src/Avalonia/HandyControl_Avalonia/Controls/Panel/ElementGroup.cs
new file mode 100644
index 00000000..59492b97
--- /dev/null
+++ b/src/Avalonia/HandyControl_Avalonia/Controls/Panel/ElementGroup.cs
@@ -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> CornerRadiusDict;
+
+ public static readonly StyledProperty OrientationProperty =
+ StackPanel.OrientationProperty.AddOwner(
+ new StyledPropertyMetadata(defaultValue: Orientation.Horizontal));
+
+ public Orientation Orientation
+ {
+ get => GetValue(OrientationProperty);
+ set => SetValue(OrientationProperty, value);
+ }
+
+ public static readonly StyledProperty LayoutProperty =
+ AvaloniaProperty.Register(nameof(Layout), defaultValue: LinearLayout.Uniform);
+
+ public LinearLayout Layout
+ {
+ get => GetValue(LayoutProperty);
+ set => SetValue(LayoutProperty, value);
+ }
+
+ static ElementGroup()
+ {
+ AffectsMeasure(OrientationProperty, LayoutProperty);
+
+ var defaultCornerRadius = ResourceHelper.GetResource("DefaultCornerRadius");
+
+ CornerRadiusDict = new Dictionary>
+ {
+ [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 GetVisibleChildren()
+ {
+ return Items.OfType().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
+ }
+}
diff --git a/src/Avalonia/HandyControl_Avalonia/Data/Enum/LinearLayout.cs b/src/Avalonia/HandyControl_Avalonia/Data/Enum/LinearLayout.cs
new file mode 100644
index 00000000..0a45afe6
--- /dev/null
+++ b/src/Avalonia/HandyControl_Avalonia/Data/Enum/LinearLayout.cs
@@ -0,0 +1,8 @@
+namespace HandyControl.Data;
+
+public enum LinearLayout
+{
+ Stack,
+
+ Uniform
+}
diff --git a/src/Avalonia/HandyControl_Avalonia/Themes/Styles/ElementGroup.axaml b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/ElementGroup.axaml
new file mode 100644
index 00000000..79a946cf
--- /dev/null
+++ b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/ElementGroup.axaml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia/HandyControl_Avalonia/Themes/Styles/MergedStyles.axaml b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/MergedStyles.axaml
index 1eec387e..2e475daa 100644
--- a/src/Avalonia/HandyControl_Avalonia/Themes/Styles/MergedStyles.axaml
+++ b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/MergedStyles.axaml
@@ -58,6 +58,7 @@
+