[WPF自定义控件库]好用的VisualTreeExtensions

2019-07-12 11:59:34 浏览数 (2)

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的功能

代码语言:javascript复制
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上也没问题的就是以上的功能。使用代码大致这样:

代码语言:javascript复制
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 深度优先还是广度优先

代码语言:javascript复制
<StackPanel Margin="8">
    <GroupBox Header="GroupBox" >
        <TextBox Margin="8" Text="FirstTextBox"/>
    </GroupBox>
    <TextBox Margin="8"
             Text="SecondTextBox" />
</StackPanel>

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

代码语言:javascript复制
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中的主要结构:

代码语言:javascript复制
<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可以方便地查找各级祖先元素,一直查找到根元素,例如要找到根元素可以这样使用:

代码语言:javascript复制
element.GetVisualAncestors().Last()

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

代码语言:javascript复制
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

Popup 概述 Microsoft Docs

8. 源码

VisualTreeExtensions.cs at master · DinoChan_Kino.Toolkit.Wpf

0 人点赞