把WinForm TreeView 转换为 WPF TreeView

3
我已经编写了一个在winforms中生成树形视图的函数。它包括递归的子文件夹和文件。现在我想将其转换到wpf中。
我遇到了处理类的困难。我知道我必须制作自己的自定义类'treenode',它将具有类似于winforms treenode的属性“parent”。
然而,在wpf中,我需要两种不同类型的treenodes,以便可以通过数据类型正确地绑定wpf。我在wpf中使用家族的工作示例,只是不确定如何将我的winform版本翻译成wpf。有人能帮我让我的winform版本在wpf上运行吗?
最终目标是使用与winforms示例中相同的目录和文件填充WPF中的树形视图。但是,WPF版本的样式应保持显示文件和文件夹的“图标”。
我希望有人能帮助我使其正常工作。欢迎任何建议和评论。

enter image description here


ViewModel.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Input;
using System.Linq;

namespace WpfApplication1
{
    public class ViewModel : ObservableObject
    {
        // Properties
        private ObservableCollection<DirectoryNode> directoryNodes;
        public ObservableCollection<DirectoryNode> DirectoryNodes
        {
            get { return directoryNodes; }
            set
            {
                directoryNodes = value;
                NotifyPropertyChanged("DirectoryNodes");
            }
        }

        private ObservableCollection<string> formats;
        public ObservableCollection<string> Formats
        {
            get { return formats; }
            set
            {
                formats = value;
                NotifyPropertyChanged("Formats");
            }
        }

        private ObservableCollection<string> directories;
        public ObservableCollection<string> Directories
        {
            get { return directories; }
            set
            {
                directories = value;
                NotifyPropertyChanged("Directories");
            }
        }

        // Creating data for testings
        public ViewModel()
        {
            Formats = new ObservableCollection<string>();
            Directories = new ObservableCollection<string>();
            DirectoryNodes = new ObservableCollection<DirectoryNode>();

            // create some dummy test data, eventually will be push to GUI
            Formats.Add(".txt");
            Formats.Add(".png");
            Directories.Add(System.Environment.GetEnvironmentVariable("USERPROFILE"));

            PopulateTree(Directories);
        }

        // Functions
        static bool IsValidFileFormat(string filename, ObservableCollection<string> formats)
        {
            if (formats.Count == 0) return true;

            string ext = Path.GetExtension(filename);
            bool results = formats.Any(fileType => fileType.Equals(ext, StringComparison.OrdinalIgnoreCase));
            return results;
        }

        public static DirectoryNode CreateDirectoryNode(DirectoryInfo directoryInfo)
        {
            DirectoryNode directoryNode = new DirectoryNode(){Filename=directoryInfo.Name};

            foreach (var directory in directoryInfo.GetDirectories())
            {
                try
                {
                    directoryNode.Children.Add(CreateDirectoryNode(directory));
                }
                catch (UnauthorizedAccessException) { }
            }
            foreach (var file in directoryInfo.GetFiles())
            {
                if (IsValidFileFormat(file.FullName, Formats))
                {
                    FileNode node = new FileNode() { Filename = file.FullName };
                    directoryNode.Children.Add(node);
                }
            }

            return directoryNode;
        }

        public void PopulateTree(ObservableCollection<string> directories)
        {
            foreach (string directoryPath in directories)
            {
                if (Directory.Exists(directoryPath))
                {
                    DirectoryInfo directoryInfo = new DirectoryInfo(directoryPath);
                    DirectoryNodes.Add(CreateDirectoryNode(directoryInfo));
                }
            }
        }
    }

    public class FileNode
    {
        public string Filepath { get; set; }
        public string Filename { get; set; }
        public DirectoryNode Parent { get; set; }
    }

    public class DirectoryNode
    {
        public string Filepath { get; set; }
        public string Filename { get; set; }
        public DirectoryNode Parent { get; set; }
        public ObservableCollection<FileNode> Children { get; set; }
    }

    public class ObservableObject : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

MainWindow.Xaml

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:self="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="350" Width="300"
        WindowStartupLocation="CenterScreen">

    <Window.DataContext>
        <self:ViewModel/>
    </Window.DataContext>

