mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-28 06:07:28 +08:00
Feature(RadioButtonGroup): Add new RadioButtonGroup control
This commit is contained in:
@@ -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>
|
||||
|
||||
+20
-2
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user