酢ろぐ!

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

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は以下のようになります。

|xml| <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」とします。

|cs| 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の個数分生成します。

|cs| 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に設定しています。

|cs| 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を使わない理由としては、選択不可能なリストコントロールとして扱いたいからです。