[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起码就可以统一按 ...
随机推荐
- dotnetspider
http://www.cnblogs.com/modestmt/p/5525467.html nuget :DotnetSpider2.Core
- InnoSetup提升系统管理员权限(通过破解方式修改?)
PrivilegesRequired=admin 1 2 3 4 5 找到```INNO```安装目录下的```SetupLdr.e32```文件(其实就是一个exe程序),将程序中的```Man ...
- Win10《芒果TV》春季商店版更新v3.3.0:全新视觉蜕变&支持快男直播
在微软发布Win10创意者更新正式版前夕,Win10版<芒果TV>迅速更新至v3.3.0,主要是全新升级视觉交互,新增大咖快男个人直播,全面优化底层架构,启动大提速. Win10版< ...
- Oracle序列使用:建立、删除、使用
Oracle序列使用:建立.删除 在开始讲解Oracle序列使用方法之前,先加一点关于Oracle client sqlplus的使用,就是如果执行多行语句的话一定要加“/”才能表示结束,并执行!本篇 ...
- Qt-vs-addin失效的问题
Qt-vs-addin的小问题 使用Visual Studio进行Qt开发的时候,需要安装一个插件.然而有时候这个插件的一些工具却莫名其妙的失效: 其中qt5appwrapper.exe用于编辑Qt工 ...
- Android零基础入门第66节:RecyclerView点击事件处理
前面两期学习了RecyclerView的简单使用,并为其item添加了分割线.在实际运用中,无论是List还是Grid效果,基本都会伴随着一些点击操作,那么本期就来一起学习RecyclerView的点 ...
- CS224n笔记一:开端
何为自然语言处理 自然语言处理的目标是让计算机处理或者"理解"自然语言,以完成有意义的任务,如QA等. 自然语言处理涉及的层次 输入有两个来源:语音和文本,所以第一级是语音识别,O ...
- Qt 使用 Google Breakpad 捕获程序崩溃报告(dump文件) good
http://blog.csdn.net/GoForwardToStep/article/details/56685810
- Windows Mount NFS Share from e.g. Linux
Note: Not Stable, so steps below are for reference only ************ Linux Configuration NFS Share 1 ...
- Linux下的wfopen(手工打造)
Of Linux on wfopen (open wide-character version of the file name and mode) to achieve Not directly a ...