WPF 中 Tree View 的基本理解






4.90/5 (112投票s)
本文解释了在树视图控件中显示内容的不同方式。

引言
本文描述了 WPF 提供的 TreeView
控件的使用。它将为您提供创建简单树、自定义、模板和绑定的知识。它主要关注如何在树视图中显示内容。本文将帮助您理解树视图的基础知识,并为您提供根据您的要求在树上显示内容的深入知识。
我必须在我的一个项目中使用树视图,我需要像大多数 Windows 树一样显示图像和文本。这个问题开始了我的学习 WPF 提供的功能的旅程。在旅程的开始,我发现了很多困难,有时我说 Windows 应用程序包含更多用户友好的控件,但随着时间的推移,我发现 WPF 控件更加用户友好。
本文将涵盖以下 6 个主要领域
创建简单树

如果您想创建一个简单的树,那么 WPF 提供了一种简单的方法来绘制树。只需在您的页面上添加 TreeView
控件,然后通过 XAML 或代码隐藏添加项目。
使用 XAML
您可以使用 XAML 轻松创建树。
<TreeView>
<TreeViewItem Header="North America">
<TreeViewItem Header="USA"></TreeViewItem>
<TreeViewItem Header="Canada"></TreeViewItem>
<TreeViewItem Header="Mexico"></TreeViewItem>
</TreeViewItem>
<TreeViewItem Header="South America">
<TreeViewItem Header="Argentina"></TreeViewItem>
<TreeViewItem Header="Brazil"></TreeViewItem>
<TreeViewItem Header="Uruguay"></TreeViewItem>
</TreeViewItem>
</TreeView>
使用代码
如果您想从代码隐藏填充树,那么只需将您的树放在表单上,并根据您的树层次结构添加树项目。
<TreeView Name="tvMain">
</TreeView>
TreeViewItem treeItem = null;
// North America
treeItem = new TreeViewItem();
treeItem.Header = "North America";
treeItem.Items.Add(new TreeViewItem() { Header = "USA" });
treeItem.Items.Add(new TreeViewItem() { Header = "Canada" });
treeItem.Items.Add(new TreeViewItem() { Header = "Mexico" });
tvMain.Items.Add(treeItem);
自定义树

如果您想在内容中添加其他控件,例如复选框、图像等,那么您可以轻松设计您的树而无需太多努力。您只需要自定义 TreeViewItem
的 HeaderTemplate
。您还可以创建派生自 TreeViewItem
的类,并根据您的要求更改其 Header
。
使用 XAML
要自定义树项目,只需更改项目的 Header
。
<TreeView >
<TreeViewItem >
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal">
<Border Background="Green" Width="8" Height="12"
BorderBrush="#00000000"></Border>
<Label Content="North America"></Label>
</StackPanel>
</TreeViewItem.Header>
<!-- Child Item -->
<TreeViewItem>
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal">
<Image Source="../Images/usa.png"></Image>
<Label Content="USA"></Label>
</StackPanel>
</TreeViewItem.Header>
</TreeViewItem>
</TreeViewItem>
</TreeView>
使用代码
如果您想从代码隐藏创建标头,那么 WPF 不会让您失望。您可以非常智能地更改标头模板。
private TreeViewItem GetTreeView(string text, string imagePath)
{
TreeViewItem item = new TreeViewItem();
item.IsExpanded = true;
// create stack panel
StackPanel stack = new StackPanel();
stack.Orientation = Orientation.Horizontal;
// create Image
Image image = new Image();
image.Source = new BitmapImage
(new Uri("pack://application:,,/Images/" + imagePath));
// Label
Label lbl = new Label();
lbl.Content = text;
// Add into stack
stack.Children.Add(image);
stack.Children.Add(lbl);
// assign stack to header
item.Header = stack;
return item;
}
使用重写 TreeViewItem
您还可以通过为自定义项目编写新的派生类来自定义 TreeViewItem
。这也很容易。只需创建标头模板并将其分配给 TreeViewItem
的 Header
属性。
public class ImageTreeViewItem : TreeViewItem
{
#region Data Member
Uri _imageUrl = null;
Image _image = null;
TextBlock _textBlock = null;
#endregion
#region Properties
public Uri ImageUrl
{
get { return _imageUrl; }
set
{
_imageUrl = value;
_image.Source = new BitmapImage(value);
}
}
public string Text
{
get { return _textBlock.Text; }
set { _textBlock.Text = value; }
}
#endregion
#region Constructor
public ImageTreeViewItem()
{
CreateTreeViewItemTemplate();
}
#endregion
#region Private Methods
private void CreateTreeViewItemTemplate()
{
StackPanel stack = new StackPanel();
stack.Orientation = Orientation.Horizontal;
_image = new Image();
_image.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
_image.VerticalAlignment = System.Windows.VerticalAlignment.Center;
_image.Width = 16;
_image.Height = 16;
_image.Margin = new Thickness(2);
stack.Children.Add(_image);
_textBlock = new TextBlock();
_textBlock.Margin = new Thickness(2);
_textBlock.VerticalAlignment = System.Windows.VerticalAlignment.Center;
stack.Children.Add(_textBlock);
Header = stack;
}
#endregion
}
标头模板

