From 25a53019ea12b4e8436844f2bcd75a45cbea7ebf Mon Sep 17 00:00:00 2001 From: chinboy Date: Fri, 26 Dec 2025 14:17:47 +0800 Subject: [PATCH] Refactor TreeView Add custom tree node expand/close icon --- .../DataDisplay/TreeViewViewModel.cs | 8 + .../Views/DataDisplay/TreeViewShowCase.axaml | 265 ++++++++++-------- .../DataDisplay/TreeViewShowCase.axaml.cs | 8 + .../Icon/IconFuncTemplate.cs | 24 ++ .../Icon/IconTemplate.cs | 16 ++ .../Themes/TreeViewItemHeaderTheme.axaml | 1 + .../TreeView/TreeView.DragAndDrop.cs | 9 - .../TreeView/TreeView.cs | 143 ++++++++-- 8 files changed, 326 insertions(+), 148 deletions(-) create mode 100644 src/AtomUI.Controls.Shared/Icon/IconFuncTemplate.cs create mode 100644 src/AtomUI.Controls.Shared/Icon/IconTemplate.cs diff --git a/controlgallery/AtomUIGallery/ShowCases/ViewModels/DataDisplay/TreeViewViewModel.cs b/controlgallery/AtomUIGallery/ShowCases/ViewModels/DataDisplay/TreeViewViewModel.cs index 549243c42..4d2f7c3b6 100644 --- a/controlgallery/AtomUIGallery/ShowCases/ViewModels/DataDisplay/TreeViewViewModel.cs +++ b/controlgallery/AtomUIGallery/ShowCases/ViewModels/DataDisplay/TreeViewViewModel.cs @@ -45,6 +45,14 @@ public class TreeViewViewModel : ReactiveObject, IRoutableViewModel set => this.RaiseAndSetIfChanged(ref _basicTreeViewDefaultExpandedPaths, value); } + private IList? _customizeCollapseExpandTreeDefaultExpandedPaths; + + public IList? CustomizeCollapseExpandTreeDefaultExpandedPaths + { + get => _customizeCollapseExpandTreeDefaultExpandedPaths; + set => this.RaiseAndSetIfChanged(ref _customizeCollapseExpandTreeDefaultExpandedPaths, value); + } + private IList? _basicTreeViewDefaultSelectedPaths; public IList? BasicTreeViewDefaultSelectedPaths diff --git a/controlgallery/AtomUIGallery/ShowCases/Views/DataDisplay/TreeViewShowCase.axaml b/controlgallery/AtomUIGallery/ShowCases/Views/DataDisplay/TreeViewShowCase.axaml index 858eeabb6..8dff05c49 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Views/DataDisplay/TreeViewShowCase.axaml +++ b/controlgallery/AtomUIGallery/ShowCases/Views/DataDisplay/TreeViewShowCase.axaml @@ -31,125 +31,152 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + showLine: + + + + + showIcon: + + + + + ShowLeafIcon: + + + + + Node hover mode: + Default + Block + WholeLine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/controlgallery/AtomUIGallery/ShowCases/Views/DataDisplay/TreeViewShowCase.axaml.cs b/controlgallery/AtomUIGallery/ShowCases/Views/DataDisplay/TreeViewShowCase.axaml.cs index 525417516..8657d1bff 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Views/DataDisplay/TreeViewShowCase.axaml.cs +++ b/controlgallery/AtomUIGallery/ShowCases/Views/DataDisplay/TreeViewShowCase.axaml.cs @@ -18,6 +18,7 @@ public partial class TreeViewShowCase : ReactiveUserControl InitBasicTreeViewData(viewModel); viewModel.TreeViewNodeHoverMode = TreeItemHoverMode.Default; InitBasicTreeNodes(viewModel); + InitCustomizeCollapseExpandTreeDefaultExpandedPaths(viewModel); } }); InitializeComponent(); @@ -119,4 +120,11 @@ public partial class TreeViewShowCase : ReactiveUserControl } ]; } + + private void InitCustomizeCollapseExpandTreeDefaultExpandedPaths(TreeViewViewModel viewModel) + { + viewModel.CustomizeCollapseExpandTreeDefaultExpandedPaths = [ + new TreeNodePath("0-0/0-0-0") + ]; + } } \ No newline at end of file diff --git a/src/AtomUI.Controls.Shared/Icon/IconFuncTemplate.cs b/src/AtomUI.Controls.Shared/Icon/IconFuncTemplate.cs new file mode 100644 index 000000000..c5299f72c --- /dev/null +++ b/src/AtomUI.Controls.Shared/Icon/IconFuncTemplate.cs @@ -0,0 +1,24 @@ +using Avalonia.Controls; +using Avalonia.Styling; + +namespace AtomUI.Controls; + +public interface IIconTemplate : ITemplate +{} + +public class IconFuncTemplate : IIconTemplate +{ + private readonly Func _func; + + public IconFuncTemplate(Func func) + { + _func = func ?? throw new ArgumentNullException(nameof(func)); + } + + public PathIcon? Build() + { + return _func(); + } + + object? ITemplate.Build() => Build(); +} diff --git a/src/AtomUI.Controls.Shared/Icon/IconTemplate.cs b/src/AtomUI.Controls.Shared/Icon/IconTemplate.cs new file mode 100644 index 000000000..2a71737d5 --- /dev/null +++ b/src/AtomUI.Controls.Shared/Icon/IconTemplate.cs @@ -0,0 +1,16 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Metadata; +using Avalonia.Styling; + +namespace AtomUI.Controls; + +[ControlTemplateScope] +public class IconTemplate : IIconTemplate +{ + [Content] + [TemplateContent] + public object? Content { get; set; } + public PathIcon? Build() => (PathIcon?)TemplateContent.Load(Content)?.Result; + object? ITemplate.Build() => Build(); +} \ No newline at end of file diff --git a/src/AtomUI.Desktop.Controls/TreeView/Themes/TreeViewItemHeaderTheme.axaml b/src/AtomUI.Desktop.Controls/TreeView/Themes/TreeViewItemHeaderTheme.axaml index 0426b2ffd..cc22404e1 100644 --- a/src/AtomUI.Desktop.Controls/TreeView/Themes/TreeViewItemHeaderTheme.axaml +++ b/src/AtomUI.Desktop.Controls/TreeView/Themes/TreeViewItemHeaderTheme.axaml @@ -22,6 +22,7 @@ CollapseIcon="{TemplateBinding SwitcherCollapseIcon}" ExpandIcon="{TemplateBinding SwitcherExpandIcon}" LoadingIcon="{TemplateBinding SwitcherLoadingIcon}" + RotationIcon="{TemplateBinding SwitcherRotationIcon}" LeafIcon="{TemplateBinding SwitcherLeafIcon}" IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}" IsLoading="{TemplateBinding IsLoading}" /> diff --git a/src/AtomUI.Desktop.Controls/TreeView/TreeView.DragAndDrop.cs b/src/AtomUI.Desktop.Controls/TreeView/TreeView.DragAndDrop.cs index d0784e78b..e6133d00c 100644 --- a/src/AtomUI.Desktop.Controls/TreeView/TreeView.DragAndDrop.cs +++ b/src/AtomUI.Desktop.Controls/TreeView/TreeView.DragAndDrop.cs @@ -289,15 +289,6 @@ public partial class TreeView } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - if (change.Property == DefaultSelectedPathsProperty) - { - ConfigureDefaultSelectedPaths(); - } - } - private void HandlePrepareDrag() { _beingDraggedTreeItem = GetNodeByPositionSelfFirst(_lastPoint!.Value); diff --git a/src/AtomUI.Desktop.Controls/TreeView/TreeView.cs b/src/AtomUI.Desktop.Controls/TreeView/TreeView.cs index d8c2582cc..b42268cc3 100644 --- a/src/AtomUI.Desktop.Controls/TreeView/TreeView.cs +++ b/src/AtomUI.Desktop.Controls/TreeView/TreeView.cs @@ -43,20 +43,20 @@ public partial class TreeView : AvaloniaTreeView, IMotionAwareControl, IControlS public static readonly StyledProperty NodeHoverModeProperty = AvaloniaProperty.Register(nameof(NodeHoverMode), TreeItemHoverMode.Default); - public static readonly StyledProperty SwitcherExpandIconProperty = - AvaloniaProperty.Register(nameof(SwitcherExpandIcon)); + public static readonly StyledProperty SwitcherExpandIconProperty = + AvaloniaProperty.Register(nameof(SwitcherExpandIcon)); - public static readonly StyledProperty SwitcherCollapseIconProperty = - AvaloniaProperty.Register(nameof(SwitcherCollapseIcon)); + public static readonly StyledProperty SwitcherCollapseIconProperty = + AvaloniaProperty.Register(nameof(SwitcherCollapseIcon)); - public static readonly StyledProperty SwitcherRotationIconProperty = - AvaloniaProperty.Register(nameof(SwitcherRotationIcon)); + public static readonly StyledProperty SwitcherRotationIconProperty = + AvaloniaProperty.Register(nameof(SwitcherRotationIcon)); - public static readonly StyledProperty SwitcherLoadingIconProperty = - AvaloniaProperty.Register(nameof(SwitcherRotationIcon)); + public static readonly StyledProperty SwitcherLoadingIconProperty = + AvaloniaProperty.Register(nameof(SwitcherRotationIcon)); - public static readonly StyledProperty SwitcherLeafIconProperty = - AvaloniaProperty.Register(nameof(SwitcherLeafIcon)); + public static readonly StyledProperty SwitcherLeafIconProperty = + AvaloniaProperty.Register(nameof(SwitcherLeafIcon)); public static readonly StyledProperty IsShowLeafIconProperty = AvaloniaProperty.Register(nameof(IsShowLeafIcon)); @@ -118,31 +118,31 @@ public partial class TreeView : AvaloniaTreeView, IMotionAwareControl, IControlS set => SetValue(NodeHoverModeProperty, value); } - public PathIcon? SwitcherExpandIcon + public IconTemplate? SwitcherExpandIcon { get => GetValue(SwitcherExpandIconProperty); set => SetValue(SwitcherExpandIconProperty, value); } - public PathIcon? SwitcherCollapseIcon + public IconTemplate? SwitcherCollapseIcon { get => GetValue(SwitcherCollapseIconProperty); set => SetValue(SwitcherCollapseIconProperty, value); } - public PathIcon? SwitcherRotationIcon + public IconTemplate? SwitcherRotationIcon { get => GetValue(SwitcherRotationIconProperty); set => SetValue(SwitcherRotationIconProperty, value); } - public PathIcon? SwitcherLoadingIcon + public IconTemplate? SwitcherLoadingIcon { get => GetValue(SwitcherLoadingIconProperty); set => SetValue(SwitcherLoadingIconProperty, value); } - public PathIcon? SwitcherLeafIcon + public IconTemplate? SwitcherLeafIcon { get => GetValue(SwitcherLeafIconProperty); set => SetValue(SwitcherLeafIconProperty, value); @@ -522,6 +522,12 @@ public partial class TreeView : AvaloniaTreeView, IMotionAwareControl, IControlS disposables.Add(BindUtils.RelayBind(this, ItemTemplateProperty, treeViewItem, TreeViewItem.HeaderTemplateProperty)); } + SetTreeViewItemIcon(treeViewItem, TreeViewItem.SwitcherExpandIconProperty, SwitcherExpandIcon); + SetTreeViewItemIcon(treeViewItem, TreeViewItem.SwitcherCollapseIconProperty, SwitcherCollapseIcon); + SetTreeViewItemIcon(treeViewItem, TreeViewItem.SwitcherRotationIconProperty, SwitcherRotationIcon); + SetTreeViewItemIcon(treeViewItem, TreeViewItem.SwitcherLoadingIconProperty, SwitcherLoadingIcon); + SetTreeViewItemIcon(treeViewItem, TreeViewItem.SwitcherLeafIconProperty, SwitcherLeafIcon); + disposables.Add(BindUtils.RelayBind(this, IsMotionEnabledProperty, treeViewItem, TreeViewItem.IsMotionEnabledProperty)); disposables.Add(BindUtils.RelayBind(this, NodeHoverModeProperty, treeViewItem, TreeViewItem.NodeHoverModeProperty)); disposables.Add(BindUtils.RelayBind(this, IsShowLineProperty, treeViewItem, TreeViewItem.IsShowLineProperty)); @@ -530,11 +536,6 @@ public partial class TreeView : AvaloniaTreeView, IMotionAwareControl, IControlS TreeViewItem.IsShowLeafIconProperty)); disposables.Add(BindUtils.RelayBind(this, IsSwitcherRotationProperty, treeViewItem, TreeViewItem.IsSwitcherRotationProperty)); disposables.Add(BindUtils.RelayBind(this, ToggleTypeProperty, treeViewItem, TreeViewItem.ToggleTypeProperty)); - disposables.Add(BindUtils.RelayBind(this, SwitcherExpandIconProperty, treeViewItem, TreeViewItem.SwitcherExpandIconProperty)); - disposables.Add(BindUtils.RelayBind(this, SwitcherCollapseIconProperty, treeViewItem, TreeViewItem.SwitcherCollapseIconProperty)); - disposables.Add(BindUtils.RelayBind(this, SwitcherRotationIconProperty, treeViewItem, TreeViewItem.SwitcherRotationIconProperty)); - disposables.Add(BindUtils.RelayBind(this, SwitcherLoadingIconProperty, treeViewItem, TreeViewItem.SwitcherLoadingIconProperty)); - disposables.Add(BindUtils.RelayBind(this, SwitcherLeafIconProperty, treeViewItem, TreeViewItem.SwitcherLeafIconProperty)); PrepareTreeViewItem(treeViewItem, item, index, disposables); @@ -550,6 +551,19 @@ public partial class TreeView : AvaloniaTreeView, IMotionAwareControl, IControlS throw new ArgumentOutOfRangeException(nameof(container), "The container type is incorrect, it must be type TreeViewItem."); } } + + private void SetTreeViewItemIcon(TreeViewItem treeViewItem, AvaloniaProperty iconProperty, IIconTemplate? iconTemplate) + { + if (iconTemplate == null) + { + treeViewItem.SetValue(iconProperty, null); + } + else + { + treeViewItem.SetValue(iconProperty, iconTemplate.Build()); + } + } + protected virtual void PrepareTreeViewItem(TreeViewItem treeViewItem, object? item, int index, CompositeDisposable compositeDisposable) { @@ -980,4 +994,93 @@ public partial class TreeView : AvaloniaTreeView, IMotionAwareControl, IControlS } #endregion + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == DefaultSelectedPathsProperty) + { + ConfigureDefaultSelectedPaths(); + } + else if (change.Property == SwitcherRotationIconProperty) + { + HandleSwitcherRotationIconChanged(); + } + else if (change.Property == SwitcherExpandIconProperty) + { + HandleSwitcherExpandIconChanged(); + } + else if (change.Property == SwitcherCollapseIconProperty) + { + HandleSwitcherCollapseIconChanged(); + } + else if (change.Property == SwitcherLoadingIconProperty) + { + HandleSwitcherLoadingIconChanged(); + } + else if (change.Property == SwitcherLeafIconProperty) + { + HandleSwitcherLeafIconChanged(); + } + } + + protected void HandleSwitcherRotationIconChanged() + { + for (var i = 0; i < ItemCount; i++) + { + var container = ContainerFromIndex(i); + if (container is TreeViewItem treeViewItem) + { + SetTreeViewItemIcon(treeViewItem, TreeViewItem.SwitcherRotationIconProperty, SwitcherRotationIcon); + } + } + } + + protected void HandleSwitcherExpandIconChanged() + { + for (var i = 0; i < ItemCount; i++) + { + var container = ContainerFromIndex(i); + if (container is TreeViewItem treeViewItem) + { + SetTreeViewItemIcon(treeViewItem, TreeViewItem.SwitcherExpandIconProperty, SwitcherExpandIcon); + } + } + } + + protected void HandleSwitcherCollapseIconChanged() + { + for (var i = 0; i < ItemCount; i++) + { + var container = ContainerFromIndex(i); + if (container is TreeViewItem treeViewItem) + { + SetTreeViewItemIcon(treeViewItem, TreeViewItem.SwitcherCollapseIconProperty, SwitcherCollapseIcon); + } + } + } + + protected void HandleSwitcherLoadingIconChanged() + { + for (var i = 0; i < ItemCount; i++) + { + var container = ContainerFromIndex(i); + if (container is TreeViewItem treeViewItem) + { + SetTreeViewItemIcon(treeViewItem, TreeViewItem.SwitcherLoadingIconProperty, SwitcherLoadingIcon); + } + } + } + + protected void HandleSwitcherLeafIconChanged() + { + for (var i = 0; i < ItemCount; i++) + { + var container = ContainerFromIndex(i); + if (container is TreeViewItem treeViewItem) + { + SetTreeViewItemIcon(treeViewItem, TreeViewItem.SwitcherLeafIconProperty, SwitcherLeafIcon); + } + } + } } \ No newline at end of file