GridViewのスクロール位置を復元するには?(その1)[Win 8]WinRT/Metro TIPS

ほかの画面から元の画面に戻ってきたときに、GridViewコントロールの以前のスクロール位置を復元する方法として、ScrollViewerコントロールを使う方法を説明する。

» 2013年03月21日 16時22分 公開
[山本康彦BluewaterSoft]
WinRT/Metro TIPS
業務アプリInsider/Insider.NET

powered by Insider.NET

「WinRT/Metro TIPS」のインデックス

連載目次

 GridViewコントロールを使っているアプリの中には、ある画面からほかの画面に遷移してから元の画面に戻ってきたときに、以前のスクロール位置を復元するものがある。これはどのようにしたら実現できるのだろうか? その方法を2回に分けて解説する。

 本稿では、まず簡単な手法としてScrollViewerコントロールを使う方法を説明する。本稿のサンプルは「Windows Store app samples:MetroTips #29」からダウンロードできる。

事前準備

 Windows 8(以降、Win 8)向けのWindowsストア・アプリを開発するには、Win 8とVisual Studio 2012(以降、VS 2012)が必要である。これらを準備するには、第1回のTIPSを参考にしてほしい。本稿では64bit版Win 8 ProとVS 2012 Express for Windows 8を使用している。

スクロール位置を復元するアプリの例

 例えばWin 8に標準で付属する「ビデオ」アプリを見てみよう。アプリを起動した直後の画面(=トップ画面)は、グループ化したGridViewコントロール(Windows.UI.Xaml.Controls名前空間)を使っているようだ。そこで、トップ画面で適当な位置までスクロールしておいてから、[映画ストア]をクリックするなどしてほかの画面に移り、その後で[戻る]ボタンをクリックしてトップ画面に戻ると、以前と同じ位置にスクロールされている(次の図)。

「ビデオ」アプリの画面遷移 「ビデオ」アプリの画面遷移
詳細画面に遷移してからトップ画面に戻ると、スクロール位置が復元される。

 さらに、アプリの中には中断→終了*1した後に再び起動しても、スクロール位置が復元されるものがある。画面遷移に伴うスクロール位置の保存と復元だけならページのNavigationCacheModeプロパティを設定すれば対応できる*2が、アプリのライフ・タイムを越えてのスクロール位置の保存と復元はどのようにしたら実現できるのだろうか? いろいろな方法が考えられるが、本稿ではScrollViewerコントロール(Windows.UI.Xaml.Controls名前空間)を使った簡易な方法を説明する。

*2 ページのNavigationCacheModeプロパティを設定する方法は、「画面遷移する前の状態を保持するには?[Win 8]」を参照してほしい。


サンプル・アプリを作る

 今回は、VS 2012の「分割アプリケーション (XAML)」テンプレートから始めよう。プロジェクトを作成したら、動作確認時にトップ画面でスクロールを行えるようにサンプル・データの数を増やしておく。そのためには、「SampleDataSource.cs/.vb」ファイル内のSampleDataSourceクラスのコンストラクタの末尾に、次のようにしてダミー・データを追加するコードを記述する。

 public SampleDataSource()
{
  ……
 
  // ダミー・データを追加するコード
  for (int i = 7; i <= 20; i++)
  {
    string n = i.ToString();
    var g = new SampleDataGroup("Group-" + n,
                  "Group Title: " + n,
                  "Group Subtitle: " + n,
                  "Assets/DarkGray.png",
                  "Group Description: ");
    this.AllGroups.Add(g);
  }
}

 Public NotInheritable Class SampleDataSource
  ……
  Public Sub New()
    ……
    ' ダミー・データを追加するコード
    For i As Integer = 7 To 20
      Dim n As String = i.ToString()
      Dim g = New SampleDataGroup("Group-" + n,
                    "Group Title: " + n,
                    "Group Subtitle: " + n,
                    "Assets/DarkGray.png",
                    "Group Description: ")
      Me.AllGroups.Add(g)
    Next
  End Sub
End Class

