連続したアニメーションをつなぐために、Completed イベントでつなげる

今、寄り道になってしまっているのは、WinRT でモーダルダイアログを出す場合には async/await が必須っぽいのと、アニメーション自体は非同期で動くのだから、それを連続させる場合には async/await を使ったほうが便利ではないか?という思惑と、ならば、Awaitable パターンを実装してみるのが良いのでは?というのと、MessageDialog は IAsyncOperation を実装していて…ってところで、非同期のパターンを同期っぽく書くために await を連続させれば、非同期処理を手続の連続で「わかりやすく」書けるのでは?っての確認のためです。ええ、何を言っているかややこしいのですが、これから一手ずつ試していきます。

■参考資料はここから

自分の資料がてら、だらだらと。

非同期メソッドの内部実装 (C# によるプログラミング入門)
http://ufcpp.net/study/csharp/sp5_awaitable.html
非同期メソッド入門 (8) ? コンパイラ要件 : xin9le note
http://xin9le.net/archives/147
WinRT と await を掘り下げる – Windows 8 アプリ開発者ブログ – Site Home – MSDN Blogs
http://blogs.msdn.com/b/windowsappdev_ja/archive/2012/04/30/winrt-await.aspx
.NET タスクを WinRT 非同期処理として公開する – Windows 8 アプリ開発者ブログ – Site Home – MSDN Blogs
http://blogs.msdn.com/b/windowsappdev_ja/archive/2012/06/25/net-winrt.aspx
WinRT と await を掘り下げる – Windows 8 アプリ開発者ブログ – Site Home – MSDN Blogs
http://blogs.msdn.com/b/windowsappdev_ja/archive/2012/04/30/winrt-await.aspx

■アニメーションを連続させる

まずは、アニメーションの連続技をどう書けばよいのか?ってのがスタートです。

[WinRT] 指定位置からコントロールをアニメーションさせる | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/4066

なところをやっているのは、花札ゲームのアニメーションが、

  1. 手札から1枚、場に出すアニメーション
  2. 役ができれば、それを知らせるアニメーション
  3. マッチした札を取り札に加えるアニメーション
  4. 山から1枚、場に出すアニメーション
  5. 役ができれば、それを知らせるアニメーション
  6. マッチした札を取り札に加えるアニメーション

なんてのを一連の流れで動かすわけです。途中に2枚選択のモーダルダイアログも表示するので、このあたりは「状態遷移」の問題もあります。で、yeild return を使ってコード的に連続させる(イテレーションを使う)っていう技を知った

C# の yield return の使い道 – カタチづくり
http://d.hatena.ne.jp/u_1roh/20080302/1204471238
Hisuiチュートリアル / 直線作図機能を作ってみよう (1)
http://www.quatouch.com/products/hisui/hisui-1_5_0_0-20080204/doc/tutorial/putline.html

ので、これでも良いかも、と思ったものの、花札ゲームの場合はモーダルダイアログが間に入ってくるしどうしたものか?と悩むわけです。

で、モーダルダイアログの悩みはさておき、アニメーションを連続させる場合にはどう実装するのかを確かめてみます。
アニメーション自体は、storyboard を使って、あらかじめ blend で作っておくわけですが、1 から 6 までのアニメをあらかじめ作っておくことはできません。それぞれの部分に分けて storyboard を作って置くし、開始と終了の座標がことなるので、実行時に位置を変えないといけません。

本格的なゲームであれば、アニメーションの座標を動的に計算するのがいいんでしょうが、花札ゲームの場合は「XAML を活用すること」を(私が)主旨にしているので、できるだけ storyboard を活用したい…と言いますか、活用できるかどうかの実験っていう意味もあります。

■Storyboard の Completed イベントで連続させるアイデア

storyboard にはアニメーションが完了した時に Completed イベントが発生します。なので、それぞれの Completed イベントで次の storyboard を Begin させれば、それぞれの storyboard を連続で動作できるはずです。

sb1.Completed += (s,e) => { sb2.Begin(); };
sb2.Completed += (s,e) => { sb3.Begin(); };
sb3.Completed += (s,e) => { sb4.Begin(); };
sb4.Completed += (s,e) => { ; };

ラムダ式で書けばこんな風に、Completed イベントで連続させるわけです。なかなか壮観な感じがしますが、単純に連続させるだけならば、これでもいいかもしれません。

■実際に作ってみる

あらかじめ、こんな風に4つの Image を置いておきます。移動用の松の札が、ぐるぐる連続して回るようにするわけです。

テンプレート用の storyboard はこんな感じ。開始位置と終了位置を変更するために、sbStartX などの名前を付けておきます。

<Storyboard x:Name=&quot;sbMove&quot;>
	<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=&quot;(UIElement.RenderTransform).(CompositeTransform.TranslateX)&quot; Storyboard.TargetName=&quot;pictAni&quot;>
		<EasingDoubleKeyFrame x:Name=&quot;sbStartX&quot; KeyTime=&quot;0&quot; Value=&quot;180.597&quot;/>
        <EasingDoubleKeyFrame x:Name=&quot;sbEndX&quot; KeyTime=&quot;0:0:1&quot; Value=&quot;182.09&quot;/>
	</DoubleAnimationUsingKeyFrames>
	<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=&quot;(UIElement.RenderTransform).(CompositeTransform.TranslateY)&quot; Storyboard.TargetName=&quot;pictAni&quot;>
        <EasingDoubleKeyFrame x:Name=&quot;sbStartY&quot; KeyTime=&quot;0&quot; Value=&quot;-152.239&quot;/>
        <EasingDoubleKeyFrame x:Name=&quot;sbEndY&quot; KeyTime=&quot;0:0:1&quot; Value=&quot;171.642&quot;/>
	</DoubleAnimationUsingKeyFrames>
</Storyboard>

アニメーションの位置計算を簡単にするために、移動用の Image は (0,0) に移動させてしまいます。
札自体をクリックしたらアニメーションを開始。もう一度クリックすると停止します。

public MainPage()
{
    this.InitializeComponent();

	// 最初の位置を(0,0) にしてしまう
	this.pictAni.Margin = new Thickness(0);
}


/// <summary>
/// 札をクリックする
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
private void pictAniClick(object sender, TappedRoutedEventArgs e)
{
	if (_moving == false)
	{
		// アニメーションを開始する
		SetMovePos(pict1, pict2);
		this.sbMove.Completed += sbMove_Completed;
		this.sbMove.Begin();
		_moving = true;
		_count = 0;
	}
	else
	{
		// アニメーションを停止する
		this.sbMove.Stop();
		this.sbMove.Completed -= sbMove_Completed;
		_moving = false;
	}
}
bool _moving = false;
int _count = 0;

/// <summary>
/// アニメーション完了時のイベント
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
void sbMove_Completed(object sender, object e)
{
	_count = (_count + 1) % 4;
	switch (_count)
	{
		case 0: SetMovePos(pict1, pict2); break;
		case 1: SetMovePos(pict2, pict3); break;
		case 2: SetMovePos(pict3, pict4); break;
		case 3: SetMovePos(pict4, pict1); break;
	}
	sbMove.Begin();
}

/// <summary>
/// 開始座標と終了座標の設定
/// </summary>
/// <param name=&quot;pictStart&quot;></param>
/// <param name=&quot;pictEnd&quot;></param>
void SetMovePos(Image pictStart, Image pictEnd)
{
	Point pt1 = pictStart.TransformToVisual(null).TransformPoint(new Point());
	Point pt2 = pictEnd.TransformToVisual(null).TransformPoint(new Point());

	this.sbStartX.Value = pt1.X;
	this.sbStartY.Value = pt1.Y;
	this.sbEndX.Value = pt2.X ;
	this.sbEndY.Value = pt2.Y ;

	Debug.WriteLine(&quot;{0},{1} -> {2},{3}&quot;,
		this.sbStartX.Value, this.sbStartY.Value,
		this.sbEndX.Value, this.sbEndY.Value);
}

ひとつの storyboard しか利用していないので、Completed イベントでは座標の切り替えしかしてません。この部分は、Completed が別々になるとなかなかややこしくなるかと。逆にいえば、複数の Completed イベントを同じイベントメソッドにしてしまって、そのイベントメソッドに状態遷移を書けばコードがややこしくならないかも。

で、このアニメーション部分は非同期なんだから、async/await で書いたらどうなるのか?ってのが次。

カテゴリー: C#, WinRT | 連続したアニメーションをつなぐために、Completed イベントでつなげる はコメントを受け付けていません

[WinRT] 指定位置からコントロールをアニメーションさせる

[WinRT] Gridに貼り付けたコントロールの絶対座標を取得する | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/4062

の続きです。この絶対座標を使ってアニメーションさせれば ok っだね、ってことで先に絶対座標を計算いたのですが、実はアニメーション自体は「相対座標」になるので、この手の Margin のややこしい計算は必要がなかったんですよね~、というオチです。

条件としては、適当に移動コントロールを用意しておいて、適当な開始位置から適当な終了位置までにアニメーションします。
アニメーション自体は、Blend を使って大体の位置で作成しておきます。「大体」でよいのは、開始位置と終了位置はプログラムのほうで制御するためです。

<Page.Resources>
	<Storyboard x:Name=&quot;sbMove1&quot;>
		<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=&quot;(UIElement.RenderTransform).(CompositeTransform.TranslateX)&quot; Storyboard.TargetName=&quot;pict1&quot;>
			<EasingDoubleKeyFrame x:Name=&quot;startPosX&quot; KeyTime=&quot;0&quot; Value=&quot;405.97&quot;/>
			<EasingDoubleKeyFrame x:Name=&quot;endPosX&quot; KeyTime=&quot;0:0:3&quot; Value=&quot;441.791&quot;/>
		</DoubleAnimationUsingKeyFrames>
		<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=&quot;(UIElement.RenderTransform).(CompositeTransform.TranslateY)&quot; Storyboard.TargetName=&quot;pict1&quot;>
			<EasingDoubleKeyFrame x:Name=&quot;startPosY&quot; KeyTime=&quot;0&quot; Value=&quot;277.612&quot;/>
			<EasingDoubleKeyFrame x:Name=&quot;endPosY&quot;  KeyTime=&quot;0:0:3&quot; Value=&quot;-11.941&quot;/>
		</DoubleAnimationUsingKeyFrames>
	</Storyboard>
</Page.Resources>

あらかじめ作った storyboard に対して変更部分に名前をつけていきます。開始位置(startPosX,startPosY) と終了位置(endPosX,endPosY) だけで ok です。アニメーションする時間を変更したい場合は、endPosX.KeyTime, endPosY.KeyTime で指定できるので汎用的に使えます。

■アニメーションを開始する

これを使ってアニメーションさせるのが次のコードです。

private void Button3Click(object sender, RoutedEventArgs e)
{
	// 移動対象の絶対座標
	var pt1 = _pt1;
	// 開始位置の絶対座標
	var pt2 = pict2.TransformToVisual(null).TransformPoint(new Point(0, 0));
	// 終了位置の絶対座標
	var pt3 = pict3.TransformToVisual(null).TransformPoint(new Point(0, 0));
	// 開始、終了位置を相対座標で設定
	startPosX.Value = pt2.X - pt1.X;
	startPosY.Value = pt2.Y - pt1.Y;
	endPosX.Value = pt3.X - pt1.X;
	endPosY.Value = pt3.Y - pt1.Y;
	// アニメーション開始
	sbMove1.Begin();
	sbMove1.Completed += (_, __) => { ; };
}

startPosX.Value に設定するのは、相対座標なので、pt2.X – pt1.X になります。元の pt1 を取っておかないとダメなのがちょっとダサいですが、移動用のコントロールの座標を(0,0)にして左上にしておけば、pt1 の値はいりません。

アニメーションの終了は、Completed イベントで拾えるので、ラムダ式で設定しておくとコードが簡単になるかと。移動用のコントロールを消すとか、元の位置に戻すとかを、

	sbMove1.Completed += (_, __) =>
	{
		sbMove1.Stop();
		grid1.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
	};

のようにしておくとよいかと。

カテゴリー: C#, WinRT | [WinRT] 指定位置からコントロールをアニメーションさせる はコメントを受け付けていません

[WinRT] Gridに貼り付けたコントロールの絶対座標を取得する

さて、手持ちの札を場に捨てるところのアニメーションを作成開始、ってことでちょっと悩んだのは、札の座標はどうやって取るのだろうか?ってことです。XAML では Grid か Canvas にコントロールをペタペタはりつけて位置決めをしていくのですが、この Margin の値は親の Grid からの相対座標なんですよね。移動するコントロールをルートの grid 上に張り付けられてば計算は簡単なのですが、Grid の中に Grid があって、その中にさらに Grid があって、その中に Image がある、っていう入れ子になっている場合には、ちょっと計算が大変。さらに言えば、花札の場合には、手札コントロールとか場コントロールとか、それぞれが別々のユーザーコントロールになっているので、複数のコントロール間の座標はどうやって取るの?ってのが、疑問でありました、って話です。

アニメーションとしては、

  1. 手札コントロールのクリックした札の絶対座標(pt1)を取得
  2. 場コントロールのマッチする札の絶対座標(pt2)を取得
  3. pt1 から pt2 に札をアニメーション

させたいわけです。手札と場のコントロールが別々になっているのは、UI として自由にデザインしたいからです。いやいや、花札ゲームコントロールとして全体をコントロールするという手もあるのですが、なんかごちゃごちゃしたので、コントロール毎に分けたんですよね。アニメーションがない場合は、これのほうが使いやすいのですが、相互に座標を参照するようなアニメーションの場合には結構手間 orz になってしまったということです。このあたり、XAML の場合は、リソースベースで別の XAML を読み込むってほうが素直なのかも。このあたりは後で検討してみます。

で、アニメーションをさせるのはいいけれど、手札の座標はどうやって取るのか?ってことです。

■指定したコントロールの絶対座標を取る。

var pt = pict.TransformToVisual(null).TransformPoint(new Point(0, 0));

tips っぽいですが、pict という Image コントロールの絶対座標は、TransformToVisual メソッドと TransformPoint メソッドの組み合わせで取れます。TransformToVisual メソッドはベースとなる座標軸を決めるってな具合で、null を指定すると画面全体が対象になります。普通は座標軸の原点を指定するために親の grid などを指定するのでしょう。TransformPoint メソッドは、現在の座標軸から変換先の座標軸へ移すために使います。この場合は、Image コントロール(pict)の左上の座標(0,0)を変換したいので、new Point(0,0) としています。
まあ、原理はさておき、こうやると grid の入れ子とか関係なく画面の左上からの座標が取れますね。

■コントロールを、指定した絶対位置に移動する。

左上からの絶対座標が取れたので、これで gird とか canvas とかの相対座標が関係なく移動できます…って訳にもいかず grid の margin に指定するのは grid に対しての「相対座標」なんですよね。なので、コントロールを任意の位置に移動させるためには、対 grid の相対座標値を計算し直さないといけません。この手の計算がいやな場合は、あらかじめ左上を原点として grid や canvas を配置しておくとよいです。たぶん、そっちのほうが簡単です。

アニメーションさせるためのコントロールを、適当な grid の中に適当な Image を貼り付けます。計算が面倒な場合には、左上に接するように作ればいいのですが、ここでは実験のために「あえて適当な」場所に配置しています。

<Grid x:Name=&quot;grid1&quot; 
	HorizontalAlignment=&quot;Left&quot; 
	VerticalAlignment=&quot;Top&quot; Height=&quot;289&quot; Width=&quot;335&quot; Margin=&quot;343,200,0,0&quot;>
    <Image x:Name=&quot;pict1&quot; 
	HorizontalAlignment=&quot;Left&quot; Height=&quot;100&quot; 
	Margin=&quot;56,44,0,0&quot; 
	VerticalAlignment=&quot;Top&quot; Width=&quot;64&quot; 
	Source=&quot;/Images/FC001.png&quot;/>
</Grid>

これを見ると、Margin=”56,44,0,0″ となっているので、Image は Grid に対して(56,44)という位置にありますね。正確に言うと Margin は外側の「余白」になるので、0,0 のところは、右と下の余白を意味しています…と言いますか、この 0,0 ってなんなんでしょうね?

で、いろいろやって試しに、Image を Grid からはみ出るように配置させます。

<Grid x:Name=&quot;grid1&quot; 
	HorizontalAlignment=&quot;Left&quot; 
	VerticalAlignment=&quot;Top&quot; Height=&quot;289&quot; Width=&quot;335&quot; Margin=&quot;343,200,0,0&quot;>
    <Image x:Name=&quot;pict1&quot; HorizontalAlignment=&quot;Left&quot; Height=&quot;100&quot; 
		Margin=&quot;356,135,-85,0&quot; 
		VerticalAlignment=&quot;Top&quot; Width=&quot;64&quot; 
		Source=&quot;/Images/FC001.png&quot; />
    </Image>
</Grid>

Image の Margin が “356,135,-85,0” になっています。356,135 は、x座標, y座標として計算できそうですが、-85 って何なんでしょうね?ってのをかなり悩みました。どうやら、Grid 内に含まれている間は、right と bottom は 0,0 なんですが、grid からはみ出た場合ははみ出た分の「余白」をマイナス値で設定しているようなのです。ためしに、ここを 0 にして「”356,135,0,0″」にすると、

な感じで Image が表示されなくなります。いろいろ試すと表示領域のクリッピングと関係があるようなので、このマイナス値の余白も真面目に計算しないとダメなんですね。

private void Button1Click(object sender, RoutedEventArgs e)
{
	// 移動対象の絶対座標
	var pt1 = pict1.TransformToVisual(null).TransformPoint(new Point(0, 0));
	// 開始位置の絶対座標
	var pt2 = pict2.TransformToVisual(null).TransformPoint(new Point(0, 0));

	// 開始位置に移動
	double dx = pt2.X - pt1.X;
	double dy = pt2.Y - pt1.Y;
	double l = pict1.Margin.Left + dx;
	double t = pict1.Margin.Top + dy;
	double r = grid1.Width - (l + pict1.Width );
	double b = grid1.Height - (t + pict1.Height);
	pict1.Margin = new Thickness(l,t,r,b);
}

移動元と移動先の絶対値をあらかじめ pt1, pt2 で取得しておいて、grid の対して相対値で margin を計算するってことをやります。margin の right と bottom の値は grid からはみ出た分を計算するのです。なんかややこしいですね。

で、実はこのコードではダメなんです。一度 margin を設定してしまうと、pict1.Margin.Left の値が変わってしまうので、実はもともとの margin の値を使わないダメというオチが(相対値を計算すればよいのかな?)

なので、最初の margin と pt1 の値を起動時にキープしておきます。

public MainPage()
{
    this.InitializeComponent();

	_ma1 = pict1.Margin;
	_pt1 = pict1.TransformToVisual(null).TransformPoint(new Point(0, 0));
}

Thickness _ma1;
Point _pt1;

そして、元の位置から計算しなおすというダサい方法が…

private void Button1Click(object sender, RoutedEventArgs e)
{
	// 移動対象の絶対座標
	var pt1 = _pt1;
	// 開始位置の絶対座標
	var pt2 = pict2.TransformToVisual(null).TransformPoint(new Point(0, 0));

	// 開始位置に移動
	double dx = pt2.X - pt1.X;
	double dy = pt2.Y - pt1.Y;
	double l = _ma1.Left + dx;
	double t = _ma1.Top + dy;
	double r  = grid1.Width - (l + pict1.Width );
	double b = grid1.Height - (t + pict1.Height);
	pict1.Margin = new Thickness(l,t,r,b);
}

まあ、これでも動くといえば動くのですが、カプセル化するにしても元の値を取っておくところがひどすぎるので、grid と image の位置を計算が簡単になるように左上の(0,0)にしておきます。
すると、次のように元の値を取っておく必要がなくなります。

private void Button1Click(object sender, RoutedEventArgs e)
{
	// 開始位置の絶対座標
	var pt2 = pict2.TransformToVisual(null).TransformPoint(new Point(0, 0));

	// 開始位置に移動
	double dx = pt2.X;
	double dy = pt2.Y;
	double l = dx;
	double t = dy;
	double r  = grid1.Width - (l + pict1.Width );
	double b = grid1.Height - (t + pict1.Height);
	pict1.Margin = new Thickness(l,t,r,b);
}

まあ、これはこれで動くから良いかな、と思っているわけで、さてこれをアニメーションに応用するのは…続きます。

カテゴリー: C#, WinRT | [WinRT] Gridに貼り付けたコントロールの絶対座標を取得する はコメントを受け付けていません

[WinRT] 自動的に閉じるダイアログを作る

モーダルダイアログ風なものができたので、これを応用して自動的に閉じるダイアログを作ります。

のようにメッセージを表示して、しばらくしたら閉じるっていうメッセージダイアログです。よくゲームであるパターンだし、他のアプリでも応用が利きます。
フォームアプリの場合にはタイマーを使ってイベント待ちをするのが定番ですが、WinRT の場合(というか XAMLの場合)には、storyboard を使って、3秒後の閉じる、ってのがコード的に楽です。

自動的に閉じるダイアログは、Popup でも Canvas でもいいのですが、ここでは、popup を使って実現してみます。

■popup でダイアログを作る

popup コントロールを使って、ダイアログを作成。

<Popup x:Name=&quot;popYaku&quot; 
        Margin=&quot;320,160,-320,328&quot; Grid.Row=&quot;1&quot;>
    <Grid Background=&quot;Red&quot; Height=&quot;213&quot; Width=&quot;399&quot;>
        <TextBlock 
            x:Name=&quot;popYakuText&quot;
            FontSize=&quot;40&quot;
            HorizontalAlignment=&quot;Left&quot; Margin=&quot;34,32,0,0&quot; TextWrapping=&quot;Wrap&quot; Text=&quot;役ができました&quot; VerticalAlignment=&quot;Top&quot;/>
        <Image x:Name=&quot;pictYaku1&quot; HorizontalAlignment=&quot;Left&quot; Height=&quot;100&quot; Margin=&quot;34,96,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;64&quot; Source=&quot;/Images/FC097-2.png&quot;/>
        <Image x:Name=&quot;pictYaku2&quot; HorizontalAlignment=&quot;Left&quot; Height=&quot;100&quot; Margin=&quot;103,96,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;64&quot; Source=&quot;/Images/FC097-2.png&quot;/>
        <Image x:Name=&quot;pictYaku3&quot; HorizontalAlignment=&quot;Left&quot; Height=&quot;100&quot; Margin=&quot;172,96,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;64&quot; Source=&quot;/Images/FC097-2.png&quot;/>
        <Image x:Name=&quot;pictYaku4&quot; HorizontalAlignment=&quot;Left&quot; Height=&quot;100&quot; Margin=&quot;241,96,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;64&quot; Source=&quot;/Images/FC097-2.png&quot;/>
        <Image x:Name=&quot;pictYaku5&quot; HorizontalAlignment=&quot;Left&quot; Height=&quot;100&quot; Margin=&quot;310,96,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;64&quot; Source=&quot;/Images/FC097-2.png&quot;/>
    </Grid>
</Popup>

この場合には、花札の役を表示するので、あらかじめ image コントロールを貼り付けておきます。

■blend で storyboard を追加する

3秒経ったらダイアログを閉じるタイムラインを、blend を使って作ります。

<Page.Resources>
    <!-- TODO: Delete this line if the key AppName is declared in App.xaml -->
    <x:String x:Key=&quot;AppName&quot;>花札 こいこい</x:String>
    <Storyboard x:Name=&quot;sbPopYaku&quot;>
        <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=&quot;(UIElement.Opacity)&quot; Storyboard.TargetName=&quot;popYaku&quot;>
        	<EasingDoubleKeyFrame KeyTime=&quot;0:0:2.5&quot; Value=&quot;1&quot;/>
        	<EasingDoubleKeyFrame KeyTime=&quot;0:0:3&quot; Value=&quot;0&quot;/>
        </DoubleAnimationUsingKeyFrames>
    </Storyboard>
</Page.Resources>

プログラムから storyboard を起動するので名前をつけておくのと、単に消えると WinRT らしくないので、最後の 0.5 秒だけは透明度を変更するという方式にします。この手のアニメーションは blend を使うとひじょうに楽ですね。

■アニメーション終了時の処理を追加

ポップアップを開くときは、IsOpen プロパティを true にすれば ok。ストリーボードの開始は sbPopYaku.Begin() な感じで実行ができます。
で、忘れちゃいけないのが、アニメーションが終了した時に、ストリーボードを Stop で終了させておくことと、ポップアップを IsOpen = false で閉じておくこと。

/// <summary>
/// 役を表示
/// </summary>
/// <param name=&quot;y&quot;></param>
void dispPopYaku( Yaku y )
{
	this.pictYaku1.Visibility = Windows.UI.Xaml.Visibility.Visible;
	this.pictYaku2.Visibility = Windows.UI.Xaml.Visibility.Visible;
	this.pictYaku3.Visibility = Windows.UI.Xaml.Visibility.Visible;
	this.pictYaku4.Visibility = Windows.UI.Xaml.Visibility.Visible;
	this.pictYaku5.Visibility = Windows.UI.Xaml.Visibility.Visible;
	if (y.GoKou != null)
	{
		this.popYakuText.Text = &quot;五光&quot;;
		this.pictYaku1.Source = CardUI.GetResName(y.GoKou[0].ID);
		this.pictYaku2.Source = CardUI.GetResName(y.GoKou[1].ID);
		this.pictYaku3.Source = CardUI.GetResName(y.GoKou[2].ID);
		this.pictYaku4.Source = CardUI.GetResName(y.GoKou[3].ID);
		this.pictYaku5.Source = CardUI.GetResName(y.GoKou[4].ID);
	}
// 略

	this.sbPopYaku.Completed += sbPopYaku_Completed;
	this.popYaku.IsOpen = true;
	this.sbPopYaku.Begin();
}

/// <summary>
/// タイマー完了時
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
void sbPopYaku_Completed(object sender, object e)
{
	this.sbPopYaku.Stop();
	this.popYaku.IsOpen = false;
}

これで、適当なところで dispPopYaku メソッドを呼び出せばポップアップが表示されます。

アニメーションは非同期で行われるので、下記のように、dispPopYaku でポップアップを表示させた後は、_curPlayer.GetCard などのメソッドは即時実行されます。さきゆきは、場からマッチしたときのアニメーションも追加するので、このあたりの整合性(画面の動き的な整合性)をあわせる必要がありますね。

/// <summary>
/// 場に出した札をクリックしてマッチさせる
/// </summary>
/// <param name=&quot;obj&quot;></param>
void baControl1_ClickPutCard(Card obj)
{
	var lst = _board.Game.MatchBa(_board.Ba, _board.Ba.PlayerCard, _curPlayer);
	if (lst != null)
	{
		// カードが決定した場合、画面を更新
		_board.Ba.GetCard(lst);
		// 役の計算
		Yaku y = _board.Game.CalcYaku(lst, _curPlayer.TakenCards);
		if (y != null)
		{
			// 一定時間、役を表示
			dispPopYaku(y);
		}
		// 手持ちに加える
		_curPlayer.GetCard(lst);
		// 画面の更新
		ScrUpdate();
	}
}

■ダイアログをタップした瞬時の閉じる処理を追加

自動的にポップアップは閉じるのですが、タップしたときに即時終了させる処理も追加しておきます。

ポップアップの grid 部分に Tapeed イベントを追加しておきます。ためしに Popup 自身に Tapped を追加してみたのですが呼び出されませんでした。

<Popup x:Name=&quot;popYaku&quot; 
        Margin=&quot;320,160,-320,328&quot; Grid.Row=&quot;1&quot;>
    <Grid Background=&quot;Red&quot; Height=&quot;213&quot; Width=&quot;399&quot;
            Tapped=&quot;popClick&quot;
            >
        <TextBlock 
            x:Name=&quot;popYakuText&quot;
            FontSize=&quot;40&quot;
            HorizontalAlignment=&quot;Left&quot; Margin=&quot;34,32,0,0&quot; TextWrapping=&quot;Wrap&quot; Text=&quot;役ができました&quot; VerticalAlignment=&quot;Top&quot;/>
        <Image x:Name=&quot;pictYaku1&quot; HorizontalAlignment=&quot;Left&quot; Height=&quot;100&quot; Margin=&quot;34,96,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;64&quot; Source=&quot;/Images/FC097-2.png&quot;/>
        <Image x:Name=&quot;pictYaku2&quot; HorizontalAlignment=&quot;Left&quot; Height=&quot;100&quot; Margin=&quot;103,96,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;64&quot; Source=&quot;/Images/FC097-2.png&quot;/>
        <Image x:Name=&quot;pictYaku3&quot; HorizontalAlignment=&quot;Left&quot; Height=&quot;100&quot; Margin=&quot;172,96,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;64&quot; Source=&quot;/Images/FC097-2.png&quot;/>
        <Image x:Name=&quot;pictYaku4&quot; HorizontalAlignment=&quot;Left&quot; Height=&quot;100&quot; Margin=&quot;241,96,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;64&quot; Source=&quot;/Images/FC097-2.png&quot;/>
        <Image x:Name=&quot;pictYaku5&quot; HorizontalAlignment=&quot;Left&quot; Height=&quot;100&quot; Margin=&quot;310,96,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;64&quot; Source=&quot;/Images/FC097-2.png&quot;/>

    </Grid>
</Popup>

アニメーションの完了イベントを発生させるために、SkipToFill メソッドを使って最終点まで移動させます。これでタップしたときには即時終了します。

/// <summary>
/// ポップアップをタップした時
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
private void popClick(object sender, TappedRoutedEventArgs e)
{
	this.sbPopYaku.SkipToFill();
}

他のアニメーションとの整合性もあるのですが(札を手持ちに加えるアニメーションとか)、ひとまず役をできたときのポップアップはこれで ok ということで。

カテゴリー: C#, WinRT | [WinRT] 自動的に閉じるダイアログを作る はコメントを受け付けていません

オブジェクト指向と await の相性は良いのか?の落とし穴を考える

で、めでたく await を使ったモーダルダイアログができたわけですが、これが花札ゲームのロジックにあてはめられるかを試してみます。本来ならば「あてはめられるかどうか?」を UML なりを使ってシーケンス的にチェックする訳ですが、どうやら async/await の組み合わせ…というか、WinRT の UI の限界(あるいは、従来からある Windows フォーム系のイベント指向からのパラダイムシフト)の違いが顕著に表れる例で、どうも UML のシーケンス部分と(あるいは関数呼び出しとしてのシーケンス)との相性が悪いような気がしています。まぁ、IO 系を中心としたドライバーの非同期の呼び出し(call した後に return を非同期で受けるようなコード)が慣れている場合はいいのですが、もともと UML 自体に非同期のロジックが不足していた歴史的経緯を思い出し、Java を発祥とするオブジェクト指向記述型の UML であったことをあわせると、UI とロジックの分離のところで、UI とロジックが密接になってしまうというジレンマに陥るのでは?ってことです。

■発端は、UI なしで考えることから。

花札ゲームの場合、ゲームロジックを共通化することを優先したので、データ構造とロジックから作りはじめています。なので、「UI は飾りであって、取り換え可能である」という論理のもとに UI とロジックを分離させて、UI をチープなものからリッチなものへ変化させる、あるいはアニメーション部分は UI の担当なので後から変更できるようにする、というのが最初の思惑です。

Game クラスがゲームのロジック全般をつかさどるところ、Player クラスがユーザーのクラスですね。Player 自体はコンピュータの AI と切り替えを可能にするので(CPU 対戦というやつ)、ユーザーに問い合わせをする UI を使いません。インターフェースだけを使って、プレイヤーの思考プログラムを組むというやり方は、もとはシミュレーションの「エージェント指向」、近いところでは「アスペクト指向」って方法です。最近では、その手の「○○指向」的な話が沈静化しているので(フレームワークの話とか、DI の話とか)どういう動向で作っているのかが不明なところのなのですが、「温故知新」ってことで掘り出してみれば、20年前のオブジェクト指向ロジックですね。

さて、Pleyer がユーザーの場合には、ユーザーの選択を確かめるために「問い合わせ」を行います。これが、モーダルダイアログで、シーケンスの中に UI を挿入してユーザーの選択を待つ、という仕掛けです。このモーダルダイアログってのはウィンドウシステムの鬼っ子なところがあって、ユーザーから見れば「どこからでもアプリの制御ができる」というモードレスな画面に対して、どちらかを選択しないといけないモードがある画面が突如として出てくる、というシーケンスです。まあ、今となっては当たり前なのですが、コマンドラインで順番にコマンドを流す、という「対話形式」の入力方式から、Excel のようにどこからで操作ができるというウィンドウへのジャンプは結構大きかったのですよ。が、ジャンプが大きかったものの、YES/NO だけを選択させる画面が出るというのはシーケンスを阻害する意味もあり、しかしロジック的にはどちらかを選択しないと次のロジックに進めないというジレンマもあり、ってことで、モーダルダイアログ(問い合わせダイアログ)ってのは、コード的にも貴重な「鬼っ子」であるのです。

そんな訳で、場に花札を出したときに、マッチする札が2枚ある場合には、どちらを選択するかユーザーに問い合わせをするのです。ええ、ユーザーインターフェース的には、札を出すときにマッチする札へドロップさせる、という方法もあるわけで、実は「この時点でロジックが UI を制限している」ってのを意識しないといけないのですが…従来の流れもあってあまり意識せずにつくってしまいます…というのが今回の落とし穴ですね。

画面から選択させるのであれば、モーダルダイアログを出して「どちらを選択しますか?」で OK と考えるわけえで、シーケンス的には、Player から UI に SelCard 呼び出しをして、UI に問い合わせダイアログを開くのが簡単な方法です。
ユーザーが選択したら SelCard 関数の戻り値が返ってくるので、めでたく SelCard の戻り値で Card が選択されるので処理を続けられるというフローになります。ええ、わかりやすいですよね。

■イベントドリブンってことで、最初の一発は UI から始まる

この設計で OK ってことで、Game クラスと Player クラスを組むわけです。最初はチープな画面を作ってテストをしたり、ロジック部分を NUnit を使ってテストしたりして、そこそこ強固なロジックができあがります。

さて、先のシーケンス図では、Player への呼び出しはどこから始まるのか?を明確にしていませんが、実際のところ「場に札を捨てる」という動作は、ユーザーが UI で「手札をクリックする」という動作からはじまります。ここで注意したいのは UI を持っている場合は、ユーザーのアクションからロジックがスタートするってところです。なので、VBの時代には、Click イベントのがしがしとロジックを書いてスパゲッティになってしまったり、逆にロジックを無理矢理分離させるためじ Click イベントにロジックのワンメソッドが書いてあったりするわけですが、UI とロジックの分離は実は、もうちょっと複雑な関係なってきています。

最初の左矢印の putBa が UI から呼び出されるところ、その後に SelCard のところで再び UI に戻ってきて問い合わせダイアログ。札を選択したら山から1枚撮ってくるので、再び SelCard を呼び出して UI に表示して…ってな感じで、putBa の呼び出しから、内部で再帰的に UI が呼び出されます。実は、これは「状態遷移」の問題で、シーケンス的には関数呼び出しなのか、状態遷移なのか(メッセージングなのか)がわかりづらいのですが、これをクラス化して、シーケンスで動かそうとするといきおい、最初のメソッドが内部のメソッドを呼び出して、さらに内部のメソッドが内部の内部のメソッドを呼び出す…これを何回か繰り返した後に戻り値で戻ってくるという「構造化プログラミング」になっています。関数呼び出し≒スタック積み上げの方式は、これはこれでよいのですが、あまり入れ子になってしまうと昔はスタックの問題があったので、これを一度 main ルーチンに戻してから次のステップへ移動させる、っていう「状態遷移」の方法を取ります。この場合、状態を持たなければボタンクリックなどの「イベント駆動」のことになって、ある状態を持って開始から終了まで遷移をすれば、yacc とかなんらかのステートマネージャーを用意することになります。

■ブロッキングを使うか、状態遷移を使うかの選択

ロジックのシーケンスを作るときに UI に問い合わせが発生した場合には、ブロッキング(モーダルダイアログ)にするか、ノンブロッキング(状態遷移)にするかが悩ましいところです…といいますか、花札の2枚選択のようなユーザーに問い合わせをしてしまうパターンの場合には、「モーダルダイアログ」で OK ですよね?「はい」「いいえ」だけとか、場から札を選択させるだけなので、状態遷移なんて考えたこともありませんでした…ってのが罠なんですよ~。

先に書いた通り、UI のイベント駆動の場合には、シーケンスのスタートは UI になります。なので、ロジックの間に再び UI が挟まるという形になります。この部分、MessageBox.Show とか、普通に使っていますが、内部的に、Application.DoEvent を使うとか、別の WinProc を使っているとか、実は問い合わせを出しているダイアログでは、ちょっと複雑なことになっています。といいますか、DoEvent の場合はメッセージループを元に戻すとか、MessageBox の場合は別スレッド(別ウィンドウ)にするとか、それぞれ UI の実装の仕方が違います。

■WinRT はひとつの UI スレッドしかない

問い合わせダイアログを出すときは、従来の Windows フォームの場合には MessageBox を使って別のウィンドウに出ますが、Windows ストア アプリの場合には MessageDialog の ShowAsync メソッドを使って一見、Windows フォームように使えますが…実は大幅に実装が異なります。

private async void Button_Click_DeleteItem(object sender, RoutedEventArgs e)
{
    // 問い合わせを開く
    var dlg = new MessageDialog("アイテムを削除しますか?");
    dlg.Commands.Add(new UICommand("はい"));		
    dlg.Commands.Add(new UICommand("いいえ"));			
    dlg.DefaultCommandIndex = 1; // 「いいえ」をデフォルトボタンにする
    var cmd = await dlg.ShowAsync();				
    // キャンセルの場合
    if (cmd == dlgCommands[1] )		//(*7)
    {
        return;
    }
    // アイテムを削除
    var item = (SampleDataItem)this.flipView.SelectedItem;
    SampleDataSource.DeleteItem(item);
    // メインページに戻る
    this.Frame.GoBack();
}

一見、ShowAsync メソッドで「待ち」をしているように見えるのですが、await がついて Sync がついているのでこれは非同期メソッドです。なので、実はメソッドを呼び出したときにはこの Button_Click_DeleteItem メソッドは一度終了していて、ShowAsync メソッド内のタスクが終了した後に(戻り値が確定したときに)再び Button_Click_DeleteItem が呼び出されて、次の if (cmd == dlgCommands[1] ) の行から実行されるように「状態遷移」が隠されているのですね。

なるほど。WinRT の場合には、UI をブロックさせないようにするために、時間がかかかるロジックは非同期メソッドが用意されている、かつ、それのコードを軽減させるために async/await が用意されている…っていう流れで、これらを UI のイベントから呼び出す場合には、async をメソッドに付ければよいのでめでたしめでたし、ってな訳なのですが、いえいえ、話をシーケンスに戻すと、かなり複雑な現象に陥ります。

■async メソッドを呼び出すメソッドも async を付ける

2枚から選択するときに SelCard メソッド内で問い合わせダイアログを出します。ここでは、コードを簡単にするために MessageDialog.ShowAsync を使うのですが…このメソッドは非同期メソッドなので await を使うわけですね。で、いきおい、SelCard メソッドに async を付けないといけないわけです。

public async Card SelCard(Card c1, Card c2)
{

    // 問い合わせを開く
    var dlg = new MessageDialog("札を選択してください);
    dlg.Commands.Add(new UICommand(c1.ID));		
    dlg.Commands.Add(new UICommand(c2.ID));			
    dlg.DefaultCommandIndex = 1; 
    var cmd = await dlg.ShowAsync();				
    // キャンセルの場合
    if (cmd == dlgCommands[0] ) {
		return c1;
	} else {
		return c2;
	}
}

ここで、問題が発生します。SelCard はロジックなので、非同期を想定していないし、メソッド呼び出しとして同期的に戻り値が取りたい。また SelCard に async を付けると、実は状態遷移になっているので、先のシーケンス図とは動作が異なってしまう。という罠です。UI が非同期なので、ロジック部分も非同期にしないといけないのは当然なのですが、このゲームロジックを作る前提として、「UI の動作には関係なくロジックを流用する」というのがスタートだったので、後から UI の動作によってロジックの動きを変更する、っていうのは未来予測的に違反な訳です…っていうか、落とし穴ですね。フレームワークの調査不足。

なので、Windows フォームの場合には、DoEvent を使って回避することが可能なのですが、WinRT の場合は回避できません。いや、できるとしても、なんかかなり妙なコードを書かないとダメな気が。

また、この ShowAsync メソッドで await を使ってしまうと、内部ロジックにも await を使わないといけなくて、さらに呼び出し元の呼び出し元も await を付けるという await 変更地獄に陥ります。UI から Async を呼び出すときにはいいのですが、ロジック内では、await を使うとかなり妙なことになりそう。

■なので、素直に非同期を考慮したシーケンスを作る

なので、WinRT では UI 待ちは非同期に必ず直す、っていう方針に変えてしまって、await を使わないようにします。UI のところでは、コールバック関数を用意しておいて、結果が出たときのメソッドを別にしておきます。これはあらかじめデリゲートで用意してもよいし、ラムダ式で用意しても OK。
実は、見た目上は、中身が同期なのか非同期なのかはわかりません。というのも Player クラスを AI にした場合には非同期ではない(問い合わせダイアログが必要ない)ので、同期メソッド的に動かないとダメなのです。このあたり、悩ましいところですが、WinRT の async/await に関係なく、同期/非同期を同じようにコードで扱う場合には、この方法がよいかと。

void Player1_EventSelCard(Card c1, Card c2)
{
	// 場コントロールで選択する
	baControl1.SelCard(c1, c2, SelCardResult);
}
void Player2_EventSelCard(Card c1, Card c2)
{
	baControl1.SelCard(c1, c2, SelCardResult);
}
void SelCardResult(Card card)
{
	// 選択後の処理
	var lst = _board.Game.SelCard( card );
	_board.Ba.GetCard(lst);
	_curPlayer.GetCard(lst);
	// 画面の更新
	ScrUpdate();
}

それで、非同期型の UI は、状態遷移型にしておいて、ユーザーのボタン押下待ちになります。AI の場合には、結果のコールバック関数を直接呼ぶロジックに変えてもよいし、そもそも Player クラスの実装が別になるので、この UI 問い合わせの SelCard は呼び出されません。

public void SelCard(Card c1, Card c2, Action<Card> result)
{
	// 指定の2枚に枠を付ける
	var pic1 = _pics.Find(p => p.Source == CardUI.GetResName(c1.ID));
	var pic2 = _pics.Find(p => p.Source == CardUI.GetResName(c2.ID));
	this.rectangleShape1.Margin = pic1.Margin;
	this.rectangleShape2.Margin = pic2.Margin;
	this.rectangleShape1.Visibility = Windows.UI.Xaml.Visibility.Visible;
	this.rectangleShape2.Visibility = Windows.UI.Xaml.Visibility.Visible;

	pic1.Tapped += selPic_Click;
	pic2.Tapped += selPic_Click;

	_selPic1 = pic1;
	_selPic2 = pic2;
	_selCard1 = c1;
	_selCard2 = c2;

	// 選択待ち
	_selCard = null;
	_result = result;	// 結果用のコールバック
}
private void selPic_Click(object sender, TappedRoutedEventArgs e)
{
	_selPic1.Tapped -= selPic_Click;
	_selPic2.Tapped -= selPic_Click;
	this.rectangleShape1.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
	this.rectangleShape2.Visibility = Windows.UI.Xaml.Visibility.Collapsed;

	Image pic = (Image)sender;
	if (pic == _selPic1)
	{
		_selCard = _selCard1;
	}
	else
	{
		_selCard = _selCard2;
	}
	_result(_selCard);
}

■あらためて非同期を考慮してシーケンスを書く

そんな訳で、あらためて非同期を考慮してシーケンスを書き直します…というか、元の画像と同じですね。

右の SelCard のところで「非同期」ってのが明記されているのがミソです。この result が、SelCard メソッドの戻り値ではなくて、Result というメッセージが Player に流れるという感じなんですよね…実はややこしい。

なので、WinRT の場合には UI を操作する部分は「必ず非同期」を考慮しないといけない、ってのがスムースに流れます(awit 無しのモーダルダイアログが出てくるまでは)。シーケンスとしては、簡単な問い合わせをするためだけに非同期シーケンスを考慮しないといけないのが、面倒なところなのですが、そのあたりは必ず状態遷移を使うってことで了解しておけばよいかと。シーケンス的に、こんなに複雑ではない場合(UI から問い合わせダイアログを出すだけの場合)には、await を使えば Ok だし、先の自作モーダルダイアログのようなものを使えば ok です。要は、呼び出しメソッドをさかのぼって async 付けまくりのメソッド直しだけは避けたいかという話です。

■そういえば、オブジェクト指向のシーケンスは元はメッセージングだった

で、最初のオブジェクト指向のシーケンスの話に戻るのですが、オブジェクト指向の発端は、クラス図とかではなくて、メッセージングだよって話があります。メッセージングってのは、データを関数で伝えるのではなくて、イベントキューとして伝える方式ですね。MFC でいえば、SendMessage(即時処理) と PostMessage(遅延処理) の違いです。

クラスメソッドの場合、引数を渡して、戻り値で処理の結果を得るという形で関数(メソッド)呼び出しをしますが、クラスとクラスの間のデータ受け渡しには、元のクラスからデータを渡して、先のクラスでデータを受け取る、という一方向のメッセージングにするパターンがあります。この場合、処理の結果は、先のクラスから元のクラスに再びメッセージが来る、という流れになるわけで、関数呼び出しとはかなり違います。戻り値がないので常に void method を呼び出している感じですね。

で、最初のシーケンス図は、メッセージのやり取りを規定するものであったので、メソッドの戻り値のところはなくて、これを Java で実装するときにクラスメソッドの戻り値が導入されて、戻り値で処理結果を得ることが主流になって、その後に非同期処理というのが明示的に導入された、という歴史的な経緯があるので、ええ、そうなると実は、シーケンス図は、非同期→同期→非同期 っていう歴史的な経緯がある訳です。シーケンス図が状態遷移図をワンセットになるので、シーケンスの矢印が戻り値を必要としない一方向だったってのはうなずける話で、そういう経緯を考えると、WinRT の非同期 UI フレームワークと、オブジェクト指向型のクラス構造との受け渡しは「メッセージング方式」にするのがベターかなと思い直しています。

ちなみに、メッセージング方式は、メッセージを送るときのコストが高くて、正確なコーディングは、キューに設定した後に受け側がメッセージを受け取るという部分を実装しないといけません。単なるクラス間のデータのやり取りにいちいちキューを使うのもコストがかかるので、通常は同期的な処理でよいのですが、状態遷移の場合にはキューを使うしかないんですかね…。事前の策としては、状態遷移を持っているステータスマネージャのクラスを使って、そのクラスから処理を振り分けるという WinProc 的なことをやることになるのですが、これはこれで悩ましいところ。

カテゴリー: C#, WinRT | 1件のコメント

[win8] Windows ストア アプリでモーダルダイアログを作る

[win8] Windows ストア アプリでモーダルダイアログは作れるのか? | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/4030

の続きで、カスタムのモーダルダイアログができたので、晒しておきます。難点としては、ちょっとコードが強引なのと、リサイズに対応していない(縦横にしたときとか)ので、後でライブラリ的に修正するのがベターかと。

■使い方

private async void ModalClick2(object sender, RoutedEventArgs e)
{
	ModalDialog dlg = new ModalDialog();
	int n = await dlg.ShowAsync();
	textResult.Text = n.ToString();
}

MessageDialog 風に await で待ちができます。これでシーケンスが簡単になるかなと。

モーダルダイアログ自体は、ユーザーコントロールで作っておいて、画面に表示します。ダイアログが表示している間は、他のボタンが押せないので、実質モーダルダイアログとして働きます。

■モーダルダイアログを作る

背景をかぶせる部分と、ダイアログとして表示する部分の2つの grid を作ります。

<UserControl
    x:Class=&quot;SampleModal.ModalControl&quot;
    xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
    xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
    xmlns:local=&quot;using:SampleModal&quot;
    xmlns:d=&quot;http://schemas.microsoft.com/expression/blend/2008&quot;
    xmlns:mc=&quot;http://schemas.openxmlformats.org/markup-compatibility/2006&quot;
    mc:Ignorable=&quot;d&quot;
    d:DesignHeight=&quot;341&quot;
    d:DesignWidth=&quot;610&quot;>
    <Grid x:Name=&quot;grid&quot;>
        <Rectangle x:Name=&quot;rect&quot; Fill=&quot;black&quot; Opacity=&quot;.2&quot; />
        <Grid
          x:Name=&quot;msg&quot;
            Background=&quot;Green&quot;
          Width=&quot;380&quot;
          Height=&quot;140&quot;
          >
            <TextBlock 
            FontSize=&quot;32&quot;
            HorizontalAlignment=&quot;Left&quot; Margin=&quot;33,27,0,0&quot; TextWrapping=&quot;Wrap&quot; Text=&quot;こいこいしますか?&quot; VerticalAlignment=&quot;Top&quot;/>
            <Button
            Click=&quot;YesClick&quot;
            Content=&quot;こいこい&quot; HorizontalAlignment=&quot;Left&quot; Margin=&quot;33,89,0,0&quot; VerticalAlignment=&quot;Top&quot;/>
            <Button 
            Click=&quot;NoClick&quot;
            Content=&quot;勝負&quot; HorizontalAlignment=&quot;Left&quot; Margin=&quot;254,89,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;87&quot;/>

        </Grid>
    </Grid>
</UserControl>

背景のところは grid という名前を付けて、ダイアログ自身は msg という名前を付けておきます。grid のほうは、画面いっぱいに広げて他のボタンを押せなくするようにするために使います。

■モーダルダイアログのコード

モーダルダイアログ自身の ModalControl と、それを表示するための ModalDialog クラスを作っておきます。

public sealed partial class ModalControl : UserControl
{
	public ModalControl()
	{
		this.InitializeComponent();
	}

	public Action<int> OnButtonClick;
	public int Result;

	private void YesClick(object sender, RoutedEventArgs e)
	{
		if (OnButtonClick != null)
		{
			this.IsOpen = false;
			Result = 0;
			OnButtonClick(0);
		}
	}

	private void NoClick(object sender, RoutedEventArgs e)
	{
		if (OnButtonClick != null)
		{
			this.IsOpen = false;
			Result = 1;
			OnButtonClick(1);
		}
	}

	private bool _isOpen;
	public bool IsOpen
	{
		get { return _isOpen; }
		set
		{
			_isOpen = value;
			if (value)
			{
				this.Visibility = Windows.UI.Xaml.Visibility.Visible;
			}
			else
			{
				this.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
			}
		}
	}
}

public class ModalDialog
{
	public ModalControl _dlg;

	public ModalDialog()
	{
		_dlg = new ModalControl();
		_dlg.OnButtonClick += OnButtonClick;

		var frame = Window.Current.Content as Frame;
		var page = frame.Content as Page;
		// ここでは Grid 固定
		// 親が Canvas の場合にも対応するべき
		var root = page.Content as Grid;
		root.Children.Add(_dlg);

		// 画面いっぱいに表示する
		double sw = Window.Current.Bounds.Width;
		double sh = Window.Current.Bounds.Height;
		_dlg.HorizontalAlignment = HorizontalAlignment.Left;
		_dlg.VerticalAlignment = VerticalAlignment.Top;
		_dlg.Margin = new Thickness(0, 0, -sw, -sh);
		_dlg.Width = sw;
		_dlg.Height = sh;
	}

	/// <summary>
	/// 通常の表示
	/// OnResult イベントをフックする
	/// </summary>
	public void Show()
	{
		_dlg.IsOpen = true;
	}

	/// <summary>
	/// 非同期に表示する
	/// </summary>
	/// <returns></returns>
	public async Task<int> ShowAsync()
	{
		_dlg.IsOpen = true;
		var n = await Task.Run( async () =>
		{
			while ( _dlg.IsOpen == true ) {
				// イベント待ち
				await Task.Delay(100);
			}
			return _dlg.Result;
		});
		return n;
	}

	public event Action<int> OnResult;
	private void OnButtonClick(int btn)
	{
		if (OnResult != null)
		{
			OnResult(btn);
		}
	}
}

ボタンのイベント待ちのところは、Awaitlbe パターン

非同期メソッドの内部実装 (C# によるプログラミング入門)
http://ufcpp.net/study/csharp/sp5_awaitable.html

を使えばいいのでしょうが、なんかうまく作れなかったので、await Task.Delay() でキーボード待ちをします。Applecation.DoEvents() と同じことができるので、まあ、これはこれでよいかと。100 msec で待てばさほど CPU に負担をかけずに済みます。ちなみに、ここを while ループだけにしてしまうと、CPU を 100% 喰ってしまいます。

■モーダルダイアログとシーケンス図の関係

で、モーダルダイアログの実装ができた訳ですが、果たして

無理矢理流れに乗せてみると意外とうまくいって無理矢理ではなかったというオチ | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/4022

のようにフォームアプリの Applcation.DoEvent の問題とか、ロジックとUIのシーケンスの問題(UMLで設計をするときの罠)が解決しているかどうかはさだかではないのですよ。ってなわけで、さらに寄り道をしてお次はこれを検証するということで。

カテゴリー: C# | [win8] Windows ストア アプリでモーダルダイアログを作る はコメントを受け付けていません

[win8] Windows ストア アプリでモーダルダイアログは作れるのか?

無理矢理流れに乗せてみると意外とうまくいって無理矢理ではなかったというオチ | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/4022/comment-page-1#comment-26496

なところで、花札の2枚選択を作ってみたのですが、さて、これが WinRT の場合にはどうなるのか?ってのが疑問でありました。ダイアログの出し方が、非同期になってしまっているので、Game -> Player -> UI の流れで Modal ダイアログが出せないとシーケンス上困るわけです…が、実際 WinRT に直してみると、これができないということがわかり、シーケンスを直す破目に。これって、普通の Windows フォームのロジックを、Windows ストア アプリに直そうとしたときに UML ベースでハマる罠で、ちょっと問題かなあと。

■ポップアップ画面を表示する

まずは、WinRT でポップアップ画面を出すにはどうするのか?ってのサンプル的に。メッセージダイアログを表示する場合には、MessageDialog というクラスがあって、

    var dlg = new MessageDialog("アイテムを削除しますか?");
    dlg.Commands.Add(new UICommand("はい"));
    dlg.Commands.Add(new UICommand("いいえ"));
    dlg.DefaultCommandIndex = 1; // 「いいえ」をデフォルトボタンにする
    var cmd = await dlg.ShowAsync();

な感じで書いておいて、フォームアプリと同じく MessageBox.Show のように使えます。

しかし、MessageDialog の場合には、画像を張り付けたりとかできないわけで「こいこいしますか?」みたいなゲーム画面の場合はどうするかというと、(たぶん)Popup コントロールを使います。が、この Popup コントロールの使い方がちょっと厄介で、Popup をデザイナ上に置いただけだと、左上に引っ付くんですよね。

これを適当な位置に移動させるには、Popup タグのところをつかんで移動しないとダメという罠があります。

<Popup x:Name=&quot;pop1&quot; Margin=&quot;184,230,-184,258&quot; Grid.Row=&quot;1&quot;>
    <Grid Background=&quot;red&quot; Width=&quot;373&quot; Height=&quot;161&quot;>
        <TextBlock
            FontSize=&quot;32&quot;
            HorizontalAlignment=&quot;Left&quot; Margin=&quot;23,24,0,0&quot; TextWrapping=&quot;Wrap&quot; Text=&quot;ここにメッセージを書きます&quot; VerticalAlignment=&quot;Top&quot;/>
        <Button
            Click=&quot;OkClick&quot;
            Content=&quot;OK&quot; HorizontalAlignment=&quot;Left&quot; Margin=&quot;23,106,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;131&quot;/>
        <Button
            Click=&quot;CancelClick&quot;
            Content=&quot;Cancel&quot; HorizontalAlignment=&quot;Left&quot; Margin=&quot;212,106,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;131&quot;/>
    </Grid>
</Popup>

まあ、Windows ストア アプリの流儀としては、別の画面に遷移させるとか、チャームの画面を利用するとかいう話もあるのですが、ポップアップだけを「簡単に出したい」というのが無理な構造になっているのは、かなり問題かなと思ってます。

/// <summary>
/// Popup を開く
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
private void Open_Click(object sender, RoutedEventArgs e)
{
	// Popup を表示する
	pop1.IsOpen = true;
}

/// <summary>
/// OKボタンイベント
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
private void OkClick(object sender, RoutedEventArgs e)
{
	pop1.IsOpen = false;
	textResult.Text = &quot;OKをクリックした&quot;;
}

private void CancelClick(object sender, RoutedEventArgs e)
{
	pop1.IsOpen = false;
	textResult.Text = &quot;Cancelをクリックした&quot;;
}

Popup の表示自体は、IsOpen プロパティを操作します。Ok ボタンとか Cancel ボタンのイベントは、画面のほうに返ってくるので、ここでボタンイベントの処理をします。非同期そのままの実装がでてくるので、モーダルダイアログっぽいものを出したい場合には、結構手間かなぁと。

で、この Popup の場合には、画面に表示させているときに他のボタンなどをガードしているわけではありません。いわゆるモードレスダイアログのような振る舞いになっているわけで、Popup を表示している間に、裏にあるボタンとかテキストボックスをクリックできます。確かに、フライアウト的な表示をする場合にはいいのですが、従来通り、「はい」「いいえ」をユーザーに選択させている間、他のボタンが押せない状態にしておく、モーダルダイアログの場合にはどうしたらいいのか?ってのが問題ですね。

■モーダルダイアログ風にPopupを使う

もうちょっと、検討しないとダメなんですが、ひとまずアイデアとして、こんな風にするのがコーディング上、手軽ではないかという例を示しておきます。

まずは、Popup コントロールを画面に張り付けて、画面いっぱいに grid を広げます。
広げた grid に半透明の rectange とダイアログを表示する grid を追加します。そう、画面いっぱいに rectangle を広げてしまって、後ろのクリックイベントなどをガードしてしまうという古い方法ですね。
でも、結構有効に働きます。

<Popup x:Name=&quot;pop2&quot;>
    <Grid Height=&quot;2000&quot; Width=&quot;2000&quot; >
        <Rectangle Fill=&quot;Black&quot; Opacity=&quot;.2&quot; />
        <Grid Background=&quot;Green&quot; Margin=&quot;329,272,0,0&quot; HorizontalAlignment=&quot;Left&quot; VerticalAlignment=&quot;Top&quot; Height=&quot;224&quot; Width=&quot;570&quot;>
            <TextBlock
                FontSize=&quot;32&quot;
                HorizontalAlignment=&quot;Left&quot; Margin=&quot;23,24,0,0&quot; TextWrapping=&quot;Wrap&quot; Text=&quot;こいこいしますか?&quot; VerticalAlignment=&quot;Top&quot;/>
                        <Button
                Click=&quot;YesClick&quot;
                Content=&quot;こいこい&quot; HorizontalAlignment=&quot;Left&quot; Margin=&quot;26,155,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;131&quot;/>
                        <Button
                Click=&quot;NoClick&quot;
                Content=&quot;勝負&quot; HorizontalAlignment=&quot;Left&quot; Margin=&quot;215,155,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;131&quot;/>
            <Image
                Source=&quot;/images/FC001.png&quot;
                HorizontalAlignment=&quot;Left&quot; Height=&quot;169&quot; Margin=&quot;393,24,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;111&quot;/>
        </Grid>
    </Grid>
</Popup>

半透明のほうは Opacity の値で指定します。ここでは、0.2 にしていますが、0 にして完全に透明にしても後ろのボタンをクリックできません。なので事実上、モーダルダイアログにできます。
まぁ、AppMenu を張り付けるときは、たぶんスライドで出てしまうので完全に、という訳ではないでしょうが。

ボタンイベントは普通の Popup と同じです。

/// <summary>
/// モーダル風に開く
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Open2_Click(object sender, RoutedEventArgs e)
{
	pop2.IsOpen = true;
}
private void YesClick(object sender, RoutedEventArgs e)
{
	pop2.IsOpen = false;
	textResult.Text = "こいこいします";
}
private void NoClick(object sender, RoutedEventArgs e)
{
	pop2.IsOpen = false;
	textResult.Text = "勝負します";
}

■シーケンス上、なにが問題になるのか?

さて、これをシーケンス図にして考えてみると、通常のモーダルダイアログの場合には logic -> UI -> logic の場合で、UI の中で待ってくれるのだが、Popup コントロールの場合は、logic -> UI … UI -> loigc な漢字で、非同期に UI から結果が飛んでくる。最初の logic -> UI のところで UI の結果が取れる手続き風に書きたいところなのだが、非同期なので一度関数が戻ってきた後に、戻り値が非同期で呼び出されるということになりちょっと複雑。

なので、この Popup の非同期な部分を、同期っぽくみせるために、

    var cmd = await pop1.ShowAsync();

な方法で組みたいわけなのだが、これがなかなかうまくいかない。ってなわけで、ひとまず Popup はモーダル風にする、非同期に呼び出されるので花札のシーケンスを書き換える、ってな方向で考えている途中。MessageDialog ができているので、できないことはないと思うんだが…ひとまず。

カテゴリー: C# | [win8] Windows ストア アプリでモーダルダイアログは作れるのか? はコメントを受け付けていません

無理矢理流れに乗せてみると意外とうまくいって無理矢理ではなかったというオチ

あの日はこう考えていたけど、今はこう考えていて、それを実装しようとするときに、以前の他人の自分を「他人」として捉えるのも良し。いやいや、同一人物なんだから同じ考えでオーケーでは?と再び考えてみて、それなりにリスペクトしたコードを書いてみれば、なるほど、そう考えていたのか、と自分に納得する、という現象なのか。

■2枚を選択させるUIを場コントロールに追加する

2つの選択肢をしめすために、RectangleShape を使って枠線を描画する、ってのはいいとして、もともとのメイン画面からの呼び出しが、こんな風にコールバックになっているわけです。

/// <summary>
/// 2枚から選択する
/// </summary>
/// <param name=&quot;c1&quot;></param>
/// <param name=&quot;c2&quot;></param>
/// <returns></returns>
Card Player1_EventSelCard(Card c1, Card c2)
{
	string msg = string.Format(
		&quot;{0} と {1} があります。{2} を選択しますか?&quot;,
		c1.ID, c2.ID, c1.ID);
	var btn = MessageBox.Show(msg, &quot;&quot;, MessageBoxButtons.YesNo);
	if (btn == System.Windows.Forms.DialogResult.Yes)
	{
		return c1;
	}
	else
	{
		return c2;
	}
}

これを場コントロールで選択できる、という風に仮定すると、こんな感じなるのが理想的…つーか、最初はコールバック関数を作るとか、モロモロ変な感じなコードになっていたのですが、Player1_EventSelCard メソッド自体が、コールバック関数なので、この中で処理をしないといけないんですよね。非同期の async/await が使えればそれはそれでいいのだけど、今回の場合は Windows フォームなので、それを使うのも変な話だし。

/// <summary>
/// 2枚から選択する
/// </summary>
/// <param name=&quot;c1&quot;></param>
/// <param name=&quot;c2&quot;></param>
/// <returns></returns>
Card Player1_EventSelCard(Card c1, Card c2)
{
	return baControl1.SelCard(c1, c2);
}

という訳で、場コントロールには、次のロジックを追加。

PictureBox _selPic1;
PictureBox _selPic2;
Card _selCard1;
Card _selCard2;
Card _selCard;

/// <summary>
/// 2枚から選択する
/// </summary>
/// <param name=&quot;c1&quot;></param>
/// <param name=&quot;c2&quot;></param>
/// <returns></returns>
public Card SelCard(Card c1, Card c2)
{
	// 指定の2枚に枠を付ける
	var pic1 = _pics.Find( p => p.Image == CardUI.GetResName(c1.ID));
	var pic2 = _pics.Find( p => p.Image == CardUI.GetResName(c2.ID));
	this.rectangleShape1.Location = new Point(pic1.Location.X - 3, pic1.Location.Y - 3);
	this.rectangleShape2.Location = new Point(pic2.Location.X - 3, pic2.Location.Y - 3);
	this.rectangleShape1.Visible = true;
	this.rectangleShape2.Visible = true;

	pic1.Click += selPic_Click;
	pic2.Click += selPic_Click;

	_selPic1 = pic1;
	_selPic2 = pic2;
	_selCard1 = c1;
	_selCard2 = c2;

	// 選択待ち
	_selCard = null;
	while (_selCard == null)
	{
		Application.DoEvents();
	}
	return _selCard;
}

private void selPic_Click(object sender, EventArgs e)
{
	_selPic1.Click -= selPic_Click;
	_selPic2.Click -= selPic_Click;
	this.rectangleShape1.Visible = false;
	this.rectangleShape2.Visible = false;

	PictureBox pic = (PictureBox)sender;
	if (pic == _selPic1)
	{
		_selCard = _selCard1;
	} else {
		_selCard = _selCard2;
	}
}

メソッド内でユーザーの選択待ちの動作が入ってしまうので、Application.DoEvents() でクリック待ちをするという荒業です。これを使わない場合には、メイン画面のほうにコールバック関数を作って、という具合になるわけですが、そうなるとロジックと UI の部分の分離がうまくできない。ここは鬼っ子なのか?というのは後で検証するとして、まあ、これで2枚の花札を選択するという動作ができます。

■お次は「動的なデータ表現」

大体、静的なデータ表現ができたので、今度は動的なデータ表現を作ります。「動的な」ってのは、アニメーションとか、一連の動作のところですね。手札がから1枚場に捨てる場合、一連の動作としては、

  1. 手札から場に1枚出す。
  2. 場にマッチさせる。
  3. 山から1枚引いて、場に出す。
  4. 場にマッチさせる。
  5. マッチした札を取り札にする。

というところまで1連の動作なわけで、これを「静的なデータ」だけで表現すると、1の手順を行ったときに、一瞬で5になってしまって「何が起こったのかさっぱりわからない」状況になります。なので、このあたりを「時間」を差し込んで、アニメーションで表す、ってところですね。この時間を差し込むところは、業務アプリではほとんど使わないかと。強いて言えば、ドラッグ&ドロップのところぐらいでしょうか。リストから選んだり、テキストを入力してボタンを押した直後に、何か結果が出てくるのが業務アプリの特徴。ゲームの場合は、この間にアクションを差し込んで「わかりやすく」≒「エンターテインメント性を持たせる」ってことになります。

Windows メトロアプリの場合、XAML を使って storyboard を使うと、このゲーム性の部分が比較的簡単にできるのでは?と思っています。このあたりの実験も兼ねて。そう、まずは、Windows フォーム版を、XAML 版に移植するところから始めます。WPF を作ってから Windows メトロのほうがいいのかな?ちょっと思案中。

カテゴリー: C#, 花札ゲーム | 1件のコメント

IT技術を周回遅れで追ってみれば、枯れた温故知新が見つかるのか、単なる当時の幻想が見つかるのか

花札ゲームの対象は、最終的に Windows 8 のメトロアプリ(XAMLアプリ)と iPad のアプリな訳で、データ構造やゲームの内部構造を共通化すると同時に、表面的には、XAML の部分と、xcode の storyboard のところ(たぶん、ユーザーコントロールを自作)を作ることになるハズ。その前提として、データ指向的なデータバインドの方法と、少し古めの MVVM の方法(observerパターンを使うという点で)を組み入れる予定です。なので、View からデータへの連結はできるだけ簡単な機構を使いたい、ってのがある。
ゲームロジックと画面への描画をべたべたに書くほうが楽だとは思うのだけど、腐心しているのは「後から修正可能にする」ということと、同時に「後から修正を短時間で行えること」を目標とするわけで。このとき「後から」ってのは、今から未来を見通すことになるわけで、その昔…というかオブジェクト指向的にいうと拡張可能にしたり、あらかじえインターフェースを用意したりしようとする方法に陥るのだが、いやいや、そのような「未来予想のコスト」は、変更時のコストがかなり安くなてしまったので(というか、作り直すというコストがかなり安くなってきている)、枠というか「考え方」だけ残してざっくりと作り直したほうが、バグ修正も含めて早かったりする。
となると、今度は「作り直すコスト」を下げるために、今のコードを作る、という不思議なコーディングスタイルがあっても良いかなと思ったり。

■山のコントロールを作る

山のコントロールは作る必要もないのだけど、山から花札を取るという動作をチープな画面では「get」ボタンに割り当てていたので、同じように山のイメージだけ作っておきます。
他のコントロールと同じように、DataSource プロパティがあるわけですが、中身が空っぽです。花札の画像をクリックしたときに、クリックイベントを画面のほうに投げるだけです。正確に作るならば、DataSource に山用の List<Card> を渡してやって、クリック時に1枚メイン画面に渡す、ってコードになるわけですが、1枚山のリストから引っ張ってくるというコードはメイン画面に書いてあるし、ってことでそのまま。

public partial class YamaControl : UserControl
{
	public YamaControl()
	{
		InitializeComponent();
	}
	protected List<Card> _data;
	public List<Card> DataSource
	{
		get { return _data; }
		set
		{
			_data = value;
			UIUpdate();
		}
	}
	protected void UIUpdate()
	{
	}

	public event Action<Card> ClickCard;
	/// <summary>
	/// 山をクリック
	/// </summary>
	/// <param name=&quot;sender&quot;></param>
	/// <param name=&quot;e&quot;></param>
	private void pict_Click(object sender, EventArgs e)
	{
		if (ClickCard != null)
		{
			ClickCard(null);
		}
	}
}

さらに理屈を加えると、大抵の花札のゲームでは山から1枚取るのは自動なんですよね。ゲームの動きとしては、

  1. 手札から1枚場に出す。
  2. 場とマッチングさせる。
  3. 山から1枚引く。
  4. 場とマッチングさせる。
  5. 役があれば紹介アニメを表示する。
  6. マッチした札を取り札に移動する。

まで一連の流れになるわけで(実際には、2枚マッチがあった場合にはユーザーに選択させるわけですが)、ここで使っているテスト用の「チープな画面」では、このあたりの操作はボタンを押して遷移させています。
まあ、そうしないと一瞬で画面から札を取ってしまって途中経過がわからないと、この「途中経過」の部分をどうやって MVVM なのか、データバインドなのか、データ指向なのか、に仕立てるということで、外してあります….というのは半分嘘で、アニメーションのほうを先に考えたら失敗したので、「静的データの表現」から「動的データの表現」へと開発の向きを変えてみたわけです。

■場に札を出したときの処理を加える

場に一枚、札を出したときのコントロールを別に作ろうかと思ってもみたのですが、「場に札を出す」という動作から考えて、場コントロールを拡張させることにします。

ここでちょっと悩んだのが、データバインドの DataSource プロパティをどう拡張しようか?ってことなのですが、いやいや、そんなデータ構造を複雑にせずに、バインドするプロパティを DataSource と PutCard という2つにすれば良いのだ、ということに気づきました。そうそう、データバインドって、なんとなくひとつだけというイメージがあったのですが、別に複数あっても構いません。ただし、あまり多くなってしまうと、どれをどのようにバインドしたらよいのかと、コントロールを使う側が迷ってしまうので、少な目にしておきます。ってのがポイントですね。

このあたり、16駒パズルを作ったときに失敗したのですが、枠線やら画像やら位置やらのひとつひとつにバインドをさせるのは、面倒…と言いますか、バインドの指向的に間違っています。「設定する値」というデータの意味的な部分と、「視覚的に表現する形」という UI 的な部分とは、別々にするとコードが簡単になるし、簡単になりますね。そのあたりの反省もあって、今回は「ユーザーコントロール」を多用しています。下手に、標準的なコントロール(この場合は PictureBox とか)を画面から直接扱うようにするとデータとの連携が複雑になってしまいますが、DataSource プロパティというデータバインドという「インターフェース」に絞ってしまうことで、コードの修正がかなり楽になっています。このあたりは別途機会を作ってまとめるということで。

public partial class BaControl : UserControl
{
	public BaControl()
	{
		InitializeComponent();

		_pics = new List<PictureBox>();
		_pics.Add(this.pictureBox1);
		_pics.Add(this.pictureBox2);
		_pics.Add(this.pictureBox3);
		_pics.Add(this.pictureBox4);
		_pics.Add(this.pictureBox5);
		_pics.Add(this.pictureBox6);
		_pics.Add(this.pictureBox7);
		_pics.Add(this.pictureBox8);
		_pics.Add(this.pictureBox9);
		_pics.Add(this.pictureBox10);
		_pics.Add(this.pictureBox11);
		_pics.Add(this.pictureBox12);
	}
	protected List<PictureBox> _pics;

	// 場のカード
	protected List<Card> _data;
	public List<Card> DataSource
	{
		get { return _data; }
		set
		{
			_data = value;
			UIUpdate();
		}
	}
	// 山や手札から出したカード
	protected Card _putCard;
	public Card PutCard
	{
		get { return _putCard; }
		set
		{
			_putCard = value;
			UIUpdate();
		}
	}

	protected void UIUpdate()
	{
		int i = 0;
		if (this.DataSource != null)
		{
			foreach (var c in this.DataSource)
			{
				_pics[i++].Image = CardUI.GetResName(c.ID);
			}
		}
		for (; i < _pics.Count; ++i)
		{
			_pics[i].Image = CardUI.GetUra();
		}
		if (_putCard != null)
		{
			this.pictureBoxPut.Image = CardUI.GetResName(_putCard.ID);
		}
		else
		{
			this.pictureBoxPut.Image = CardUI.GetUra();
		}
	}
	public event Action<Card> ClickPutCard;
	/// <summary>
	/// 場をクリック
	/// </summary>
	/// <param name=&quot;sender&quot;></param>
	/// <param name=&quot;e&quot;></param>
	private void pict_Click(object sender, EventArgs e)
	{
		if (ClickPutCard != null)
		{
			ClickPutCard(null);
		}
	}
}

■表示位置を調節してゲームらしく

フォームの位置を変えて、チープ画面用の ListBox などを隠すと、ほら花札ゲームのようになります。
リッチ UI 絡みのほうはだいたいできたのですが、もうひとつ問題があって、場に2枚マッチしたときに、どちらかを選ばさせないといけません。いままのままだと無骨なダイアログがでるので、これを場コントロールを使ってなんとか表現させたいのです。

方法はいくつかって、他の花札ゲームのように場の札を枠線であらわして選択させるとか、札を点滅させるとか、別の画像つきのダイアログを出して選択するとか、まあ、いろいろ。ただ、win タブレットとか iphone を考えると、ダイアログが出るよりは、枠を光らせて選択させる、ってのが「自然」ではありますね。

という訳で、お次は、2枚選択の部分を「場コントロール」にフィードバックして表現させる、ってところを実装しています。

カテゴリー: C#, 花札ゲーム | IT技術を周回遅れで追ってみれば、枯れた温故知新が見つかるのか、単なる当時の幻想が見つかるのか はコメントを受け付けていません

優先順序が決まったら一手動いて再び優先順位を見直す、場合はコストを考える

そういえば、PPCM の法則に優先順位を決めたら優先順位が変わらない間はそのままの実行順序で行く。その間に優先順位が変わるような状況があったら、再び優先順位を見直す、てのがある。PDCA の基本でもあるし、アジャイルのアジャイルたるところでもあるし、手順を作って突っ走るほうのが一番早いウォーターフォール開発ってのもその意味では正しい。が、これらの「優先順位づけ」が「コストが掛かる」≒「時間がかかる」てのが前提条件になっていて、実際いろいろな状況を鑑みて計画を練り直す(Excel シートに書き付けた計画シートを書き直す、という作業的なものも含めて)のはコストがかかるわけで、ある程度の期間は計画はそのままというのがベター。となると、逆に計画変更のコストが安くなれば?ってのが plan-language の発端だったなぁ、と思い返してみたり。いやいや、ひとりでやっているときは、タイムボックス的なところの計画と、詳細のタスク分割とは切り離して考えるのがベターかなと。

■取り札のコントロールを作る(その1)

昨日の続きで、取った札をユーザーコントロールに

最初は、動的に PictureBox を増減させていたのですが、簡単のため≒バグを少なくするために、あらかじめ花札の枚数分だけ用意しておきます。デザイナでペタペタと張り付けて…というのは大変なので、コントロールのコンストラクタで作ります。このあたり、最終的には XAML にするパターンになるのでかなり違ってしまうのが難点ですが、思想は同じようにできる感じで。
なぜか Controls に逆順で挿入しないとダメなのと(デザイナ的には、順方向でいいはずなんですが)、一度 PictureBox を表示しておかないと、上下がおかしくなるというバグ的な動きが。まあ、これはプロトタイプなので、そのままにしておきます。

public partial class GainControl : UserControl
{
	public GainControl()
	{
		InitializeComponent();

		// あらかじめ 48 枚分用意しておく
		this.pictureBox1.Visible = false;
		_pics = new List<PictureBox>();
		int x = this.pictureBox1.Location.X;
		int y = this.pictureBox1.Location.Y;
		for (int i = 0; i < 48; i++)
		{
			var pic = new PictureBox();
			pic.Size = this.pictureBox1.Size;
			pic.Location = new Point(x, y);
			pic.SizeMode = this.pictureBox1.SizeMode;
			pic.Visible = true;
			pic.Image = CardUI.GetUra();
			x += 20;
			_pics.Add(pic);
		}
		// 逆順で Controls に追加
		_pics.Reverse();
		foreach (var p in _pics)
		{
			this.Controls.Add(p);
		}
		// 元に戻す
		_pics.Reverse();

	}
	protected List<PictureBox> _pics;
	protected List<Card> _data;
	public List<Card> DataSource
	{
		get { return _data; }
		set
		{
			_data = value;
			UIUpdate();
		}
	}

	protected void UIUpdate()
	{
		if (_data == null)
			return;

		foreach (var p in _pics)
		{
			p.Visible = false;
		}
		int i = 0;
		foreach (var c in _data)
		{
			_pics[i].Image = CardUI.GetResName(c.ID);
			_pics[i].Visible = true;
			i++;
		}
	}
}

画面のほうからは DataSource プロパティでバインド。

	gainControl2.DataSource = _board.Player2.TakenCards;

まあ、これで動くといえば動くのですが、花札の場合、タネとか短冊とかの枚数がわからないと次の手を打ちにくいので、これを「普通の花札ゲーム」のように4つに分けます。

■取り札のコントロールを作る(その2)

まずは、上記のように4つの置き場所を作ります。これにデータバンドをしたいわけですが、さて、それぞれのリストに対して DataSource プロパティを作るべきか?と悩んだのですが…やめました。ユーザーコントロールから設定する場合は、DataSource プロパティひとつに設定しておいて、内部的にリストに設定すれば OK なのですね。なんか、ListBox などのバインドイメージがあったので、List のコレクションをそのままバインドしないといけない気になっていた訳です。

public partial class TakenControl : UserControl
{
	public TakenControl()
	{
		InitializeComponent();

		// あらかじめ20,10,5,1札の場所を用意しておく
		this.pictureBox1.Visible = false;
		this.pictureBox2.Visible = false;
		this.pictureBox3.Visible = false;
		this.pictureBox4.Visible = false;

		_pics20 = InitPics(this.pictureBox1, 5);
		_pics10 = InitPics(this.pictureBox2, 10);
		_pics5 = InitPics(this.pictureBox3, 10);
		_pics1 = InitPics(this.pictureBox4, 20);
	}

	private List<PictureBox> InitPics(PictureBox ptemp, int num)
	{
		var pics = new List<PictureBox>();
		ptemp.Visible = false;
		int x = ptemp.Location.X;
		int y = ptemp.Location.Y;
		for (int i = 0; i < num; i++)
		{
			var pic = new PictureBox();
			pic.Size = ptemp.Size;
			pic.Location = new Point(x, y);
			pic.SizeMode = ptemp.SizeMode;
			pic.Visible = true;
			pic.Image = CardUI.GetUra();
			x += 20;
			pics.Add(pic);
			// this.Controls.Add(pic);
		}
		// 逆順で Controls に追加
		pics.Reverse();
		foreach (var p in pics)
		{
			this.Controls.Add(p);
		}
		// 元に戻す
		pics.Reverse();
		return pics;
	}

	protected List<PictureBox> _pics20, _pics10, _pics5, _pics1;

	protected List<Card> _data;
	public List<Card> DataSource
	{
		get { return _data; }
		set
		{
			_data = value;
			UIUpdate();
		}
	}

	protected void UIUpdate()
	{
		if (_data == null)
			return;
		foreach (var p in _pics20) { p.Visible = false; }
		foreach (var p in _pics10) { p.Visible = false; }
		foreach (var p in _pics5) { p.Visible = false; }
		foreach (var p in _pics1) { p.Visible = false; }

		// 点数ごとに分けて表示する
		int i20 = 0;
		int i10 = 0;
		int i5 = 0;
		int i1 = 0;
		foreach (var c in _data)
		{
			switch (c.Ten)
			{
				case 20:
					_pics20[i20].Image = CardUI.GetResName(c.ID);
					_pics20[i20].Visible = true;
					i20++;
					break;
				case 10:
					_pics10[i10].Image = CardUI.GetResName(c.ID);
					_pics10[i10].Visible = true;
					i10++;
					break;
				case 5:
					_pics5 [i5 ].Image = CardUI.GetResName(c.ID);
					_pics5 [i5 ].Visible = true;
					i5++;
					break;
				case 1:
					_pics1 [i1 ].Image = CardUI.GetResName(c.ID);
					_pics1 [i1 ].Visible = true;
					i1++;
					break;
			}
		}
	}
}

置き場所にあらかじめ PictureBox を作っておくのは同じで、画面を更新するための UIUpdate メソッドで DataSource プロパティの値(内部的には _data)のカードを振り分けます。こうすると、インターフェース的には、DataSource プロパティひとつでいける訳で、コードが簡単になる、ってことで。

これでずいぶん花札ゲームらしくなったということで。

お次は、「山のコントロール」と「手札や山から場に1枚だしたときのコントロール」。場に出したときのコントロールは、別のコントロールにしようと思ったのですが、場コントロールに含めてみた、という話を。

カテゴリー: C#, 花札ゲーム | 優先順序が決まったら一手動いて再び優先順位を見直す、場合はコストを考える はコメントを受け付けていません