1. 前言

A long time ago in a galaxy far, far away....微软在Silverlight Toolkit里提供了一个好用的VisualTreeExtensions,里面提供了一些查找VisualTree的扩展方法。在那个时候(2009年),VisualTreeExtensions对我来说正好是个很棒的Linq和扩展方法的示例代码,比那时候我自己写的FindChildByName之类的方法好用一万倍,所以我印象深刻。而且因为很实用,所以我一直在用这个类(即使是在WPF中),而这次我也把它添加到Kino.Wpf.Toolkit中,可以在 这里 查看源码。

2. VisualTreeExtensions的功能

public static class VisualTreeExtensions
{
/// 获取 visual tree 上的祖先元素
public static IEnumerable<DependencyObject> GetVisualAncestors(this DependencyObject element) { } /// 获取 visual tree 上的祖先元素及自身
public static IEnumerable<DependencyObject> GetVisualAncestorsAndSelf(this DependencyObject element) { } /// 获取 visual tree 上的子元素
public static IEnumerable<DependencyObject> GetVisualChildren(this DependencyObject element) { } /// 获取 visual tree 上的子元素及自身
public static IEnumerable<DependencyObject> GetVisualChildrenAndSelf(this DependencyObject element) { } /// 获取 visual tree 上的后代元素
public static IEnumerable<DependencyObject> GetVisualDescendants(this DependencyObject element) { } /// 获取 visual tree 上的后代元素及自身
public static IEnumerable<DependencyObject> GetVisualDescendantsAndSelf(this DependencyObject element) { } /// 获取 visual tree 上的同级别的兄弟元素
public static IEnumerable<DependencyObject> GetVisualSiblings(this DependencyObject element) { } /// 获取 visual tree 上的同级别的兄弟元素及自身.
public static IEnumerable<DependencyObject> GetVisualSiblingsAndSelf(this DependencyObject element) { }
}

VisualTreeExtensions封装了VisualTreeHelper并提供了各种查询Visual Tree的方法,日常中我常用到的,在Wpf上也没问题的就是以上的功能。使用代码大致这样:

foreach (var item in this.GetVisualDescendants().OfType<TextBlock>())
{
}

3.使用问题

VisualTreeExtensions虽然好用,但还是有些问题需要注意。

3.1 不要在OnApplyTemplate中使用

FrameworkElement在生成当前模板并构造Visual Tree时会调用OnApplyTemplate函数,但这时候最好不要使用VisualTreeExtensions去获取Visual Tree中的元素。所谓的最好,是因为WPF、Silverlight、UWP控件的生命周期有一些出入,我一时记不太清楚了,总之根据经验运行这个函数的时候可能Visual Tree还没有构建好,VisualTreeHelper获取不到子元素。无论我的记忆是否出错,正确的做法都是使用 GetTemplateChild 来获取ControlTemplate中的元素。

3.2 深度优先还是广度优先

<StackPanel Margin="8">
<GroupBox Header="GroupBox" >
<TextBox Margin="8" Text="FirstTextBox"/>
</GroupBox>
<TextBox Margin="8"
Text="SecondTextBox" />
</StackPanel>

假设有如上的页面,执行下面这句代码:

this.GetVisualDescendants().OfType<Control>().FirstOrDefault(c=>c.IsTabStop).Focus();

这段代码的意思是找到此页面第一个可以接受键盘焦点的控件并让它获得焦点。直觉上FirstTextBox是这个页面的第一个表单项,应该由它获得焦点,但GetVisualDescendants的查找方法是广度优先,因为SecondTextBox比FirstTextBox深了一层,所以SecondTextBox获得了焦点。

3.3 Popup的问题

Popup没有自己的Visual Tree,打开Popup的时候,它的Child和Window不在同一个Visual Tree中。以ComboBox为例,下面是ComboBox的ControlTemplate中的主要结构:

<Grid Name="templateRoot"
SnapsToDevicePixels="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition MinWidth="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}"
Width="0" />
</Grid.ColumnDefinitions>
<Popup Name="PART_Popup"
AllowsTransparency="True"
Margin="1"
Placement="Bottom"
Grid.ColumnSpan="2"
PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}"
IsOpen="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
<theme:SystemDropShadowChrome x:Name="shadow"
Color="Transparent"
MaxHeight="{TemplateBinding ComboBox.MaxDropDownHeight}"
MinWidth="{Binding ActualWidth, ElementName=templateRoot}">
...
</theme:SystemDropShadowChrome>
</Popup>
<ToggleButton Name="toggleButton"/>
<ContentPresenter Name="contentPresenter"/>
</Grid>

在实时可视化树视图中可以看到有两个VisualTree,而Popup甚至不在里面,只有一个叫PopupRoot的类。具体可参考 Popup 概述 这篇文档。

不过ComboBox的Popup在逻辑树中是存在的,如果ComboBoxItem想获取ComboBox的VisualTree的祖先元素,可以配合逻辑树查找。

3.4 查找根元素

GetVisualAncestors可以方便地查找各级祖先元素,一直查找到根元素,例如要找到根元素可以这样使用:

element.GetVisualAncestors().Last()

但如果元素不在Popup中,别忘了直接使用GetWindow更快捷:

Window.GetWindow(element)

5. 其它方案

很多控件库都封装了自己的查找VisualTree的工具类,下面是一些常见控件库的方案:

6. 结语

VisualTreeExtensions的代码很简单,我估计在UWP中也能使用,不过UWP已经在WindowsCommunityToolkit中提供了一个新的版本,只因为出于习惯,我还在使用Silverlight Toolkit的版本。而且Toolkit中的FindDescendantByName(this DependencyObject element, string name)让我回忆起了我当年抛弃的FindChildByName,一点都不优雅。

