ブログ「サイバー少年」

ブログ「サイバー少年」へようこそ!
小学六年生ごろからプログラミングを趣味にしている高校生のブログです。
勉強したことについての記事などを書いています。フリーソフトも制作、公開しています。
(当ブログについて詳しくは「ブログ概要紹介」を参照)

サイバー少年が作ったフリーソフトは「サイバー少年の作品展示場」へ

[WPF] KeyDownイベントとPreviewKeyDownイベントの違い

WPFではコントロールにKeyDownというイベントとPreviewKeyDownというイベントがあります。

「サルでもわかる小遣い帳」を作っているときに違いがよくわからなかったので、検証してみました。

とりあえずPreviewKeyDownはKeyDownよりも先に発生するんじゃないかと思えますね。

つまり、キーイベントは2回発生するということで、発生タイミングが違うじゃないかというわけです。


そこで、試しにWPFで、Windowの中にPanel、その中にButtonという構成のウィンドウを作ってみました。

なお、ロジック部分の使用言語はC#です。



<Window ~~~~~ KeyDown="Window_KeyDown" PreviewKeyDown="Window_PreviewKeyDown">
<Grid KeyDown="Grid_KeyDown" PreviewKeyDown="Grid_PreviewKeyDown">
<Button KeyDown="Button_KeyDown" PreviewKeyDown="Button_PreviewKeyDown"/>
</Grid>
</Window>



そしてこのように各コントロールにKeyDownイベントとPreviewKeyDownイベントのハンドラを設定します。

そして、ハンドラは

Window_KeyDownはConsole.WriteLine("Window_KeyDown")
Window_PreviewKeyDownはConsole.WriteLine("Window_PreviewKeyDown")

というふうに、出力をするコードを書きます。
(本当はTrace.WriteLineを使いましたが)

Gridのハンドラは上の文字列を"Grid_***"、Buttonなら"Button_***"というようにします。


さて、このアプリケーションを実行して、まずはButtonにフォーカスをあてずにキーを押下してみます。

出力
Window_PreviewKeyDown
Window_KeyDown


Windowのイベントハンドラだけ呼ばれたようです。


次に、Buttonにフォーカスを設定してキーを押下してみます。

出力
Window_PreviewKeyDown
Grid_PreviewKeyDown
Button_PreviewKeyDown
Button_KeyDown
Grid_KeyDown
Window_KeyDown


この結果から推測するに、キーが押下されると、
そのときフォーカスが設定されていたコントロールを格納しているコントロールたちが、より親のコントロールから順にPreviewKeyDownを発生させていくことがわかります。

この例でいうと、Buttonにフォーカスがあったので、まずButtonの先祖で一番の親であるWindowのPreviewKeyDownイベントが発生します。

次に、Windowの子でフォーカスがあったButttonを格納しているのはGridなので、GridのPreviewKeyDownイベントが発生します。

次に、Gridの下にはもう直接Buttonが配置されているので、ButtonのPreviewKeyDownイベントが発生します。


さて、発生すべきPreviewKeyDownイベントが全て発生すると、今度はフォーカスが設定されているコントロールから順に親に向かってKeyDownイベントを発生させるということがわかります。

とりあえずフォーカスが設定されているButton自身のKeyDownイベントが発生します。

次に、Buttonの親のGridのKeyDownイベントが発生し、最後にGridの親であるWindowのKeyDownイベントが発生するということです。


要するに、親→子→親というようにイベントが流れるように発生するわけです。

調べてみると、WPF用語で親→子の流れのイベントはトンネリングイベント、子→親の流れのイベントはルーティングイベントというそうです。

WPFはなんとなくで使っていて、あまり詳しくないので説明できないのですが…。


さて、“流れるようにイベントが発生する”と書きましたが、ここで注意すべきはこの“流れ”を途中でキャンセルさせることが可能であるということです。

キャンセルさせる方法の一つは、イベントハンドラでKeyEventArgs型の引数がありますので、そこのCancelプロパティをTrueにすることです。

例えば上の例の、GridのPreviewKeyDownイベントハンドラでConsole.WriteLineのあとに

e.Cancel = true;

という処理を付け加えてみます。
eというのはKeyEventArgs型の引数です。

そして、Buttonにフォーカスをあててキーを押下します。
すると

出力
Window_PreviewKeyDown
Grid_PreviewKeyDown


ここで止まってしまいます。

GridのPreviewKeyDownイベントで流れがキャンセルされたため、Buttonまでたどり着かなかったわけです。

では次は、GridのPreviewKeyDownではなくKeyDownイベントのハンドラに

e.Cancel = true; を付け加えてみます。

そして、同じようにキーを押下します。

出力
Window_PreviewKeyDown
Grid_PreviewKeyDown
Button_PreviewKeyDown
Button_KeyDown
Grid_KeyDown


GridのKeyDownイベントで流れがキャンセルされたため、WindowのKeyDownだけ呼ばれなかったということです。

こんな感じで流れをキャンセルすることができます。
流れのキャンセルにはもう一つあります。

コントロールの操作に使う特別なキー、いわゆる「アクセスキー」を押下すると自動で流れは止められてしまうのです。


たとえば、Buttonのアクセスキーには、ボタンを押すスペースキーや、違うコントロールにフォーカスを移すタブキーがあります。

また、実験していませんが、TextBoxなんかはほぼ全てのキーがアクセスキーなんじゃないかと思います。


コントロールに対してアクセスキーが押下されると、とりあえずそのコントロールのPreviewKeyDownが発生して、そこで止まります。

たとえば、上と同じ状況でButtonにフォーカスをあててスペースキーを押すと

出力
Window_PreviewKeyDown
Grid_PreviewKeyDown
Button_PreviewKeyDown


これらだけ出力されます。
ButtonのPreviewKeyDownのあとにButtonではClickイベントが発生しています。


ですから、全てのキー押下イベントをキャッチしたい場合は、一番の親であるWindowのPreviewKeyDownイベントに処理を書いて、

キャンセルされたりアクセスキーとしての使用を全くされずに、未使用の状態となったキー押下のみをキャッチしたい場合は、
これも一番の親であるWindowの、今度はKeyDownイベントに処理を書けばいいということですね。


PreviewKeyDownイベントとKeyDownイベントはこのように使い分けましょう。


KeyUpイベントをはじめ、ほかにも“なんちゃら”イベントと“Previewなんちゃら”イベントの2つが存在するイベントがありますが、

これも今回のKeyDownの件と同じように作動するんじゃないかと思います。(未検証ですが…)


それにしても、WPFはわからないことだらけですね。
トンネリングイベントとか、ルーティングイベントとか、ややこしい…。

tag: WPF イベント キーボード

コメント

コメントの投稿

トラックバック

トラックバック URL
http://cyberboy6.blog.fc2.com/tb.php/364-d670a346
この記事にトラックバックする(FC2ブログユーザー)

当ブログをご利用(閲覧等)になる場合は必ず「当ブログの利用規定」をお守りください。