feat: Add Dialog styles and demo.

This commit is contained in:
NaBian committed 2026-06-27 17:36:21 +08:00
1 parent cb7e6fbb98
commit 2c0875ebd6
20 files changed
+695 -2

No files matched your search

@@ -42,6 +42,7 @@
"group": true,
"demoItemList": [
[ "ConfettiCannon", "ConfettiCannonDemo", "Brush.Favorite", "", "Interacting" ],
[ "Dialog", "DialogDemo", "Brush.Dialog", "", "Feedback" ],
[ "Gravatar", "GravatarDemo", "Brush.User", "", "DataDisplay" ],
[ "ButtonGroup", "ButtonGroupDemo", "Brush.ButtonGroup", "", "Button" ],
[ "ProgressButton", "ProgressButtonDemo", "Brush.ProgressBarStatusBar8", "", "Button" ],
@@ -1,13 +1,20 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using HandyControl.Controls;
using HandyControlDemo.Data;
using HandyControlDemo.UserControl;
using HandyControlDemo.ViewModel;
namespace HandyControlDemo;
public partial class MainWindow : Window
public partial class MainWindow : Avalonia.Controls.Window
{
public MainWindow() => InitializeComponent();
public MainWindow()
{
InitializeComponent();
Dialog.SetToken(this, MessageToken.MainWindow);
}
protected override void OnLoaded(RoutedEventArgs e)
{
@@ -0,0 +1,22 @@
<Border x:Class="HandyControlDemo.UserControl.InteractiveDialog"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
CornerRadius="10"
Width="400"
Height="247"
Background="{DynamicResource RegionBrush}">
<StackPanel VerticalAlignment="Center"
Margin="32"
Spacing="32">
<TextBlock Theme="{StaticResource TextBlockLargeBold}"
Text="{Binding Message}" />
<TextBox Text="{Binding Result, Mode=TwoWay}"
Theme="{StaticResource TextBoxExtend}" />
<Button Width="120"
Command="{Binding CloseCommand}"
Theme="{StaticResource ButtonPrimary}"
HorizontalAlignment="Center"
Content="OK" />
</StackPanel>
</Border>
@@ -0,0 +1,14 @@
using Avalonia.Controls;
using HandyControlDemo.ViewModel;
namespace HandyControlDemo.UserControl;
public partial class InteractiveDialog : Border
{
public InteractiveDialog()
{
InitializeComponent();
DataContext = new InteractiveDialogViewModel();
}
}
@@ -0,0 +1,25 @@
<Border x:Class="HandyControlDemo.UserControl.TextDialog"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
CornerRadius="10"
Width="400"
Height="247"
Background="{DynamicResource RegionBrush}">
<Panel>
<TextBlock Theme="{StaticResource TextBlockLargeBold}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="Please wait..." />
<Button x:Name="CloseButton"
Width="22"
Height="22"
Theme="{StaticResource ButtonIcon}"
Foreground="{DynamicResource PrimaryBrush}"
hc:IconElement.Geometry="{StaticResource ErrorGeometry}"
Padding="0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="0,4,4,0" />
</Panel>
</Border>
@@ -0,0 +1,15 @@
using Avalonia.Controls;
using Avalonia.VisualTree;
using HandyControl.Controls;
namespace HandyControlDemo.UserControl;
public partial class TextDialog : Border
{
public TextDialog()
{
InitializeComponent();
CloseButton.Click += (_, _) => this.FindAncestorOfType<Dialog>()?.Close();
}
}
@@ -0,0 +1,35 @@
<Border x:Class="HandyControlDemo.UserControl.TextDialogWithTimer"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
CornerRadius="10"
Width="400"
Height="247"
Background="{DynamicResource RegionBrush}">
<Panel>
<Viewbox Width="80"
Height="80"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock TextWrapping="Wrap"
Theme="{StaticResource TextBlockLargeBold}"
Text="Please wait..." />
</Viewbox>
<Button x:Name="CloseButton"
Width="22"
Height="22"
Foreground="{DynamicResource PrimaryBrush}"
hc:IconElement.Geometry="{StaticResource ErrorGeometry}"
Padding="0"
Theme="{StaticResource ButtonIcon}"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="0,4,4,0" />
<hc:CircleProgressBar x:Name="ProgressBarTimer"
ShowText="False"
Width="120"
Height="120"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Panel>
</Border>
@@ -0,0 +1,44 @@
using System;
using Avalonia.Controls;
using Avalonia.Threading;
using Avalonia.VisualTree;
using HandyControl.Controls;
using HandyControlDemo.ViewModel;
namespace HandyControlDemo.UserControl;
public partial class TextDialogWithTimer : Border
{
public TextDialogWithTimer()
{
InitializeComponent();
DataContext = new InteractiveDialogViewModel();
var startTime = DateTime.Now;
var timer = new DispatcherTimer(DispatcherPriority.ApplicationIdle)
{
Interval = TimeSpan.FromMilliseconds(50)
};
timer.Tick += (_, _) =>
{
var elapsed = (DateTime.Now - startTime).TotalMilliseconds;
var progress = Math.Min(elapsed / 5000.0 * 100, 100);
ProgressBarTimer.Value = progress;
if (progress >= 100)
{
timer.Stop();
this.FindAncestorOfType<Dialog>()?.Close();
}
};
timer.Start();
CloseButton.Click += (_, _) =>
{
timer.Stop();
this.FindAncestorOfType<Dialog>()?.Close();
};
}
}
@@ -0,0 +1,46 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:vm="clr-namespace:HandyControlDemo.ViewModel"
xmlns:sys="clr-namespace:System;assembly=System.Runtime"
x:Class="HandyControlDemo.UserControl.DialogDemo"
DataContext="{Binding DialogDemo, Source={x:Static vm:ViewModelLocator.Instance}}"
Background="{DynamicResource RegionBrush}"
MinWidth="500">
<hc:DialogContainer>
<TransitioningContentControl>
<Panel>
<StackPanel Margin="32"
VerticalAlignment="Center">
<Button Content="TextDialog"
Command="{Binding ShowTextCommand}" />
<Button Margin="0,32,0,0"
Content="TextDialog(In control)"
Command="{Binding ShowTextCommand}"
CommandParameter="{Binding $parent[UserControl]}" />
<Button Margin="0,32,0,0"
Content="TextDialog with timer"
Command="{Binding ShowInteractiveDialogCommand}">
<Button.CommandParameter>
<sys:Boolean>True</sys:Boolean>
</Button.CommandParameter>
</Button>
<TextBox Margin="0,32,0,0"
Text="{Binding DialogResult}"
Theme="{StaticResource TextBoxExtend}"
Watermark="Please input..." />
<Button Margin="0,10,0,0"
Content="InteractiveDialog"
Command="{Binding ShowInteractiveDialogCommand}">
<Button.CommandParameter>
<sys:Boolean>False</sys:Boolean>
</Button.CommandParameter>
</Button>
<Button Margin="0,32,0,0"
Content="New window"
Command="{Binding NewWindowCommand}" />
</StackPanel>
</Panel>
</TransitioningContentControl>
</hc:DialogContainer>
</UserControl>
@@ -0,0 +1,16 @@
using HandyControl.Controls;
using HandyControlDemo.Data;
using HandyControlDemo.ViewModel;
namespace HandyControlDemo.UserControl;
public partial class DialogDemo : Avalonia.Controls.UserControl
{
public DialogDemo()
{
InitializeComponent();
DataContext = ViewModelLocator.Instance.DialogDemo;
Dialog.SetToken(this, MessageToken.DialogContainer);
}
}
@@ -0,0 +1,20 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HandyControl.Tools.Extension;
namespace HandyControlDemo.ViewModel;
public partial class InteractiveDialogViewModel : ObservableObject, IDialogResultable<string>
{
public Action CloseAction { get; set; }
[ObservableProperty]
private string _result;
[ObservableProperty]
private string _message;
[RelayCommand]
private void Close() => CloseAction?.Invoke();
}
@@ -0,0 +1,70 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HandyControl.Controls;
using HandyControl.Tools.Extension;
using HandyControlDemo.Data;
using HandyControlDemo.UserControl;
using HandyControlDemo.Window;
namespace HandyControlDemo.ViewModel;
public partial class DialogDemoViewModel : ObservableObject
{
[ObservableProperty]
private string _dialogResult;
[RelayCommand]
private void ShowText(Control? element)
{
if (element == null)
{
Dialog.Show(new TextDialog());
}
else
{
Dialog.Show(new TextDialog(), MessageToken.DialogContainer);
}
}
[RelayCommand]
private async Task ShowInteractiveDialog(bool withTimer)
{
if (!withTimer)
{
DialogResult = await Dialog.Show<InteractiveDialog>()
.Initialize<InteractiveDialogViewModel>(vm => vm.Message = DialogResult)
.GetResultAsync<string>();
}
else
{
await Dialog.Show<TextDialogWithTimer>(MessageToken.MainWindow).GetResultAsync<string>();
}
}
[RelayCommand]
private void NewWindow()
{
var window = new DialogDemoWindow();
window.Show();
}
[RelayCommand]
private void ShowWithToken(string token)
{
if (!string.IsNullOrEmpty(token))
{
Dialog.Show(new TextDialog(), token);
}
}
[RelayCommand]
private void CloseMainWindowDialog(string token)
{
if (!string.IsNullOrEmpty(token))
{
Dialog.Close(token);
}
}
}
@@ -21,6 +21,7 @@ public class ViewModelLocator
services.AddTransient<MainViewModel>();
services.AddTransient<InputElementDemoViewModel>();
services.AddTransient<CardDemoViewModel>();
services.AddTransient<DialogDemoViewModel>();
_serviceProvider = services.BuildServiceProvider();
}
@@ -30,4 +31,6 @@ public class ViewModelLocator
public InputElementDemoViewModel InputElementDemo => _serviceProvider.GetService<InputElementDemoViewModel>()!;
public CardDemoViewModel CardDemo => _serviceProvider.GetService<CardDemoViewModel>()!;
public DialogDemoViewModel DialogDemo => _serviceProvider.GetService<DialogDemoViewModel>()!;
}
@@ -0,0 +1,29 @@
<Window x:Class="HandyControlDemo.Window.DialogDemoWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:vm="clr-namespace:HandyControlDemo.ViewModel"
xmlns:data="clr-namespace:HandyControlDemo.Data"
x:DataType="vm:DialogDemoViewModel"
DataContext="{Binding DialogDemo, Source={x:Static vm:ViewModelLocator.Instance}}"
Width="800"
Height="500"
Background="{DynamicResource SecondaryRegionBrush}">
<TransitioningContentControl>
<hc:UniformSpacingPanel Margin="32"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Orientation="Vertical"
Spacing="10">
<Button Content="ShowInCurrentWindow"
Command="{Binding ShowWithTokenCommand}"
CommandParameter="{Binding $parent[Window].DialogToken}" />
<Button Content="ShowInMainWindow"
Command="{Binding ShowWithTokenCommand}"
CommandParameter="{x:Static data:MessageToken.MainWindow}" />
<Button Content="CloseMainWindowDialog"
Command="{Binding CloseMainWindowDialogCommand}"
CommandParameter="{x:Static data:MessageToken.MainWindow}" />
</hc:UniformSpacingPanel>
</TransitioningContentControl>
</Window>
@@ -0,0 +1,19 @@
using System;
using Avalonia.Controls;
using HandyControl.Controls;
using HandyControlDemo.Data;
namespace HandyControlDemo.Window;
public partial class DialogDemoWindow : Avalonia.Controls.Window
{
public string DialogToken { get; }
public DialogDemoWindow()
{
InitializeComponent();
DialogToken = $"{MessageToken.DialogDemoWindow}+{DateTime.Now:yyyyMMddHHmmssfff}";
Dialog.SetToken(this, DialogToken);
}
}
@@ -0,0 +1,211 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.VisualTree;
using HandyControl.Tools;
namespace HandyControl.Controls;
[TemplatePart("PART_BackElement", typeof(Border))]
public class Dialog : ContentControl
{
private const string BackElement = "PART_BackElement";
private string? _token;
private Border? _backElement;
private Panel? _hostPanel;
private static readonly ControlTokenManager<Control> TokenManager = new();
private static readonly Dictionary<string, Dialog> DialogDict = new();
public static readonly StyledProperty<bool> IsClosedProperty =
AvaloniaProperty.Register<Dialog, bool>(nameof(IsClosed));
public bool IsClosed
{
get => GetValue(IsClosedProperty);
internal set => SetValue(IsClosedProperty, value);
}
public static readonly AttachedProperty<bool> MaskCanCloseProperty =
AvaloniaProperty.RegisterAttached<Dialog, AvaloniaObject, bool>("MaskCanClose", false);
public static void SetMaskCanClose(AvaloniaObject element, bool value) =>
element.SetValue(MaskCanCloseProperty, value);
public static bool GetMaskCanClose(AvaloniaObject element) =>
element.GetValue(MaskCanCloseProperty);
public static readonly StyledProperty<IBrush?> MaskBrushProperty =
AvaloniaProperty.Register<Dialog, IBrush?>(nameof(MaskBrush));
public IBrush? MaskBrush
{
get => GetValue(MaskBrushProperty);
set => SetValue(MaskBrushProperty, value);
}
public static readonly AttachedProperty<string?> TokenProperty =
AvaloniaProperty.RegisterAttached<Dialog, AvaloniaObject, string?>("Token");
static Dialog()
{
TokenProperty.Changed.AddClassHandler<AvaloniaObject>(TokenManager.OnTokenChanged);
}
public static void SetToken(AvaloniaObject element, string? value) =>
element.SetValue(TokenProperty, value);
public static string? GetToken(AvaloniaObject element) =>
element.GetValue(TokenProperty);
public static Dialog Show<T>(string? token = null) where T : new() => Show(new T(), token);
public static Dialog Show(object content, string? token = null)
{
var dialog = new Dialog
{
_token = token,
Content = content
};
if (string.IsNullOrEmpty(token))
{
var window = GetActiveWindow();
if (window != null)
{
ShowOnWindow(dialog, window);
}
}
else
{
Close(token);
DialogDict[token] = dialog;
if (TokenManager.TryGetControl(token, out var ctrl) && ctrl != null)
{
if (ctrl is Window window)
{
ShowOnWindow(dialog, window);
}
else
{
var panel = FindDialogContainer(ctrl);
if (panel != null)
{
dialog._hostPanel = panel;
dialog.IsClosed = false;
dialog.Width = panel.Bounds.Width;
dialog.Height = panel.Bounds.Height;
panel.Children.Add(dialog);
panel.InvalidateMeasure();
}
else
{
var win = GetActiveWindow();
if (win != null)
ShowOnWindow(dialog, win);
}
}
}
}
return dialog;
}
private static void ShowOnWindow(Dialog dialog, Window window)
{
var overlay = OverlayLayer.GetOverlayLayer(window);
if (overlay == null) return;
var size = overlay.Bounds.Size;
if (size.Width <= 0 || size.Height <= 0)
{
var topLevel = TopLevel.GetTopLevel(overlay);
if (topLevel != null)
size = topLevel.ClientSize;
}
dialog._hostPanel = overlay;
dialog.IsClosed = false;
dialog.Width = size.Width;
dialog.Height = size.Height;
overlay.Children.Add(dialog);
void OnOverlayResized(object? sender, SizeChangedEventArgs e)
{
if (dialog._hostPanel == null) return;
dialog.Width = e.NewSize.Width;
dialog.Height = e.NewSize.Height;
}
overlay.SizeChanged += OnOverlayResized;
}
public static void Close(string? token)
{
if (!string.IsNullOrEmpty(token) && DialogDict.TryGetValue(token, out var dialog))
{
dialog.Close();
}
}
public void Close()
{
if (_hostPanel == null) return;
_hostPanel.Children.Remove(this);
_hostPanel = null;
IsClosed = true;
if (!string.IsNullOrEmpty(_token))
{
DialogDict.Remove(_token);
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_backElement = e.NameScope.Find<Border>(BackElement);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
if (e.InitialPressMouseButton == MouseButton.Left &&
GetMaskCanClose(this) && _backElement != null && _backElement.IsPointerOver)
{
Close();
}
}
private static DialogContainer? FindDialogContainer(Visual root)
{
foreach (var child in root.GetVisualChildren())
{
if (child is DialogContainer found)
return found;
var nested = FindDialogContainer(child);
if (nested != null)
return nested;
}
return null;
}
private static Window? GetActiveWindow()
{
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
return desktop.Windows.FirstOrDefault(w => w.IsActive) ?? desktop.MainWindow;
}
return null;
}
}
@@ -0,0 +1,8 @@
using Avalonia.Controls;
namespace HandyControl.Controls;
/// <summary>
/// A panel that can host Dialog overlays when targeted by a matching token.
/// </summary>
public class DialogContainer : Panel;
@@ -0,0 +1,30 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal"
xmlns:hc="clr-namespace:HandyControl.Controls">
<ControlTheme x:Key="DialogBaseStyle"
TargetType="hc:Dialog">
<Setter Property="Focusable" Value="False" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="MaskBrush" Value="{DynamicResource DarkOpacityBrush}" />
<Setter Property="Template">
<ControlTemplate TargetType="hc:Dialog">
<Border Name="PART_BackElement"
Background="{TemplateBinding MaskBrush}">
<ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type hc:Dialog}"
BasedOn="{StaticResource DialogBaseStyle}"
TargetType="hc:Dialog" />
</ResourceDictionary>
@@ -57,6 +57,7 @@
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Calendar.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/CalendarDatePicker.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/DatePicker.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Dialog.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Clock.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/TimePicker.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/TextBox.axaml" />
@@ -0,0 +1,77 @@
using System;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Interactivity;
using HandyControl.Controls;
namespace HandyControl.Tools.Extension;
public interface IDialogResultable<T>
{
T Result { get; set; }
Action CloseAction { get; set; }
}
public static class DialogExtension
{
public static Task<TResult> GetResultAsync<TResult>(this Dialog dialog)
{
var tcs = new TaskCompletionSource<TResult>();
try
{
if (dialog.IsClosed)
{
SetResult();
}
else
{
dialog.Unloaded += OnUnloaded;
dialog.GetViewModel<IDialogResultable<TResult>>().CloseAction = dialog.Close;
}
}
catch (Exception e)
{
tcs.TrySetException(e);
}
return tcs.Task;
void OnUnloaded(object? sender, RoutedEventArgs args)
{
dialog.Unloaded -= OnUnloaded;
SetResult();
}
void SetResult()
{
try
{
tcs.TrySetResult(dialog.GetViewModel<IDialogResultable<TResult>>().Result);
}
catch (Exception e)
{
tcs.TrySetException(e);
}
}
}
public static Dialog Initialize<TViewModel>(this Dialog dialog, Action<TViewModel> configure)
{
configure?.Invoke(dialog.GetViewModel<TViewModel>());
return dialog;
}
public static TViewModel GetViewModel<TViewModel>(this Dialog dialog)
{
if (!(dialog.Content is Control frameworkElement))
throw new InvalidOperationException("The dialog is not a derived class of the FrameworkElement. ");
if (!(frameworkElement.DataContext is TViewModel viewModel))
throw new InvalidOperationException($"The view model of the dialog is not the {typeof(TViewModel)} type or its derived class. ");
return viewModel;
}
}