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 です。