WPF仿VS TreeView

发布时间 2023-11-07 17:17:24作者: HotSky
    [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);
        }
    }
View Code
    <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>
View Code
[StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(OTreeViewItem))]
public class OTreeView : TreeView
{
    protected override DependencyObject GetContainerForItemOverride()
    {
        return new OTreeViewItem();
    }
}
View Code

使用参考(文件夹目录):

    public class TextboxDialog : Window
    {
        public TextBox TextBox { get; set; }
        public EventHandler<string> Resulted { 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 (TextBox == null)
            {
                TextBox = new TextBox
                {
                    MinWidth = 300,
                    Height = 25,
                    VerticalContentAlignment = VerticalAlignment.Center,
                };
            }
            TextBox.Focus();
            TextBox.KeyDown += (s, e) =>
            {
                if (e.Key == Key.Enter)
                {
                    string text = TextBox.Text.Trim();
                    Resulted?.Invoke(this, text);
                    DialogResult = true;
                    Close();
                }
            };
            Content = TextBox;
        }

        void Cancel()
        {
            DialogResult = false;
            Close();
        }
    }

    public class FileNode
    {
        public FileNode 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 virtual void Rename()
        {
            var window = new TextboxDialog(App.Current.MainWindow);
            window.Resulted += (s, text) =>
            {
                if (string.IsNullOrWhiteSpace(text)) return;
                string path = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(Path), text);
                if (Directory.Exists(path))
                {
                    MessageBox.Show("名称已存在");
                    return;
                }
                File.Move(Path, path);
                WpfApp2.Commons.BindingBase.Instance.RaisePropertyChanged(this, nameof(Value));
            };
            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.Resulted += (s, text) =>
            {
                if (string.IsNullOrWhiteSpace(text)) return;
                string path = System.IO.Path.Combine(new DirectoryInfo(Path).Parent.FullName, text);
                if (Directory.Exists(path))
                {
                    MessageBox.Show("名称已存在");
                    return;
                }
                Directory.Move(Path, path);
                (Parent as FolderNode).LoadChildren();
            };
            window.ShowDialog();
        }

        private void NewDirectory()
        {
            var window = new TextboxDialog(App.Current.MainWindow);
            window.Resulted += (s, text) =>
            {
                if (string.IsNullOrWhiteSpace(text)) return;
                string path = System.IO.Path.Combine(Path, text);
                if (Directory.Exists(path))
                {
                    MessageBox.Show("名称已存在");
                    return;
                }
                Directory.CreateDirectory(path);
                AddChild(path, 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 }
View Code

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;