65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 中 Tree View 的基本理解

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (112投票s)

2010年11月4日

CPOL

6分钟阅读

viewsIcon

496193

downloadIcon

16987

本文解释了在树视图控件中显示内容的不同方式。

引言

本文描述了 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);

自定义树

如果您想在内容中添加其他控件,例如复选框、图像等,那么您可以轻松设计您的树而无需太多努力。您只需要自定义 TreeViewItemHeaderTemplate。您还可以创建派生自 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。这也很容易。只需创建标头模板并将其分配给 TreeViewItemHeader 属性。

 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;
 } 

只需将此模板分配给每个 TreeViewitemHeaderTemplate

 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 我们可以编写分层模板。我们也可以使用代码隐藏创建分层模板,但在此示例中我没有这样做,我正在通过其他方式实现解决方案。这种技术将为您提供一种新的工作方式,您可以在其他项目控件(如 ListViewListBox 等)中实现它。但在最后一个示例中,我将从代码隐藏创建分层模板。

 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 事件,该事件在状态更改后触发,之后您可以获取容器。

数据绑定

您还可以将您的树与任何源绑定,就像您绑定 DataGridListView 等一样。您只需要为您的项目创建模板,就像您在其他绑定控件中创建的那样。

使用 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>
© . All rights reserved.