2015年02月13日

モーダルちゃんとモードレスちゃん

 こないだ飲むヨーグルトを自作する話をしましたが、どうせならヘルシーな方がいいので、試しに豆乳で割ってみたんですが、まず豆乳の方が強すぎてあんま美味しくない。そしてなぜか、日を追うにつれてドロドロになる。何コレ何かが育っちゃってる?。トウフジョージ?


 ‥‥‥さて、今日はダイアログのお話です。
 現在、C#.NET でのツール作りの習作という名目で、 「iTunes のプレイリストを m3u に変換しつつ、必要なファイルだけ一括コピーするツール」 を作ってたのですが、コピーするファイル数が多いと結構時間がかかるので、こんな感じのダイアログとか出して、進捗が分かるようにしたいですよね。
20150212a.png
 C# でダイアログを出す事自体は簡単で、フォームを新規作成して好きにレイアウトしたら、あとはコードを2行くらい書くだけで簡単に出せます。そう、こんな風に。
using ( var dialog = new フォームクラス() )
{
    dialog.ShowDialog();
}
 で、ダイアログには、出しているあいだ親フォームの動作が止まる 「モーダルダイアログ」 と、出しているあいだも親フォームを操作できる 「モードレスダイアログ」 というのがあるのですが、後者の 「モードレスダイアログ」 が存外めんどくさかったというお話。

 ちなみに、ダイアログと呼ばれる物のほとんどはモーダルダイアログで、モードレスダイアログって滅多に使わないんだけどね。今回のツールでも全く必要ないし。つい興味本位で 「モーダル・モードレス両対応ダイアログクラス」 を作ろうとしたら2日くらい持っていかれた。

ダイアログを開きたいときー

モーダルちゃん
フォームを Form.ShowDialog() メソッド で開くとモーダルダイアログになる。
ダイアログを閉じるまでメソッドから帰ってこないので、その間、呼び出し元のフォームの動作は止まり、操作も一切受け付けない。
フォームの Load イベントと Shown イベントは ShowDialog() のたびに発生。
モードレスちゃん
フォームを Form.Show() メソッド で開くとモードレスダイアログになる。
ダイアログが開くとすぐメソッドから帰ってくるので、ダイアログが開いている間も、呼び出し元のフォームは普通に操作できる。
フォームの Load イベントと Shown イベントは初回の Show() でしか発生しない。
 ふんふん、開くメソッドを変えれば動作が変わるんだね。なーんだ簡単。

 Load イベントはフォームが表示される直前、Shown イベントはフォームが表示された直後に発生するイベントで、どちらも 「フォームが初めて表示されたときにのみ発生します」、と MSDN には書いてある (このページ超重要) のですが、なぜかモーダルダイアログでは開くたびに発生します。
 まあ、Load も Shown もダイアログの初期化に使いたいイベントなので、毎回発生してくれた方がありがたいんですけどね。特に Shown。
 ということで、モーダルダイアログや、モードレスダイアログも再利用しないのであれば、Load や Shown イベントでダイアログの内容を初期化すればOKです。逆に、絶対に1度しか呼ばれない前提でコードを書くと罠になり得るので注意。

 モードレスダイアログを何度も開きたい場合は、2回目以降の Show() では Load や Shown は起きないので、Activate や VisibleChanged イベントを使うしかありませんが、Show() 以外にもあちこちで起きるイベントなので、フラグなどで Show() の直後の1回のみ処理する作りにしないといけません。

ダイアログを閉じたいときー

モーダルちゃん
以下のいずれかで閉じる
  • ダイアログが自分で Close() メソッドを呼ぶ
  • ダイアログが自分の DialogResult プロパティ に、None 以外の値を入れる。
  • ダイアログ上にある、DialogResult プロパティが設定されたボタンを押す。
  • ダイアログの 「×」 ボタンを押す。
閉じたあと、以後使わないなら Dispose() で後始末しないといけない。
モードレスちゃん
以下のいずれかで閉じる
  • 誰かがダイアログの Close() メソッドを呼ぶ。
  • ダイアログの 「×」 ボタンを押す。
Close() で閉じると同時に自動的に Dispose() される ので、Dispose() は不要。
 Close() や「×」ボタンで閉じるのは分かるとして、 「DialogResult プロパティに値を入れるとダイアログが閉じます」 という仕様はどうかと思う。しかもモーダルでだけ。
this.DialogResult = DialogResult.OK;
 例えばこんなただの代入にしか見えないコードで、これによってダイアログが閉じるとは絶対思わないでしょう?。昔からプロパティで何でもかんでもやるのはあんま好きになれません。個人的には、単純な代入以上の事が必要ならメソッドにしたい。ゲッタの中でクソ重い事する人も超ムカつく。

一度閉じたダイアログをまた開きたいときー

