WPF动态TreeView项遍历:用户自定义层级TreeView构建技术咨询
Got it, let's break down how to build this custom, unlimited-hierarchy TreeView with MenuItem and MenuSubmenu nodes step by step. I'll cover class implementations, viewmodel setup, and XAML configuration to make it all work seamlessly.
First, we'll flesh out the abstract base class with essential properties—including INotifyPropertyChanged support so the UI updates automatically when properties change (critical for WPF bindings):
using System.ComponentModel; using System.Runtime.CompilerServices; using System.Collections.Generic; public abstract class TreeviewMenuItem : INotifyPropertyChanged { private string _text; private bool _isSelected; private bool _isExpanded; public virtual string Text { get => _text; set => SetProperty(ref _text, value); } public bool IsSelected { get => _isSelected; set => SetProperty(ref _isSelected, value); } public bool IsExpanded { get => _isExpanded; set => SetProperty(ref _isExpanded, value); } // Helper for property change notifications protected bool SetProperty<T>(ref T backingField, T value, [CallerMemberName] string propertyName = "") { if (EqualityComparer<T>.Default.Equals(backingField, value)) return false; backingField = value; OnPropertyChanged(propertyName); return true; } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
This is your basic clickable menu item with no children. Add an ICommand property if you want to bind actions to clicks:
using System.Windows.Input; public class MenuItem : TreeviewMenuItem { private ICommand _command; public ICommand Command { get => _command; set => SetProperty(ref _command, value); } // Optional: Add a parameter for the command public object CommandParameter { get; set; } }
This node holds a collection of child menu items/submenus. Use ObservableCollection<T> instead of List<T> so the UI updates automatically when children are added or removed:
using System.Collections.ObjectModel; public class MenuSubmenu : TreeviewMenuItem { public MenuSubmenu() { Children = new ObservableCollection<TreeviewMenuItem>(); } public ObservableCollection<TreeviewMenuItem> Children { get; } }
Create a viewmodel to define your menu structure—here's an example with 3 levels of hierarchy to demonstrate unlimited nesting:
public class MainViewModel { public ObservableCollection<TreeviewMenuItem> TreeviewMenuItems { get; } public MainViewModel() { TreeviewMenuItems = new ObservableCollection<TreeviewMenuItem>(); // Build sample menu structure var fileMenu = new MenuSubmenu { Text = "File" }; fileMenu.Children.Add(new MenuItem { Text = "New", Command = new RelayCommand(() => System.Console.WriteLine("New clicked")) }); fileMenu.Children.Add(new MenuItem { Text = "Open", Command = new RelayCommand(() => System.Console.WriteLine("Open clicked")) }); var saveSubmenu = new MenuSubmenu { Text = "Save As" }; saveSubmenu.Children.Add(new MenuItem { Text = "Save as PDF", Command = new RelayCommand(() => System.Console.WriteLine("Save PDF clicked")) }); saveSubmenu.Children.Add(new MenuItem { Text = "Save as Word", Command = new RelayCommand(() => System.Console.WriteLine("Save Word clicked")) }); fileMenu.Children.Add(saveSubmenu); var editMenu = new MenuSubmenu { Text = "Edit" }; editMenu.Children.Add(new MenuItem { Text = "Cut", Command = new RelayCommand(() => System.Console.WriteLine("Cut clicked")) }); editMenu.Children.Add(new MenuItem { Text = "Copy", Command = new RelayCommand(() => System.Console.WriteLine("Copy clicked")) }); TreeviewMenuItems.Add(fileMenu); TreeviewMenuItems.Add(editMenu); } } // Simple RelayCommand implementation (you can also use Prism/MvvmLight commands if preferred) public class RelayCommand : ICommand { private readonly Action _execute; private readonly Func<bool> _canExecute; public RelayCommand(Action execute, Func<bool> canExecute = null) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true; public void Execute(object parameter) => _execute(); public event EventHandler CanExecuteChanged { add => CommandManager.RequerySuggested += value; remove => CommandManager.RequerySuggested -= value; } }
Use HierarchicalDataTemplate to tell the TreeView how to render each node type. We'll define templates for both MenuSubmenu (with children) and MenuItem (leaf nodes):
<Window x:Class="YourNamespace.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:YourNamespace" Title="Custom TreeView Menu" Height="450" Width="300"> <Window.DataContext> <local:MainViewModel /> </Window.DataContext> <Grid> <TreeView ItemsSource="{Binding TreeviewMenuItems}"> <TreeView.Resources> <!-- Template for leaf MenuItem nodes --> <DataTemplate DataType="{x:Type local:MenuItem}"> <StackPanel Orientation="Horizontal" Margin="2,0"> <TextBlock Text="{Binding Text}" /> </StackPanel> </DataTemplate> <!-- Template for container MenuSubmenu nodes --> <HierarchicalDataTemplate DataType="{x:Type local:MenuSubmenu}" ItemsSource="{Binding Children}"> <StackPanel Orientation="Horizontal" Margin="2,0"> <TextBlock Text="{Binding Text}" /> </StackPanel> </HierarchicalDataTemplate> </TreeView.Resources> <TreeView.ItemContainerStyle> <Style TargetType="{x:Type TreeViewItem}"> <!-- Bind selection/expansion state to viewmodel properties --> <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/> <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/> </Style> </TreeView.ItemContainerStyle> </TreeView> </Grid> </Window>
You can easily extend the TreeviewMenuItem class to include an Icon property (e.g., ImageSource) and update the DataTemplates to display icons alongside text:
// Add to TreeviewMenuItem private ImageSource _icon; public ImageSource Icon { get => _icon; set => SetProperty(ref _icon, value); }
Update the XAML DataTemplate to include the icon:
<StackPanel Orientation="Horizontal" Margin="2,0"> <Image Source="{Binding Icon}" Width="16" Height="16" Margin="0,0,4,0"/> <TextBlock Text="{Binding Text}" /> </StackPanel>
This setup gives you a fully customizable TreeView that supports unlimited levels of submenus and menu items. You can dynamically add/remove nodes at runtime, and the UI will update automatically thanks to ObservableCollection and INotifyPropertyChanged.
内容的提问来源于stack exchange,提问作者Bob_74

