[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中,能夠在 這裏 查看源碼。html

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上也沒問題的就是以上的功能。使用代碼大體這樣:git

foreach (var item in this.GetVisualDescendants().OfType<TextBlock>())
{
}

3.使用問題

VisualTreeExtensions雖然好用,但仍是有些問題須要注意。github

3.1 不要在OnApplyTemplate中使用

FrameworkElement在生成當前模板並構造Visual Tree時會調用OnApplyTemplate函數,但這時候最好不要使用VisualTreeExtensions去獲取Visual Tree中的元素。所謂的最好,是由於WPF、Silverlight、UWP控件的生命週期有一些出入,我一時記不太清楚了,總之根據經驗運行這個函數的時候可能Visual Tree尚未構建好,VisualTreeHelper獲取不到子元素。不管個人記憶是否出錯,正確的作法都是使用 GetTemplateChild 來獲取ControlTemplate中的元素。windows

3.2 深度優先仍是廣度優先

<StackPanel Margin="8">
    <GroupBox Header="GroupBox" >
        <TextBox Margin="8" Text="FirstTextBox"/>
    </GroupBox>
    <TextBox Margin="8"
             Text="SecondTextBox" />
</StackPanel>

假設有如上的頁面,執行下面這句代碼:api

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 概述 這篇文檔。ui

不過ComboBox的Popup在邏輯樹中是存在的,若是ComboBoxItem想獲取ComboBox的VisualTree的祖先元素,能夠配合邏輯樹查找。this

3.4 查找根元素

GetVisualAncestors能夠方便地查找各級祖先元素,一直查找到根元素,例如要找到根元素能夠這樣使用:spa

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

原文出處:https://www.cnblogs.com/dino623/p/VisualTreeExtensions.html

相關文章
相關標籤/搜索