WPFにはShownイベントがない
WPFアプリケーションを開発していて、Windowが最初に表示されたときにある条件が成立した場合に警告メッセージのダイアログを表示するという処理が必要になりました。
Windows Froms であれば、Formが最初に表示されたときに発生するForm.Shownというイベントがあるのですが、残念ながらWPFのWindowクラスには同様のイベントは存在しません。
しかたないので他の方法を考えることにしました。
とりあえず、テストとしてWindowの表示後、無条件にメッセージボックスを表示するプログラムを書いてみることにします。
Loadedイベントを利用する
まず最初は、WindowクラスのFrameworkElement.Loadedイベントを利用することを考えました。
MSDN:FrameworkElement.Loaded イベント
要素の配置、描画、および操作の準備が完了したときに発生します。
説明を読むとこれでうまくいきそうです。 しかし、
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += (s, e) => { MessageBox.Show("Loaded", "TEST"); };
}
}
というコードを書いて確認してみると、メッセージボックスが表示された段階ではWindowの中に何も表示されていませんでした。
このタイミングだと Window内のコントロールの描画が完了していないようです。
通常の初期処理であればこのタイミングで問題ないのですが、今回はWindowが表示された後に処理したかったのでこの方法を使えません。
Activatedイベントを利用する
次に考えたのはWindowクラスのWindow.Activatedイベントを利用する方法です。
ウィンドウが前面ウィンドウになるときに発生します。
とりあえず以下のようなコードを書いて試してみます。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Activated += (s, e) => { MessageBox.Show("Activated","TEST"); };
}
}
今度はWindowの表示が完了してから、メッセージボックスが表示されました。
しかし、メッセージボックスのOKボタンをクリックしてメッセージボックスをクローズさせてもすぐにまた表示されてしまいます。
ActivatedイベントはWindowがアクティブになるたびに呼び出されます。したがってメッセージボックスが表示されWindowがディアクティベート状態になったのが、メッセージボックスがクローズされることでWindowが再びアクティブになることでActivatedイベントが無限ループ状態に陥ってしまったのでした。
処理を行いたいのは最初の1回だけなので、フラグとなるメンバを一つ追加してハンドリングすることにします。
public partial class MainWindow : Window
{
private bool flg = true;
public MainWindow()
{
InitializeComponent();
Activated += (s, e) =>
{
if (flg)
{
flg = false;
MessageBox.Show("ACTIVATED", "TEST");
}
};
}
}
論理型のメンバ変数flgを追加し、Activatedイベントではflgが真の場合のみ処理するようにします。処理するときにflgを偽にすることで、次にWindowがアクティブになっても処理は行われなくなります。
このとき面白いことに気づきました。メンバ変数flgに偽をセットするタイミングで動作が異なるのです。
flg = false;
MessageBox.Show("ACTIVATED", "TEST");
メッセージボックスを表示する前にflgに偽をセットすると正常に動作します。
しかし、これを
MessageBox.Show("ACTIVATED", "TEST");
flg = false;
とメッセージボックスの表示後に偽をセットするように変更するとうまく動かず、メッセージボックス表示の無限ループになってしまいます。
これは、メッセージボックスが閉じられたことによって発生する2回目以降のActivatedイベントがMessageBox.Showメソッドより処理が戻るよりも前に発生するのが原因です。うっかりすると犯しがちなミスなので注意が必要です。
閑話休題。とりあえずこれで当初の目標は達成できました。しかし、プログラム実行中何度も呼び出されるであろうActivatedイベントに最初の1回だけしか処理しない処理を記述したり、そのためにわざわざメンバ変数を一つ追加しているのはなんだか美しくないような気がします。
再び Loaded イベントを利用してみる
‘Loaded`イベントを利用したときの問題は、イベントが発生したタイミングではまだWindow内のコントロールの描画が完了していないということでした。
そこで、コントロールの描画が完了するのを待つことにします。
コントロールの描画が完了したかどうかは、WindowがアクティベートになかったかどうかをIsActivateプロパティを監視することで判断することにします。
やり方はいろいろあるかと思いますが、ここではasync/awaitを利用した非同期処理でWindowがアクティブになるのを待つことにしました。
public partial class MainWindow : Window
{
private async void LoadedProc(object sender, RoutedEventArgs e)
{
// WindowがActiveになるまで待つ
await Task.Run(() =>
{
do
{
Thread.Sleep(100);
} while (!Application.Current.Dispatcher.Invoke(() => { return IsActive; }));
});
MessageBox.Show("Loaded","TEST");
}
public MainWindow()
{
InitializeComponent();
Loaded += LoadedProc;
}
}
async/awaitについては
MSDN:Async および Await を使用した非同期プログラミング
などを参考に。IsActiveプロパティの参照がUIスレッド以外からはできないため、Application.Current.Dispatcher を利用してUIスレッド経由で参照するようにしています。
Activated版と比べてプログラムの構造そのものはこちらの方がスッキリしてますが、コードそのものは複雑化していてどちらが美しいかというと微妙なところでしょうか?
何かもっと良い書き方があるような気もしますが、とりあえず今回はここまでにします。
追記
ContentRendered を利用する
コメント欄でWPFにはContentRendered イベントがあるよと教えてもらいました。
MSDN:Window.ContentRendered イベント
ウィンドウのコンテンツが描画された後に発生します。
こんなイベントが存在するのを完全に見落としていましたが、なるほどこれは使えそうです。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ContentRendered += (s, e) => { MessageBox.Show("ContentRendered", "TEST"); };
}
}
↑のようなコードを書いて試してみたところ期待通りの動作を確認できました。
目的はこれですっきりした形で果たせそうです。
なんだか腑に落ちないので調べてみたら余計に混乱した
とりあえず目的は達したのですが、なんだか腑に落ちないところがあるので調べてみたら、余計に混乱する結果になりました。
とりあえずイベントの発生順序を
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Activated += (s, e) => { System.Diagnostics.Debug.WriteLine("Activated"); };
Loaded += (s, e) => { System.Diagnostics.Debug.WriteLine("Loaded"); };
ContentRendered += (s, e) => { System.Diagnostics.Debug.WriteLine("ContentRendered"); };
}
}
というコードを書いて確認してみたところ、
- Activated
- Loaded
- ContentRendered
の順番で発生することが判りました。
- Activated でメッセージボックス表示→Windowのコンテンツは表示されている。
- Loaded でメッセージボックス表示→Windowのコンテンツは表示されていない。
という状況からてっきりLoadedイベント→Activatedイベントの順番で発生すると思い込んでいましたがとんだ勘違いだったようです。
コンテンツの描画は、LoadedイベントからContentRenderedイベントの間に行われます。したがってActivatedイベントの発生時にはコンテンツの描画が行われていません。
Activatedイベントでメッセージボックスを表示させたとき、コンテンツの描画が完了しているようにみえたのは、MessageBox.Show はメッセージループをブロックしないため、メッセージボックスを表示しているその裏で、Loadedイベント→ContentRenderedイベントと処理が粛々と進行していた所為なのでした。
Loadedイベントでメッセージボックスを表示させた場合も、裏でContentRenderedイベントは発生しています。しかし何故かWindowのコンテンツは表示されません。
メッセージは処理されていても、Loadedイベントのハンドラはブロックされているので、それが原因かもと思い、MessageBox.Showではなく、別のWindowの派生クラスのオブジェクトを生成しShowDialogで表示させてみたところ、こんどは正常にWindowのコンテンツが表示されました。また、正常に動作しているようにみえたasync/await版のコードですが、Task.Runに渡すラムダ式の中身を空にしても動くことが判りました。正常に動作しているようにみえたのは別スレッドで処理待ちしていたからではなく、awaitの時点で処理が非同期実行に移り、呼び出し元に処理が返されたからと考えるべきでしょう。
要するに問題は
Loadedイベント内でMessageBox.Showでメッセージボックスを表示した場合、Windowのコンテンツの描画がされない。
ということのようです。
何なのでしょう? 環境依存の問題でしょうか? 他の環境がないので確認できませんが。
ちなみに環境は、Windows8.1 / .Net Framework 4.5 / Visual C# 2012 です。