‥‥‥さて、今日はダイアログのお話です。
現在、C#.NET でのツール作りの習作という名目で、 「iTunes のプレイリストを m3u に変換しつつ、必要なファイルだけ一括コピーするツール」 を作ってたのですが、コピーするファイル数が多いと結構時間がかかるので、こんな感じのダイアログとか出して、進捗が分かるようにしたいですよね。 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 プロパティが設定されたボタンを押す。
- ダイアログの 「×」 ボタンを押す。
- モードレスちゃん
-
以下のいずれかで閉じる
- 誰かがダイアログの Close() メソッドを呼ぶ。
- ダイアログの 「×」 ボタンを押す。
this.DialogResult = DialogResult.OK;
一度閉じたダイアログをまた開きたいときー
- モーダルちゃん
- 一度 Close() したあとも、Dispose() するまでは ShowDialog() で何度でも開ける。
- モードレスちゃん
-
一度 Close() すると勝手に Dispose() されてしまうため、再利用できない。このあと再度 Show() で開こうとすると ObjectDisposedException で死ぬ。
再利用したい場合は、Close() で閉じるのではなく、Hide() で隠す。ただし、ダイアログの 「×」 ボタンを押すと Close() してしまう ので細工が必要。
と言う事で、再利用するモードレスダイアログについては 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にすると閉じるのを拒否 } }
なお、問答無用で閉じるのを拒否してしまうと、アプリ終了時にこのフォームを閉じられず、終了できなくなるので、CloseReason を調べて、ユーザーによる Close() 呼び出しの時のみ Hide() するようにしています。アプリ終了時は CloseReason が別の値なので、正常に閉じます。
この方法に落ち着くまでいろんなサイトのお世話になりました。スマートにまとまってよかった。
親フォームの真ん中辺にダイアログを開きたいときー
- モーダルちゃん
- フォームの StartPosition プロパティ に CenterParent を設定すると、自動的に親フォームの真ん中辺に表示してくれる。神。
- モードレスちゃん
-
StartPosition は華麗にスルーされる。ケチ。親フォームの真ん中辺に表示したいなら、自分で位置合わせをしないといけない。
常に親フォームより手前に表示したい場合は、引数付きの Show() を使う。
で、そりゃ計算すれば真ん中辺には出せるのは分かってますが、いちいち計算するのはめんどいので、何か楽な方法がないものかと探してみたら、モーダルダイアログには神機能がありましたとさ。モードレスでも対応してくれればいいのに。
モードレスの場合の計算はこちらが詳しいです。StartPosition もこちらで教わりました。というか、他にもいろんな事をこのサイトから教わってます。超ありがてえ。 それと、モードレスダイアログの場合、ダイアログを出してから親フォームを操作すると、親フォームの後ろにダイアログが隠れてしまって大変 イラッ ときますので、そんな時は Show() に親フォームを渡してあげれば背後には回らなくなります。
ただ、これはこれで、巨大ダイアログを出すと超ジャマなので、ケースバイケースですが。
ちょっとやりすぎた
なんか気が付いたら書きすぎた。記事書くのに3日とかかけてんじゃないよ!Win32API の時にも、CreateWindow() を呼んでからウインドウが出るまでの仕組みを最初にミッチリ把握したおかげでその後の習得が楽だったので、C#.NET でもここはミッチリと。モーダルとの比較という事で 「モードレスダイアログ」 と書いてきましたが、要するに普通のウインドウの挙動ですよね。
いろいろ検証するためにテストプログラムを作りましたので、せっかくだから置いておきます。以下の事ができます。
- モーダルダイアログとモードレスダイアログの違いを体験できます。
- ダイアログを出してから閉じるまでに発生する主要なイベントが、いつ、どんな順序で発生するのかログに出力しています。
- せっかくなので、冒頭の進捗報告ダイアログもオマケで付いてます。
時間がかかる処理を別スレッドで実行しながら、進捗を表示できます。途中キャンセルにも対応。今回は BackgroundWorker を使いました。使い方については以下が詳しいです。 - 進捗表示しながら、もし途中でエラーが起きたらログで羅列できるようにしてあります。効率のよいログの出力は以下が参考になりました。
コードについては何かのお役に立ちそうでしたら流用していただいて構いませんが、流用した結果、もし何か問題や損害等が生じても当方は責任は持ちません。