如果所有元素的样式都相同,那么最好一次创建 header
模板。因为上一个示例的问题是对于相同的设计,我们为每个树项目添加模板。
使用 XAML
为了创建通用的 TreeViewItem
项目模板,请在应用程序级别、窗口级别或控件级别资源中创建 Template
资源。在此示例中,我已在控件级别创建资源,并设置 TargetType=”TreeViewItem”
,还设置了 TreeViewItem
的 “HeaderTemplate”
属性。
<TreeView Name="tvMain">
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox Name="chk" Margin="2" Tag="{Binding}" >
</CheckBox>
<Image Margin="2" Source="{Binding Converter=
{StaticResource CustomImagePathConvertor}}"></Image>
<TextBlock Text="{Binding}"></TextBlock>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TreeView.Resources>
<TreeViewItem Header="North America" IsExpanded="True">
<TreeViewItem Header="USA"></TreeViewItem>
<TreeViewItem Header="Canada"></TreeViewItem>
<TreeViewItem Header="Mexico"></TreeViewItem>
</TreeViewItem>
<TreeViewItem Header="South America" IsExpanded="True">
<TreeViewItem Header="Argentina"></TreeViewItem>
<TreeViewItem Header="Brazil"></TreeViewItem>
<TreeViewItem Header="Uruguay"></TreeViewItem>
</TreeView>
这里有一个非常有趣的点,我没有为每个国家/地区传递 Image
路径,但是 TreeView
显示每个国家/地区的旗帜。我通过编写自定义转换器 CustomImagePathConverter
实现。
<Image Margin="2" Source="{Binding Converter=
{StaticResource CustomImagePathConverter}}"></Image>
从 IValueConverter
实现 CustomImagePathConverter
。您可以将值转换器与绑定关联。在此示例中,我从国家/地区名称获取图像路径,如您在代码中看到的。
public class CustomImagePathConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
return "../Images/" + GetImageName(value.ToString());
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
return "";
}
#endregion
private string GetImageName(string text)
{
string name = "";
name = text.ToLower() + ".png";
return name;
}
}
使用代码
您可以从代码隐藏文件轻松创建模板。FrameworkElementFactory
为您提供了创建模板的功能。让我们看看如何实现这个激动人心的功能。
private DataTemplate GetHeaderTemplate()
{
//create the data template
DataTemplate dataTemplate = new DataTemplate();
//create stack pane;
FrameworkElementFactory stackPanel = new FrameworkElementFactory(typeof(StackPanel));
stackPanel.Name = "parentStackpanel";
stackPanel.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);
// Create check box
FrameworkElementFactory checkBox = new FrameworkElementFactory(typeof(CheckBox));
checkBox.Name = "chk";
checkBox.SetValue(CheckBox.NameProperty, "chk");
checkBox.SetValue(CheckBox.TagProperty , new Binding());
checkBox.SetValue(CheckBox.MarginProperty, new Thickness(2));
stackPanel.AppendChild(checkBox);
// Create Image
FrameworkElementFactory image = new FrameworkElementFactory(typeof(Image));
image.SetValue(Image.MarginProperty, new Thickness(2));
image.SetBinding(Image.SourceProperty, new Binding()
{ Converter = new CustomImagePathConverter() });
stackPanel.AppendChild(image);
// create text
FrameworkElementFactory label = new FrameworkElementFactory(typeof(TextBlock));
label.SetBinding(TextBlock.TextProperty, new Binding());
label.SetValue(TextBlock.ToolTipProperty, new Binding());
stackPanel.AppendChild(label);
//set the visual tree of the data template
dataTemplate.VisualTree = stackPanel;
return dataTemplate;
}
只需将此模板分配给每个 TreeViewitem
的 HeaderTemplate
。
DataTemplate template = GetHeaderTemplate();
foreach (WorldArea area in WorldArea.GetAll())
{
TreeViewItem item = new TreeViewItem();
item.HeaderTemplate = template;
item.Header = area.Name;
.
.
.
.
}
获取选定的已勾选项目
您可以轻松地从模板中获取子项目。仅仅为了示例,我将向您展示如何从树视图中获取选定的复选框。WPF 以分层结构管理控件,您可以使用 VisualTreeHelper
类访问任何子项。
private List<CheckBox> GetSelectedCheckBoxes(ItemCollection items)
{
List<CheckBox> list = new List<CheckBox>();
foreach (TreeViewItem item in items)
{
UIElement elemnt = GetChildControl(item, "chk");
if (elemnt != null)
{
CheckBox chk = (CheckBox)elemnt;
if (chk.IsChecked.HasValue && chk.IsChecked.Value)
{
list.Add(chk);
}
}
List<CheckBox> l = GetSelectedCheckBoxes(item.Items);
list = list.Concat(l).ToList();
}
return list;
}
private UIElement GetChildControl(DependencyObject parentObject, string childName)
{
UIElement element = null;
if (parentObject != null)
{
int totalChild = VisualTreeHelper.GetChildrenCount(parentObject);
for (int i = 0; i < totalChild; i++)
{
DependencyObject childObject = VisualTreeHelper.GetChild(parentObject, i);
if (childObject is FrameworkElement &&
((FrameworkElement)childObject).Name == childName)
{
element = childObject as UIElement;
break;
}
// get its child
element = GetChildControl(childObject, childName);
if (element != null) break;
}
}
return element;
}
自定义对象

