可视化树和逻辑树
我们先来理解一下什么是可视化树和逻辑树。
可视化树:包含最初指定的大多数元素(在XAML或.cs中)以及控件模板中的元素。
通俗点来讲,就是整个元素的构成树,从最上面的结点到最后一个结点(包括控件模板)。
逻辑树:是可视化树的一个子集,它省略了控件模板中的元素。
通俗点来讲,就是不包含控件模板的可视化树
以上的解释仅仅用于简单理解这两个概念以及区别,完整的解释请参考文末的链接,MSDN上的文档将会更准确。
下面我们创建一个Window来演示,XAML如下:
1 <Window>
3 <Grid Name="grid" Grid.Column="1">
4 <Button HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,80,0" Grid.Row="1" Content="获取可视化树" Click="GetVisualTree_Click"></Button>
5 <Button HorizontalAlignment="Right" VerticalAlignment="Center" Content="获取逻辑树" Grid.Row="1" Click="GetLogicalTree_Click"></Button>
6 </Grid>
7 </Window>
在WPF中提供了VisualTreeHelper 和 LogicalTreeHelper 两个类来对可视化树和逻辑树进行操作。
使用VisualTreeHelper来获取Grid的可视化树
1 public void PrintVisualTree(DependencyObject parent, int level)
2 {
3 string typeName = parent.GetType().FullName ?? parent.GetType().Name;
4 string name = (string)(parent.GetValue(FrameworkElement.NameProperty) ?? "");
5 AppendText(" ".Substring(0, level * 2));
6 AppendText($"{typeName}:",true);
7 AppendText($" {name}");
8 AppendText(Environment.NewLine);
9 for (int i = 0; i != VisualTreeHelper.GetChildrenCount(parent); ++i)
10 {
11 DependencyObject child = VisualTreeHelper.GetChild(parent, i);
12 PrintVisualTree(child, level + 1);
13 }
14 }
输出结果如下:
可以看到Grid的视觉树不仅打印了两个Button,还打印了Button的控件模板。
Button控件的控件模板如下:
1 <Border TextBlock.Foreground="{TemplateBinding Foreground}"
2 x:Name="Border"
3 CornerRadius="2"
4 BorderThickness="1">
5 <ContentPresenter Margin="2"
6 HorizontalAlignment="Center"
7 VerticalAlignment="Center"
8 RecognizesAccessKey="True" />
9 </Border>
使用LogicalTreeHelper来获取Grid的逻辑树
1 public void PrintLogicalTree(object parent, int level)
2 {
3 var parentObj = parent as DependencyObject;
4 var typeName = parent.GetType().FullName;
5 var name = "";
6 if (parentObj != null)
7 {
8 name = (string)(parentObj.GetValue(FrameworkElement.NameProperty) ?? "");
9 }
10 else
11 {
12 name = parent.ToString();
13 }
14
15 AppendText(GetIndentString().Substring(0, level * 2));
16 AppendText($"{typeName}:", true);
17 AppendText($" {name}");
18 AppendText(Environment.NewLine);
19
20 if (parentObj == null)
21 return;
22
23 var children = LogicalTreeHelper.GetChildren(parentObj);
24 foreach (object child in children)
25 {
26 PrintLogicalTree(child, level + 1);
27 }
28 }
运行结果如下:
可以看到Grid的逻辑树只显示到了Button的内容这一层。
可视化树的用途
在上面的Button模板中,我们定义了一个Border,并命名为Border,
如果我们想查找这个控件,并为之添加事件处理程序或设置一些属性,都可以通过可视化树来实现。
首先我们定义一个遍历可视化树的方法
1 public Visual EnumVisual(Visual myVisual,string controlName)
2 {
3 for (int i = 0; i < VisualTreeHelper.GetChildrenCount(myVisual); i++)
4 {
5
6 Visual childVisual = (Visual)VisualTreeHelper.GetChild(myVisual, i);
7 var nameObj = childVisual.GetValue(NameProperty);
8
9 if (nameObj != null && nameObj.ToString() == controlName)
10 return childVisual;
11
12 EnumVisual(childVisual,controlName);
13 }
14
15 return null;
16 }
然后我们查找Border,并设置它的CornerRadius属性
1 //查找控件模板中的名称为Border的控件
2 var border = EnumVisual(this.btn1, "Border");
3 if (border != null)
4 {
5 var borderObj = border as Border;
6
7 if (borderObj != null)
8 {
9 borderObj.CornerRadius = new CornerRadius(10);
10 }
11 }
运行效果
实际上我们还可以通过FrameworkTemplate.FindName方法进行查找,使用方法如下:
1 object findObj = this.btn1.Template.FindName("Border",this.btn1);
FrameworkTemplate.FindName内部没有直接调用VisualTreeHelper,它是利用缓存查找的。
逻辑树的用途
借助逻辑树,内容模型可以方便地循环访问其可能的子对象,从而实现扩展。
FrameworkElement提供了一个FindName的方法,可以通过名称查找子对象
1 public object FindName(string name);
它内部的实现实际就是借助的逻辑树
1 internal object FindName(string name, out DependencyObject scopeOwner)
2 {
3 INameScope scope = FindScope(this, out scopeOwner);
4 if (scope != null)
5 {
6 return scope.FindName(name);
7 }
8 return null;
9 }
1 internal static INameScope FindScope(DependencyObject d, out DependencyObject scopeOwner)
2 {
3 while (d != null)
4 {
5 INameScope scope = NameScope.NameScopeFromObject(d);
6 if (scope != null)
7 {
8 scopeOwner = d;
9 return scope;
10 }
11 DependencyObject parent = LogicalTreeHelper.GetParent(d);
12 d = (parent != null) ? parent : Helper.FindMentor(d.InheritanceContext);
13 }
14 scopeOwner = null;
15 return null;
16 }