かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

WPF の TreeView で任意の項目が表示されるようにスクロールする

というネタを見つけたのでやってみます。久しぶりの WPF ネタ!因みにせっかくなので .NET Core 3.0 Preview 7 で VS2019 Preview 使ってやってみます。

表示用データはこれ!

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;

namespace TreeViewScrollSample
{
    public class Person
    {
        public string Name { get; set; }
        public IEnumerable<Person> Children { get; set; }
    }
}

画面が側はこんな感じでいきましょう。

<Window
    x:Class="TreeViewScrollSample.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:TreeViewScrollSample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>

        <Button Click="ScrollButton_Click" Content="Scroll" />
        <TreeView x:Name="treeView" Grid.Row="1">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                    <TextBlock Text="{Binding Name}" />
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </Grid>
</Window>

これで、こんな感じでコードを書けばスクロールします。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace TreeViewScrollSample
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private IEnumerable<Person> People { get; } = Enumerable.Range(1, 100)
            .Select(x => new Person
            {
                Name = $"Tanaka {x}",
                Children = Enumerable.Range(1, 10)
                    .Select(y => new Person
                    {
                        Name = $"Kimura {x}-{y}",
                    })
                    .ToArray(),
            })
            .ToArray();

        public MainWindow()
        {
            InitializeComponent();
            treeView.ItemsSource = People;
        }

        private async void ScrollButton_Click(object sender, RoutedEventArgs e)
        {
            // ContainersGenerated になるまで待つ
            Task waitUntilContainersGenerated(TreeViewItem container)
            {
                var tcs = new TaskCompletionSource<object>(container);
                void statusChanged(object _, EventArgs __)
                {
                    if (container.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
                    {
                        tcs.SetResult(null);
                        container.ItemContainerGenerator.StatusChanged -= statusChanged;
                        return;
                    }
                }

                container.ItemContainerGenerator.StatusChanged += statusChanged;
                return tcs.Task;
            }

            // 一番最後の最後の要素にスクロールする予定
            var parent = People.Last();
            var target = parent.Children.Last();

            // ツリービュー直下の最後の要素のTreeViewItemを取得
            var container = (TreeViewItem)treeView.ItemContainerGenerator.ContainerFromItem(parent);
            // 開く
            container.IsExpanded = true;
            // 子の ItemContainerGenerator のステータスが Generated になるまで待つ
            if (container.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
            {
                await waitUntilContainersGenerated(container);
            }

            // スクロール先を取得してスクロール
            var targetContainer = (TreeViewItem)container.ItemContainerGenerator.ContainerFromItem(target);
            targetContainer.BringIntoView();
        }
    }
}

実行すると…

f:id:okazuki:20190725111302g:plain

動いた!!

ソースコードは GitHub に上げておきました。

github.com