この記事では、「プログラミング Windows 第6版」を使って WPF XAML の学習を支援することを目的にしています。この目的から、書籍と併せて読まれることをお勧めします。
第12章 ページとナビゲーション
本章では、WinRT XAML におけるページとナビゲーションを説明しています。この説明は、XAML 系の UI 技術が提供するナビゲーション メカニズムを採用するのであれば、WPF XAML にも当てはまることが多くなります。もちろん、違いも存在します。最初に、説明するのはどのようなナビゲーションを使用するかというころです。
- WinRT XAML では、Frame クラスを使ったナビゲーションを採用した方が良いでしょう。
この理由は、ページの進むや戻るという実装が容易になるからです。 - WPF XAML では、Windows Forms と同じようにウィンドウを基準にするのであれば、プログラマーが自由に設計します。
WinRT XAML と同じように Frame クラスを使ったナビゲーションを利用することもできますが、この場合は Window クラスではなく NavigationWindow クラスを使用するという制約があります。
Visual Studio 2013 が提供するプロジェクト テンプレートには、次に示す 4 種類があります。
- WPF アプリケーション
Window クラスを継承する MainWindow.xaml が含まれます。 - WPF ブラウザー アプリケーション
Page クラスを継承する Page1.xaml が含まれます。 - WPF ユーザー コントロール ライブラリ
ユーザー コントロールのクラス ライブラリ(DLL)を作成するプロジェクトです。 - WPF カスタム コントロール ライブラリ
カスタム コントロールのクラス ライブラリ(DLL)を作成するプロジェクトです。
ナビゲーションを使用する WPF アプリケーションの場合は、WPF アプリケーションで作成されるファイル(MainWindow.xaml と MainWindow.xaml.cs)の Window クラスを NavigationWindow クラスへ書き換える必要があります。この理由は、ナビゲーション可能なページは Page クラスを継承したものであり、Page クラスを表示できるウィンドウが NavigationWindow という制約があるためです。
WPF ブラウザー アプリケーション(XBAP)は、ブラウザー内で実行する WPF アプリケーションで、ブラウザーが Page クラスを表示するウィンドウの役割を持つものになります。詳しく知りたい場合は、ナビゲーションの概要ドキュメントをお読みください。
ここまで説明すると Windows ストア アプリの Page クラスを表示するためにウィンドウがあるのかないのかという点が、疑問になることでしょう。Windows ストア アプリの場合は、内部的に CoreWindowというウィンドウが作成されて、この CoreWindow内でナビゲーション メカニズムがサポートされるという仕組みになっています。
※Windows ストア アプリでは、HTML/JavaScript でもアプリを開発することができます。このアプリも内部的には CoreWindowを使用しています。
12.1 画面解像度の問題
本節では、DPI 設定によって解像度がどのようになるかを説明しています。基本的な考え方は WPF XAML でも同じになりますが、WinRT と WPF ではサンプルの WhatRes プロジェクトに対するアプローチを変更しなければなりません。最初に、WhatRes プロジェクトの MainWindow.xaml の抜粋を示します。
<Window ... ><Grid><TextBlock x:Name="textBlock" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="24" /></Grid></Window>
この XAML は、Page を Window に変更したのと組み込みのスタイルを除けば WinRT と同じになります。WhatRes プロジェクトが WPF XAML に対応させるための大きな変更は、WinRT 固有の機能である DisplayPropeties クラスなどを変更することです。それでは、 MainWindow.xaml.cs の抜粋を示します。
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.SizeChanged += OnMainPageSizeChanged; this.Loaded += (s, args) => { UpdateDisplay(); }; } void OnMainPageSizeChanged(object sender, SizeChangedEventArgs e) { UpdateDisplay(); } void UpdateDisplay() { var primaryMonitor = System.Windows.Forms.Screen.PrimaryScreen; var dpi = GetDPI(); textBlock.Text = String.Format("Window size = {0} x {1}\r\n" + "Screen size = {2} x {3}\r\n" + "DPI = {4}\r\n" + "Bit / Pixel = {5}\r\n", this.ActualWidth, this.ActualHeight, primaryMonitor.Bounds.Width, primaryMonitor.Bounds.Height, dpi.X, primaryMonitor.BitsPerPixel); } Point GetDPI() { var result = new Point(); WindowInteropHelper whnd = new WindowInteropHelper(Application.Current.MainWindow); System.Drawing.Graphics graphics = System.Drawing.Graphics.FromHwnd(whnd.Handle); result.X = graphics.DpiX; result.Y = graphics.DpiY; return result; } }
この コードの WinRT XAML に対する変更点を次に示します。
- コンストラクタから、DisplayProperties クラスの使用を削除。
- GetDpi メソッドの追加。
System.Drawing.Graphics.FromHwnd メソッドによって、System.Drawing.Graphics オブジェクトを取得。
System.Drawing.Graphics オブジェクトを使って DPI を Point 構造体で返す。 - UpdateDisplay メソッドのコードを変更。
System.Windows.Forms.Screen.PrimaryScreen プロパティを使って、画面サイズを取得。
出力内容を変更。 - Windows Forms 用のアセンブリの参照設定を追加。
System.Windows.Forms と System.Drawing。
表示されている値を変更していますが、Windows Forms のアセンブリを使用することで DPI などを表示することができました。
ここでは、デスクトップ アプリにおける DPI 設定を少しだけ説明します。詳しい説明は、Writing DPI-Aware Desktop and Win32 Applicationsを参照してください。最初に DPI と スケーリングの関係を示します。
DPI | スケーリング |
96 | 100% |
120 | 125% |
144 | 150% |
192 | 200% |
Windows 8/8.1 以降で DPI 設定を変更するには、ディスプレイのプロパティを使用します。最初に、ディスプレイごとに DPI が設定できる場合を示します。
ディスプレイのプロパティに「小さくする から 大きくする というスライダー」があるのが、分かります。このスライダーを変更しても、サインオフしなおさないとプログラムが取得できる DPI の値は変更されない点に注意が必要です。今度が、「すべてのディスプレイで同じ拡大率を使用する」がチェックされている場合を示します。
この設定では、スケーリングに応じてラジオボタンが表示されています。そして、スケーリングを変更するとサインオフのしなおしを促します。つまり、プログラムが API によって取得できる DPI の値を適切に取得するには、DPI 変更後のサインオフが必須となります。一方で Windows シミュレータはどうでしょうか。私が試した限りでは、DPI の値を変更しているのではなく、Windows ストア アプリに対してだけ疑似的に DPI 変更をシミュレーションしているように思えます。つまり、DPI 変更に対応するプログラムのテストにおいては、Windows シミュレータを使うことはできないという制約があるということになります。
書籍には、DPI がスケーリングされることでどのような状況が起きるかについて詳しく説明しています。もし、高 DPI 対応などを考えている場合は、熟読をお願いします。
12.2(P597) スケーリングの問題
本節では、WinRT XAML におけるスケーリングの注意点を説明しています。結論から言えば、Windows ストア アプリではスケーリングに応じて自動的に画像ファイルなどのリソースに対して適切なサイズが選択されるようになっています。アプリ側では、スケーリングに応じた画像を用意するだけになっています。画像ファイル名が「name.拡張子」だとすれば、次のような命名規則なっています。
スケーリング | ファイル名 | サブフォルダー |
100% | name-scale-100.拡張子 | scale-100\name.拡張子 |
140% | name-scale-140.拡張子 | scale-140\name.拡張子 |
180% | name-scale-180.拡張子 | scale-180\name.拡張子 |
どちらの命名規則を使ったとしても、プログラムからは「name.拡張子」でアクセスできるようになっています。
WPF XAML においては、フォントなどは自動的にスケーリングされますが、画像などをスケーリングに応じて自動的に選択する機能はありません。このため、スケーリングに応じて画像などを入れ替える場合は、DPI などから表示する画像を選択するロジックの開発が必要となります。DPI を取得する��ジックは、12.1 の WhatRes サンプルで提示しています。このような事情で、書籍のサンプルである AutoImageSelection プロジェクトを移植していません。移植するにしても、必要な情報をすべて説明していますので、自分でチャレンジしてみてください。
12.3(P601) スナップ ビュー
本節では、Windows ストア アプリに固有となるビューであるスナップ(Windows 8.1 ではウィンドウ化モード)を説明しています。このようなビューの状態を判定できるのは、WinRT 固有の機能であり、WPF XAML には無い機能になります。従って、WhatSnap プロジェクトも WPF XAML へ移植していません。WhatSnap プロジェクトを移植しようとすれば、表示されている画面サイズに応じて処理を行うということになります。これは、デスクトップ アプリから考えるとウィンドウのリサイズへ対応するということでしかありません。ウィンドウ リサイズへ対応で、ある閾値によって適切なレイアウトに構成し直すことでスナップ ビューのようなビューへの対応を行うことができると言えるでしょう。
12.4(P607) 画面の向き
本節では、画面の向きに応じて制御する方法を説明しています。この中で使用している CurrentOrientation プロパティなどを含めて WinRT 固有の機能になりました。従って、WPF XAML で異なる手法を利用しないと、画面の向きを判断することはできません。この理由から、NativeUp プロジェクトも移植していません。それでは、WPF XAML で画面の向きを判断する手法の幾つかを示します。
- 画面サイズの縦横を比較して判断する。
- システム イベントの DisplaySettingsChangedイベントで処理する(もしくは、WM_DISPLAYCHANGED メッセージを ウィンドウ プロシージャで処理します)。
これらの説明が、「Detecting Screen Orientation and Screen Rotation in Tablet PC Applications」という ドキュメントに記載されていますので、参考にして対応すれば良いことになります。
12.2 スケーリングの問題から本節までを振り返ってみれば、Windows ストア アプリに対してはスケーリング、画面サイズの変更、画面の向きの変更に対して自動的に対応する仕組みが用意されているということです。WPF XAML などのデスクトップ アプリの世界では、これらの対応はプログラマに任されているということになります。従って、スケーリング、画面サイズの変更、画面の向きの変更に対して対応するかどうかを、実装者が判断しなければなりません。もちろん、Windows 8 対応のタブレット PCなどの導入が決まっている場合は、対応させた方が良いでしょう。その代わり、新しいレイアウトや対応コードなどの追加のコストが掛かりますが、利用するユーザーが好きなスタイルでタブレット PC を使えるようになるというメリットも生まれます。
12.5(P610) 簡単なナビゲーション
本節では、WinRT XAML が提供するナビゲーション フレームワークを説明しています。本章の冒頭で簡単に説明しましたが、WPF XAML でも基本的な考え方は同じになりますが、WinRT XAML との違いもあります。WPF XAML では、Page クラスを使用したナビゲーションには、NavigationWindow クラスか XBAP(XAML ブラウザー アプリケーション)である必要があります。そして、最初にページを指定するには、NavigationWindow クラスの Source プロパティにページを定義した XAML のファイル名を指定します(もしくは、Navigate メソッドを使用します)。Source プロパティでナビゲーション先のページを指定するのは、WPF XAML 固有であり、WinRT XAML にはありません。今度は、Navigate メソッドのシグネチャを示します。
- this.Frame.Navigate(pageType):WinRT XAML
- NavigationService.Navigate(pageObject):WPF XAML
WinRT XAML では、引数にページの型オブジェクトを指定し、WPF XAML ではページ オブジェクトのインスタンスを指定します。また、Navigate メソッドを提供するのも、WinRT であれば Frame オブジェクトであり、WPF XAML では NavigationService クラスという違いもあります。それでは、SimpleNavigation プロジェクトの MainWindow.xaml の抜粋を示します。
<NavigationWindow x:Class="SimplePageNavigation.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="SimplePageNavigation" Height="350" Width="525" WindowState="Maximized" Source="FirstPage.xaml"></NavigationWindow>
この XAML は、WPF XAML に固有な NavigationWindow を使用して最初のページとして「FirstPage.xaml」を指定しています。そして、FirstPage.xaml が 書籍の MainPage.xaml に相当しますので、抜粋を示します。
<Page ... ><Grid><StackPanel><TextBlock Text="Main Page" FontSize="48" HorizontalAlignment="Center" Margin="48" /><TextBox x:Name="txtbox" Width="320" FontSize="20" HorizontalAlignment="Center" Margin="48" /><Button Content="Go to Second Page" HorizontalAlignment="Center" Margin="48" Click="OnGotoButtonClick" /><Button x:Name="forwardButton" Content="Go Forward" HorizontalAlignment="Center" Margin="48" Click="OnForwardButtonClick" /><Button Name="backButton" Content="Go Back" HorizontalAlignment="Center" Margin="48" Click="OnBackButtonClick" /></StackPanel></Grid></Page>
この XAML は、組み込みのスタイルを除けば WinRT XAML の MainPage.xaml と同じになります。それでは、FirstPage.xaml.cs の抜粋を示します。
public partial class FirstPage : Page { public FirstPage() { InitializeComponent(); this.Loaded += (s, args) => { forwardButton.IsEnabled = this.NavigationService.CanGoForward; backButton.IsEnabled = this.NavigationService.CanGoBack; }; } private void OnGotoButtonClick(object sender, RoutedEventArgs e) { this.NavigationService.Navigate(new Uri("SecondPage.xaml", UriKind.RelativeOrAbsolute)); } private void OnForwardButtonClick(object sender, RoutedEventArgs e) { this.NavigationService.GoForward(); } private void OnBackButtonClick(object sender, RoutedEventArgs e) { this.NavigationService.GoBack(); } }
このコードと WinRT XAML の MainPage.xaml.cs との違いを示します。
- OnNavigatedTo メソッドは WinRT XAML 固有。
Loaded イベント ハンドラーに置き換え。 - Navigate メソッドを NavigationService の静的メソッドに置き換え。
ここでは、Uri 表記を使用。 - Frame クラスを NavigationService クラスへ置き換え。
SecondPage.xaml は、組み込みスタイルを除けば WinRT XAML と同じなので、SecondPage.xaml.cs の OnGotoButtonClick イベント ハンドラーを示します。
private void OnGotoButtonClick(object sender, RoutedEventArgs e) { var frame = new Frame(); this.NavigationService.Navigate(new FirstPage()); }
このコードの変更点は、FirstPage.xaml.cs で説明したことと同じであり、今度は Navigate メソッドにページのインスタンスを指定している点が、FirstPage.xaml.cs と異なる箇所になります。これは、Navigate メソッドのオーバーロードを説明するために、このようにしています。それでは、実行結果を示します。
画面の上にあるナビゲーション バーと「Go Forward」ボタン、「Go Back」ボタンが無効になっています。「Go To Second Page」ボタンをクリックした結果を示します。
今度は、画面の上にあるナビゲーション バーと「Go Back」ボタンが有効になりました。これは、ページ ナビゲーション履歴が格納されたことが理由であり、このボタンの有効化と無効化を設定するために Loaded イベント ハンドラーを指定しています。
this.Loaded += (s, args) => { forwardButton.IsEnabled = this.NavigationService.CanGoForward; backButton.IsEnabled = this.NavigationService.CanGoBack; };
書籍では、このコードをデータ バインドで説明しています。もちろん、NavigationService をデータ バインドすると、正常に動かないこともありますので、WPF では標準のナビゲーション バー(画面の上)を利用するのが良いでしょう。今度が、Navigate メソッドを呼び出した結果として発生するイベント シーケンスを説明します。最初に、WinRT XAML の Navigate メソッドを示します。
WinRT XAML では、ナビゲート先のページのインスタンスが作成されてから、OnNavigatedTo が呼び出されて、呼び出し元のページで OnNavigatedFrom メソッドが呼び出されてます。この OnNavigatedTo と OnNavigatedFrom メソッドは、WinRT XAML に固有なメソッドです。今度は、WPF XAML の Navigate メソッドを示します。
WPF XAML では、ナビゲート先のインスタンスが作成されてから(Uri を指定した場合)、ナビゲート先の Load イベントが発生し、呼び出し元のページで Navigated イベントが発生します。この Navigated イベントに渡される引数は、ナビゲート先のページになります。WPF XAML では、ナビゲートが行われる時に呼び出される Navigated イベント は、ナビゲート元のページで発生するようになっています。この点は、WinRT XAML と大きく違う点になります。これは、WPF XAML でナビゲーション システムを提供してから、フィードバックなどから使い易くするために改良された結果だと考えられます。必ずしも正確では無いかもしれませんが、Navigate メソッドをもう少し説明すると、引数にはページ インスタンスか、Uri を指定することができます。つまり、ページ インスタンスが作成された時点では、必ずしも NavigationService が設定されていなことを意味しています。この理由で、Load イベントで this.NavigationService.CanGoForward のように記述していました。これらは、WPF というフレームワークが自動的に NavigationService と Page クラスを関連付ける癖のようなものなので、このように考えて頂ければ結構です。
12.6(P616) バック スタック
本節では、ナビゲーションがなぜ進むや戻るを実現できるのかを説明しています。バック スタックは、Windows 8.1 では容易なアクセスが可能になるような改良がおこなわれています。WPF XAML では、RemoveBackEntry メソッドで最新の履歴の削除、AddBackEntry メソッドによる独自の戻るエントリーの追加のみができるようになっています。どのようなエントリーを追加するかは、開発者が CustomContentClass 抽象クラスを継承した実装クラスを作成しなければなりません。
ナビゲーション履歴に関しては、WPF よりも WinRT XAML の方��使いやすくなっています。これは、WinRT XAML はナビゲーションが必須であることからナビゲーション システムの使い難さを始めとして様々な改良が行われたと考えることができます。
12.7(P619) ナビゲーション イベントとページの復元
本節は、次節に共通しますが Windows ストア アプリのプロセス状態に基づいてページ状態を保存するための手法を説明しています。Windows ストア アプリでは、プロセスが休止し、システム リソースが不足すればプロセスが終了します。プロセスが終了した後に、ユーザーが同じアプリを起動すれば、同じ状態をユーザーに提示することがアプリに求められます。つまり、最後に表示されていたページの状態を復元することが求められます。このための手法を説明しています。
WPF XAML では、プロセスが強制的に終了させられるようなことはありません。ユーザーとの対話操作の中で、終了操作などがあるだけになります。つまり、ページの復元なども必要がなければ何も考慮しないということになります。書籍の説明は、考え方としては WPF XAML でも応用できますので、状態を保存する手法として学習するのであれば、書籍を参照してください。
12.8(P623) アプリケーション状態の保存と復元
本節では、前節で説明したWindows ストア アプリのプロセス状態に基づいてページ状態を保存するための手法を説明しています。
従って、前節と同じで WPF XAML では気にする必要がありませんから、考え方を応用するという感覚での学習をお願いします。
12.9(P628) ナビゲーション アクセラレーターとマウス ボタン
本節では、アクセラレータとマウス ボタンによるナビゲーションを説明しています。WPF XAML では、OnKeyDown イベントや OnMouseLeftButton、OnMouseRightDown イベント などを組み合わせて実現します。つまり、キーボード操作やマウス操作によりナビゲーションを実装する場合に、実装するということです。実装する場合に、書籍に記載されている内容を参考にすることができます。
12.10(P631) データの受け渡し
本節では、ページ間でデータを受け渡す方法を説明しています。考え方は、WPF XAML にも利用できますが、異なる箇所もありますので、この点は順を追って説明します。それでは、DataPassingAndReturning プロジェクトの MainWindow.xaml の抜粋を示します。
<NavigationWindow ... Source="MainPage.xaml" ShowsNavigationUI="True" ... ></NavigationWindow>
この XAML は、WPF XAML に固有の NavigationWindow の定義です。ここでは、意図的に ShowsNavigation プロパティを設定しています。このプロパティは、ウィンドウの上部に表示されるナビゲーション バーの表示を抑制する時に「False」を設定します。それでは、DialogPage.xaml の抜粋を示します。
<Page ... ><Grid><StackPanel><TextBlock Text="Color Dialog" FontSize="48" HorizontalAlignment="Center" Margin="48" /><StackPanel x:Name="radioStack" HorizontalAlignment="Center" Margin="48"><RadioButton Content="Red" Margin="12"><RadioButton.Tag><Color>Red</Color></RadioButton.Tag></RadioButton><RadioButton Content="Green" Margin="12"><RadioButton.Tag><Color>Green</Color></RadioButton.Tag></RadioButton><RadioButton Content="Blue" Margin="12"><RadioButton.Tag><Color>Blue</Color></RadioButton.Tag></RadioButton></StackPanel><Button Content="Finished" HorizontalAlignment="Center" Margin="48" Click="OnReturnButtonClick" /></StackPanel></Grid></Page>
この XAML は、組み込みスタイルを除けば WinRT と同じになります。今度は、MainPage.xaml の抜粋を示します。
<Page ... ><Grid x:Name="contentGrid"><StackPanel><TextBlock Text="Main Page" FontSize="48" HorizontalAlignment="Center" Margin="48" /><StackPanel x:Name="radioStack" HorizontalAlignment="Center" Margin="48"><RadioButton Content="Red" Margin="12"><RadioButton.Tag><Color>Red</Color></RadioButton.Tag></RadioButton><RadioButton Content="Green" Margin="12" IsChecked="True"><RadioButton.Tag><Color>Green</Color></RadioButton.Tag></RadioButton><RadioButton Content="Blue" Margin="12"><RadioButton.Tag><Color>Blue</Color></RadioButton.Tag></RadioButton></StackPanel><Button Content="Get Color" HorizontalAlignment="Center" Margin="48" Click="OnGotoButtonClick" /></StackPanel></Grid></Page>
この XAML は、組み込みスタイルを除けば WinRT XAML と同じになります。今度は、ページ間で受け渡すデータである PassData.cs を示します。
using System.Windows.Media; namespace DataPassingAndReturning { public class PassData { public Color InitializeColor { set; get; } } }
このコードは、名前空間を除けば WinRT XAML と同じになります。今度は、DialogPage から MainPage へ返されるデータである ReturnData.cs を示します。
using System.Windows.Media; namespace DataPassingAndReturning { public class ReturnData { public Color ReturnColor { set; get; } } }
このコードも、名前空間を除けば WinRT XAML と同じになります。
今度は、MainPage から DialogPage へデータを受け渡す OnGotoButtonClick イベント ハンドラー を MainPage.xaml.cs より抜粋して示します。
private void OnGotoButtonClick(object sender, RoutedEventArgs e) { // Create PassData object PassData passData = new PassData(); // Set the InitializeColor property from the RadioButton controls foreach (UIElement child in radioStack.Children) if ((child as RadioButton).IsChecked.Value) passData.InitializeColor = (Color)(child as RadioButton).Tag; // Pass that object to Navigate this.NavigationService.Navigate(new DialogPage(), passData); }
このコードは、Frame を NavigationService に置き換えただけとなります。Navigate メソッドの第2引数に渡すデータである PassData クラスのインスタンスを指定しているのも同じになります。今度は、DialogPage がデータを受け取るためのイベントである Navigated を MainPage.xaml より抜粋して示します。
void OnNavigated(object sender, NavigationEventArgs e) { if (e.Content.GetType().Equals(typeof(DialogPage))) { var dialogPage = e.Content as DialogPage; dialogPage.Completed += OnDialogPageCompleted; PassData passData = e.ExtraData as PassData; // ナビゲーション履歴を使用する場合(ShowsNavigationUI) // GoToボタンが押されていないため、PassDataがnull if (passData == null) { passData = new PassData(); foreach (UIElement child in radioStack.Children) if ((child as RadioButton).IsChecked.Value) passData.InitializeColor = (Color)(child as RadioButton).Tag; } var dialogStack = dialogPage.radioStack; foreach (UIElement child in dialogStack.Children) if ((Color)(child as RadioButton).Tag == passData.InitializeColor) (child as RadioButton).IsChecked = true; } }
このコードは、WinRT XAML とは考え方が異なっています。すでに説明したように、WPF XAML には NavigatedTo や NavigatedFrom が無く、Navigated イベントがあるだけです。Dialog ページが表示される前に、設定されているイベント ハンドラーが MainPage であることから、MainPage の Navigated イベントになっています。コードの意味を次に示します。
- NavigationEventArgs の Content プロパティの型が DialogPage 型かどうかを確認して処理します。
- DialogPage の Completed イベント ハンドラーを設定(説明は、後で行います)。
- NavigationEventArgs の ExtraData プロパティをキャストして、PassData を取り出す。
WinRT XAML では、NavigationEventArgs の Parameter プロパティです。 - PassData が null の場合を処理。
この処理は、ナビゲーション バーの進むを使う時のためで、MainPage より PassData を作成しています。
WinRT XAML でも GoFoward メソッドのみで進むを使うのであれば必要になります。 - foreach を同じにするために DialogPage の radioStack を取得。
今度は、DialogPage.xaml から MainPage へ戻るための OnReturnButtonClick イベント ハンドラーを DialogPage.xaml.cs より抜粋して示します。
private void OnReturnButtonClick(object sender, RoutedEventArgs e) { this.NavigationService.GoBack(); }
このコードは、Frame を NavigationService に書き換えただけになります。書籍と同じで、GoBack メソッドにはデータを引き渡すような機能はないので、DialogPage クラスに Completed というイベント ハンドラーを設定しますので、DialogPage.xaml.cs より抜粋して示します。
public partial class DialogPage : Page { public event EventHandler<ReturnData> Completed; public DialogPage() { InitializeComponent(); this.Loaded += (s, e) => { this.NavigationService.Navigated += OnNavigated; }; this.Unloaded += (s, e) => { if (this.NavigationService == null) return; this.NavigationService.Navigated -= OnNavigated; }; } void OnNavigated(object sender, NavigationEventArgs e) { if (e.Content.GetType().Equals(typeof(MainPage))) { if (Completed != null) { // Create ReturnData object ReturnData returnData = new ReturnData(); // Set the ReturnColor property from the RadioButton controls foreach (UIElement child in radioStack.Children) if ((child as RadioButton).IsChecked.Value) returnData.ReturnColor = (Color)(child as RadioButton).Tag; // Fire the Completed event Completed(this, returnData); } } } ... }
このコードは、イベント ハンドラーの定義が同じだけになります。そして、すでに説明したように NavigatedTo や NavigatedFrom が WPF XAML にないことから Navigated イベント ハンドラーを Loaded イベント で設定しています。OnNavigated イベント ハンドラーでは、MainPage.xaml.cs で説明したのと同じで NavigationEventArgs の Content プロパティの型が MainPage 型であることを確認してから、ReturnData を設定して Completd イベントを呼び出しています。もちろん、ReturnData の作成から Complete イベント呼び出しまでは、WinRT XAML と同じであることは言うまでもありません。それでは、MainPage.xaml.cs の OnDialogPageCompleted を抜粋して示します。
private void OnDialogPageCompleted(object sender, ReturnData e) { // Set background from returned color contentGrid.Background = new SolidColorBrush(e.ReturnColor); // Set RadioButton for returned color foreach (UIElement child in radioStack.Children) if ((Color)(child as RadioButton).Tag == e.ReturnColor) (child as RadioButton).IsChecked = true; (sender as DialogPage).Completed -= OnDialogPageCompleted; }
このコードは、WinRT XAML と同じになります。それも当然のことで、自分で定義したイベントなので使い方に違いが無くて当たり前なのです。それでは、実行結果を示します。
WPF XAML でナビゲーション フレームワークを使うと、ウィザード形式のダイアログ パターンを簡単に実現することができます。もちろん、戻るや進むというナビゲーション バーが不要であれば、ShowsNavigationUI プロパティを「False」にする必要があります。また、ナビゲーションを使用した場合は、ナビゲーションが完了するとナビゲーション元のページ インスタンスが破棄されることになりますから、戻るなどを行えば新しくページのインスタンスが作成されることになります。このページ インスタンスの動き自体は、WPF XAML も WinRT XAML と同じになります。ページ インスタンスを破棄しない場合は、WinRT XAML では NavigationCacheModeプロパティに Disabled 以外の値を設定して、キャッシュします。WPF XAML の場合は、KeepAliveプロパティに True を設定することでインスタンスをキャッシュするようになります。もちろん、ページをキャッシュすれば同じインスタンスになりますから、データの受け渡しや Navigated イベント などに注意をしなけいといけませんので、ご注意ください。
12.11(P637) Visual Studio の標準テンプレート
本節では、Windows ストア アプリ向けに提供されているプロジェクト テンプレートを説明しています。Windows ストア アプリのグリッド アプリケーションにようにカスタマイズされたプロジェクト テンプレートは、WPF XAML にはありません。しかし、LayoutAwarePage クラスや BindableBase クラスの考え方が役に立つ場合があります。書籍では、基本的な考え方のみを説明しています。このため詳細に知りたい場合は、私が公開した記事であるVisual Studio 2012 のグリッド アプリケーションと Visual Studio 2013 のグリッドアプリケーションを参照してください。
12.12(P644) ビューモデル とコレクション
本節から、今までに説明した知識を活用してアプリを作っていく場合の様々な課題を説明しています。この課題として、テキサス州エルパソ公立図書館で公開されているエルパソ高校の記念卒業アルバムのデータを活用した ElPasoHeighSchool プロジェクトを使用しています。それでは、ElPasoHighSchool プロジェクトの Student.cs を示します。
using System.ComponentModel; using System.Runtime.CompilerServices; namespace ElPasoHighSchool { public class Student : INotifyPropertyChanged { string fullName, firstName, middleName, lastName, sex, photoFilename; double gradePointAverage; public event PropertyChangedEventHandler PropertyChanged; public string FullName { set { SetProperty<string>(ref fullName, value); } get { return fullName; } } public string FirstName { set { SetProperty<string>(ref firstName, value); } get { return firstName; } } public string MiddleName { set { SetProperty<string>(ref middleName, value); } get { return middleName; } } public string LastName { set { SetProperty<string>(ref lastName, value); } get { return lastName; } } public string Sex { set { SetProperty<string>(ref sex, value); } get { return sex; } } public string PhotoFilename { set { SetProperty<string>(ref photoFilename, value); } get { return photoFilename; } } public double GradePointAverage { set { SetProperty<double>(ref gradePointAverage, value); } get { return gradePointAverage; } } protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null) { if (object.Equals(storage, value)) return false; storage = value; OnPropertyChanged(propertyName); return true; } protected void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } }
このコードは、WinRT XAML と同じになります。今までに説明してきたように、ビューモデル、正確には INotifyPropertyChanged インタフェースを継承したクラスには WinRT XAML と WPF XAML の違いがないのです。今度は、Student クラスのコレクションを ObservableCollection で定義した StudentBody.cs を示します。
using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; namespace ElPasoHighSchool { public class StudentBody : INotifyPropertyChanged { string school; ObservableCollection<Student> students = new ObservableCollection<Student>(); public event PropertyChangedEventHandler PropertyChanged; public string School { set { SetProperty<string>(ref school, value); } get { return school; } } public ObservableCollection<Student> Students { set { SetProperty<ObservableCollection<Student>>(ref students, value); } get { return students; } } protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null) { if (object.Equals(storage, value)) return false; storage = value; OnPropertyChanged(propertyName); return true; } protected void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } }
このコードも WinRT XAML と同じになります。ビュー モデルを使用するときは、コレクションとして ObservableCollection を活用すると便利です。この理由は、ObservableCollection が INotifyCollectionChanged インタフェース と INotifyPropertyChanged インタフェースを実装しているからです。書籍では、使用するデータの XML を提示しています。内容を確認するには、こちらを参照してください。今度は、データである XML から StudentBody クラスを作成する StudentBodyPresenter.cs を示します。
using System; using System.ComponentModel; using System.IO; using System.Net.Http; using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Xml.Serialization; namespace ElPasoHighSchool { public class StudentBodyPresenter : INotifyPropertyChanged { StudentBody studentBody; Random rand = new Random(); //Window currentWindow = Application.Current.MainWindow; System.Threading.Timer timer; // DispatcherTimer を変更 public event PropertyChangedEventHandler PropertyChanged; public StudentBodyPresenter() { // Download XML file HttpClient httpClient = new HttpClient(); Task<string> task = httpClient.GetStringAsync("http://www.charlespetzold.com/Students/students.xml"); task.ContinueWith(GetStringCompleted); } void GetStringCompleted(Task<string> task) { if (task.Exception == null && !task.IsCanceled) { string xml = task.Result; // Deserialize XML StringReader reader = new StringReader(xml); XmlSerializer serializer = new XmlSerializer(typeof(StudentBody)); this.StudentBody = serializer.Deserialize(reader) as StudentBody; // Set a timer for random changes timer = new System.Threading.Timer(OnTimerCallback, null, 100, 100); } } public StudentBody StudentBody { set { SetProperty<StudentBody>(ref studentBody, value); } get { return studentBody; } } // Mimic changing grade point averages void OnTimerCallback(object state) { System.Diagnostics.Trace.WriteLine("timercallback"); int index = rand.Next(studentBody.Students.Count); Student student = this.StudentBody.Students[index]; double factor = 1 + (rand.NextDouble() - 0.5) / 5; student.GradePointAverage = Math.Max(0.0, Math.Min(5.0, (int)(100 * factor * student.GradePointAverage) / 100.0)); } protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null) { if (object.Equals(storage, value)) return false; storage = value; OnPropertyChanged(propertyName); return true; } protected void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } }
このコードは、名前空間の違いに伴って少しだけ WPF XAML 用に変更しています。
- DispatcherTimer を System.Threading.Timer クラスに変更しています。
この変更に伴って、Window クラスが不要になっています。
この変更は、WinRT XAML と WPF XAML の大きな違いになります。私が色々と試した限りにおいては、ElPasoHighSchool プロジェクトで DispacherTimer を使用すると OnTimerCallback メソッドが正常に動作しなかったことから、System.Threading.Timer クラスに置き換えています。System.Threading.Timer クラスは、WinRT XAML で使用することはできません。これは、WinRT XAML が明示的なスレッドの使用を許可していないことが理由であり、WinRT XAML では DispacherTimer クラスのみが使用可能なタイマーとなっているからです。
書籍では、作成した ELPasoHighSchool プロジェクトのビューモデルを使った様々なデータ バインディングを説明していきます。そして、基本的な考え方が理解できたところで、データ テンプレートを使用する DisplayHightSchoolStudents プロジェクトの MainPage.xaml の抜粋を示します。
<Page ... xmlns:local="clr-namespace:DisplayHighSchoolStudents" xmlns:common="clr-namespace:DisplayHighSchoolStudents.Common" xmlns:elpaso="clr-namespace:ElPasoHighSchool;assembly=ElPasoHighSchool" ... ><Page.Resources><!-- デザイナーはビジュアル編集ができません(Visual Studio 2012でも同じです) 理由は、StudentBodyPresenterが内部で非同期メソッドを使用して、task.ContinueWithでデータソースを初期化するからです --><elpaso:StudentBodyPresenter x:Key="presenter" /><DataTemplate x:Key="studentTemplate"><StackPanel HorizontalAlignment="Center"><Image Source="{Binding PhotoFilename}" Width="240" /><TextBlock Text="{Binding Sex}" HorizontalAlignment="Center" Margin="10" /><StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="10"><TextBlock Text="GPA = " /><TextBlock Text="{Binding GradePointAverage}" /></StackPanel></StackPanel></DataTemplate></Page.Resources><Grid DataContext="{Binding Source={StaticResource presenter}, Path=StudentBody}"><Grid.RowDefinitions><RowDefinition Height="140" /><RowDefinition Height="*" /></Grid.RowDefinitions><Grid Grid.Row="0"><Grid.ColumnDefinitions><ColumnDefinition Width="120" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions><Button x:Name="btnDeatil" Content="Detail" Grid.Column="0" FontSize="24" IsEnabled="False" HorizontalAlignment="Center" VerticalAlignment="Center" Click="OnBtnDetailClicked"/><TextBlock Name="pageTitle" Text="{Binding School}" Grid.Column="1" FontSize="48" VerticalAlignment="Bottom" Margin="0,0,30,40" /></Grid><DataGrid x:Name="dataGrid" Grid.Row="1" ItemsSource="{Binding Students}" Padding="116 0 40 46" RowDetailsTemplate="{StaticResource studentTemplate}" SelectionMode="Single" SelectionChanged="OnDataGridSelectionChanged"></DataGrid></Grid></Page>
この XAML は、名前空間と組み込みスタイル以外にも変更していますので、次に示します。
- DataTemplate の studentTemplate の内容を詳細表示用に変更。
Student の一覧表示は、DataGrid 標準で対応。 - タイトル欄に、タイトルと詳細表示ボタンを追加。
DataGrid の詳細表示機能を確認するためです。 - GridView、ListView、VisualStateManager を DataGrid に置き換え。
DataGrid の列の自動生成機能を使用しています(データソースから自動生成します)
GridView は WinRT XAML 固有のためです。
ListView と VisualStateManager は、Windows ストア アプリのビュー切り替え対応のためです。
今度は、MainPage.xaml.cs の抜粋を示します。
public partial class MainPage : Page { public MainPage() { InitializeComponent(); this.Loaded += (sender, e) => { this.NavigationService.Navigated += OnNavigated; }; } private void OnNavigated(object sender, NavigationEventArgs e) { // DataContext の設定 if (e.Content.GetType().Equals(typeof(StudentPage))) { if (e.ExtraData != null) { ((e.Content) as StudentPage).DataContext = e.ExtraData; } } } private void OnDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) { if (dataGrid.SelectedItem != null) { btnDeatil.IsEnabled = true; var selected = dataGrid.SelectedItem as ElPasoHighSchool.Student; if (selected == null || string.IsNullOrEmpty(selected.FirstName)) btnDeatil.IsEnabled = false; } else btnDeatil.IsEnabled = false; } private void OnBtnDetailClicked(object sender, RoutedEventArgs e) { var data = dataGrid.SelectedItem as ElPasoHighSchool.Student; if (data != null) this.NavigationService.Navigate(new StudentPage(), data); } }
この コードは、色々な面で手を加えています。
- Navigated イベントで、StudentPage の DataContext を設定。
- OnDataGridSelectionChanged イベントでの処理。
詳細ボタンの有効化と無効化の切り替え。 - 詳細ボタンによる StudentPage へのナビゲート。
デザイン自体も大きく変更していますが、DisplayHighSchoolStudents の実行結果を示します。
DataGrid の行を選択すると、RowDetailsTemplate を使って詳細が表示されます。
詳細ボタンをクリックすると、StudentPage へナビゲートします。
デザイン自体は、大きく変更になっていますが、基本的な機能に変更はありません。ここまでで示していない、MainWindow.xaml の抜粋を示します。
<NavigationWindow x:Class="DisplayHighSchoolStudents.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Source="MainPage.xaml" Title="DisplayHighSchoolStudents" Height="350" Width="525" WindowState="Maximized"></NavigationWindow>
この XAML は、WPF XAML に固有のものになります。今度は、StudentPage.xaml の抜粋を示します。
<Page ... ><Grid><Grid.RowDefinitions><RowDefinition Height="140" /><RowDefinition Height="*" /></Grid.RowDefinitions><Grid Grid.Row="0"><Grid.ColumnDefinitions><ColumnDefinition Width="120" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions><TextBlock Name="pageTitle" Text="{Binding FullName}" Grid.Column="1" FontSize="48" VerticalAlignment="Bottom" Margin="0,0,30,40"/></Grid><StackPanel Grid.Row="1" HorizontalAlignment="Center"><Image Source="{Binding PhotoFilename}" Width="240" /><TextBlock Text="{Binding Sex}" HorizontalAlignment="Center" Margin="10" /><StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="10"><TextBlock Text="GPA = " /><TextBlock Text="{Binding GradePointAverage}" /></StackPanel></StackPanel></Grid></Page>
この XAML は、組み込みのスタイルと VisualStateManager 、戻るボタンを除けば WinRT XAML と同じになります。戻るボタンについては、標準のナビゲーション バーを使用することにして削除しました。今度は、SutudentPage.xaml.cs は、WinRT XAML と違ってビューの切り替えが不要なことから、追加したコードはありません。
ここまでの説明と書籍を照らし合わせれば、大きな違いは MainPage のデザインに集約されることでしょう。これは、WinRT XAML がタッチを前提にしたタイル上のデザインになっているのに対して、WPF XAML では従来通りの表形式のデザインになっているためです。WPF XAML で使用した DataGrid もデータテンプレートを使って表示をカスタマイズすることもできますが、より柔軟性の高いカスタマイズを行うのであれば、サードパーティー製のコントロールを使った方が良いでしょう。
12.13(P667) アイテムのグループ化
本節では、GridView や ListView コントロールを使用する時に利用できるデータ ソースのグループ化を説明しています。GridView は、WinRT XAML に固有のコントロールであることから、WPF XAML としてはコレクション データを使った標準のグループ化について説明します。データ ソースのグループ化を定義するには、CollectionViewSource を使用しますので、GroupBySex プロジェクトの MainPage.xaml の定義を抜粋します。
<Page ... xmlns:elpaso="clr-namespace:ElPasoHighSchool;assembly=ElPasoHighSchool" ... ><Page.Resources><elpaso:StudentBodyPresenter x:Key="presenter" /><CollectionViewSource x:Key="collectionView" Source="{Binding Source={StaticResource presenter}, Path=StudentBody.Students}" ><CollectionViewSource.GroupDescriptions><PropertyGroupDescription PropertyName="Sex" /></CollectionViewSource.GroupDescriptions></CollectionViewSource></Page.Resources> ... </Page>
この XAML は、書籍の WinRT XAML のアプローチとは違っています。
- StudentGroup クラスを定義しないで、StudentBodyPresenter を使用しています。
- CollectionViewSource 定義では、GroupDescriptions プロパティを使用しています。
IsSourceGrouped と ItemsPath プロパティは WinRT XAML 固有です。
WPF XAML では、GroupDescriptions プロパティを使用します。
今度は、MainPage.xaml の表示を定義している箇所を抜粋します。
<Grid><DataGrid ItemsSource="{Binding Source={StaticResource collectionView} }" Padding="116 0 40 46" SelectionMode="Single"><DataGrid.GroupStyle><GroupStyle><GroupStyle.ContainerStyle><Style TargetType="{x:Type GroupItem}"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="{x:Type GroupItem}"><Expander IsExpanded="True"><Expander.Header><StackPanel Orientation="Horizontal"><TextBlock Text="{Binding Path=Name}" /><TextBlock Text="{Binding Path=ItemCount}" Margin="10,0"/></StackPanel></Expander.Header><Expander.Content><ItemsPresenter /></Expander.Content></Expander></ControlTemplate></Setter.Value></Setter></Style></GroupStyle.ContainerStyle></GroupStyle></DataGrid.GroupStyle></DataGrid></Grid>
この XAML の Grid 以降の定義は、組み込みのスタイル以外にも違いがあります。それは、すでに説明したように GridView を WPF XAML がサポートしていないからです。
- GridView を DataGrid へ置き換え。
- DataGrid.GroupStyle でグループの表示方法を定義しています。
実行結果を見れば理解できますが、GroupStyle で定義した Expander コントロールによってデータを折り畳んだりすることができます。GroupStyle を定義した理由は、グループ名と件数を一行で表示したかったからです。コードとして示していませんが、MainWindow.xaml で NavigationWindow を定義しています。
コレクション データのグループ化に関するアプローチは、WinRT XAML と WPF XAML には明確な違いがあります。どちらが良いというものでは、ありません。むしろ、データを表示するアプローチが違うことから、データの表現の 1つとしてのアプローチの違いだと言えるでしょう。GroupDescriptions というアプローチを有効活用するためのビュー モデルを作成するという手法を、採用することもあるでしょう。または、サードパーティー製のコントロールに応じたビュー モデルを作成するという手法も考えられることでしょう。これらのアプローチは、どれかが正解というようなものではありません。作成するプログラムに応じて、最適な手法を選択すれば良いというだけになります。書籍とここまでの記事を読んできたのであれば、ビュー モデル やデータ テンプレート、データ バインディングをすでに理解していますから、理解した内容を応用して、自分の課題に応じて応用すれば良いだけになります。
最後に、GridView ですが、正確には WPF XAML に存在しないのではありません。GridView コントロールという単独のコントロールが、WPF XAML に存在しないだけで、ListViewコントロールの表示状態の 1つとして GridViewが提供されています。
ここまで説明してた違いを意識しながら、第12章を読むことで WPF にも書籍の内容を応用することができるようになることでしょう。