[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# パーマリンク