xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

VisualTreeの子孫要素を取得する

非常に稀なことかもしれませんが、UIコントロールの奥底にある要素にアクセスして操作したい場合があります。例えば、DatePickerコントロールとして提供されているけれども、何かの都合でその中のTextBox部分をイジらなければならない、と言ったケースです。普段は奥底に潜んでいて触れない要素ですが、それを少し簡単に取得する方法を紹介します。

論理ツリーとビジュアルツリー

WPF、Silverlgiht、WindowsストアアプリのようなXAMLでUIを記述するアプリケーションは、内部がツリー構造になっています。このツリー構造には考え方が2種類あり、論理ツリー (Logical Tree) とビジュアルツリー (Visual Tree) と言います。論理ツリーは「XAMLで定義した階層構造」、ビジュアルツリーは「どんなVisual要素から成っているか (Visual基本クラスによって表されるオブジェクトの階層構造)」を示します。詳細は以下のドキュメントをご覧ください。

WPFのツリー

言葉では大変に分かりづらいので、視覚的に見てみましょう。以下のような特に意味のないテキトーなXAMLがあるとします。

<Window x:Class="WpfApplication.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="300" Width="300">
    <DockPanel>
        <TextBlock DockPanel.Dock="Top">TextBlock is here.</TextBlock>
        <StackPanel Orientation="Horizontal">
            <Button>
                <CheckBox Content="Checkbox in button." />
            </Button>
            <DatePicker SelectedDate="2013-10-28" />
        </StackPanel>
    </DockPanel>
</Window>

これを論理ツリーで確認すると次のようになります。上記のXAMLそのまんまでなのが分かります。

LogicalTree

次に、これをビジュアルツリーで確認してみます。たったあれだけのXAMLだったのに、見る気が失せるくらいの階層が出来ています。これは、ボタンやチェックボックスなどは実はもっとたくさんのビジュアル要素から成っているためです。つまり、ビジュアルツリーを辿れば論理ツリーに表れない内部の要素に触れることができるということです。

VisualTree

ここではツリー構造の確認にWPF Inspectorを利用しています。大変に便利なので、ぜひ一度使ってみてください。

WPF Inspector

Visual Treeの要素を取得する

ビジュアルツリーの操作はVisualTreeHelperクラスを利用して行うことができます。ただこれが結構に低級なAPIで、子要素の取得しかできません。しかもGetChildrenCountメソッドで子要素の数を取得し、GetChildメソッドを利用してインデックスアクセスで子要素を取るという、LINQはおろかforeachすらどこ行った的な残念さ...。なので、楽できるように補助メソッドを作りましょう。

public static class DependencyObjectExtensions
{
    //--- 子要素を取得
    public static IEnumerable<DependencyObject> Children(this DependencyObject obj)
    {
        if (obj == null)
            throw new ArgumentNullException("obj");

        var count = VisualTreeHelper.GetChildrenCount(obj);
        if (count == 0)
            yield break;

        for (int i = 0; i < count; i++)
        {
            var child = VisualTreeHelper.GetChild(obj, i);
            if (child != null)
                yield return child;
        }
    }

    //--- 子孫要素を取得
    public static IEnumerable<DependencyObject> Descendants(this DependencyObject obj)
    {
        if (obj == null)
            throw new ArgumentNullException("obj");

        foreach (var child in obj.Children())
        {
            yield return child;
            foreach (var grandChild in child.Descendants())
                yield return grandChild;
        }
    }

    //--- 特定の型の子要素を取得
    public static IEnumerable<T> Children<T>(this DependencyObject obj)
        where T : DependencyObject
    {
        return obj.Children().OfType<T>();
    }

    //--- 特定の型の子孫要素を取得
    public static IEnumerable<T> Descendants<T>(this DependencyObject obj)
        where T : DependencyObject
    {
        return obj.Descendants().OfType<T>();
    }
}

これを使えば、次のように簡単に子要素や子孫要素を取得できるようになります。やっぱりLINQですよね!

var button = window                  //--- Windowの
           .Descendants<Button>()    //--- ボタン型の子孫要素のうち
           .Where(x => x.IsEnabled)  //--- 有効なボタンの
           .FirstOrDefault();        //--- 最初に見つかったものを取得