WPF仿VS TreeView
[TemplatePart(Name = "PART_Content", Type = typeof(ToggleButton))] [TemplatePart(Name = "Expander", Type = typeof(Panel))] public class OTreeViewItem : TreeViewItem { Panel? partContent; ToggleButton? partExpander; public Visibility ExpanderVisible { get { return (Visibility)GetValue(ExpanderVisibleProperty); } set { SetValue(ExpanderVisibleProperty, value); } } // Using a DependencyProperty as the backing store for ExpanderVisible. This enables animation, styling, binding, etc... public static readonly DependencyProperty ExpanderVisibleProperty = DependencyProperty.Register("ExpanderVisible", typeof(Visibility), typeof(OTreeViewItem), new PropertyMetadata(Visibility.Hidden)); static OTreeViewItem() { DefaultStyleKeyProperty.OverrideMetadata(typeof(OTreeViewItem), new FrameworkPropertyMetadata(typeof(OTreeViewItem))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); partExpander = (ToggleButton)GetTemplateChild("Expander");//Expander partContent = (Panel)GetTemplateChild("PART_Content"); } protected override Size MeasureOverride(Size constraint) { var a = this.Parent; int i = 0; while (a is TreeViewItem item) { i++; a = item.Parent; } partContent!.Margin = new Thickness(i * 10.0, 0, 0, 0); return base.MeasureOverride(constraint); } }
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Stroke" Color="#FF818181"/> <SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Fill" Color="#FFFFFFFF"/> <SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Stroke" Color="#FF27C7F7"/> <SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Fill" Color="#FFCCEEFB"/> <SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Checked.Stroke" Color="#FF262626"/> <SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Checked.Fill" Color="#FF595959"/> <SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Checked.Stroke" Color="#FF1CC4F7"/> <SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Checked.Fill" Color="#FF82DFFB"/> <PathGeometry x:Key="TreeArrow" Figures="M0,0 L0,6 L6,0 z"/> <Style x:Key="ExpandCollapseToggleStyle" TargetType="{x:Type ToggleButton}"> <Setter Property="Focusable" Value="False"/> <Setter Property="Width" Value="16"/> <Setter Property="Height" Value="16"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ToggleButton}"> <Border Background="Transparent" Height="16" Padding="5,5,5,5" Width="16"> <Path x:Name="ExpandPath" Data="{StaticResource TreeArrow}" Fill="{StaticResource TreeViewItem.TreeArrow.Static.Fill}" Stroke="{StaticResource TreeViewItem.TreeArrow.Static.Stroke}"> <Path.RenderTransform> <RotateTransform Angle="135" CenterY="3" CenterX="3"/> </Path.RenderTransform> </Path> </Border> <ControlTemplate.Triggers> <Trigger Property="IsChecked" Value="True"> <Setter Property="RenderTransform" TargetName="ExpandPath"> <Setter.Value> <RotateTransform Angle="180" CenterY="3" CenterX="3"/> </Setter.Value> </Setter> <Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.Static.Checked.Fill}"/> <Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.Static.Checked.Stroke}"/> </Trigger> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Stroke}"/> <Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Fill}"/> </Trigger> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="IsMouseOver" Value="True"/> <Condition Property="IsChecked" Value="True"/> </MultiTrigger.Conditions> <Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Checked.Stroke}"/> <Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Checked.Fill}"/> </MultiTrigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style x:Key="TreeViewItemFocusVisual"> <Setter Property="Control.Template"> <Setter.Value> <ControlTemplate> <Rectangle/> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style TargetType="{x:Type control:OTreeViewItem}"> <Setter Property="Background" Value="Transparent"/> <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/> <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/> <Setter Property="Padding" Value="1,0,0,0"/> <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/> <Setter Property="FocusVisualStyle" Value="{StaticResource TreeViewItemFocusVisual}"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type control:OTreeViewItem}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition/> </Grid.RowDefinitions> <Border x:Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true"> <DockPanel x:Name="PART_Content"> <ToggleButton x:Name="Expander" ClickMode="Press" Visibility="{TemplateBinding ExpanderVisible}" IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}" Style="{StaticResource ExpandCollapseToggleStyle}"/> <ContentPresenter x:Name="PART_Header" ContentSource="Header" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/> </DockPanel> </Border> <ItemsPresenter x:Name="ItemsHost" Grid.Row="1"/> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsExpanded" Value="false"> <Setter Property="Visibility" TargetName="ItemsHost" Value="Collapsed"/> </Trigger> <Trigger Property="IsSelected" Value="true"> <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/> <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/> </Trigger> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="IsSelected" Value="true"/> <Condition Property="IsSelectionActive" Value="false"/> </MultiTrigger.Conditions> <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/> <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}}"/> </MultiTrigger> <Trigger Property="IsEnabled" Value="false"> <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> <Style.Triggers> <Trigger Property="VirtualizingPanel.IsVirtualizing" Value="true"> <Setter Property="ItemsPanel"> <Setter.Value> <ItemsPanelTemplate> <VirtualizingStackPanel/> </ItemsPanelTemplate> </Setter.Value> </Setter> </Trigger> </Style.Triggers> </Style>
[StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(OTreeViewItem))] public class OTreeView : TreeView { protected override DependencyObject GetContainerForItemOverride() { return new OTreeViewItem(); } }
使用参考(文件夹目录):
namespace WpfApp2 { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); Loaded += MainWindow_Loaded; } private void MainWindow_Loaded(object sender, RoutedEventArgs e) { FolderNode project = new FolderNode { Path = "D:\\", FileType = FileType.Folder }; project.LoadChildrenCmd.Execute(null); tvRoot.ItemsSource = project.Children; } } public class TextEventArgs : EventArgs { public bool InValid { get; set; } public string Text { get; set; } public TextBox Source { get; set; } } public class TextboxDialog : Window { public TextBox TextBox { get; set; } public EventHandler<TextEventArgs> Submit { get; set; } public TextboxDialog(Window owner) { Owner = owner; SizeToContent = SizeToContent.WidthAndHeight; WindowStyle = WindowStyle.None; WindowStartupLocation = WindowStartupLocation.CenterOwner; Loaded += TextboxDialog_Loaded; } private void TextboxDialog_Loaded(object sender, RoutedEventArgs e) { KeyUp += (s, e) => { if (e.Key == Key.Escape) Cancel(); }; //可以自定义内容 if (Content != null) return; if (TextBox == null) { TextBox = new TextBox { MinWidth = 300, Height = 25, VerticalContentAlignment = VerticalAlignment.Center, }; } Content = TextBox; TextBox.Focus(); TextBox.KeyDown += (s, e) => { if (e.Key == Key.Enter) { string text = TextBox.Text.Trim(); var args = new TextEventArgs { Text = text, Source = TextBox }; Submit?.Invoke(this, args); if (args.InValid) return; DialogResult = true; Close(); } }; } void Cancel() { DialogResult = false; Close(); } } public class FileNode { public FolderNode Parent { get; set; } public virtual FileType FileType { get; set; } public string Value { get; set; } public string Path { get; set; } public SimpleCommand DeleteCmd { get; set; } public SimpleCommand RenameCmd { get; set; } public FileNode() { DeleteCmd = new SimpleCommand(Delete); RenameCmd = new SimpleCommand(Rename); } protected bool ValidatePath(string path) { string specialChar = "\\/:*?\"<>|"; if (path.Intersect(specialChar).Count() > 0) { MessageBox.Show("名称不能包含" + specialChar); return false; } if (FileType == FileType.Folder) { if (Directory.Exists(path)) { MessageBox.Show("名称已存在"); return false; } } else if(File.Exists(path)) { MessageBox.Show("名称已存在"); return false; } return true; } protected virtual void Rename() { var window = new TextboxDialog(App.Current.MainWindow); window.Submit = (s, e) => { if (string.IsNullOrWhiteSpace(e.Text)) return; string path = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(Path), e.Text); if (!ValidatePath(path)) { e.InValid = true; return; } File.Move(Path, path); Parent.LoadChildren(); }; window.ShowDialog(); } void Delete() { if(FileType== FileType.Folder) IOHelper.DeleteDirectory(Path); else IOHelper.DeleteFile(Path); (Parent as FolderNode)?.Children.Remove(this); } public override string ToString() { return Path; } } public class FolderNode : FileNode { [DllImport("shlwapi.dll")] public static extern bool PathIsDirectoryEmpty(string pszPath); public override FileType FileType { get => FileType.Folder; } public bool IsEmpty { get; set; } public ObservableCollection<FileNode> Children { get; set; } = new ObservableCollection<FileNode>(); public SimpleCommand NewDirectoryCmd { get; set; } public SimpleCommand LoadChildrenCmd { get; set; } public FolderNode() { NewDirectoryCmd = new SimpleCommand(NewDirectory); LoadChildrenCmd = new SimpleCommand(LoadChildren); } protected override void Rename() { var window = new TextboxDialog(App.Current.MainWindow); window.Submit = (s, e) => { if (string.IsNullOrWhiteSpace(e.Text)) return; string path = System.IO.Path.Combine(new DirectoryInfo(Path).Parent.FullName, e.Text); if (!ValidatePath(path)) { e.InValid = true; return; } Directory.Move(Path, path); Parent.LoadChildren(); }; window.ShowDialog(); } private void NewDirectory() { var window = new TextboxDialog(App.Current.MainWindow); window.Submit = (s, e) => { if (string.IsNullOrWhiteSpace(e.Text)) return; string path = System.IO.Path.Combine(new DirectoryInfo(Path).Parent.FullName, e.Text); if (!ValidatePath(path)) { e.InValid = true; return; } Directory.CreateDirectory(path); AddChild(path, e.Text, FileType.Folder); }; window.ShowDialog(); } public FileNode AddChild(string path, string value, FileType fileType) { var child = new FileNode(); if(fileType == FileType.Folder) { var c = new FolderNode(); c.IsEmpty = PathIsDirectoryEmpty(path); child = c; } child.Path = path; child.Value = value; child.Parent = this; Children.Add(child); return child; } public void LoadChildren() { try { Children?.Clear(); string[] dirs = System.IO.Directory.GetDirectories(Path); foreach (var dir in dirs) { DirectoryInfo info = new DirectoryInfo(dir); if ((info.Attributes & FileAttributes.System) != 0) continue; _ = AddChild(dir, dir.Split("\\").Last(), FileType.Folder); } string[] files = System.IO.Directory.GetFiles(Path); foreach (var file in files) { FileInfo info = new FileInfo(file); if ((info.Attributes & FileAttributes.System) != 0) continue; _ = AddChild(file, file.Split("\\").Last(), FileType.File); } } catch { } } } public enum FileType { File, Folder } }
XAML:
<Window.Resources> <ContextMenu x:Key="FolderContextMenu"> <MenuItem Header="新建文件夹" Command="{Binding NewDirectoryCmd}"/> <MenuItem Header="重命名" Command="{Binding RenameCmd}"/> <MenuItem Header="删除" Command="{Binding DeleteCmd}"/> </ContextMenu> <ContextMenu x:Key="FileContextMenu"> <MenuItem Header="重命名" Command="{Binding RenameCmd}"/> <MenuItem Header="删除" Command="{Binding DeleteCmd}"/> </ContextMenu> </Window.Resources> <control:OTreeView x:Name="tvRoot"> <control:OTreeView.ItemContainerStyle> <Style TargetType="control:OTreeViewItem" BasedOn="{StaticResource OTreeViewItemStyle}"> <Setter Property="ExpandCommand" Value="{Binding LoadChildrenCmd}"/> <Style.Triggers> <DataTrigger Binding="{Binding FileType}" Value="Folder"> <Setter Property="ContextMenu" Value="{StaticResource FolderContextMenu}"/> </DataTrigger> <DataTrigger Binding="{Binding FileType}" Value="File"> <Setter Property="ContextMenu" Value="{StaticResource FileContextMenu}"/> </DataTrigger> <DataTrigger Binding="{Binding IsEmpty}" Value="True"> <Setter Property="ExpanderVisible" Value="Hidden"/> </DataTrigger> <DataTrigger Binding="{Binding IsEmpty}" Value="False"> <Setter Property="ExpanderVisible" Value="Visible"/> </DataTrigger> </Style.Triggers> </Style> </control:OTreeView.ItemContainerStyle> <control:OTreeView.ItemTemplate> <HierarchicalDataTemplate ItemsSource="{Binding Children}"> <TextBlock Text="{Binding Value}" Margin="4,2,0,2"/> </HierarchicalDataTemplate> </control:OTreeView.ItemTemplate> </control:OTreeView>
C#:
FolderNode project = new FolderNode { Path = "D:\\", FileType = FileType.Folder }; project.LoadChildrenCmd.Execute(null); tvRoot.ItemsSource = project.Children;