WPF中的可视化树(VisualTree)和逻辑树(LogicalTree)

可视化树和逻辑树

我们先来理解一下什么是可视化树和逻辑树。

可视化树:包含最初指定的大多数元素(在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中提供了VisualTreeHelperLogicalTreeHelper 两个类来对可视化树和逻辑树进行操作。

使用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 }

示例代码

参考链接:

https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/advanced/trees-in-wpf?view=netframeworkdesktop-4.8

相关推荐
九鼎科技-Leo7 小时前
什么是 WPF 中的依赖属性?有什么作用?
windows·c#·.net·wpf
麻花201317 小时前
C#之WPF的C1FlexGrid空间的行加载事件和列事件变更处理动态加载的枚举值
开发语言·c#·wpf
lcintj17 小时前
【WPF】Prism学习(九)
学习·wpf·prism
界面开发小八哥17 小时前
界面控件DevExpress WPF中文教程:网格视图数据布局的列和卡片字段
wpf·界面控件·devexpress·ui开发·用户界面
△曉風殘月〆17 小时前
如何在WPF中嵌入其它程序
wpf
Crazy Struggle18 小时前
功能齐全的 WPF 自定义控件资源库(收藏版)
.net·wpf·ui控件库
shepherd枸杞泡茶1 天前
WPF动画
c#·.net·wpf
lcintj1 天前
【WPF】Prism学习(十)
学习·wpf·prism
wyh要好好学习1 天前
WPF数据加载时添加进度条
ui·wpf
code_shenbing1 天前
跨平台WPF框架Avalonia教程 三
前端·microsoft·ui·c#·wpf·跨平台·界面设计