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. UVE开发环境搭建及项目启动

    1.IDE安装visual studio code ,略: 2.node安装(node-v10.5.0-win-x64.zip),解压即可: 3.配置node环境变量,cmd 输入node -v.np ...

  2. iPhone开发笔记(20)EGOImageView的使用方法及注意事项

    EGOImageView是一种实现网络图片的异步加载和缓存的第三方类库,具有相同功能的第三方类库还有SDWebImage.但是相比两个类库的安装和使用来说,EGOImageView更简单一些,下面就介 ...

  3. WPF DataGrid自定义列DataGridTextColumn.ElementStyle和DataGridTemplateColumn.CellTemplate

    <Window x:Class="DataGridExam.MainWindow"        xmlns="http://schemas.microsoft.c ...

  4. Android Camera2 拍照(三)——切换摄像头,延时拍摄和闪光模式

    原文:Android Camera2 拍照(三)--切换摄像头,延时拍摄和闪光模式 一.切换摄像头 在前后摄像头之间切换,首先需要关闭之前打开的摄像头,关闭preview,之后重新打开新的摄像头,重新 ...

  5. 自绘LISTVIEW的滚动条(Delphi实现)

    因项目需要准备对LISTVIEW的滚动条进行自绘.于是在网上搜了一下,问题没解决,却搜出一篇令人不愉快的帖子 .确实,那时候实力是不够的,但现在应该是没问题了,为这个目的才不断磨练自己的. LISTV ...

  6. ArcGIS数据生产与精细化制图之中国年降水量分布图的制作

    原文:ArcGIS数据生产与精细化制图之中国年降水量分布图的制作 楼主按:在今年的Esri中国用户大会上,我听了几场关于ArcGIS用于制图方面的讲座,也在体验区与Esri中国的技术老师有一些交流.一 ...

  7. /etc/passwd和/etc/group文件详解

    用户管理 想要知道, 系统中有哪些用户, 可以查看这个文件: /etc/passwd root:x:::root:/root:/bin/bash bin:x:::bin:/bin:/sbin/nolo ...

  8. 使用VC2005编译真正的静态Qt程序 good

    首先,你应该该知道什么叫静态引用编译.什么叫动态引用编译.我这里只是简单的提提,具体的可以google一下. 动态引用编译,是指相关的库,以dll的形式引用库.动态编译的Exe程序尺寸比较小,因为相关 ...

  9. Android零基础入门第89节:Fragment回退栈及弹出方法

    在上一期分享的文章末尾留了一个课后作业,有去思考如何解决吗?如果已经会了那么恭喜你,如果还不会也没关系,本期一起来学习. 一.回退栈 在前面两期的示例中,当我们完成一些操作后,如果想要回到操作之前的状 ...

  10. 用Delphi实现文件下载的几种方法(三种使用控件的方法)

    有个API就是UrlDownloadToFile.不仅如此,Delphi的一些控件也可以轻松实现下载,如NMHTTP,指定NMHTTP1.InputFileMode := ture; 指定Body为本 ...