将winform treeview变成wpf treeview
我已经构建了一个在winforms中生成树视图的函数。 它包括带有递归的子文件夹和文件。 现在我想把它翻译成wpf。
我无法弄清楚如何处理这些类。 我知道我必须制作我自己的自定义类’treenode’,它具有类似于winforms treenode的属性’ parent ‘。
但是在wpf中我需要两种不同类型的treenode,所以我可以通过数据类型正确绑定wpf。 我在使用家庭的wpf中有一个工作示例,我只是不确定如何将我的winform版本转换为wpf。 有人可以帮助我让我的winform版本在wpf中运行吗?
然后最终目标是在WPF中使用目录和文件填充我的树视图,如我的winforms示例中所示。 但是,WPF版本的样式应该保持文件和文件夹的“图标”显示。
我希望有人可以帮助我正常工作。 欢迎任何建议和意见。
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 directoryNodes; public ObservableCollection DirectoryNodes { get { return directoryNodes; } set { directoryNodes = value; NotifyPropertyChanged("DirectoryNodes"); } } private ObservableCollection formats; public ObservableCollection Formats { get { return formats; } set { formats = value; NotifyPropertyChanged("Formats"); } } private ObservableCollection directories; public ObservableCollection Directories { get { return directories; } set { directories = value; NotifyPropertyChanged("Directories"); } } // Creating data for testings public ViewModel() { Formats = new ObservableCollection(); Directories = new ObservableCollection(); DirectoryNodes = new ObservableCollection(); // 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 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 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 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
工作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 formats = new List(); public Form1() { InitializeComponent(); //add userfolder List Directories = new List(); Directories.Add(System.Environment.GetEnvironmentVariable("USERPROFILE")); // get formats accepted formats.Add(".txt"); formats.Add(".png"); PopulateTree(Directories, formats); } static bool IsValidFileFormat(string filename, List 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 directories, List formats) { // main collection of nodes which are used to populate treeview List treeNodes = new List(); foreach (string directoryPath in directories) { if (Directory.Exists(directoryPath)) { DirectoryInfo directoryInfo = new DirectoryInfo(directoryPath); treeNodes.Add(CreateDirectoryNode(directoryInfo)); } } treeView1.Nodes.AddRange(treeNodes.ToArray()); } } }
看看你的例子,我不确定到底发生了什么。 您可以查看输出,看看问题是否源于在运行时未找到的绑定。
但我会建议您将逻辑分开一些,将其中的一部分移到模型中。 我还建议您将模型隐藏在界面后面。 这允许您的视图模型包含单个集合,而视图根据类型呈现该集合的内容。 您当前的实现仅限于显示文件,作为目录的子项,而不是目录和文件。 以下是适合您的工作示例。
模特
索引节点
创建INode
接口将允许您为要呈现到Treeview的每个内容项创建不同的实现。
namespace DirectoryTree { public interface INode { string Name { get; } string Path { get; } } }
我们的INode
只需要两个属性。 一个表示节点的名称(通常是文件夹或文件名),另一个表示它所代表的文件夹或文件的完整路径。
DirectoryNode
这是我们所有节点的根节点。 在大多数情况下,所有其他节点将通过父子关系与DirectoryNode
相关联。 DirectoryNode
将负责构建自己的子节点集合。 这会将逻辑移动到模型中,在模型中它可以validation自身并创建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 children; public DirectoryNode(DirectoryInfo directoryInfo) { this.Directory = directoryInfo; this.Children = new ObservableCollection (); } public DirectoryNode(DirectoryInfo directoryInfo, DirectoryNode parent) : this(directoryInfo) { this.Parent = parent; } public event PropertyChangedEventHandler PropertyChanged; /// /// Gets the name of the folder associated with this node. /// public string Name { get { return this.Directory == null ? string.Empty : this.Directory.Name; } } /// /// Gets the path to the directory associated with this node. /// public string Path { get { return this.Directory == null ? string.Empty : this.Directory.FullName; } } /// /// Gets the parent directory for this node. /// public DirectoryNode Parent { get; private set; } /// /// Gets the directory that this node represents. /// public DirectoryInfo Directory { get; private set; } /// /// Gets or sets the children nodes that this directory node can have. /// public ObservableCollection Children { get { return this.children; } set { this.children = value; this.OnPropertyChanged(); } } /// /// 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. /// 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 (); 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 (new List { new EmptyFolderNode(this) }); } else { // We fill our Children collection with the folder and file nodes we previously created above. this.Children = new ObservableCollection (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
项的集合转换为DirectoryInfo
项的集合时,我们将自己传递给每个子DirectoryNode
以便在需要时可以访问其父项。
Children
系列是INode
模型的集合。 这意味着DirectoryNode
可以容纳各种不同类型的节点,并且可以轻松扩展以支持更多节点。 您只需要更新BuildChildrenNodes
方法。
EmptyFolderNode
我们将实现的最简单的nodel是一个空文件夹节点。 如果双击文件夹,并且没有任何内容,我们将向用户显示一个节点,让他们知道它是空的。 此节点将具有预定义的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; } } }
这里没有太多进展,我们将名称指定为“空”并默认我们的父路径。
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; } /// /// Gets the parent of this node. /// public DirectoryNode Parent { get; private set; } /// /// Gets the file this node represents. /// public FileInfo File { get; private set; } /// /// Gets the filename for the file associated with this node. /// public string Name { get { return this.File == null ? string.Empty : this.File.Name; } } /// /// Gets the path to the file that this node represents. /// 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 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 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
中的TreeViewItem
时, Execute
方法将触发。 此方法将清除所选目录的子集合,或构建子集合。 这样,当用户折叠视图中的文件夹时,我们会自行清理并清空集合。 这也意味着在打开/关闭目录时,将始终刷新集合。
风景
这是最复杂的项目,但一看到它就相当简单。 就像您的示例一样,每个节点类型都有模板。 在我们的例子中,Treeview被数据绑定到我们的视图模型INode
集合。 然后,我们为INode
接口的每个实现提供了一个模板。
记录XAML代码以解释发生了什么,所以我不会添加它。
最终结果如下:
这应该可以得到你想要的。 如果没有,请告诉我。 如果你想要的只是一个目录 – >文件关系,那么你可以在构建它的Children
集合时更新BuildChildrenNodes()
方法以跳过目录查找。
最后要展示的是您现在在视图中的灵活性。 由于FileNode
包含其父DirectoryNode
及其表示的FileInfo
因此您可以使用数据触发器有条件地更改在视图中显示内容的方式。 下面,我FileNode
您展示FileNode
数据模板上的两个数据触发器。 如果文件扩展名为.dll,则将TextBlock变为红色;如果扩展名为.exe,则将TextBlock变为蓝色。
最终结果如下:
您还可以在Execute
方法中Execute
条件逻辑,以不同方式处理每种不同类型的文件。 如果调用Execute
方法,并且文件扩展名为.exe,而不是像我们现在那样忽略文件,则可以启动可执行文件。 此时你有很大的灵活性。