Windows Phone 7 開発
Windows Phone 7 向け数独ゲーム
Adam Miller
数独は、ここ 10 年で人気のゲームになり、ほとんどの新聞のクロスワード パズルのすぐ隣に見かけるようになりました。数独に基づくクイズ番組も制作されています。数独をよく知らない方のために、数独とは数字を並べるゲームです。ゲーム盤は 9x9 のグリッドで構成され、この各行各列に 1 ~ 9 の数字を重複しないように並べます。さらに、9x9 のグリッドを 9 つの 3x3 のグリッドに分け、この各グリッドにも 1 ~ 9 の数字がそれぞれ 1 つだけになるように並べます。このゲームはその性質上、ポータブル デバイスでプレイするのに適しており、Windows Phone 7 も例外ではありません。最新バージョンの Windows Phone 7 がリリースされたらすぐに数独アプリケーションがいくつか市場に出回るでしょう。また、今回の記事の説明に従って、Windows Phone 7 アプリケーションのリストに独自のアプリケーションを追加することもできます。
MVVM の導入
今回説明するアプリケーションは、モデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) 設計パターンにほぼ従います。(このアプリケーションにはデータベース ストレージが必要ないため) 実際のモデルは存在しませんが、ViewModel はこのパターンの中核要素となるため、このアプリケーションが優れた学習ツールになることに変わりありません。
MVVM パターンを理解するにはちょっとした学習が必要になるかもしれませんが、理解してしまえば、UI とビジネス ロジックを明確に分離できるようになります。さらに、MVVM パターンを使用すると、UI を更新する面倒なコードの大半が必要なくなり (FirstNameTextBox.Text = MyPerson.FirstName というコードは過去のものになります)、Silverlight のデータ バインドの能力が明らかになります。Silverlight のデータ バインドの詳細については、MSDN ライブラリの記事「データ バインディング」(https://msdn.microsoft.com/ja-jp/library/cc278072(VS.95).aspx) を参照してください。
このアプリケーションのサイズとシンプルさ、および今回の記事の目的を考え、サード パーティ製の MVVM フレームワークは使用しません。ただし、読者の皆さんが開発するアプリケーションが今回のアプリケーションよりも複雑になるのであれば、MVVM Light Toolkit (mvvmlight.codeplex.com、英語) などのサード パーティ製フレームワークを使用して作業に着手することをお勧めします。MVVM Light Toolkit には、無償のテスト済みのコードが用意されています。経験から言えば、このようなコードはどのみち作成することになります。
アプリケーションを作成する
xbox.http://xbox.create.msdn.com から開発ツールをインストールしたら、まず、新しい Windows Phone 7 プロジェクトを作成します。そのためには、Visual Studio を起動し、[File] (ファイル) メニューの [New Project] (新しいプロジェクト) をクリックします。次に、[New Project] (新しいプロジェクト) ダイアログ ボックスで [Visual C#] (Visual C#) を展開し、[Silverlight for Windows Phone] (Silverlight for Windows Phone) をクリックして、[Windows Phone Application] (Windows Phone アプリケーション) をクリックします。その後、一般的な MVVM パターンに従って、Views および ViewModels という 2 つのフォルダーを新規作成します。SDK の一部として提供されるエミュレーターを利用すれば、この時点でデバッグを開始することもできます。
数独ゲームは、個々のマス (通常の 9x9 のグリッドで構成されるゲーム盤では合計 81 個)、マスを収容するゲーム盤全体、および 1 ~ 9 の数字を入力するグリッドという 3 種類の概念に分けることができます。これら 3 種類のビューを作成するため、Views フォルダーを右クリックし、[Add] (追加) をポイントして、[New Item] (新しい項目) をクリックします。表示されるダイアログ ボックスで [Windows Phone User Control] (Windows Phone ユーザー コントロール) をクリックし、最初のファイルに GameBoardView.xaml という名前を付けます。これと同じ手順を繰り返し、SquareView.xaml と InputView.xaml というファイルも作成します。ここで、ViewModels フォルダーに GameBoardViewModel クラスと SquareViewModel クラスを追加します。Input ビューに ViewModel は必要ありません。コードの重複を避けるため、ViewModel の基本クラスを作成してもかまいません。その場合は、ViewModels フォルダーに ViewModelBase クラスを追加します。この時点のソリューションは図 1 のようになります。
図 1 Views フォルダーと ViewModels フォルダーを備えた Windows Phone 7 数独ソリューション
ViewModelBase クラス
ViewModelBase クラスには、System.ComponentModel の INotifyPropertyChanged インターフェイスを実装します。このインターフェイスにより、ViewModel のパブリック プロパティをビューのコントロールにバインドできます。INotifyPropertyChanged インターフェイスの実装は非常に簡単で、PropertyChanged イベントを実装するだけです。ViewModelBase.cs クラスは、次のようになります (System.ComponentModel のステートメントを忘れずに使用してください)。
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler
PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this,
new PropertyChangedEventArgs(info));
}
}
}
サード パーティ製 MVVM フレームワークのほとんどには、この定型コードを含む ViewModel 基本クラスがあります。すべての ViewModel は、ViewModelBase から継承します。UI のバインド先となる ViewModel のプロパティでは、setter 内で NotifyPropertyChanged を呼び出す必要があります。その結果、プロパティ値が変更されたときに UI を自動更新できます。この方法ですべてのプロパティを実装するのは少し面倒なので、UI を更新する必要がないコードも検討の余地があります。
個別のマスを実装する
まず、SquareViewModel クラスを実装します。パブリック プロパティをいくつか追加し、Value、Row、および Column は整数型に、IsSelected、IsValid、および IsEditable はブール型にします。UI を Value プロパティに直接バインドすることもできますが、未割り当てのマスに "0" が表示されることになり、これは問題です。これを解決するには、バインディング コンバーターを実装するか、Value プロパティが 0 の場合に空文字列を返す読み取り専用の "StringValue" プロパティを作成します。
SquareViewModel クラスは、そのクラスの現在状態を UI に通知する処理も行います。このアプリケーションの各マスには 4 つの状態があり、それぞれ Default、Invalid、Selected、および UnEditable です。通常、この状態は列挙型として実装することになりますが、Silverlight フレームワークの列挙型には一部不足があり、完全な Microsoft .NET Framework の列挙型にあるメソッドがいくつか存在しません。これはシリアル化中に例外がスローされる原因となるため、状態は次のように定数として実装します。
public class BoxStates
{
public const int Default = 1;
public const int Invalid = 2;
public const int Selected = 3;
public const int UnEditable = 4;
}
ここで、SquareView.xaml を開きます。フォントのサイズと色に、コントロール レベルでいくつかスタイルが適用されています。通常、プリセットのスタイル リソースは別々のリソース ファイルに含まれていますが、今回の場合は Windows Phone 7 によって既定でアプリケーションに提供されます。このリソースについては、MSDN ライブラリの「Windows Phone 用テーマのリソース」(tinyurl.com/WP7Resources、英語) を参照してください。このアプリケーションではこれらのスタイルの一部を使用するため、アプリケーションの色はユーザーが選択するテーマに合わせて変化します。テーマはエミュレーターで選択できます。これを行うには、ホーム画面に移動し、矢印、[Settings] (設定)、[theme] (テーマ) の順にタップします。この画面で背景色とアクセント カラーを変更できます (図 2 参照)。
図 2 Windows Phone 7 のテーマ設定画面
SquareView.xaml でグリッド内に Border と TextBlock を配置します。このコードを次に示します。
<Grid x:Name="LayoutRoot" MouseLeftButtonDown=
"LayoutRoot_MouseLeftButtonDown">
<Border x:Name="BoxGridBorder"
BorderBrush="{StaticResource PhoneForegroundBrush}"
BorderThickness="{Binding Path=BorderThickness}">
<TextBlock x:Name="MainText"
VerticalAlignment="Center" Margin="0" Padding="0"
TextAlignment="Center" Text=
"{Binding Path=StringValue}">
</TextBlock>
</Border>
</Grid>
SquareView.xaml.cs の分離コードは、付属のコード ダウンロードに含めています。コンストラクターには、SquareViewModel のインスタンスが必要です。このインスタンスは、ゲーム盤をバインドするときに提供されます。また、ユーザーがグリッド内をタップしたときに発生するカスタム イベントがあります。カスタム イベントの使用は、ViewModel の相互通信を実現する 1 つの方法です。ただし、大規模アプリケーションの場合、この方法は複雑になる可能性があります。別の方法として、通信を円滑にする Messenger クラスを実装する方法があります。ほとんどの MVVM フレームワークには、(Mediator とも呼ばれる) Messenger クラスが用意されています。
MVVM を純粋に信奉する開発者にとっては、分離コードを使用して UI を更新するのは面倒なように思われるかもしれませんが、これらの項目は BindingConverter には役立ちません。BoxGridBorder の BorderThickness は、BindingConverter からは容易にアクセスできないアプリケーション リソースの 2 つのプロパティ (Foreground ブラシと Background ブラシ) に基づいています。
ゲーム盤を実装する
これで、GameBoard ビューと ViewModel を実装できるようになりました。ビューは 9x9 のシンプルなグリッドです。コード ダウンロードで提供する分離コードはシンプルで、ViewModel を公開するパブリック プロパティと、子ボックスのタップとゲーム配列のバインドを処理するいくつかのプライベート メソッドしかありません。
ViewModel には、大量のコードが含まれています。ユーザーの入力後にゲーム ボードを検証するメソッド、パズルを解くためのメソッド、ゲーム盤を保存してストレージから読み込むメソッドなどがあります。ゲーム盤は保存時に XML にシリアル化し、IsolatedStorage を使用してファイルを保存します。完全な実装については、ソース コードをダウンロードしてご確認ください。ゲーム盤の保存コードは最も興味深いコードです。このコードを図 3 に示します (System.Xml.Serialization への参照が必要な点に注意してください)。
図 3 ゲーム盤の保存コード
public void SaveToDisk()
{
using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
{
if (store.FileExists(FileName))
{
store.DeleteFile(FileName);
}
using (IsolatedStorageFileStream stream = store.CreateFile(FileName))
{
using (StreamWriter writer = new StreamWriter(stream))
{
List<SquareViewModel> s = new List<SquareViewModel>();
foreach (SquareViewModel item in GameArray)
s.Add(item);
XmlSerializer serializer = new XmlSerializer(s.GetType());
serializer.Serialize(writer, s);
}
}
}
}
public static GameBoardViewModel LoadFromDisk()
{
GameBoardViewModel result = null;
using (IsolatedStorageFile store = IsolatedStorageFile.
GetUserStoreForApplication())
{
if (store.FileExists(FileName))
{
using (IsolatedStorageFileStream stream =
store.OpenFile(FileName, FileMode.Open))
{
using (StreamReader reader = new StreamReader(stream))
{
List<SquareViewModel> s = new List<SquareViewModel>();
XmlSerializer serializer = new XmlSerializer(s.GetType());
s = (List<SquareViewModel>)serializer.Deserialize(
new StringReader(reader.ReadToEnd()));
result = new GameBoardViewModel();
result.GameArray = LoadFromSquareList(s);
}
}
}
}
return result;
}
入力グリッドを実装する
Input ビューもシンプルで、StackPanel に入れ子にしたボタンがいくつかあるだけです。図 4 に示す分離コードでは、タップされたボタンの値をアプリケーションに送信するカスタム イベント、およびこのゲームを縦モードまたは横モードでプレイできるようにする 2 つのメソッドを公開しています。
図 4 Input ビューの分離コード
public event EventHandler SendInput;
private void UserInput_Click(object sender, RoutedEventArgs e)
{
int inputValue = int.Parse(((Button)sender).Tag.ToString());
if (SendInput != null)
SendInput(inputValue, null);
}
public void RotateVertical()
{
TopRow.Orientation = Orientation.Vertical;
BottomRow.Orientation = Orientation.Vertical;
OuterPanel.Orientation = Orientation.Horizontal;
}
public void RotateHorizontal()
{
TopRow.Orientation = Orientation.Horizontal;
BottomRow.Orientation = Orientation.Horizontal;
OuterPanel.Orientation = Orientation.Vertical;
}
MainPage.xaml にすべてのビューをまとめる
最後に、このアプリケーションを MainPage.xaml の実装にまとめます。Input ビューと GameBoard ビューをグリッドに配置します。このアプリケーションでは、画面上のすべての領域を使用できる必要があるため、プロジェクトの作成時に自動挿入された PageTitle TextBlock を削除します。ApplicationTitle TextBlock は、縦モードでのみ表示されます。Windows Phone 7 Application Bar も利用します。Application Bar を使用すると、アプリケーションが携帯電話と密接に統合された感じになり、ユーザーが新しいパズルを解いたり、リセットしたり、開始したりできる優れたインターフェイスが数独アプリケーションに提供されます。
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar IsVisible="True" IsMenuEnabled="True">
<shell:ApplicationBarIconButton x:Name="NewGame"
IconUri="/Images/appbar.favs.rest.png" Text="New Game"
Click="NewGame_Click"></shell:ApplicationBarIconButton>
<shell:ApplicationBarIconButton x:Name="Solve"
IconUri="/Images/appbar.share.rest.png" Text="Solve"
Click="Solve_Click"></shell:ApplicationBarIconButton>
<shell:ApplicationBarIconButton x:Name="Clear"
IconUri="/Images/appbar.refresh.rest.png" Text="Clear"
Click="Clear_Click"></shell:ApplicationBarIconButton>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
画像は、マイクロソフトが提供するWindows Phone 7 専用の一連のアイコンから取得します。これらのアイコンは、ツールと共に C:\Program Files (x86)\Microsoft SDKs\Windows Phone\v7.0\Icons にインストールされます。画像をプロジェクトにインポートした後、画像のプロパティを選択し、[Build Action] (ビルド アクション) を [Resource] (リソース) から [Content] (コンテンツ) に変更して、[Copy to Output Directory] (出力ディレクトリにコピー) を [Do Not Copy] (コピーしない) から [Copy If Newer] (新しい場合はコピーする) に変更します。
このアプリケーション パズルの最後のピースとして、MainPage 分離コードを実装します。SupportedOrientations プロパティをコンストラクターで設定し、ユーザーが携帯電話を回転したらアプリケーションの表示も回転するようにします。また、InputView の SendInput イベントを処理し、入力値を GameBoard に転送します。このコードを次に示します。
public MainPage()
{
InitializeComponent();
SupportedOrientations = SupportedPageOrientation.Portrait |
SupportedPageOrientation.Landscape;
InputControl.SendInput += new
EventHandler(InputControl_SendInput);
}
void InputControl_SendInput(object sender, EventArgs e)
{
MainBoard.GameBoard.SendInput((int)sender);
}
ゲーム盤の読み込みと保存を処理するために、Navigation メソッドも実装します。
protected override void OnNavigatedTo(NavigationEventArgs e)
{
GameBoardViewModel board =
GameBoardViewModel.LoadFromDisk();
if (board == null)
board = GameBoardViewModel.LoadNewPuzzle();
MainBoard.GameBoard = board;
base.OnNavigatedTo(e);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
MainBoard.GameBoard.SaveToDisk();
base.OnNavigatedFrom(e);
}
携帯電話を回転すると、アプリケーションは通知を受け取ります。通知を受け取ると、InputView をゲーム盤の下部から右側に移動し、回転します (図 5 参照)。
図 5 携帯電話の回転に対処するコード
protected override void OnOrientationChanged(OrientationChangedEventArgs e)
{
switch (e.Orientation)
{
case PageOrientation.Landscape:
case PageOrientation.LandscapeLeft:
case PageOrientation.LandscapeRight:
TitlePanel.Visibility = Visibility.Collapsed;
Grid.SetColumn(InputControl, 1);
Grid.SetRow(InputControl, 0);
InputControl.RotateVertical();
break;
case PageOrientation.Portrait:
case PageOrientation.PortraitUp:
case PageOrientation.PortraitDown:
TitlePanel.Visibility = Visibility.Visible;
Grid.SetColumn(InputControl, 0);
Grid.SetRow(InputControl, 1);
InputControl.RotateHorizontal();
break;
default:
break;
}
base.OnOrientationChanged(e);
}
また、ここではメニュー項目のタップ操作も処理します。
private void NewGame_Click(object sender, EventArgs e)
{
MainBoard.GameBoard = GameBoardViewModel.LoadNewPuzzle();
}
private void Solve_Click(object sender, EventArgs e)
{
MainBoard.GameBoard.Solve();
}
private void Clear_Click(object sender, EventArgs e)
{
MainBoard.GameBoard.Clear();
}
これでゲームが完成し、プレイできるようになります (図 6 および図 7 参照)。
図 6 縦モードの数独ゲーム
図 7 横モードの解き終わったゲーム
これで終わりです。Windows Phone 7 購入の列に並ぶとき、退屈しないで済むすばらしいゲームができました。今回の記事では、Silverlight ベースの Windows Phone 7 アプリケーションを作成する方法について説明しました。また、シリアル化とユーザー ストレージを使用してアプリケーションを保存する方法、およびアプリケーションで複数の向きをサポートできるようにする方法も紹介しました。MVVM パターンについて、および MVVM パターンと共にデータ バインドを使用する方法についても詳しく理解していただけたことでしょう。
Adam Miller は、ネブラスカ州のリンカーンにある Nebraska Global のソフトウェア エンジニアです。彼のブログは blog.milrr.com (英語) でご覧いただけます。
この記事のレビューに協力してくれた技術スタッフの Larry Lieberman と Nick Sherrill に心より感謝いたします。