モーダルちゃん
一度 Close() したあとも、Dispose() するまでは ShowDialog() で何度でも開ける。
モードレスちゃん
一度 Close() すると勝手に Dispose() されてしまうため、再利用できない。このあと再度 Show() で開こうとすると ObjectDisposedException で死ぬ。
再利用したい場合は、Close() で閉じるのではなく、Hide() で隠す。ただし、ダイアログの 「×」 ボタンを押すと Close() してしまう ので細工が必要。
 なんでモードレスは勝手に Dispose() する仕様なのか、最初は理解に苦しみましたが、よく考えたら、いつ誰が閉じるか分からないフォームを常に監視して Dispose() しなきゃないのは確かにダルイですね。仕方ないのか。選択制にしてくれるといいんだけど。

 と言う事で、再利用するモードレスダイアログについては Close() で閉じるのはご法度で、代わりに Hide() で隠す使い方が基本になるのですが、「×」 ボタンを押したら勝手に Close() されちゃうし、そもそもダイアログによって Hide() と Close() を使い分けるとかバグの温床になるので超イヤです。

 だったら Close() を改造しちゃえばいいじゃない!。Close() の内部では、実際にフォームを閉じる前に、まず本当に閉じていいのか検証する FormClosing イベント が発生するので、そこで閉じるのを拒否したうえ、代わりに Hide() を呼ぶようにしましょう。
private void SampleDialog_FormClosing(object sender, FormClosingEventArgs e)
{
    // モードレスで、ユーザー操作による Close() であれば、閉じずに Hide()
    if ( !Modal && (e.CloseReason == CloseReason.UserClosing) )
    {
        Hide();
        e.Cancel = true;      // これをtrueにすると閉じるのを拒否
    }
}
 こうすれば、モーダルだろうがモードレスだろうが、Close() で閉じても再利用できるようになります。不要になったら直接 Dispose() を呼びましょう。
 なお、問答無用で閉じるのを拒否してしまうと、アプリ終了時にこのフォームを閉じられず、終了できなくなるので、CloseReason を調べて、ユーザーによる Close() 呼び出しの時のみ Hide() するようにしています。アプリ終了時は CloseReason が別の値なので、正常に閉じます。

 この方法に落ち着くまでいろんなサイトのお世話になりました。スマートにまとまってよかった。

親フォームの真ん中辺にダイアログを開きたいときー

モーダルちゃん
フォームの StartPosition プロパティ に CenterParent を設定すると、自動的に親フォームの真ん中辺に表示してくれる。神。
モードレスちゃん
StartPosition は華麗にスルーされる。ケチ。親フォームの真ん中辺に表示したいなら、自分で位置合わせをしないといけない。
常に親フォームより手前に表示したい場合は、引数付きの Show() を使う。
 最後は小ネタ。なんかこう、今使ってるアプリと全然関係ないところにダイアログが出ると イラッ ときますよね。親フォームの真ん中辺に出てほしいものです。
 で、そりゃ計算すれば真ん中辺には出せるのは分かってますが、いちいち計算するのはめんどいので、何か楽な方法がないものかと探してみたら、モーダルダイアログには神機能がありましたとさ。モードレスでも対応してくれればいいのに。

 モードレスの場合の計算はこちらが詳しいです。StartPosition もこちらで教わりました。というか、他にもいろんな事をこのサイトから教わってます。超ありがてえ。  それと、モードレスダイアログの場合、ダイアログを出してから親フォームを操作すると、親フォームの後ろにダイアログが隠れてしまって大変 イラッ ときますので、そんな時は Show() に親フォームを渡してあげれば背後には回らなくなります。
 ただ、これはこれで、巨大ダイアログを出すと超ジャマなので、ケースバイケースですが。

ちょっとやりすぎた

 なんか気が付いたら書きすぎた。記事書くのに3日とかかけてんじゃないよ!
 Win32API の時にも、CreateWindow() を呼んでからウインドウが出るまでの仕組みを最初にミッチリ把握したおかげでその後の習得が楽だったので、C#.NET でもここはミッチリと。モーダルとの比較という事で 「モードレスダイアログ」 と書いてきましたが、要するに普通のウインドウの挙動ですよね。

 いろいろ検証するためにテストプログラムを作りましたので、せっかくだから置いておきます。以下の事ができます。
  • モーダルダイアログとモードレスダイアログの違いを体験できます。
  • ダイアログを出してから閉じるまでに発生する主要なイベントが、いつ、どんな順序で発生するのかログに出力しています。
  • せっかくなので、冒頭の進捗報告ダイアログもオマケで付いてます。
    時間がかかる処理を別スレッドで実行しながら、進捗を表示できます。途中キャンセルにも対応。今回は BackgroundWorker を使いました。使い方については以下が詳しいです。
  • 進捗表示しながら、もし途中でエラーが起きたらログで羅列できるようにしてあります。効率のよいログの出力は以下が参考になりました。

 コードについては何かのお役に立ちそうでしたら流用していただいて構いませんが、流用した結果、もし何か問題や損害等が生じても当方は責任は持ちません。
posted by ひこざ at 23:54| Comment(0) | 開発 - 全般
この記事へのコメント
コメントを書く
お名前: [必須入力]

メールアドレス:

ホームページアドレス:

コメント: [必須入力]

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。