WPF 为您提供了多种填充树的方法。您可以直接将您的对象作为 TreeViewItem
添加到树中,WPF 会尊重您的对象并按您希望的方式显示它。您只需要告诉它哪个字段将显示在项目中。
使用 XAML
要在树中填充自定义对象,您只需要为您的对象创建模板。我使用 HierarchicalDataTemplate
来设计模板。
<TreeView Name="tvMain">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Countries}">
<StackPanel Orientation="Horizontal" Margin="4" Background="LightSeaGreen">
<CheckBox Name="chk" Margin="2" Tag="{Binding Path=Name}" ></CheckBox>
<Image Margin="2" Source="{Binding Path=ImageUrl}" ></Image>
<TextBlock Text="{Binding Path=Name}" Margin="2" >
</TextBlock>
<StackPanel.Effect>
<DropShadowEffect BlurRadius="2" Color="LightGray"
Opacity=".2" ></DropShadowEffect>
</StackPanel.Effect>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
创建模板后,您只需要从代码隐藏文件添加自定义对象,如您在下面的代码中看到的。我只是将父对象放入树中。但是当您运行此代码时,您还会看到子国家/地区也显示出来。原因是我在 XAML 中使用 ItemsSource="{Binding Path=Countries}"
为子项目定义了模板。
private void FillTree()
{
foreach (WorldArea area in WorldArea.GetAll())
{
tvMain.Items.Add(area);
}
}
使用代码
您也可以像我们在上一个示例中创建的那样,从代码隐藏文件为您的对象创建模板。这里的难点在于,我们如何以分层方式添加自定义对象?因为使用 XAML 我们可以编写分层模板。我们也可以使用代码隐藏创建分层模板,但在此示例中我没有这样做,我正在通过其他方式实现解决方案。这种技术将为您提供一种新的工作方式,您可以在其他项目控件(如 ListView
、ListBox
等)中实现它。但在最后一个示例中,我将从代码隐藏创建分层模板。
private void FillTree()
{
tvMain.ItemTemplate = GetHeaderTemplate();
tvMain.ItemContainerGenerator.StatusChanged +=
new EventHandler(ItemContainerGenerator_StatusChanged);
foreach (WorldArea area in _list)
{
tvMain.Items.Add(area);
}
}
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (tvMain.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
foreach (WorldArea area in _list)
{
TreeViewItem item =
(TreeViewItem)tvMain.ItemContainerGenerator.ContainerFromItem(area);
if (item == null) continue;
item.IsExpanded = true;
if (item.Items.Count == 0)
{
foreach (Country country in area.Countries)
{
item.Items .Add(country);
}
}
}
}
}
正如您在代码中看到的那样,在设置模板后,我已注册 tvMain.ItemContainerGenerator.StatusChanged
事件。ItemContainerGenerator
为每个自定义对象生成容器。当我们将自定义对象添加到 TreeView
时,ItemContainerGenerator
会在单独的线程中开始生成容器。因此,在添加对象后,我们无法在下一行中获取容器。所以您需要注册 StatusChanged
事件,该事件在状态更改后触发,之后您可以获取容器。
数据绑定