    <Grid Margin="5">        
        <TreeView ItemsSource="{Binding Directories}" Grid.Row="1" Grid.ColumnSpan="2">
            <TreeView.Resources>
                <HierarchicalDataTemplate DataType="{x:Type self:DirectoryNode}" ItemsSource="{Binding Children}">
                    <StackPanel Orientation="Horizontal">
                        <Label VerticalAlignment="Center" FontFamily="WingDings" Content="1"/>
                        <TextBlock Text="{Binding Filename}" />
                    </StackPanel>
                </HierarchicalDataTemplate>
                <DataTemplate DataType="{x:Type self:FileNode}">
                    <StackPanel Orientation="Horizontal">
                        <Label VerticalAlignment="Center" FontFamily="WingDings" Content="2"/>
                        <TextBlock Text="{Binding Filename}" />
                    </StackPanel>
                </DataTemplate>
            </TreeView.Resources>
        </TreeView>
    </Grid>

</Window>

工作中的Winforms示例

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using System.Linq;


namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public static List<string> formats = new List<string>();

        public Form1()
        {
            InitializeComponent();

            //add userfolder
            List<string> Directories = new List<string>();
            Directories.Add(System.Environment.GetEnvironmentVariable("USERPROFILE"));

            // get formats accepted
            formats.Add(".txt");
            formats.Add(".png");

            PopulateTree(Directories, formats);
        }

        static bool IsValidFileFormat(string filename, List<string> formats)
        {
            if (formats.Count == 0) return true;

            string ext = Path.GetExtension(filename);
            bool results = formats.Any(fileType => fileType.Equals(ext, StringComparison.OrdinalIgnoreCase));
            return results;
        }

        public static TreeNode CreateDirectoryNode(DirectoryInfo directoryInfo)
        {
            TreeNode directoryNode = new TreeNode(directoryInfo.Name);

            foreach (var directory in directoryInfo.GetDirectories())
            {
                try
                {
                    directoryNode.Nodes.Add(CreateDirectoryNode(directory));
                }
                catch (UnauthorizedAccessException) { }
            }
            foreach (var file in directoryInfo.GetFiles())
            {
                if (IsValidFileFormat(file.FullName, formats))
                {
                    TreeNode node = new TreeNode(file.FullName);
                    node.ForeColor = Color.Red;
                    directoryNode.Nodes.Add(node);
                }
            }

            return directoryNode;
        }

        public void PopulateTree(List<string> directories, List<string> formats)
        {
            // main collection of nodes which are used to populate treeview
            List<TreeNode> treeNodes = new List<TreeNode>();

            foreach (string directoryPath in directories)
            {
                if (Directory.Exists(directoryPath))
                {
                    DirectoryInfo directoryInfo = new DirectoryInfo(directoryPath);
                    treeNodes.Add(CreateDirectoryNode(directoryInfo));
                }
            }

            treeView1.Nodes.AddRange(treeNodes.ToArray());
        }
    }
}

只需将“Family”相关的数据更改为您想要的任何内容,并调整XAML以适应新属性即可。 - Federico Berasategui
增加了更多与将其转换为WPF相关的具体问题,请参见上文。 - JokerMartini
好的,谢谢。如果您不介意的话,在我进行调整时,我会更新帖子,希望您们能在一定程度上指导我。我认为在得到一些帮助片段后,我会完成大部分工作。谢谢。 - JokerMartini
所以我按照你说的做了,但好像没有成功。我已经更新了上面的代码,但它在几行上失败并返回空对象。 - JokerMartini
虽然我回复电子邮件没有问题,但我更喜欢在 Stack Overflow 上进行问答(当然,如果它们是相关主题的话),因为这样我们可以与无数其他开发人员分享经验,而不仅仅是你和我。 - Federico Berasategui
显示剩余4条评论
1个回答

13

看到你的例子,我不确定具体发生了什么。你可以查看输出,看看问题是否源于绑定在运行时找不到。

然而,我建议你将逻辑分解得更细一些,将其中一部分移动到模型中。我还建议你在接口后面隐藏模型。这样,你的视图模型可以持有一个集合,而视图根据类型呈现该集合的内容。你当前的实现仅限于显示文件作为目录的子项,而不是目录和文件。下面是一个可供参考的工作示例。

模型

INode

创建一个INode接口,可以让你创建要呈现到Treeview中的每个内容项的不同实现。

namespace DirectoryTree
{
    public interface INode
    {
        string Name { get; }
        string Path { get; }
    }
}

我们的 INode 只需要两个属性。一个代表节点的名称(通常是文件夹或文件名),另一个代表它所代表的文件夹或文件的完整路径。

DirectoryNode

这是我们所有节点的根节点。在大多数情况下,所有其他节点都将通过父子关系与 DirectoryNode 相关联。 DirectoryNode 将负责构建自己的子节点集合。这将逻辑移入模型中,可以在其中验证自身并生成 EmptyFolderNodes 或根据需要生成 FileNodes 集合。这会使视图模型变得更加简洁,因此它只需要促进与视图本身的交互。

