[WPF自定义控件库]好用的VisualTreeExtensions
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的工具类,下面是一些常见控件库的方案:
- WindowsCommunityToolkit的VisualTree
- Extended WPF Toolkit的VisualTreeHelperEx
- MahApps.Metro的TreeHelper
- Modern UI for WPF (MUI)的VisualTreeHelperEx
- WinRT XAML Toolkit 的VisualTreeHelperExtensions
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
8. 源码
VisualTreeExtensions.cs at master · DinoChan_Kino.Toolkit.Wpf
[WPF自定义控件库]好用的VisualTreeExtensions的更多相关文章
- WPF 如何创建自己的WPF自定义控件库
在我们平时的项目中,我们经常需要一套自己的自定义控件库,这个特别是在Prism这种框架下面进行开发的时候,每个人都使用一套统一的控件,这样才不会每个人由于界面不统一而造成的整个软件系统千差万别,所以我 ...
- [WPF自定义控件库] 关于ScrollViewer和滚动轮劫持(scroll-wheel-hijack)
原文:[WPF自定义控件库] 关于ScrollViewer和滚动轮劫持(scroll-wheel-hijack) 1. 什么是滚动轮劫持# 这篇文章介绍一个很简单的继承自ScrollViewer的控件 ...
- [WPF自定义控件库]使用WindowChrome自定义RibbonWindow
原文:[WPF自定义控件库]使用WindowChrome自定义RibbonWindow 1. 为什么要自定义RibbonWindow 自定义Window有可能是设计或功能上的要求,可以是非必要的,而自 ...
- [WPF自定义控件库] 让Form在加载后自动获得焦点
原文:[WPF自定义控件库] 让Form在加载后自动获得焦点 1. 需求 加载后让第一个输入框或者焦点是个很基本的功能,典型的如"登录"对话框.一般来说"登录" ...
- [WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题
1. 为什么选择Aero2 除了以外观为卖点的控件库,WPF的控件库都默认使用"素颜"的外观,然后再提供一些主题包.这样做的最大好处是可以和原生控件或其它控件库兼容,而且对于大部分 ...
- [WPF自定义控件库]简单的表单布局控件
1. WPF布局一个表单 <Grid Width="400" HorizontalAlignment="Center" VerticalAlignment ...
- [WPF自定义控件库]使用WindowChrome的问题
1. 前言 上一篇文章介绍了使用WindowChrome自定义Window,实际使用下来总有各种各样的问题,这些问题大部分都不影响使用,可能正是因为不影响使用所以一直没得到修复(也有可能别人根本不觉得 ...
- [WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互
1. 前言 WPF有一个灵活的UI框架,用户可以轻松地使用代码控制控件的外观.例设我需要一个控件在鼠标进入的时候背景变成蓝色,我可以用下面这段代码实现: protected override void ...
- [WPF自定义控件库]为Form和自定义Window添加FunctionBar
1. 前言 我常常看到同一个应用程序中的表单的按钮----也就是"确定"."取消"那两个按钮----实现得千奇百怪,其实只要使用统一的Style起码就可以统一按 ...
随机推荐
- Android数组和开发List之间的转换
1.List转换到一个数组.(这里List它是实体是ArrayList) 转让ArrayList的toArray方法. toArray public <T> T[] toArray(T[] ...
- Easyui Tab刷新
Easyui Tab刷新: function refreshTab(title){ var tab = $('#id').tab('getTab',title); $('#id').tab('upda ...
- web开发中../、./、/的区别
原文:web开发中../.././的区别 最近在业余时间慢慢玩起了网站开发,觉得挺有意思的.在开发过程中,老是分不清 ../.././三者之间的区别,也老是弄混,最后仔细搜索研究了一下,现在终于懂了. ...
- jquery mobile 笔记
1.navbar 相关 <nav data-role="navbar"> <ul> <li><a href="# ...
- WPF 4 Ribbon 开发 之 标签工具栏(Tab Toolbar)
原文:WPF 4 Ribbon 开发 之 标签工具栏(Tab Toolbar) 本篇将开始介绍标签工具栏的开发内容,标签工具栏可以说是Ribbon 的核心部分,应用程序所有的功能特性都会集中 ...
- 通通玩blend美工(7)——简约而不简单的块
原文:通通玩blend美工(7)--简约而不简单的块 最近在研发一个WPF快速开发框架,满脑子都是各种逻辑各种模式,写一篇比较休闲娱乐的博客,宣泄下我对美工的热爱. 我一直以来有意无意在手机应用或者各 ...
- 职业规划 - DREAM START
前言 最近面试了好多公司,得出一个结论:做一份详细的计划.一个程序员,不只是写写代码这么简单的事,一种更高的境界则是在代码中.系统的设计中,能找到人生的意义,简单说就是生活的道理.我一直认为:当你在一 ...
- 领域驱动设计(DDD)的实践经验分享之ORM的思考
原文:领域驱动设计(DDD)的实践经验分享之ORM的思考 最近一直对DDD(Domain Driven Design)很感兴趣,于是去网上找了一些文章来看看,发现它确实是个好东西.于是我去买了两本关于 ...
- is和==的区别以及编码和解码
一.is和==的区别 python中对象包含的三个基本要素分别是:id(身份标识),type(数据类型),value(值) is和==都是对对象进行比较判断的,但对对象比较判断的内容不同. ★==是p ...
- linux 十五个原理知识点
DNS系统架构与解析原理http协议通信原理TCP/IP的3次握手和四次断开原理MySQL主从同步原理Nginx配合php的fastcgi工作原理Lvs的4种模式工作原理Memcached工作原理(内 ...