1. 程式人生 > >[WPF自定義控制元件庫]好用的VisualTreeExtensions

[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

Popup 概述 Microsoft Docs

8. 原始碼

VisualTreeExtensions.cs at master · DinoChan_Kino.Toolkit.Wpf