Introduction
This article describes the use of TreeView
control provided by WPF. It will give you a knowledge to create simple Tree, customization, Template and Binding. It mainly focuses on how to show contents in the tree view. This article will help you to understand the basics of tree view and also gives you deep knowledge to show content on tree according to your requirement.
I had to use tree view in one of my projects where I had to show image and text as most of tree has in windows. This problem started my journey to learn features provided by WPF. At the start of my journey, I found many difficulties and sometimes I said Windows application contains more user friendly controls to work with, but with the passage of time I found WPF controls more user friendly.
This article will cover the following 6 main areas:
Create Simple Tree
If you want to create a simple tree then WPF provides you an easy way draw tree. Just add TreeView
control on your page and add items in either using XAML or from code behind.
Using XAML
You can easily create a tree using XAML.
Collapse | Copy Code <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>
Using code
If you want to populate tree from code behind, then simply place your tree on form and add tree item according to your tree hierarchy.
Collapse | Copy Code <TreeView Name="tvMain">
</TreeView>
Collapse | Copy Code TreeViewItem treeItem = null;
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);
Customize Tree
If you want to add some other controls with the content e.g. checkbox, image, etc., then you can easily design your tree without any big effort. You just need to customize the HeaderTemplate
of the TreeViewItem
. You can also create class derived from TreeViewItem
and change its Header
according to your requirement.
Using XAML
For customizing the Tree item, simply change item’s Header
.
Collapse | Copy Code <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>
-->
<TreeViewItem>
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal">
<Image Source="../Images/usa.png"></Image>
<Label Content="USA"></Label>
</StackPanel>
</TreeViewItem.Header>
</TreeViewItem>
</TreeViewItem>
</TreeView>
Using Code
If you want to create header from code behind, then WPF will not disappoint you. You can change header template very smartly.
Collapse | Copy Code private TreeViewItem GetTreeView(string text, string imagePath)
{
TreeViewItem item = new TreeViewItem();
item.IsExpanded = true;
StackPanel stack = new StackPanel();
stack.Orientation = Orientation.Horizontal;
Image image = new Image();
image.Source = new BitmapImage
(new Uri("pack://application:,,/Images/" + imagePath));
Label lbl = new Label();
lbl.Content = text;
stack.Children.Add(image);
stack.Children.Add(lbl);
item.Header = stack;
return item;
}
Using overriding TreeViewItem
You can also customize TreeViewItem
by writing a new derived class for custom item. It is also pretty easy. Just create header template and assign it to Header
property if TreeViewItem
.
Collapse | Copy Code 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 Template
If the style of all the elements is the same, then it is better to create header
template at once. Because the problem with the last example was for the same design, we add template for each tree item.
Using XAML
For creating general TreeViewItem
item template, create Template
resource at application level, window level or at control level resource. In this example, I have created resource at control level and set theTargetType=”TreeViewItem”
and also set the “HeaderTemplate”
property of the TreeViewItem
.
Collapse | Copy Code <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>
It is a very interested point here that I did not pass Image
path for each country, but TreeView
shows flag with each country. I achieved by writing custom converter CustomImagePathConverter
.
Collapse | Copy Code <Image Margin="2" Source="{Binding Converter=
{StaticResource CustomImagePathConverter}}"></Image>
Implement CustomImagePathConverter
from IValueConverter
. You can associate a value converter with binding. In this example, I am getting image path from the country name, as you can see in code.
Collapse | Copy Code 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;
}
}
Using Code
You can easily create template from code behind. FrameworkElementFactory
provides you a facility to create templates. Let's see how can we achieve this exciting feature.
Collapse | Copy Code private DataTemplate GetHeaderTemplate()
{
DataTemplate dataTemplate = new DataTemplate();
FrameworkElementFactory stackPanel = new FrameworkElementFactory(typeof(StackPanel));
stackPanel.Name = "parentStackpanel";
stackPanel.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);
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);
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);
FrameworkElementFactory label = new FrameworkElementFactory(typeof(TextBlock));
label.SetBinding(TextBlock.TextProperty, new Binding());
label.SetValue(TextBlock.ToolTipProperty, new Binding());
stackPanel.AppendChild(label);
dataTemplate.VisualTree = stackPanel;
return dataTemplate;
}
Simply assign this template to HeaderTemplate
of each TreeViewitem
.
Collapse | Copy Code DataTemplate template = GetHeaderTemplate();
foreach (WorldArea area in WorldArea.GetAll())
{
TreeViewItem item = new TreeViewItem();
item.HeaderTemplate = template;
item.Header = area.Name;
.
.
.
.
}
Get selected checked items
You can easily get the child items from the template. Just for the example, I am showing you how to get the selected check boxes from the tree view. WPF manage control in hierarchical structure, you can access any child usingVisualTreeHelper
class.
Collapse | Copy Code 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;
}
element = GetChildControl(childObject, childName);
if (element != null) break;
}
}
return element;
}
Custom Objects
WPF provides you many ways to populate tree. You can directly add your object as a TreeViewItem
in the tree and WPF gives respect to your objects and display it as you want. You just need to tell him which field will be shown in item.
Using XAML
For populating custom object in tree, you just need to create template for your object. I usedHierarchicalDataTemplate
for designing template.
Collapse | Copy Code <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>
After creating template, you just need to add custom object from your code behind file, as you can see code below. I am just putting parent object in the tree. But when you will run this code, you will also see child countries are also being shown. The reason is that because I have define template in XAML for child items using ItemsSource="{Binding Path=Countries}"
.
Collapse | Copy Code private void FillTree()
{
foreach (WorldArea area in WorldArea.GetAll())
{
tvMain.Items.Add(area);
}
}
Using Code
You can also create template for your object from code behind file, as we created in previous example. The tricky part here, how can we add custom objects in the hierarchical way? Because using XAML we can write create hierarchical template. We can also create hierarchical template using code behind, but in this example I am not doing that, I am achieving the solution from other way. This technique will give you a new way to work and you can implement it in other Items controls like ListView
, ListBox
etc. But in the last example, I will create hierarchical template from code behind.
Collapse | Copy Code 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);
}
}
}
}
}
As you can see in code after adding setting template, I have registeredtvMain.ItemContainerGenerator.StatusChanged
event. ItemContainerGenerator
generates the containers for each custom object. When we add custom object in the TreeView
ItemContainerGenerator
starts to generate container in separate thread. So we cannot get container in the next line after adding object. So you need to registerStatusChanged
event, which fires after the status change and you can get container after that.
Data Binding
You can also bind your tree with any source as you can bind DataGrid
, ListView
, etc. You just need to create a template for your items as you create in other binding controls.
Using XAML
Create your hierarchical template as you created in the previous example. You may need to add inner hierarchical template for different example. But it is working fine for my example.
Collapse | Copy Code <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>
Simply bind tree using ItemsSource
property:
Collapse | Copy Code private void BindTree()
{
tvMain.ItemsSource = WorldArea.GetAll();
}
Using Code
For creating Hierarchical template from code behind simply create object of HierarchicalDataTemplate
class and fill childs according to your requirement and assign this template to tree.
Collapse | Copy Code private void BindTree()
{
tvMain.ItemTemplate = GetTemplate();
tvMain.ItemsSource = WorldArea.GetAll();
}
private HierarchicalDataTemplate GetTemplate()
{
HierarchicalDataTemplate dataTemplate = new HierarchicalDataTemplate();
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));
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);
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");
dataTemplate.VisualTree = grid;
return dataTemplate;
}
Template By Data Type
A very nice flexibility provided by WPF is you can create your template by data type. Suppose you want have to show different type of objects in tree and you want to differentiate them on UI. It is not a big problem in WPF. Simply create Template by Data type and bind source with tree or manually add objects. Your tree will pick template according to data type.
Using Data Template
Simply create data template in any resource as I created in tree resource. And set its data type as I did usingDataType="{x:Type loc:WorldArea}"
.
Collapse | Copy Code <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>
Using Hierarchical Template
You can also create hierarchical template by data type.
Collapse | Copy Code <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>
포커스나 논리적 포커스가 키보드에서 무슨 의미를 하는건가요??
2009/08/04 16:48 [ ADDR : EDIT/ DEL : REPLY ]키보드입력을 이용해 키보드 클래스의 관련 이벤트를 사용하려면 우선 키보드입력을 받은 요소(Control)가 무엇인지 알려줘야겠죠?.. 포커스는 요소가 입력받을 준비?, 입력 받을 요소가 입력 받을 상태가 가능한 상태로 만들어주는 것입니다..
2009/08/04 22:34 [ ADDR : EDIT/ DEL ]엔터키 예제에서... 엔터키를 누를때 마다 키 상태가 토글되는것 같은데, 제가 뭘 잘못 입력해서일까요?
2010/08/13 10:57 [ ADDR : EDIT/ DEL : REPLY ]안녕하세요 제가 휴가를 다녀오느라 답변이 늦었내요..
2010/08/30 13:36 [ ADDR : EDIT/ DEL ]질문하신 내용은 Enter키를 누를때 마다 키 상태가 토글되는 것을 말씀하신것 같은데 이영민님이 잘못하신것은 없구요. Enter키가 토글 되는것은 테스트를 해보시면 아시겠지만 Enter키 뿐만 아니라 다른키 역시 key 상태가 Down -> Toggle -> Down -> Toggle 로 변경되게 되는 것을 아실 수 있을 겁니다. 예제에서 If 문을 다음과 같이 수정해보세요.. if (Keyboard.IsKeyDown(Key.Enter) == true) 그럼 Enter Key가 Down되었을때의 Key상태를 알 수 있을 것입니다.
강좌완 크게 관계가 없겠지만 Enter키 체크 부분을 아래와 같이 고쳐주시면 정확하게 체크가 가능합니다
2012/08/23 14:47 [ ADDR : EDIT/ DEL : REPLY ]^_^;
(엔터키 누르고 다시 누르면 비정상 동작합니다 ㅎㅎ
if((Keyboard.GetKeyStates(Key.Enter) & KeyStates.Down) > 0)
강의 잘 듣고있습니다.
마지막 강의를 향하여!!! 고고싱
버그 수정까지 해주시고.. 감사합니다..
2012/08/28 09:38 [ ADDR : EDIT/ DEL ]다른 cs 파일에서 textBox1를 조작할려면 어떻게 해야 하나요?
2013/03/25 16:19 [ ADDR : EDIT/ DEL : REPLY ]