酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

Windows PhoneでiOSのUIPageControllerもどきをPivotコントロールベースで作ってみる

Windows Phone Advent Calendar "ひとり" 2011」第27日目です。延長戦。

ご存知でしょうか?iOSにはUIPageControllerと呼ばれるコントローラーが存在しています。
UIPageControllerは複数のページ要素(UIViewController)を持ち、フリックでページ要素を切り替えることが出来ます。

下図は、ページ要素が2枚あり、強調されて表示されている円からは、アクティブになっている(現在表示されている)ページが2つめであることが分かります。

このUIPageControllerをWindows Phoneで表現してみましょう。先ほどフリックでページ要素を切り替えると言いました。この特徴はPivotそのものなので、PivotコントロールベースでUIPageContollerもどきを作ってみましょう。

まずはプロジェクトを作成します。Windows Phone ピボット アプリケーションを選択します。プロジェクト名は「PageControllerTest」としましょう。

次にExpression BlendでMainPage.xamlを編集します。

Pviotコントロールを上げて、画面下部にItemsControlを置きます。*1ItemsControlは、オブジェクトへのデータバインディングで得られるリストを表示することが出来ます。

UIPageControllerと同様にページ数を表す円は横向きに追加していくようにしたいので、ItemsControl.ItemsPanelのStyleを編集します。また外観を円にしたいのでItemsControl.ItemTemplateのStyleを編集します。MainPage.xamlは以下のようになります。

<phone:PhoneApplicationPage 
    x:Class="PageControllerTest.MainPage"
	・・・省略・・・
    shell:SystemTray.IsVisible="True">

    <Grid x:Name="LayoutRoot" Background="Transparent">
    	<Grid.RowDefinitions>
    		<RowDefinition/>
    		<RowDefinition Height="48"/>
    	</Grid.RowDefinitions>
		
        <controls:Pivot Title="マイ アプリケーション" Name="pivotMain" Margin="0">
	・・・省略・・・
        </controls:Pivot>
		
        <Grid x:Name="gridPageController" Margin="0" Grid.Row="1">
            <ItemsControl Name="pageController" ItemsSource="{Binding PivotItems}" 
			              HorizontalAlignment="Center">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Horizontal"/>
                    </ItemsPanelTemplate>        
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Ellipse Width="12" Height="12" Margin="15,0,15,0"
                                 Stroke="{Binding StrokeBrush}"
                                 StrokeThickness="1">
                            <Ellipse.Fill>
                                <SolidColorBrush Color="{Binding FillColor}"/>
                            </Ellipse.Fill>
                        </Ellipse>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
             </ItemsControl>
        </Grid>
		
    </Grid>
 
</phone:PhoneApplicationPage>

Expression Blendで編集した後のスクリーンショットです。IsSelectedが更新されるとStrokeBrushとFillColorも更新されるようにしています。

次に表示するアイテムを定義します。アクティブな(現在表示されている)PivotItemかどうかをプロパティに持つViewModelを「PivotItemViewModel」とします。

using System;
using System.ComponentModel;
using System.Windows.Media;

namespace PageControllerTest.ViewModels {

    public class PivotItemViewModel : INotifyPropertyChanged {

        // INotifyPropertyChangedの実装
        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(String propertyName) {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (null != handler) {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        private bool IsSelectedVal;

        /// <summary>
        /// 現在アクティブな(現在表示されている)表示されている
        /// </summary>
        public bool IsSelected {
            get { return IsSelectedVal; }
            set {
                if (value != IsSelectedVal) {
                    IsSelectedVal = value;
                    NotifyPropertyChanged("IsSelected");

                    NotifyPropertyChanged("StrokeBrush");
                    NotifyPropertyChanged("FillColor");

                }
            }
        }

        /// <summary>
        /// 円の枠線のブラシ
        /// </summary>
        public Brush StrokeBrush {
            get {
                var key = IsSelectedVal ? "PhoneAccentBrush" : "PhoneDisabledBrush";
                return (Brush)App.Current.Resources[key];
            }
        }

        /// <summary>
        /// 円の塗りつぶしの色
        /// </summary>
        public Color FillColor {
            get {
                var key = IsSelectedVal ? "PhoneForegroundColor" : "PhoneDisabledColor";
                return (Color)App.Current.Resources[key];
            }
        }
    }
}

PivotコントロールのSelectedIndexの変更を見張るViewModelを作成します。名前は「PivotPageControllerViewModel」としています。コンストラクタではPivotコントロールを受けて、先ほどのPivotItemViewModelをPivotItemの個数分生成します。

using System.Collections.Generic;
using System.Linq;
using System.Windows.Controls;
using Microsoft.Phone.Controls;

namespace PageControllerTest.ViewModels {
    
    public class PivotPageControllerViewModel {

        public PivotPageControllerViewModel(Pivot pivot) {
            PivotItems = new List<PivotItemViewModel>();

            _pivot = pivot;
            pivot.SelectionChanged += Pivot_SelectionChanged;

            for (int i = 0; i < pivot.Items.Count; i++) {
                PivotItems.Add(new PivotItemViewModel());
            }
            PivotItems[_pivot.SelectedIndex].IsSelected = true;
        }

        private Pivot _pivot;
        public List<PivotItemViewModel> PivotItems { get; set; }

        private void Pivot_SelectionChanged(object sender, SelectionChangedEventArgs e) {
            var selected = PivotItems.SingleOrDefault(p => p.IsSelected);
            if (selected != null) {
                selected.IsSelected = false;
            }
            PivotItems[_pivot.SelectedIndex].IsSelected = true;
        }
    }
}

Windows Phone ピボットアプリケーション テンプレートを使用してプロジェクトを作成すると、リストをロードしてListBoxに設定するように作られています。既存のコードはそのままにして、オーバーライドしたOnNavigatedToメソッドで、PageControllerViewModelインスタンスを生成し、ItemsControl型のpageControllerに設定しています。

using System.Windows;
using Microsoft.Phone.Controls;
using PageControllerTest.ViewModels;

namespace PageControllerTest {

    public partial class MainPage : PhoneApplicationPage {
        // コンストラクター
        public MainPage() {
            InitializeComponent();

            // ListBox コントロールのデータ コンテキストをサンプル データに設定します
            DataContext = App.ViewModel;
            this.Loaded += new RoutedEventHandler(MainPage_Loaded);
        }

        // ViewModel Items のデータを読み込みます
        private void MainPage_Loaded(object sender, RoutedEventArgs e) {
            if (!App.ViewModel.IsDataLoaded) {
                App.ViewModel.LoadData();
            }
        }

        protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e) {
            var viewModel = new PageControllerViewModel(pivotMain);
            this.pageController.DataContext = viewModel;

        }
    }
}

綺麗にバインディング出来ていると、このように2番目のPivotItemを表示している時に、円の2番目がアクセントカラーで表示されるようになります。

Pivotコントロールだけでは、PivotItemがいくつあるのかユーザーは直感的に理解できません。たとえば漫画などユーザーに「あと何ページで終わり」を直感的に知ってもらいたい場合に、このUIPageControllerもどきのTipsが使えるのではないでしょうか。

*1:ListBoxを使わない理由としては、選択不可能なリストコントロールとして扱いたいからです。