DirectoryNode 将实现 INotifyPropertyChange,以便我们可以向任何数据绑定到我们的东西引发属性更改事件。这仅适用于此模型上的 Children 属性。其余属性将为只读。

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;

namespace DirectoryTree
{
    public class DirectoryNode : INode, INotifyPropertyChanged
    {
        private ObservableCollection<INode> children;

        public DirectoryNode(DirectoryInfo directoryInfo)
        {
            this.Directory = directoryInfo;
            this.Children = new ObservableCollection<INode>();
        }

        public DirectoryNode(DirectoryInfo directoryInfo, DirectoryNode parent) : this(directoryInfo)
        {
            this.Parent = parent;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Gets the name of the folder associated with this node.
        /// </summary>
        public string Name
        {
            get
            {
                return this.Directory == null ? string.Empty : this.Directory.Name;
            }
        }

        /// <summary>
        /// Gets the path to the directory associated with this node.
        /// </summary>
        public string Path
        {
            get
            {
                return this.Directory == null ? string.Empty : this.Directory.FullName;
            }
        }

        /// <summary>
        /// Gets the parent directory for this node.
        /// </summary>
        public DirectoryNode Parent { get; private set; }

        /// <summary>
        /// Gets the directory that this node represents.
        /// </summary>
        public DirectoryInfo Directory { get; private set; }

        /// <summary>
        /// Gets or sets the children nodes that this directory node can have.
        /// </summary>
        public ObservableCollection<INode> Children
        {
            get
            {
                return this.children;
            }

            set
            {
                this.children = value;
                this.OnPropertyChanged();
            }
        }

        /// <summary>
        /// Scans the current directory and creates a new collection of children nodes.
        /// The Children nodes collection can be filled with EmptyFolderNode, FileNode or DirectoryNode instances.
        /// The Children collection will always have at least 1 element within it.
        /// </summary>
        public void BuildChildrenNodes()
        {
            // Get all of the folders and files in our current directory.
            FileInfo[] filesInDirectory = this.Directory.GetFiles();
            DirectoryInfo[] directoriesWithinDirectory = this.Directory.GetDirectories();

            // Convert the folders and files into Directory and File nodes and add them to a temporary collection.
            var childrenNodes = new List<INode>();
            childrenNodes.AddRange(directoriesWithinDirectory.Select(dir => new DirectoryNode(dir, this)));
            childrenNodes.AddRange(filesInDirectory.Select(file => new FileNode(this, file)));

            if (childrenNodes.Count == 0)
            {
                // If there are no children directories or files, we setup the Children collection to hold
                // a single node that represents an empty directory.
                this.Children = new ObservableCollection<INode>(new List<INode> { new EmptyFolderNode(this) });
            }
            else
            {
                // We fill our Children collection with the folder and file nodes we previously created above.
                this.Children = new ObservableCollection<INode>(childrenNodes);
            }
        }

        private void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            var handler = this.PropertyChanged;
            if (handler == null)
            {
                return;
            }

            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

有几点需要注意。首先,模型将始终引用其表示为节点的 DirectoryInfo。接下来,可选择为其提供父 DirectoryNode。这使我们可以轻松支持前向导航(通过 Children 属性)和后向导航(通过 Parent 属性)在我们的模型中。当我们将子级 DirectoryInfo 集合转换为 DirectoryNode 集合时,我们将自身传递给每个子级 DirectoryNode,以便它可以访问其父级(如果需要)。 Children 集合是 INode 模型的集合。这意味着 DirectoryNode 可以容纳各种不同类型的节点,并且可以轻松扩展以支持更多节点。您只需更新 BuildChildrenNodes 方法即可。 EmptyFolderNode 我们将实现的最简单的节点是空文件夹节点。如果您双击文件夹,而其中没有任何内容,则会向用户显示一个节点,告诉他们它是空的。此节点将具有预定义的 Name,并且将始终属于父目录。
namespace DirectoryTree
{
    public class EmptyFolderNode : INode
    {
        public EmptyFolderNode(DirectoryNode parent)
        {
            this.Parent = parent;
            this.Name = "Empty.";
        }

        public string Name { get; private set; }

        public string Path
        {
            get
            {
                return this.Parent == null ? string.Empty : this.Parent.Path;
            }
        }

        public DirectoryNode Parent { get; private set; }
    }
}

这里没有太多事情要做,我们将名称指定为“Empty”,并将路径默认设置为父级。

FileNode

我们需要构建的最后一个模型是FileNode。该节点表示层次结构中的文件,并需要提供DirectoryNode。它还需要该节点所代表的FileInfo

using System.IO;

namespace DirectoryTree
{
    public class FileNode : INode
    {
        public FileNode(DirectoryNode parent, FileInfo file)
        {
            this.File = file;
            this.Parent = parent;
        }

        /// <summary>
        /// Gets the parent of this node.
        /// </summary>
        public DirectoryNode Parent { get; private set; }

        /// <summary>
        /// Gets the file this node represents.
        /// </summary>
        public FileInfo File { get; private set; }

        /// <summary>
        /// Gets the filename for the file associated with this node.
        /// </summary>
        public string Name
        {
            get
            {
                return this.File == null ? string.Empty : this.File.Name;
            }
        }

        /// <summary>
        /// Gets the path to the file that this node represents.
        /// </summary>
        public string Path
        {
            get
            {
                return this.File == null ? string.Empty : this.File.FullName;
            }
        }
    }
}

这个模型的内容应该很容易理解,所以我不会花时间进行解释。
视图模型
现在我们已经定义了我们的模型,我们可以设置视图模型来与它们交互。视图模型需要实现两个接口。第一个是INotifyPropertyChanged,以便我们可以向视图发送属性更改通知。第二个是ICommand,以便视图可以告诉视图模型何时需要加载更多的目录或文件。我建议将ICommand的内容抽象出来成为一个可以重用的单独类,或者使用现有的库,如Prism或MVVMLight,它们都有可以使用的命令对象。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace DirectoryTree
{
    public class MainWindowViewModel : INotifyPropertyChanged, ICommand
    {
        private IEnumerable<INode> rootNodes;

        private INode selectedNode;

        public MainWindowViewModel()
        {
            // We default the app to the Program Files directory as the root.
            string programFilesPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);

            // Convert our Program Files path string into a DirectoryInfo, and create our initial DirectoryNode.
            var rootDirectoryInfo = new DirectoryInfo(programFilesPath);
            var rootDirectory = new DirectoryNode(rootDirectoryInfo);

            // Tell our root node to build it's children collection.
            rootDirectory.BuildChildrenNodes();

            this.RootNodes = rootDirectory.Children;
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public event EventHandler CanExecuteChanged;

        public IEnumerable<INode> RootNodes
        {
            get
            {
                return this.rootNodes;
            }

            set
            {
                this.rootNodes = value;
                this.OnPropertyChanged();
            }
        }

        public bool CanExecute(object parameter)
        {
            // Only execute our command if we are given a selected item.
            return parameter != null;
        }

        public void Execute(object parameter)
        {
            // Try to cast to a directory node. If it returns null then we are
            // either a FileNode or an EmptyFolderNode. Neither of which we need to react to.
            DirectoryNode currentDirectory = parameter as DirectoryNode;
            if (currentDirectory == null)
            {
                return;
            }

            // If the current directory has children, then the view is collapsing it.
            // In this scenario, we clear the children out so we don't progressively
            // consume system resources and never let go.
            if (currentDirectory.Children.Count > 0)
            {
                currentDirectory.Children.Clear();
                return;
            }

            // If the current directory does not have children, then we build that collection.
            currentDirectory.BuildChildrenNodes();
        }

        private void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            var handler = this.PropertyChanged;
            if (handler == null)
            {
                return;
            }

            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

视图模型具有RootNodes的集合。这是视图将绑定到的INode实例的初始集合。该初始集合将包含Program Files目录中的所有文件和文件夹。

当用户双击视图中的TreeViewItem时,将触发Execute方法。此方法将清除所选目录的子项集合或构建子项集合。这样,当用户在视图中折叠文件夹时,我们会自动清理并清空集合。这也意味着,他们打开/关闭目录时,集合将始终得到刷新。

视图

这是最复杂的项目之一,但一旦您看到它,它就非常简单。与您的示例一样,每个节点类型都有模板。在我们的情况下,Treeview与我们的视图模型INode集合数据绑定。然后,我们为INode接口的每个实现提供一个模板。

<Window x:Class="DirectoryTree.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DirectoryTree"
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
        Title="MainWindow" Height="350" Width="525">

    <!-- Assign a view model to the window. -->
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>

    <DockPanel>
        <TreeView x:Name="FileExplorerTreeview" 
                  ItemsSource="{Binding Path=RootNodes}">

            <!--    We use an interaction trigger to map the MouseDoubleClick event onto our view model.
                    Since the view model implements ICommand, we can just bind directly to the view model. 

                    This requires that you add the System.Windows.Interactivity.dll assembly to your project references.
                    You also must add the i: namespace to your XAML window, as shown above..
            -->
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseDoubleClick">
                    <!--    When the user double clicks on a folder, we will send the selected item into the view models Execute method as a argument.
                            The view model can then react to wether or not it's a DirectoryNode or a FileNode.
                    -->
                    <i:InvokeCommandAction Command="{Binding }" CommandParameter="{Binding ElementName=FileExplorerTreeview, Path=SelectedItem}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>

            <TreeView.Resources>
                <!--    This template represents a DirectoryNode. This template databinds itself to the Children property on the DirectoryNode
                        so we can have nested folders and files as needed. 
                -->
                <HierarchicalDataTemplate DataType="{x:Type local:DirectoryNode}"
                                          ItemsSource="{Binding Path=Children}">
                    <StackPanel Orientation="Horizontal">
                        <Label Content="1"
                               FontFamily="WingDings"
                               FontWeight="Black" />
                        <!-- Need to replace w/ an image of a folder -->
                        <TextBlock Text="{Binding Path=Name}" />
                    </StackPanel>
                </HierarchicalDataTemplate>

                <!-- This template represents a FileNode. Since FileNodes can't have children, we make this a standard, flat, data template. -->
                <DataTemplate DataType="{x:Type local:FileNode}">
                    <StackPanel Orientation="Horizontal">
                        <Label Content="2"
                               FontFamily="WingDings"
                               FontWeight="Black" />
                        <!-- Need to replace w/ an image of a file -->
                        <TextBlock Text="{Binding Path=Path}" />
                    </StackPanel>
                </DataTemplate>

                <!-- This template represents an EmptyFolderNode. Since EmptyFolderNode can't have children or siblings, we make this a standard, flat, data template. -->
                <DataTemplate DataType="{x:Type local:EmptyFolderNode}">
                    <StackPanel Orientation="Horizontal">
                        <!-- Need to replace w/ an image of a file -->
                        <TextBlock Text="{Binding Path=Name}" 
                                   FontSize="10"
                                   FontStyle="Italic"/>
                    </StackPanel>
                </DataTemplate>
            </TreeView.Resources>
        </TreeView>
    </DockPanel>
</Window>

XAML代码已有详细说明,因此我不会在此添加解释。

最终结果如下所示:

enter image description here

这应该可以满足你的需求。如果不能,请告诉我。如果你只需要一个目录对应一个文件关系,那么你可以更新BuildChildrenNodes()方法,在构建其Children集合时跳过目录查找。

最后要展示的是你现在在视图中拥有的灵活性。由于FileNode包含其父DirectoryNode和它所代表的FileInfo信息,因此你可以使用数据触发器来有条件地更改在视图中显示内容的方式。下面,我向你展示了在FileNode数据模板上的两个数据触发器。一个是当文件扩展名为.dll时将TextBlock变为红色,另一个是当扩展名为.exe时将TextBlock变为蓝色。

<DataTemplate DataType="{x:Type local:FileNode}">
    <StackPanel Orientation="Horizontal">
        <Label Content="2"
                FontFamily="WingDings"
                FontWeight="Black" />
        <!-- Need to replace w/ an image of a file -->
    <TextBlock Text="{Binding Path=Path}">
        <TextBlock.Style>
            <Style TargetType="TextBlock">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Path=File.Extension}"
                                    Value=".exe">
                        <Setter Property="Foreground"
                                Value="Blue" />
                    </DataTrigger>

                    <DataTrigger Binding="{Binding Path=File.Extension}"
                                    Value=".dll">
                        <Setter Property="Foreground"
                                Value="Red" />
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </TextBlock.Style>
    </TextBlock>
    </StackPanel>
</DataTemplate>

最终结果如下:

这里输入图片描述

您还可以在Execute方法中进行条件逻辑,以便针对每种不同类型的文件进行不同的处理。如果调用了Execute方法,并且文件扩展名为.exe,则可以启动可执行文件,而不是像现在一样忽略该文件。此时,您有很大的灵活性。


哇,这太棒了,非常感谢你的帮助,Johnathon。 - JokerMartini
当我按照你所描述的重新构建程序时,我遇到了许多错误。例如,这一行“this.Directory = directoryInfo;”说它不能被分配,因为它是只读的。而这一行“public string Name => this.Directory.Name;”则提示在“=>”之后期望有一个“;”。 - JokerMartini
我是在 Visual Studio 2015 中完成的,使用了支持我上面使用的一些东西的最新版本的C#。你在用什么? - Johnathon Sullinger
这是2013年的事情。今晚我会更新到2015年并再次尝试。 - JokerMartini
这太棒了,我现在搞定了。非常感谢! - JokerMartini
1
完美的答案。感谢分享。 - NecroMancer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接