かずきのBlog@hatena

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

スクロールの制御をしたい

WPFで少しスクロールの制御をする必要がありました。ItemsControl系のコントロールでですね。ということで調べてみた感じこんな方法がありそうっていうのでメモ。

足場作り

まず、サンプルの説明用の足場を作ります。画面上部にStackPanelを置いて画面の下部にListBoxを置いただけのシンプル画面です。データを読み込むボタンと、各スクロール方法の処理を書いてるボタンが置いてあります。

<Window x:Class="ScrollSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <!-- ボタンのスタイル -->
        <Style TargetType="Button">
            <Setter Property="Margin" Value="2.5" />
            <Setter Property="MinWidth" Value="75" />
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="263*" />
        </Grid.RowDefinitions>
        <!-- ボタン置場 -->
        <StackPanel Name="stackPanel1" Orientation="Horizontal">
            <Button Content="Load" Name="buttonLoad" Click="buttonLoad_Click" />
            <Button Content="Scroll Center" Name="buttonScroll" Click="buttonScroll_Click" />
            <Button Content="Scroll Center2" Name="buttonScroll2" Click="buttonScroll2_Click" />
            <Button Content="Scroll Center3" Name="buttonScroll3" Click="buttonScroll3_Click" />
        </StackPanel>

        <!-- データ表示用ListBox -->
        <ListBox Name="listBox" Grid.Row="1" />
    </Grid>
</Window>

画面イメージは、以下のような感じになります。

表示用のデータは下記の名前だけをもったPersonクラスです。ListBoxでの表示用にToStringメソッドを定義しています。

public class Person
{
    public string Name { get; set; }
    public override string ToString()
    {
        return this.Name;
    }
}

データを読み込むためのbuttonLoad_Clickイベントハンドラを追加します。

private void buttonLoad_Click(object sender, RoutedEventArgs e)
{
    // とりあえず1000件表示
    this.listBox.ItemsSource = Enumerable.Range(1, 1000)
        .Select(i => new Person { Name = "田中 太郎 " + i })
        .ToArray();
}

これで、ボタンを押したらデータを表示するのが完成しました。実行してLoadボタンをぽちっとすると以下のようになります。

これをベースにスクロール処理を埋め込んでいきます。

ScrollIntoViewメソッド

これはListBoxに用意されてるメソッドで、引数で渡した要素が画面内に来るようにしてくれます。ListBox以外では使えない(もしかしたら同じ名前のメソッドが別クラスに定義されてるかも?)のが玉にきずですが、さくっとスクロールして精度を求められないなら楽ちんです。下記のようなコードになります。

private void buttonScroll_Click(object sender, RoutedEventArgs e)
{
    var data = this.listBox.ItemsSource as Person[];
    // とりあえず適当に真ん中へ
    this.listBox.ScrollIntoView(data[500]);
}

1000件あるデータの500番目のデータを表示しています。実行してボタンを押すと・・・。

こんな風に500番目(太郎 501さんですね)が画面に表示されます。表示されることは保障されますが、画面内の何処に表示されるかは保証されてないっぽいです。上のほうにいるかもしれないし、下のほうにいるかもしれないし。
わかりやすくしたかったら、選択状態にしてあげてもいいかもしれません。

UIAutomationを使う

UIAutomationを使うことでスクロールさせることもできます。UIAutomationを使うために、下記の2つを参照に追加します。

  • UIAutomationProvider
  • UIAutomationTypes

そして、スクロールを操作するためのIScrollProviderを下記のコードで取得します。

// ListBoxからAutomationPeerを取得
var peer = ItemsControlAutomationPeer.CreatePeerForElement(this.listBox);
// GetPatternでIScrollProviderを取得
var scrollProvider = peer.GetPattern(PatternInterface.Scroll) as IScrollProvider;
ちょっとだけスクロール/1ページスクロール

このIScrollProviderには、そのものズバリのScrollという名前のメソッドがあります。これはScrollAmountという列挙体の値を取ります。

LargeDecrement ページアップ
SmallDecrement 少し上/左にスクロール
NoAmount スクロールしない
LargeIncrement ページダウン
SmallIncrement 少し下/右にスクロール

例として、下に少しスクロールするコードを書いてみます。

private void buttonScroll2_Click(object sender, RoutedEventArgs e)
{
    // ListBoxからAutomationPeerを取得
    var peer = ItemsControlAutomationPeer.CreatePeerForElement(this.listBox);
    // GetPatternでIScrollProviderを取得
    var scrollProvider = peer.GetPattern(PatternInterface.Scroll) as IScrollProvider;
    // ちょっとスクロール
    scrollProvider.Scroll(
        // 水平方向にはスクロールしない
        ScrollAmount.NoAmount,
        // 垂直方向にはちょっとだけ下にスクロール
        ScrollAmount.SmallIncrement);
}

実行して、頑張って10回ボタンを連打した後の様子を以下に示します。

10行ぶんほどスクロールしていることが確認できます。

場所指定でのスクロール

この他に、場所を指定してスクロールすることもできます。SetScrollPercentというメソッドで水平方向と垂直方向を名前の通りパーセントで指定できます。真ん中なら50を指定するイメージです。ということで、垂直方向でど真ん中にスクロールするコード例は下記のようになります。
因みに、現在のスクロールの位置はHorizontalScrollPercentプロパティとVerticalScrollPercentプロパティで取得できます。

private void buttonScroll3_Click(object sender, RoutedEventArgs e)
{
    // ListBoxからAutomationPeerを取得
    var peer = ItemsControlAutomationPeer.CreatePeerForElement(this.listBox);
    // GetPatternでIScrollProviderを取得
    var scrollProvider = peer.GetPattern(PatternInterface.Scroll) as IScrollProvider;
    // パーセントで位置を指定してスクロール
    scrollProvider.SetScrollPercent(
        // 水平スクロールは今の位置
        scrollProvider.HorizontalScrollPercent,
        // 垂直方向はどまんなか!50%
        50.0);
}

実行してボタンを押すと以下のようになります。

縦スクロールのバーが真ん中にあるのが確認できますね。

まとめ

ということで、スクロールの方法について簡単にまとめてみました。ScrollViewerを直接触れるときは、それを使えばいいですがListBoxみたいに内部でScrollViewer使ってる人達を相手にするときは、用意されてるメソッドを使うか、今回のようにUIAutomationを使うことになるかと思います。

やろうと思えばVisualTreeをたどってScrollViewerを取得してゴニョゴニョとかも出来ますが、それはまた別のお話。