WPFは、イベントも独自の機構を構築しています。WPFのイベントシステムの特徴を説明する前に、なぜその仕組みが必要になるかというシンプルな例を示したいと思います。以下のようにButtonの中にButtonがあるシンプルなケースでのイベントについて考えてみます。
<StackPanel Margin="10"><Button Click="Button_Click"><Button Content="Button" /></Button></StackPanel>
外側のButtonのClickイベントには、以下のようなMessageBoxを表示するコードを記述しています。
privatevoid Button_Click(object sender, RoutedEventArgs e) { MessageBox.Show("Click"); }
外側のボタンをクリックしたときには、このイベントハンドラが呼び出されてMessageBoxが表示されることは予想できますが、ボタンの中に置かれたボタンをクリックしたときにはどうなるでしょうか?答えは、MessageBoxが表示されます。
WPFは、複雑にコントロールを組み合わせたUIを作ることができます。そのため、上記のような露骨なものではなくても上記の例のようにコントロール内のコントロールによって通常のCLRのイベントではボタンが本来のボタンの役割を果たさない可能性が出てきます。WPFのイベントシステムは、通常のイベントを拡張して、親要素へイベントを伝搬するバブルイベントという仕組みを提供しています。HTMLになじみのある人にとってはおなじみの動きです。
バブルイベントとは、イベント発生元でイベントが処理されなかった場合親要素へイベントを伝搬させる機能をもったイベントです。上記のボタンの例では、ボタンの中に置いたボタンがクリックされたときに、中に置いたボタンでイベントが処理されなかったため、親要素のボタンにクリックイベントが伝搬して、親要素のクリックイベントハンドラが呼び出されてMessageBoxが表示されるという動きになります。
WPFでは、バブルイベントの他にトンネルイベントという形のイベントも提供しています。一般的にPreviewという命名規則で始まるイベントがそれになります。バブルイベントが、イベントの発生元から親要素・親要素…へ伝搬していくのに対して、トンネルイベントはルートの要素からイベント発生元のオブジェクトの順番でイベントが伝搬していきます。(ちょうどトンネルイベントの逆の動きになります)トンネルイベントは、ユーザーの入力を処理するイベントに対して、プログラムが処理前に割り込むポイントを提供するために使用されます。そのため、ユーザーの入力を処理するカスタムコントロールを作成する場合以外は、自分で定義することはないでしょう。
これらの、バブルイベントやトンネルイベントなどのように、イベントの発生元だけでなくWPFのコントロールのツリー上の他のオブジェクトに対しても影響を与えるイベントをルーティングイベントと言います。
ルーティングイベントの定義方法
ルーティングイベントの定義は、EventManagerのRegisterEventメソッドを使って行います。定義の例を以下に示します。
class Person : FrameworkElement { // イベント名Eventの命名規約のstaticフィールドに格納するpublicstatic RoutedEvent ToAgeEvent = EventManager.RegisterRoutedEvent( "ToAge", // イベント名 RoutingStrategy.Tunnel, // イベントタイプtypeof(RoutedEventHandler), // イベントハンドラの型typeof(Person)); // イベントのオーナー// CLRのイベントのラッパーpublicevent RoutedEventHandler ToAge { add { this.AddHandler(ToAgeEvent, value); } remove { this.RemoveHandler(ToAgeEvent, value); } } // 子を追加するメソッドpublicvoid AddChild(Person child) { this.AddLogicalChild(child); } }
基本的には、依存関係プロパティなどと同じで専用の登録メソッドを使ってイベントを登録して、それのCLR用のラッパーを作成するという流れになります。第二引数に、トンネルかバブルかを指定します。一般的にトンネルイベントの名前はPreviewイベント名になります。
今回定義したイベントはトンネルイベントなのでイベント発生元から親へ登っていく形になります。以下にイベントを発行するプログラムの例を示します。
var parent = new Person { Name = "parent" }; var child = new Person { Name = "child" }; parent.AddChild(child); parent.ToAge += (object s, RoutedEventArgs e) => { Console.WriteLine(((Person)e.Source).Name); }; parent.RaiseEvent(new RoutedEventArgs(Person.ToAgeEvent)); child.RaiseEvent(new RoutedEventArgs(Person.ToAgeEvent));
まず、トンネルイベントの挙動を確認するための親子関係を構築しています。そして親のオブジェクトのほうでToAgeイベントハンドラの登録を行っています。ルーティングイベントでは、イベントの発生元がsenderとは限りません。(今回の例ではsenderにはparentが入ってきます)イベントの発生元を取得するには、イベント引数のSourceプロパティを利用します。今回の例では、イベントの発生元のNameプロパティの値を表示しています。
最後の2行は、parentとchildで、イベントの発行を行っています。ルーティングイベントの発行は、RaiseEventメソッドにRoutedEventArgsを渡す形で行います。RoutedEventArgsは、イベントの種類を表すRoutedEventを受け取ります。
このプログラムを実行すると以下のように表示されます。
parent child
childには、イベントハンドラを登録していませんが、親のイベントハンドラが呼び出されてることが確認できます。
イベントのキャンセル
ルーティングイベントは、RoutedEventArgsのHandledプロパティをtrueにすることで、後続のイベントをキャンセルすることが出来ます。この機能を使うと、トンネルイベントやバブルイベントを途中でインターセプトして後続のイベントの処理をキャンセルすることが出来ます。
添付イベント
WPFのイベントシステムは、トンネルイベント、バブルイベントがあることを説明しました。このようなイベントがあるとクリックイベントがボタンで発生したとき、WindowやPanel系コントロールでもClickイベントが発生することになります。このような状況に対応するためにWindowやPanel系コントロールに全てのオブジェクトの全てのイベントを実装するのは現実的ではありません。WPFでは添付イベントという仕組みで、本来そのオブジェクトで定義されてないルーティングイベントを処理する方法を提供しています。
StackPanelにButtonのClickイベントを添付イベントとして設定するXAMLを以下に示します。
<StackPanel Button.Click="StackPanel_Click"><Button Content="Button1" /></StackPanel>
基本的に添付プロパティと同じような記述になります。これと同じことをコードで記述する場合は以下のようになります。stackPanelという変数にButtonが置いてあるStackPanelが入っている場合のコード例です。
this.stackPanel.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.StackPanel_Click));
過去記事
- WPF4.5入門 その1 「はじめに」
- WPF4.5入門 その2 「WPFとは」
- WPF4.5入門 その3 「Hello world」
- WPF4.5入門 その4 「Mainメソッドはどこにいった?」
- WPF4.5入門 その5 「全てC#でHello world」
- WPF4.5入門 その6 「WPFを構成するものを考えてみる」
- WPF4.5入門 その7 「XAMLのオブジェクト要素と名前空間」
- WPF4.5入門 その8 「オブジェクト要素のプロパティ」
- WPF4.5入門 その9 「コレクション構文」
- WPF4.5入門 その10 「コンテンツ構文」
- WPF4.5入門 その11 「マークアップ拡張」
- WPF4.5入門 その12 「その他のXAMLの機能」
- WPF4.5入門 その13 「簡単なレイアウトを行うコントロール」
- WPF4.5入門 その14 「レイアウトコントロールのCanvasとStackPanel」
- WPF4.5入門 その15 「レイアウトコントロールのDockPanelとWrapPanel」
- WPF4.5入門 その16 「ViewBoxコントロール」
- WPF4.5入門 その17 「ScrollViewerコントロール」
- WPF4.5入門 その18 「Gridコントロール part 1」
- WPF4.5入門 その19 「Gridコントロール part 2」
- WPF4.5入門 その20 「レイアウトに影響を与えるプロパティ」
- WPF4.5入門 その21 「WPFのコンセプトと重要な機能つまみ食い」
- WPF4.5入門 その22 「Buttonコントロール」
- WPF4.5入門 その23 「DataGridコントロール その1」
- WPF4.5入門 その24 「DataGridコントロール その2」
- WPF4.5入門 その25 「TreeViewコントロール その1」
- WPF4.5入門 その26 「TreeViewコントロール その2」
- WPF4.5入門 その28 「Calendarコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その29 「ContextMenuコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その30「Menuコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その31 「ToolBarコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その32 「CheckBoxコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その33 「ComboBoxコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その34 「ListBoxコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その35 「RadioButtonコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その36 「Sliderコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その37 「TabControl」 - かずきのBlog@hatena
- WPF4.5入門 その38 「ファイルダイアログ」 - かずきのBlog@hatena
- WPF4.5入門 その39 「情報を表示するコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その40 「Popup、ToolTip、TextBox、Image、MediaElementコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その41 「DispatcherObject」 - かずきのBlog@hatena
- WPF4.5入門 その42 「WPFのプロパティシステム」 - かずきのBlog@hatena
- WPF4.5入門 その43 「読み取り専用の依存関係プロパティ」 - かずきのBlog@hatena
- 拡張されたプロパティメタデータ - かずきのBlog@hatena
- WPF4.5入門 その45 「添付プロパティ」 - かずきのBlog@hatena