我已经构建了一个在winforms中生成树视图的函数。它包括带有递归的子文件夹和文件。现在我想把它翻译成wpf。
我无法弄清楚如何处理这些课程。我知道我必须创建自己的自定义类'treenode',它具有类似于winforms treenode的属性' parent '。
然而在wpf中我需要两种不同类型的treenode,所以我可以通过数据类型正确绑定wpf。我在使用familys的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<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());
}
}
}
答案 0 :(得分:8)
在那里看你的例子,我不确定到底发生了什么。您可以查看输出,看看问题是否源于在运行时找不到绑定。
我建议您将逻辑分开一些,将其中的一部分移到模型中。我还建议您将模型隐藏在界面后面。这允许您的视图模型包含单个集合,而视图根据类型呈现该集合的内容。您当前的实现仅限于显示文件,作为目录的子项,而不是目录和文件。以下是适合您的工作示例。
<强> 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 强>
我们将实现的最简单的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; }
}
}
这里没有太多内容,我们将名称分配为&#34;空&#34;并默认我们的父路径。
<强> 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代码以解释发生了什么,所以我不会添加。
最终结果如下:
这应该可以满足您的需求。如果它没有,请告诉我。如果你想要的只是一个Directory-&gt;文件关系,那么你只需更新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,而不是像我们现在一样忽略文件,则可以启动可执行文件。此时你有很大的灵活性。