您还可以将您的树与任何源绑定,就像您绑定 DataGrid
、ListView
等一样。您只需要为您的项目创建模板,就像您在其他绑定控件中创建的那样。
使用 XAML
创建您的分层模板,就像您在上一个示例中创建的那样。您可能需要为不同的示例添加内部分层模板。但它对我的示例来说运行良好。
<TreeView Name="tvMain" >
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Countries}">
<Grid Background="LightSkyBlue" Margin="2" Width="100" Height="24">
<Image Margin="2" Width="32" Height="18"
Source="{Binding Path=ImageUrl}"
HorizontalAlignment="Right"
VerticalAlignment="Center" ></Image>
<TextBlock Margin="2" Text="{Binding Path=Name}"
VerticalAlignment="Center" FontWeight="Bold" />
</Grid>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
只需使用 ItemsSource
属性绑定树
private void BindTree()
{
tvMain.ItemsSource = WorldArea.GetAll();
}
使用代码
要从代码隐藏创建分层模板,只需创建 HierarchicalDataTemplate
类的对象并根据您的要求填充子项,然后将此模板分配给树。
private void BindTree()
{
tvMain.ItemTemplate = GetTemplate();
tvMain.ItemsSource = WorldArea.GetAll();
}
private HierarchicalDataTemplate GetTemplate()
{
//create the data template
HierarchicalDataTemplate dataTemplate = new HierarchicalDataTemplate();
//create stack pane;
FrameworkElementFactory grid = new FrameworkElementFactory(typeof(Grid));
grid.Name = "parentStackpanel";
grid.SetValue(Grid.WidthProperty, Convert.ToDouble(100));
grid.SetValue(Grid.HeightProperty, Convert.ToDouble(24) );
grid.SetValue(Grid.MarginProperty, new Thickness(2));
grid.SetValue(Grid.BackgroundProperty, new SolidColorBrush( Colors.LightSkyBlue));
// Create Image
FrameworkElementFactory image = new FrameworkElementFactory(typeof(Image));
image.SetValue(Image.MarginProperty, new Thickness(2));
image.SetValue(Image.WidthProperty, Convert.ToDouble(32));
image.SetValue(Image.HeightProperty, Convert.ToDouble(24));
image.SetValue(Image.VerticalAlignmentProperty, VerticalAlignment.Center );
image.SetValue(Image.HorizontalAlignmentProperty, HorizontalAlignment.Right);
image.SetBinding(Image.SourceProperty, new Binding()
{ Path = new PropertyPath("ImageUrl") });
grid.AppendChild(image);
// create text
FrameworkElementFactory label = new FrameworkElementFactory(typeof(TextBlock));
label.SetBinding(TextBlock.TextProperty,
new Binding() { Path = new PropertyPath("Name") });
label.SetValue(TextBlock.MarginProperty, new Thickness(2));
label.SetValue(TextBlock.FontWeightProperty, FontWeights.Bold);
label.SetValue(TextBlock.ToolTipProperty, new Binding());
grid.AppendChild(label);
dataTemplate.ItemsSource = new Binding("Countries");
//set the visual tree of the data template
dataTemplate.VisualTree = grid;
return dataTemplate;
}
按数据类型模板

WPF 提供的一个非常好的灵活性是您可以按数据类型创建模板。假设您必须在树中显示不同类型的对象,并且您想在 UI 上区分它们。这在 WPF 中不是一个大问题。只需按数据类型创建模板,然后将源绑定到树或手动添加对象。您的树将根据数据类型选择模板。
使用数据模板
只需在任何资源中创建数据模板,就像我在树资源中创建的那样。并设置其数据类型,就像我使用 DataType="{x:Type loc:WorldArea}"
所做的那样。
<TreeView Name="tvMain">
<TreeView.Resources>
<DataTemplate DataType="{x:Type loc:WorldArea}">
<Border Width="150" BorderBrush="RoyalBlue"
Background="RoyalBlue" BorderThickness="1"
CornerRadius="2" Margin="2" Padding="2" >
<StackPanel Orientation="Horizontal" >
<TextBlock Text="{Binding Path=Name}"
FontWeight="Bold" Foreground="White"></TextBlock>
</StackPanel>
</Border>
</DataTemplate>
<DataTemplate DataType="{x:Type loc:Country}">
<Border Width="132" Background="LightBlue" CornerRadius="2" Margin="1" >
<StackPanel Orientation="Horizontal" >
<Image Margin="2" Source="{Binding Path=ImageUrl}"></Image>
<TextBlock Margin="2" Text="{Binding Path=Name}"></TextBlock>
</StackPanel>
</Border>
</DataTemplate>
</TreeView.Resources>
</TreeView>
使用分层模板
您还可以按数据类型创建分层模板。
<TreeView Name="tvMain">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type loc:WorldArea}"
ItemsSource="{Binding Path=Countries}">
<Border Width="150" BorderBrush="RoyalBlue" Background="RoyalBlue"
BorderThickness="1" CornerRadius="2" Margin="2" Padding="2" >
<StackPanel Orientation="Horizontal" >
<TextBlock Text="{Binding Path=Name}"
FontWeight="Bold" Foreground="White"></TextBlock>
</StackPanel>
</Border>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type loc:Country}">
<Border Width="132" Background="LightBlue" CornerRadius="2" Margin="1" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="26"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock Margin="2" Text="{Binding Path=Name}"></TextBlock>
<Image Grid.Column="1" Margin="2"
Source="{Binding Path=ImageUrl}"></Image>
</Grid>
</Border>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>