やる気駆動型エンジニアの備忘録

WPF(XAML+C#)の話題を中心に.NET/Android/CI やたまに趣味に関するブログです

WPF でVisualTree のヒット テストを実行する

VisualTreeHelper を使用すると特定のコントロールのVisualTree 要素を検索したりできることは知っていたのですが、今まで使用する機会は殆どありませんでした。
今回使用したときに躓いた箇所も含めて、その使用方法をメモします。

以下、動作環境です。

VisualTreeHelper とは

WPF (と言うかXAML) には"LogicalTree" と"VisualTree" の2種類の要素ツリーが存在します。

  • LogicalTree は、論理ツリー つまりXAML で定義した内容で表されるツリーです。
  • VisualTree は、テンプレートなどの内容も含めた、その名の通りビジュアライズされた要素のツリーです。

"VisualTreeHelper" は、このVisualTree 上の要素を検索したりヒット テストを実施するためのヘルパー クラスです。

VisualTreeHelper でクリックした要素を一覧に出力する

今回はサンプルとして以下のような画面を作成しました。
画面上のクリックした位置に配置されているコントロールの型と名前を左のListBox に列挙するようにしています。
f:id:iyemon018:20170622181840p:plain

< MainWindow.xaml >

<Window x:Class="VisualTreeHelperSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:local="clr-namespace:VisualTreeHelperSample"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MainWindow"
        Width="800"
        Height="600"
        PreviewMouseDown="MainWindow_OnPreviewMouseDown"
        mc:Ignorable="d">
    <Grid x:Name="LayoutRoot">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <ListBox x:Name="TouchElementViewListBox"
                 Grid.Column="0"
                 MinWidth="200" />
        <Canvas x:Name="TouchCanvas" Grid.Column="1">
            <Border x:Name="PanelA"
                    Canvas.Left="151"
                    Canvas.Top="100"
                    Width="200"
                    Height="200"
                    Background="Red">
                <TextBlock x:Name="ContentA"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           FontSize="72"
                           Foreground="White"
                           Text="A" />
            </Border>
            <Border x:Name="PanelB"
                    Canvas.Left="311"
                    Canvas.Top="192"
                    Width="200"
                    Height="200"
                    Background="Green">
                <TextBlock x:Name="ContentB"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           FontSize="72"
                           Foreground="White"
                           Text="B" />
            </Border>
        </Canvas>
    </Grid>
</Window>

ライブビジュアルツリーはこのようになっています。
f:id:iyemon018:20170622181907p:plain

< MainWindow.xaml.cs >

private readonly List<DependencyObject> _hitResults = new List<DependencyObject>();

private void MainWindow_OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
    TouchElementViewListBox.ItemsSource = null;
    _hitResults.Clear();

    Point position = e.GetPosition(this);
    VisualTreeHelper.HitTest(this
                             , null
                             , new HitTestResultCallback(OnHitTestResultCallback)
                             , new PointHitTestParameters(position));

    TouchElementViewListBox.ItemsSource = _hitResults.OfType<FrameworkElement>()
                                                     .Select(x => $"{x.Name}:{x}")
                                                     .ToList();
}

private HitTestResultBehavior OnHitTestResultCallback(HitTestResult result)
{
    _hitResults.Add(result.VisualHit);
    return HitTestResultBehavior.Continue;
}

VisualTree 上の特定座標のオブジェクトを取得するには、"VisualTreeHelper.HitTest" を使用します。
ヒット テストについては、MSDN の以下のページがわかりやすいので一度ご確認ください。
ビジュアル層でのヒット テスト


このサンプルで"PanelA" と"PanelB" が重なっているあたりをクリックすると以下のようになります。
f:id:iyemon018:20170622183636p:plain

非表示のコントロールもHitTest の対象になる

いざ使ってみて気がついたのですが、クリックした箇所に非表示(Visibility = Visible.Collapsed, Opacity = 0 など)に設定したコントロールが配置されている場合、そのコントロールも"VisualTreeHelper.HitTest" のヒット テストの対象になります。
今回のようなクリックした要素を特定する場合はこの動作は予期しない動作となります。
そこで非表示のコントロールは"VisualTreeHelper.HitTest" から除外してしまいましょう。
以下の箇所を変更します。


< MainWindow.xaml.cs >

VisualTreeHelper.HitTest(this
                         , new HitTestFilterCallback(OnHitTestFilterCallback)
                         , new HitTestResultCallback(OnHitTestResultCallback)
                         , new PointHitTestParameters(position));
private HitTestFilterBehavior OnHitTestFilterCallback(DependencyObject target)
{
    UIElement element = target as UIElement;
    if (element != null)
    {
        if (element.Visibility != Visibility.Visible
            || element.Opacity <= 0)
        {
            return HitTestFilterBehavior.ContinueSkipSelfAndChildren; 
        }
    }
    return HitTestFilterBehavior.Continue;
}

"OnHitTestFilterCallback" でヒット テスト対象のオブジェクトをフィルターしています。
"HitTestFilterBehavior.ContinueSkipSelfAndChildren" としているのは、今回の場合、Border の"PanelB" が非表示ならその子要素の"ContentB" もヒットテスト対象外としたいからです。
こうしなければ"ContentB" をクリックするとヒット テストの対象となり、一覧に表示されてしまいます。
HitTestFilterBehavior の列挙値が表す内容については以下を参照してください。
HitTestFilterBehavior 列挙型 (System.Windows.Media)

これで実行すると以下のようになりました。
f:id:iyemon018:20170622185220p:plain

このように"VisualTreeHelper.HitTest" を使用すると簡単にVisualTree 上のヒット テストを行うことができます。
使い所は中々ありませんが、頭の片隅に覚えておくといざという時便利ですよ。

今回使用したサンプルはこちら↓↓↓
github.com