将 winform 树视图转换为 wpf 树视图
getting winform treeview into wpf treeview
我构建了一个在 winforms 中生成树视图的函数。它包括递归的子文件夹和文件。现在我想把它翻译成 wpf。
我不知道如何处理 classes。我知道我必须制作自己的自定义 class 'treenode',它会有一个类似于 winforms 树节点的 属性 'parent'。
但是在 wpf 中我需要两种不同类型的树节点,所以我可以按数据类型正确绑定 wpf。我在 wpf 中有一个使用家庭的工作示例,我只是不确定如何将我的 winform 版本翻译成 wpf。有人可以帮助我让我的 winform 版本在 wpf 中运行吗?
然后最终目标是让我在 WPF 中的树视图使用我的 winforms 示例中看到的目录和文件进行填充。但是,WPF 版本的样式应保持文件和文件夹的 'icons' 显示。
我希望有人能帮助我让它正常工作。欢迎任何建议和评论。
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());
}
}
}
看看你的例子,我不确定到底发生了什么。您可以查看输出,看看问题是否源于运行时未找到绑定。
不过,我建议您将逻辑拆分得更多一些,将其中的一些移到您的模型中。我还建议您将模型隐藏在界面后面。这允许您的视图模型保存单个集合,而视图根据类型呈现该集合的内容。您当前的实现仅限于显示文件,作为目录的子目录,而不是目录 和 文件。下面是一个工作示例。
模特
节点
创建 INode
界面将允许您为要呈现到 Treeview 的每个内容项创建不同的实现。
namespace DirectoryTree
{
public interface INode
{
string Name { get; }
string Path { get; }
}
}
我们的INode
只需要两个属性。一种表示节点的名称(通常是文件夹或文件名),另一种表示它所代表的文件夹或文件的完整路径。
目录节点
这是我们所有节点的根节点。在大多数情况下,所有其他节点都将通过父子关系与 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
方法。
空文件夹节点
我们将实现的最简单的节点是空文件夹节点。如果您双击一个文件夹,但没有任何内容,我们将向用户显示一个节点,让他们知道它是空的。此节点将具有预定义的 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
。该节点代表我们层次结构中的一个文件,需要给它一个 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
的东西抽象成一个可以重复使用的个体 class,或者使用现有的库,如 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
方法将被触发。此方法将清除所选目录的子集合,或构建子集合。这样,当用户在视图中折叠文件夹时,我们会自行清理并清空集合。这也意味着集合将始终在 open/close 目录时刷新。
风景
这是最复杂的项目,但一旦你看它就相当简单。就像您的示例一样,每种节点类型都有模板。在我们的例子中,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 代码以解释正在发生的事情,所以我不会添加。
最终结果如下所示:
这应该可以满足您的需求。如果没有,请告诉我。如果您只需要一个目录-> 文件关系,那么您可以更新 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,您可以启动可执行文件,而不是像我们现在这样忽略该文件。此时你有很大的灵活性。
我构建了一个在 winforms 中生成树视图的函数。它包括递归的子文件夹和文件。现在我想把它翻译成 wpf。
我不知道如何处理 classes。我知道我必须制作自己的自定义 class 'treenode',它会有一个类似于 winforms 树节点的 属性 'parent'。
但是在 wpf 中我需要两种不同类型的树节点,所以我可以按数据类型正确绑定 wpf。我在 wpf 中有一个使用家庭的工作示例,我只是不确定如何将我的 winform 版本翻译成 wpf。有人可以帮助我让我的 winform 版本在 wpf 中运行吗?
然后最终目标是让我在 WPF 中的树视图使用我的 winforms 示例中看到的目录和文件进行填充。但是,WPF 版本的样式应保持文件和文件夹的 'icons' 显示。
我希望有人能帮助我让它正常工作。欢迎任何建议和评论。
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());
}
}
}
看看你的例子,我不确定到底发生了什么。您可以查看输出,看看问题是否源于运行时未找到绑定。
不过,我建议您将逻辑拆分得更多一些,将其中的一些移到您的模型中。我还建议您将模型隐藏在界面后面。这允许您的视图模型保存单个集合,而视图根据类型呈现该集合的内容。您当前的实现仅限于显示文件,作为目录的子目录,而不是目录 和 文件。下面是一个工作示例。
模特
节点
创建 INode
界面将允许您为要呈现到 Treeview 的每个内容项创建不同的实现。
namespace DirectoryTree
{
public interface INode
{
string Name { get; }
string Path { get; }
}
}
我们的INode
只需要两个属性。一种表示节点的名称(通常是文件夹或文件名),另一种表示它所代表的文件夹或文件的完整路径。
目录节点
这是我们所有节点的根节点。在大多数情况下,所有其他节点都将通过父子关系与 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
方法。
空文件夹节点
我们将实现的最简单的节点是空文件夹节点。如果您双击一个文件夹,但没有任何内容,我们将向用户显示一个节点,让他们知道它是空的。此节点将具有预定义的 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
。该节点代表我们层次结构中的一个文件,需要给它一个 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
的东西抽象成一个可以重复使用的个体 class,或者使用现有的库,如 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
方法将被触发。此方法将清除所选目录的子集合,或构建子集合。这样,当用户在视图中折叠文件夹时,我们会自行清理并清空集合。这也意味着集合将始终在 open/close 目录时刷新。
风景
这是最复杂的项目,但一旦你看它就相当简单。就像您的示例一样,每种节点类型都有模板。在我们的例子中,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 代码以解释正在发生的事情,所以我不会添加。
最终结果如下所示:
这应该可以满足您的需求。如果没有,请告诉我。如果您只需要一个目录-> 文件关系,那么您可以更新 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,您可以启动可执行文件,而不是像我们现在这样忽略该文件。此时你有很大的灵活性。