延续VisualTreeExtensions的习惯,多年来我都把扩展方法写在使用-Extensions后缀命名的类里,不过我不记得有这方面的相关规范。

7. 参考

VisualTreeHelper Class (System.Windows.Media) _ Microsoft Docs

FrameworkElement.GetTemplateChild(String) Method (System.Windows) Microsoft Docs

Popup 概述 Microsoft Docs

8. 源码

VisualTreeExtensions.cs at master · DinoChan_Kino.Toolkit.Wpf

[WPF自定义控件库]好用的VisualTreeExtensions的更多相关文章

  1. WPF 如何创建自己的WPF自定义控件库

    在我们平时的项目中,我们经常需要一套自己的自定义控件库,这个特别是在Prism这种框架下面进行开发的时候,每个人都使用一套统一的控件,这样才不会每个人由于界面不统一而造成的整个软件系统千差万别,所以我 ...

  2. [WPF自定义控件库] 关于ScrollViewer和滚动轮劫持(scroll-wheel-hijack)

    原文:[WPF自定义控件库] 关于ScrollViewer和滚动轮劫持(scroll-wheel-hijack) 1. 什么是滚动轮劫持# 这篇文章介绍一个很简单的继承自ScrollViewer的控件 ...

  3. [WPF自定义控件库]使用WindowChrome自定义RibbonWindow

    原文:[WPF自定义控件库]使用WindowChrome自定义RibbonWindow 1. 为什么要自定义RibbonWindow 自定义Window有可能是设计或功能上的要求,可以是非必要的,而自 ...

  4. [WPF自定义控件库] 让Form在加载后自动获得焦点

    原文:[WPF自定义控件库] 让Form在加载后自动获得焦点 1. 需求 加载后让第一个输入框或者焦点是个很基本的功能,典型的如"登录"对话框.一般来说"登录" ...

  5. [WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题

    1. 为什么选择Aero2 除了以外观为卖点的控件库,WPF的控件库都默认使用"素颜"的外观,然后再提供一些主题包.这样做的最大好处是可以和原生控件或其它控件库兼容,而且对于大部分 ...

  6. [WPF自定义控件库]简单的表单布局控件

    1. WPF布局一个表单 <Grid Width="400" HorizontalAlignment="Center" VerticalAlignment ...

  7. [WPF自定义控件库]使用WindowChrome的问题

    1. 前言 上一篇文章介绍了使用WindowChrome自定义Window,实际使用下来总有各种各样的问题,这些问题大部分都不影响使用,可能正是因为不影响使用所以一直没得到修复(也有可能别人根本不觉得 ...

  8. [WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互

    1. 前言 WPF有一个灵活的UI框架,用户可以轻松地使用代码控制控件的外观.例设我需要一个控件在鼠标进入的时候背景变成蓝色,我可以用下面这段代码实现: protected override void ...

  9. [WPF自定义控件库]为Form和自定义Window添加FunctionBar

    1. 前言 我常常看到同一个应用程序中的表单的按钮----也就是"确定"."取消"那两个按钮----实现得千奇百怪,其实只要使用统一的Style起码就可以统一按 ...

随机推荐

  1. 图片处理拓展篇 : 图片转字符画(ascii)

    首先要明确思路, 图片是由像素组成的, 不同的像素有不同的颜色(rgb), 那么既然我们要转化为字符画, 最直接的办法就是利用字符串来替代像素, 也就是用不同的字符串来代表不同的像素. 另外图片一般来 ...

  2. 数字证书原理 good

    文中首先解释了加密解密的一些基础知识和概念,然后通过一个加密通信过程的例子说明了加密算法的作用,以及数字证书的出现所起的作用.接着对数字证书做一个详细的解释,并讨论一下windows中数字证书的管理, ...

  3. FTPHelper

    转载自 :https://blog.csdn.net/jiankunking/article/details/50016043 using System; using System.Collectio ...

  4. Win8Metro(C#)数字图像处理--2.29图像除法运算

    原文:Win8Metro(C#)数字图像处理--2.29图像除法运算  [函数名称] 图像除法函数DivisionProcess(WriteableBitmap src, WriteableBit ...

  5. window下golang生成静态库给C语言调用

    buidmod为c-archive能在window下生成 ,c-shared则不行 1.golang生成c-archive静态库 main.go package main import "C ...

  6. Advanced Installer 打包后,安装包在WIN10下重启后再次运行安装的解决办法

    原文:Advanced Installer 打包后,安装包在WIN10下重启后再次运行安装的解决办法 前几个月使用Advanced Installer 打包了一堆安装包,其中有使用默认主题的,也有根据 ...

  7. nginx 负载均衡,多站点共享Session

    原文:nginx 负载均衡,多站点共享Session nginx 负载均衡,多站点共享Session 多站点共享Session常见的作法有: 使用.net自动的状态服务(Asp.net State S ...

  8. JS获取a标签的Href 内容

    <script type="text/javascript">function getHref(obj){ alert(obj.href);} </script& ...

  9. 使用MinGW编译Boost,MSVC编译Boost的几种链接方式 good

    1.下载Boost(http://www.boost.org) 我目前用的是1.61.0版本 2.将MinGW下的bin目录完整路径设置到系统环境变量Path中,保证cmd命令行能找到gcc,g++等 ...

  10. foreach() 中用指针指向数组元素,循环结束后最好销毁指针

    之前发过一次微博,今天又遇到这个问题,并且再次犯错,于是决定再加深一下. 就举php.net里的一个例子吧 $a = array('abe','ben','cam'); foreach ($a as ...