ダミー・データを追加するコード(上:C#、下:VB)

 これでデータのグループが20個となる。サンプル・アプリをビルドして実行してみると次の画像のように横スクロールが可能になっているはずだ。なお、後で比較するためにGridViewコントロールの周囲に赤色で枠線を付けた。

サンプル・アプリの画面 サンプル・アプリの画面
「分割アプリケーション (XAML)」テンプレートのデータ・グループを増やしたもの。下端に横スクロールバーが出ている。GridViewコントロールの周囲に付けた赤枠が画面いっぱいに広がっていることから、データの表示位置はGridViewコントロールのPaddingプロパティで調整されている(GridViewコントロール内部にPaddingプロパティで指定されている空きを取り、データを表示している)と分かる。

 この状態でトップ画面(ItemsPage.xamlファイル)をスクロールした後で分割ページ(SplitPage.xamlファイル)に移動し、再度トップページに戻るなどして、スクロール位置が復元されないことを確かめておいてほしい。

ScrollViewerコントロールを使ってスクロール位置を復元するには?

 GridViewコントロールをScrollViewerコントロールの中に入れ、ScrollViewerコントロールのHorizontalOffsetプロパティとScrollToHorizontalOffsetメソッドを使って、スクロール位置の保存と復元を行う。

 まず、ItemsPage.xamlファイル(=起動時に表示される画面)を開き、次のようにScrollViewerコントロールを追加するとともに、GridViewコントロールのプロパティも変更する(太字の部分)。

 <ScrollViewer x:Name="scrollViewer1" Grid.RowSpan="2"
    BorderBrush="Yellow" BorderThickness="3"
    HorizontalScrollMode="Auto" HorizontalScrollBarVisibility="Auto"
    VerticalScrollMode="Disabled" VerticalScrollBarVisibility="Disabled">
  <GridView
    x:Name="itemGridView"
    AutomationProperties.AutomationId="ItemsGridView"
    AutomationProperties.Name="Items"
    TabIndex="1"
    Grid.RowSpan="2"
    Margin="116,136,116,46"
    ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
    ItemTemplate="{StaticResource Standard250x250ItemTemplate}"
    SelectionMode="None"
    IsSwipeEnabled="false"
    IsItemClickEnabled="True"
    ItemClick="ItemView_ItemClick"
    BorderBrush="Red" BorderThickness="3"
    SizeChanged="itemGridView_SizeChanged"
  />
</ScrollViewer>

ItemsPage.xamlファイルの変更箇所(XAML)
太字が追加/変更した部分。GridViewコントロールのプロパティは、「Padding」を「Margin」に変え、「SizeChanged」イベント・ハンドラを追加した。
また、比較のためにGridViewコントロールには赤色の外枠を、ScrollViewerコントロールには黄色の外枠を付けた。なお、GridViewコントロールのGrid.RowSpanプロパティの指定は不要になっているので、削除してしまってもよい。

 PaddingプロパティをMarginプロパティに変更しているのは、右端のデータ・アイテムの表示が欠けないようにするためである。変更しないとどうなるのかは、実際に試してみてほしい。

 また、SizeChangedイベント・ハンドラを追加しているが、これについては後述する。取りあえず空のSizeChangedイベント・ハンドラを実装して実行してみると、次の画像のようになる。

ScrollViewerコントロールを追加した画面 ScrollViewerコントロールを追加した画面
黄枠がScrollViewerコントロール、赤枠がGridViewコントロールである。先ほどとは異なり、データが表示されている範囲の外側がScrollViewerコントロールに含まれていることに注意。データの表示位置は、GridViewコントロールのMarginプロパティで設定している(Marginプロパティはこの場合、GridViewコントロールの境界線の外側にどれだけの空きを取るかを指定している)。ちなみに、ScrollViewerコントロールのPaddingプロパティで設定すると、スクロール範囲が狭くなってしまう(常に左右に空きが残ってしまう)。

 実行してみて、以前とほぼ同じように動作することを確かめておいてほしい*3

*3 GridViewコントロール上では、マウスのホイール操作でスクロールできなくなっていることに気付かれただろうか? 簡単に実装できる代わりに、この方法にはこのような欠点があるのだ。この欠点に対処するにはGridViewコントロール自体を改造するしかなさそうであり、その方法は次回に説明する。


 次に、画面が切り替えられたときにスクロール位置を保存するコードを追加する。ItemsPage.xaml.cs/.vbファイルに、次のようにSaveStateメソッドを記述する。ScrollViewerコントロールのHorizontalOffsetプロパティの値を、そのままリポジトリに保存するだけである。

 protected override void SaveState(Dictionary<string, object> pageState)
{
  base.SaveState(pageState);
  pageState["ScrollPosition"] = this.scrollViewer1.HorizontalOffset;
}

 Protected Overrides Sub SaveState(pageState As Dictionary(Of String, Object))
  MyBase.SaveState(pageState)
  pageState("ScrollPosition") = Me.scrollViewer1.HorizontalOffset
End Sub

スクロール位置を保存するためのコード(上:C#、下:VB)
ここで、文字列「"ScrollPosition"」はリポジトリに保存するときのキー名であって、好きな文字列でよい。

 スクロール位置を復元するのは、ちょっと面倒だ。ページのLoadStateメソッドで先ほど保存しておいた値を取り出しておき、GridViewコントロールのSizeChangedイベントでスクロール位置を設定する。

 まず、復元時に使用するスクロール位置を記憶しておくためのメンバ変数「_scrollPosition」を作り、LoadStateメソッドの末尾に次のコードを追加する。

 private double? _scrollPosition;
 
protected override void LoadState(Object navigationParameter,
                                  Dictionary<String, Object> pageState)
{
  ……
 
  if (pageState != null && pageState.ContainsKey("ScrollPosition"))
    _scrollPosition = pageState["ScrollPosition"] as double?;
}

 Private _scrollPosition As Double?
 
Protected Overrides Sub LoadState(navigationParameter As Object, _
                                  pageState As Dictionary(Of String, Object))
  ……
  If (pageState IsNot Nothing) _
      AndAlso pageState.ContainsKey("ScrollPosition") Then
    _scrollPosition = pageState("ScrollPosition")
  End If
End Sub

LoadStateメソッドの末尾に追加した、復元すべきスクロール位置を取得するコード(上:C#、下:VB)

 LoadStateメソッドが呼ばれた時点では、GridViewコントロールにデータは読み込まれておらず、従ってGridViewコントロールのサイズも小さいままなので、スクロール位置を設定できない(設定しても無視される)。そこで、データが読み込まれた後でGridViewコントロールのサイズが大きくなったときにSizeChangedイベントが発生することを利用する。次のようにして、GridViewコントロールのSizeChangedイベント・ハンドラでスクロール位置を設定するのだ。

 private void itemGridView_SizeChanged(object sender, SizeChangedEventArgs e)
{
  if (_scrollPosition.HasValue)
  {
    if (scrollViewer1.ScrollableWidth >= _scrollPosition.Value)
    {
      scrollViewer1.ScrollToHorizontalOffset(_scrollPosition.Value);
      _scrollPosition = null;
    }
  }
}

 Private Sub itemGridView_SizeChanged(sender As Object, e As SizeChangedEventArgs)
  If (_scrollPosition.HasValue) Then
    If (scrollViewer1.ScrollableWidth >= _scrollPosition.Value) Then
      scrollViewer1.ScrollToHorizontalOffset(_scrollPosition.Value)
      _scrollPosition = Nothing
    End If
  End If
End Sub

SizeChangedイベントでスクロール位置を設定するコード(上:C#、下:VB)

 これで完成だ。ほかの画面に遷移して戻ってきたときに、また、アプリを中断→終了させた後でまた起動したときに、スクロール位置が復元されることを確かめてほしい。

まとめ

 ScrollViewerコントロールの中にGridViewコントロールを入れることで、スクロール位置の保存と復元ができた。このScrollViewerコントロールの中に入れる手法は、ListViewコントロールやImageコントロールなどに広く応用できる。

「WinRT/Metro TIPS」のインデックス

WinRT/Metro TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。