Feature(RadioButtonGroup): Add new RadioButtonGroup control

This commit is contained in:
chinboy
2026-03-02 11:35:50 +08:00
parent bb401874b2
commit 6ebf296281
13 changed files with 362 additions and 21 deletions
@@ -1,4 +1,5 @@
using AtomUI.Controls;
using AtomUI.Desktop.Controls;
using ReactiveUI;
namespace AtomUIGallery.ShowCases.ViewModels;
@@ -10,6 +11,14 @@ public class RadioButtonViewModel : ReactiveObject, IRoutableViewModel
public IScreen HostScreen { get; }
public string UrlPathSegment { get; } = ID.ToString();
private IList<RadioButtonOption>? _radioOptions;
public IList<RadioButtonOption>? RadioOptions
{
get => _radioOptions;
set => this.RaiseAndSetIfChanged(ref _radioOptions, value);
}
public RadioButtonViewModel(IScreen screen)
{
@@ -6,11 +6,6 @@
xmlns:gallery="https://atomui.net/oss-controls/gallery"
xmlns:showCase="clr-namespace:AtomUIGallery.ShowCases.Views">
<gallery:ShowCasePanel>
<gallery:ShowCasePanel.Styles>
<Style Selector="atom|RadioButton">
<Setter Property="Margin" Value="0, 0, 5, 10" />
</Style>
</gallery:ShowCasePanel.Styles>
<gallery:ShowCaseItem
Title="Basic"
Description="The simplest use.">
@@ -40,7 +35,7 @@
<gallery:ShowCaseItem
Title="Radio Group"
Description="A group of radio components.">
<WrapPanel Orientation="Horizontal" ItemSpacing="10">
<atom:RadioButtonGroup>
<atom:RadioButton>
<StackPanel Spacing="5" Orientation="Vertical">
<antdicons:LineChartOutlined Width="18" Height="18"/>
@@ -65,18 +60,24 @@
<TextBlock>PieChart</TextBlock>
</StackPanel>
</atom:RadioButton>
</WrapPanel>
</atom:RadioButtonGroup>
</gallery:ShowCaseItem>
<gallery:ShowCaseItem
Title="Vertical Radio Group"
Description="Vertical Radio Group.">
<StackPanel Orientation="Vertical" HorizontalAlignment="Left">
<atom:RadioButtonGroup Orientation="Vertical" HorizontalAlignment="Left">
<atom:RadioButton>Option A</atom:RadioButton>
<atom:RadioButton>Option B</atom:RadioButton>
<atom:RadioButton>Option C</atom:RadioButton>
<atom:RadioButton>Option D</atom:RadioButton>
</StackPanel>
</atom:RadioButtonGroup>
</gallery:ShowCaseItem>
<gallery:ShowCaseItem
Title="Radio Group by Items source"
Description="Radio Group.">
<atom:RadioButtonGroup HorizontalAlignment="Left" ItemsSource="{Binding RadioOptions}"/>
</gallery:ShowCaseItem>
<gallery:ShowCaseItem
@@ -98,7 +99,7 @@
<atom:OptionButton>Pear</atom:OptionButton>
<atom:OptionButton>Orange</atom:OptionButton>
</atom:OptionButtonGroup>
<atom:OptionButtonGroup ButtonStyle="Outline">
<atom:OptionButton>Apple</atom:OptionButton>
<atom:OptionButton IsChecked="True">Pear</atom:OptionButton>
@@ -116,19 +117,19 @@
<atom:OptionButton Icon="{antdicons:AntDesignIconProvider Kind=LinuxOutlined}">Linux</atom:OptionButton>
<atom:OptionButton Icon="{antdicons:AntDesignIconProvider Kind=WindowsOutlined}">Windows</atom:OptionButton>
</atom:OptionButtonGroup>
<atom:OptionButtonGroup ButtonStyle="Outline">
<atom:OptionButton Icon="{antdicons:AntDesignIconProvider Kind=AppleOutlined}">macOS</atom:OptionButton>
<atom:OptionButton IsChecked="True" Icon="{antdicons:AntDesignIconProvider Kind=LinuxOutlined}">Linux</atom:OptionButton>
<atom:OptionButton Icon="{antdicons:AntDesignIconProvider Kind=WindowsOutlined}">Windows</atom:OptionButton>
</atom:OptionButtonGroup>
<atom:OptionButtonGroup ButtonStyle="Solid" IsEnabled="False">
<atom:OptionButton IsChecked="True" Icon="{antdicons:AntDesignIconProvider Kind=AppleOutlined}">macOS</atom:OptionButton>
<atom:OptionButton Icon="{antdicons:AntDesignIconProvider Kind=LinuxOutlined}">Linux</atom:OptionButton>
<atom:OptionButton Icon="{antdicons:AntDesignIconProvider Kind=WindowsOutlined}">Windows</atom:OptionButton>
</atom:OptionButtonGroup>
<atom:OptionButtonGroup ButtonStyle="Outline" IsEnabled="False">
<atom:OptionButton Icon="{antdicons:AntDesignIconProvider Kind=AppleOutlined}">macOS</atom:OptionButton>
<atom:OptionButton IsChecked="True" Icon="{antdicons:AntDesignIconProvider Kind=LinuxOutlined}">Linux</atom:OptionButton>
@@ -1,4 +1,5 @@
using AtomUIGallery.ShowCases.ViewModels;
using AtomUI.Desktop.Controls;
using AtomUIGallery.ShowCases.ViewModels;
using ReactiveUI;
using ReactiveUI.Avalonia;
@@ -8,9 +9,26 @@ public partial class RadioButtonShowCase : ReactiveUserControl<RadioButtonViewMo
{
public RadioButtonShowCase()
{
this.WhenActivated(disposables => { });
this.WhenActivated(disposables =>
{
if (DataContext is RadioButtonViewModel viewModel)
{
ConfigureRadioOptions(viewModel);
}
});
InitializeComponent();
}
private void ConfigureRadioOptions(RadioButtonViewModel viewModel)
{
viewModel.RadioOptions = new List<RadioButtonOption>
{
new () { Content = "Option A"},
new () { Content = "Option B", IsChecked = true},
new () { Content = "Option C"},
new () { Content = "Option D", IsEnabled = false},
};
}
public void ToggleDisabledStatus(object arg)
{
@@ -0,0 +1,6 @@
namespace AtomUI.Desktop.Controls;
public class CheckBoxGroup
{
}
@@ -0,0 +1,11 @@
namespace AtomUI.Desktop.Controls;
public interface ICheckBoxOption
{
}
public class CheckBoxOption : ICheckBoxOption
{
}
@@ -0,0 +1,233 @@
using System.Collections.Specialized;
using System.Reactive.Disposables;
using AtomUI.Controls;
using AtomUI.Data;
using AtomUI.Theme;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace AtomUI.Desktop.Controls;
public class RadioButtonGroup : ItemsControl,
IMotionAwareControl,
IControlSharedTokenResourcesHost
{
#region 公共属性定义
public static readonly StyledProperty<object?> CheckedItemProperty =
AvaloniaProperty.Register<RadioButtonGroup, object?>(nameof (CheckedItem));
public static readonly StyledProperty<double> ItemSpacingProperty =
AvaloniaProperty.Register<RadioButtonGroup, double>(nameof (ItemSpacing));
public static readonly StyledProperty<Orientation> OrientationProperty =
StackPanel.OrientationProperty.AddOwner<RadioButtonGroup>();
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<RadioButtonGroup>();
public object? CheckedItem
{
get => GetValue(CheckedItemProperty);
set => SetValue(CheckedItemProperty, value);
}
public double ItemSpacing
{
get => GetValue(ItemSpacingProperty);
set => SetValue(ItemSpacingProperty, value);
}
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
public bool IsMotionEnabled
{
get => GetValue(IsMotionEnabledProperty);
set => SetValue(IsMotionEnabledProperty, value);
}
#endregion
#region 公共事件定义
public event EventHandler<RadioButtonGroupCheckedChangedEventArgs>? CheckedChanged;
#endregion
#region 内部属性定义
Control IControlSharedTokenResourcesHost.HostControl => this;
string IControlSharedTokenResourcesHost.TokenId => RadioButtonToken.ID;
#endregion
private readonly Dictionary<RadioButton, CompositeDisposable> _itemsBindingDisposables = new();
private bool _ignoreSyncChecked;
static RadioButtonGroup()
{
OrientationProperty.OverrideDefaultValue<RadioButtonGroup>(Orientation.Horizontal);
RadioButton.IsCheckedChangedEvent.AddClassHandler<RadioButtonGroup>((group, args) => group.HandleRadioButtonCheckedChanged(args));
CheckedItemProperty.Changed.AddClassHandler<RadioButtonGroup>((group, args) => group.HandleCheckedItemChanged(args));
}
public RadioButtonGroup()
{
this.RegisterResources();
LogicalChildren.CollectionChanged += HandleCollectionChanged;
}
private void HandleCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems.Count > 0)
{
foreach (var item in e.OldItems)
{
if (item is RadioButton radioButton)
{
if (_itemsBindingDisposables.TryGetValue(radioButton, out var disposable))
{
disposable.Dispose();
_itemsBindingDisposables.Remove(radioButton);
}
}
}
}
}
}
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new RadioButton();
}
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<RadioButton>(item, out recycleKey);
}
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
{
base.PrepareContainerForItemOverride(container, item, index);
if (container is RadioButton radioButton)
{
var disposables = new CompositeDisposable(4);
if (item != null && item is not Visual)
{
{
if (ItemTemplate != null)
{
radioButton.SetCurrentValue(RadioButton.ContentProperty, item);
}
else
{
if (item is IRadioButtonOption radioButtonOption)
{
radioButton.SetCurrentValue(RadioButton.ContentProperty, radioButtonOption.Content);
}
}
}
{
if (item is IRadioButtonOption radioButtonOption)
{
radioButton.SetCurrentValue(RadioButton.IsEnabledProperty, radioButtonOption.IsEnabled);
radioButton.SetCurrentValue(RadioButton.IsCheckedProperty, radioButtonOption.IsChecked);
}
}
if (ItemTemplate != null)
{
disposables.Add(BindUtils.RelayBind(this, ItemTemplateProperty, radioButton, RadioButton.ContentTemplateProperty));
}
}
disposables.Add(BindUtils.RelayBind(this, IsMotionEnabledProperty, radioButton, RadioButton.IsMotionEnabledProperty));
PrepareRadioButton(radioButton, item, index, disposables);
if (_itemsBindingDisposables.TryGetValue(radioButton, out var oldDisposables))
{
oldDisposables.Dispose();
_itemsBindingDisposables.Remove(radioButton);
}
_itemsBindingDisposables.Add(radioButton, disposables);
}
else
{
throw new ArgumentOutOfRangeException(nameof(container), "The container type is incorrect, it must be type RadioButton.");
}
}
protected virtual void PrepareRadioButton(RadioButton radioButton, object? item, int index, CompositeDisposable disposables)
{
}
private void HandleRadioButtonCheckedChanged(RoutedEventArgs args)
{
if (args.Source is RadioButton radioButton && radioButton.IsChecked == true)
{
_ignoreSyncChecked = true;
if (ItemsSource != null)
{
SetCurrentValue(CheckedItemProperty, radioButton.DataContext);
}
else
{
SetCurrentValue(CheckedItemProperty, radioButton);
}
}
}
private void SyncCheckedState()
{
var isSourceMode = ItemsSource != null;
foreach (var radioButton in LogicalChildren.OfType<RadioButton>())
{
if (isSourceMode)
{
if (radioButton.DataContext == CheckedItem)
{
radioButton.SetCurrentValue(RadioButton.IsCheckedProperty, true);
}
}
else
{
if (radioButton == CheckedItem)
{
radioButton.SetCurrentValue(RadioButton.IsCheckedProperty, true);
}
}
}
}
private void HandleCheckedItemChanged(AvaloniaPropertyChangedEventArgs args)
{
CheckedChanged?.Invoke(this, new RadioButtonGroupCheckedChangedEventArgs(args.OldValue, args.NewValue));
if (_ignoreSyncChecked)
{
_ignoreSyncChecked = false;
return;
}
SyncCheckedState();
}
protected override void ContainerForItemPreparedOverride(Control container, object? item, int index)
{
base.ContainerForItemPreparedOverride(container, item, index);
if (container is RadioButton radioButton)
{
if (item == CheckedItem)
{
radioButton.SetCurrentValue(RadioButton.IsCheckedProperty, true);
}
}
}
}
@@ -0,0 +1,13 @@
namespace AtomUI.Desktop.Controls;
public class RadioButtonGroupCheckedChangedEventArgs : EventArgs
{
public object? OldCheckedItem { get; }
public object? NewCheckedItem { get; }
public RadioButtonGroupCheckedChangedEventArgs(object? oldCheckedItem, object? newCheckedItem)
{
OldCheckedItem = oldCheckedItem;
NewCheckedItem = newCheckedItem;
}
}
@@ -0,0 +1,15 @@
namespace AtomUI.Desktop.Controls;
public interface IRadioButtonOption
{
bool IsEnabled { get; }
object? Content { get; }
bool IsChecked { get; }
}
public class RadioButtonOption : IRadioButtonOption
{
public bool IsEnabled { get; set; } = true;
public object? Content { get; set; }
public bool IsChecked { get; set; } = false;
}
@@ -59,6 +59,6 @@ internal class RadioButtonToken : AbstractControlDesignToken
// internal
RadioColor = colorWhite;
RadioBgColor = colorPrimary;
TextMargin = new Thickness(SharedToken.UniformlyMarginXXS, 0, 0, 0);
TextMargin = new Thickness(SharedToken.UniformlyMarginXS, 0, SharedToken.UniformlyMarginXS, 0);
}
}
@@ -0,0 +1,37 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:atom="https://atomui.net"
x:ClassModifier="internal">
<ControlTheme x:Key="{x:Type atom:RadioButtonGroup}" TargetType="atom:RadioButtonGroup">
<Setter Property="Template">
<ControlTemplate>
<Border Name="{x:Static atom:OptionButtonGroupThemeConstants.FramePart}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}">
<ItemsPresenter Name="{x:Static atom:OptionButtonGroupThemeConstants.ItemsPresenterPart}"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</Border>
</ControlTemplate>
</Setter>
<Setter Property="ClipToBounds" Value="False" />
<Setter Property="IsMotionEnabled" Value="{DynamicResource {x:Static atom:SharedTokenKey.EnableMotion}}" />
<Setter Property="ItemsPanel">
<ItemsPanelTemplate>
<StackPanel Orientation="{Binding (atom:RadioButtonGroup).Orientation, RelativeSource={RelativeSource TemplatedParent}}"
Spacing="{Binding (atom:RadioButtonGroup).ItemSpacing, RelativeSource={RelativeSource TemplatedParent}}"/>
</ItemsPanelTemplate>
</Setter>
<Setter Property="ItemSpacing" Value="{DynamicResource {x:Static atom:SharedTokenKey.SpacingXS}}" />
<Setter Property="ItemTemplate">
<DataTemplate x:DataType="atom:IRadioButtonOption">
<atom:TextBlock Text="{Binding Content}" />
</DataTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>
@@ -41,7 +41,7 @@
Value="{DynamicResource {x:Static atom:SharedTokenKey.EnableWaveSpirit}}" />
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Margin" Value="{DynamicResource {x:Static atom:CheckBoxTokenKey.TextMargin}}" />
<Setter Property="Margin" Value="{DynamicResource {x:Static atom:RadioButtonTokenKey.TextMargin}}" />
</Style>
<Style Selector="^:disabled">
@@ -4,5 +4,6 @@
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="RadioButtonTheme.axaml"/>
<ResourceInclude Source="RadioIndicatorTheme.axaml"/>
<ResourceInclude Source="RadioButtonGroupTheme.axaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
@@ -225,10 +225,7 @@ public class Segmented : SelectingItemsControl,
if (item != null && item is not Visual)
{
if (!segmentedItem.IsSet(SegmentedItem.ContentProperty))
{
segmentedItem.SetCurrentValue(SegmentedItem.ContentProperty, item);
}
segmentedItem.SetCurrentValue(SegmentedItem.ContentProperty, item);
}
if (ItemTemplate != null)