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. sql server & .net core 使用空间数据

    使用的库 Microsoft.EntityFrameworkCore.SqlServer Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuit ...

  2. 两个同名controller导致调用崩溃

    之前遇到一个很诡异的bug,大概情况如下: 生成成功,运行正常,调试正常 但是调用目标controller的目标方法,运行自动中断,调试自动中断 没有任何明确的错误提示,包括调试都没有弹窗报错 调用其 ...

  3. 属性更改通知(INotifyPropertyChanged)——针对ObservableCollection

    问题 在开发webform中,wpf中的ObservableCollection<T>,MSDN中说,在添加项,移除项时此集合通知控件,我们知道对一个集合的操作是CURD但是恰恰没有Upd ...

  4. WPF ContextMenu 在MVVM模式中绑定 Command及使用CommandParameter传参

    原文:WPF ContextMenu 在MVVM模式中绑定 Command及使用CommandParameter传参 ContextMenu无论定义在.cs或.xaml文件中,都不继承父级的DataC ...

  5. jquery map()的用法--遍历数组

    <!DOCTYPE html><html><head><meta http-equiv="Content-Type" content=&q ...

  6. Thinkphp模板开放给第三方编辑权限时,如何禁止模板使用php代码

    首先我要吐槽一个问题:为什么在博客园发布的文章总是被其他网站采集过去,而他们采集过去后,排名比博客园还好,比如这篇文章,我把标题复制到百度搜索,结果第一页的搜索结果全部都是采集我的,而我在博客园发布的 ...

  7. huawei 通过BGP的团体属性进行路由控制

    网络拓扑 XRV1的配置: =========================================================================== # sysname ...

  8. Qt中事件处理的顺序

    本站所有文章由本站和原作者保留一切权力,仅在保留本版权信息.原文链接.原文作者的情况下允许转载,转载请勿删改原文内容, 并不得用于商业用途. 谢谢合作.原文链接:Qt中事件处理的顺序 文章内容主要来自 ...

  9. 你需要了解的 C++ 17 Top 19 新特性(附精彩评论)

    什么是 C++17? C++17(或 C++1z)是继 C++14 之后 C++ 编程语言 ISO/IEC 标准的下一次修订的非正式名称.C++17 现在功能已齐全,正在成为国际标准的路上.它的规范已 ...

  10. 问题记录,Release模式和Debug运行效果不一样,Release必须加延时

    这个程序大体是这样一个逻辑,通过win32程序与设备交互,主线程先向设备发送命令要求 循环验证 然后一个线程专门负责接收设备返回信息 两边通过全局变量的变化来交流,主线程通过接收线程收到的信息设置界面 ...