Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9739

WPF のイベントについて C# のイベントから説明をしてみる(図はない)

$
0
0

WPF でプログラムを組むときには切っても切れない WPF のイベントであるルーティング イベント。
まぁ、これは結構難しくて C# の初学者がいきなり学ぼうとしても基本的なことから積み上げないといけなくて、基本から WPF のイベントまでを順を追って説明している資料って多分ないので書いてみようと思います。

C# のイベント

まずはイベントです。詳しい説明は イベント - C# によるプログラミング入門 | ++C++にもありますが、ここでも説明しておこうと思います。

イベントは、何かが発生したことを外部に伝えて、外の人は何かが発生したときに好きな処理を行うことができるようにするものです。GUI とかで非常にわかりやすい例としては「"クリックされた"ということが起きたときに画面のプログラムで好きな処理を行う」ことなどに使われたりします。

それ以外にもイベント発生のきっかけとしては一行ぶんのデータを読み込んだ時や、テキストボックスにテキストが入力された時や、サーバーからデータを受信した時などイベントを発生されるタイミングというのはプログラムで好きなように作り込むことができます。

イベントを受け取る側では、何か一行ぶんのデータを受信したらデータを解析して画面に表示したり、解析結果を別のプログラムに渡したり、受け取ったデータをデータベースに保存したり好きなように処理を行えます。

余談:オブザーバー パターン

ちょっと余談に走りますが、オブザーバー パターンというデザイン パターンがあります。これは監視される側(サブジェクト)で何かが起きたおきに監視する側(オブザーバー)に通知を行うような処理を書くときのパターンです。

素直に必要最低限だけ実装すると以下のような下準備のクラスを用意します。

// 監視する側が実装すべきインターフェースinterfaceIObserver{// Subject に変更があったときに呼ばれるコールバックvoidUpdate(Subjects);}// 監視される側の基本クラスclassSubject{privatereadonlyList<IObserver>_observers=newList<IObserver>();// 監視してる人全員に何かがあったことを伝えるprotectedvoidNotify(){foreach(varoin_observers){o.Update(this);}}// 監視する人を追加するpublicvoidAddObserver(IObservero)=>_observers.Add(o);}

例えばカウンターを作ってカウンターの値が変わるたびに表示したりするようなプログラムを作ってみましょう。

classCounter:Subject{publicintValue{get;privateset;}publicvoidIncrement(){Value++;// カウント値が変更があったことを通知するNotify();}}// カウンターの値が変化したらコンソールに出す人classOutputToConsoleObserver:IObserver{publicvoidUpdate(Subjects){Console.WriteLine(((Counter)s).Value);}}// 特に意味はないけどカウンターの値を2倍にして出す人classDoubleObserver:IObserver{publicvoidUpdate(Subjects){Console.WriteLine($"俺の方が数が大きいぞ:{((Counter)s).Value*2}");}}publicclassProgram{publicstaticvoidMain(){// カウンターを作ってvarcounter=newCounter();// カウンターの値を監視して処理をする人を追加counter.AddObserver(newOutputToConsoleObserver());counter.AddObserver(newDoubleObserver());// カウンターの値を増やすcounter.Increment();counter.Increment();}}

実行すると以下のようになります。

1
俺の方が数が大きいぞ:2
2
俺の方が数が大きいぞ:4

こんな感じでデータや外部とやり取りするような処理と、その変化に応じて処理をする人を分離して書けるのがオブザーバー パターンです。監視される側(この例ではカウンター)と監視する側(この例では出力する人)を分離できるし、監視する側が何をするかはカウンターは何も意識する必要がありません。さらに監視する側は好きなように後で追加して増やすこともできます。

因みに C# には delegate というものがあるので、メソッド自体を変数に入れたりすることができます(.NET Framework 1.0 からあった) 。なので、わざわざ何かあったときの処理を表すためにインターフェースを作る必要もない感じです。オブザーバー パターンをインターフェースを使わないで実装すると以下のようにも実装できます。

usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;// 監視される側の基本クラスclassSubject{privatereadonlyList<Action<Subject>>_observers=newList<Action<Subject>>();protectedvoidNotify(){foreach(varoin_observers){o(this);}}// 監視する人を追加するpublicvoidAddObserver(Action<Subject>o)=>_observers.Add(o);}classCounter:Subject{publicintValue{get;privateset;}publicvoidIncrement(){Value++;// カウント値が変更があったことを通知するNotify();}}publicclassProgram{publicstaticvoidMain(){varcounter=newCounter();counter.AddObserver(Print);counter.AddObserver(PrintDouble);counter.Increment();counter.Increment();}// 表示するだけの人privatestaticvoidPrint(Subjects){Console.WriteLine(((Counter)s).Value);}privatestaticvoidPrintDouble(Subjects){Console.WriteLine($"俺の方が数が大きいぞ: {((Counter)s).Value*2}");}}

実行結果は同じです。

本題に戻る

余談のオブザーバー パターンですが、これを言語仕様として取り入れたものが C# のイベントです。EventHandlerSubject相当で、EventHandlerに登録する処理(メソッドやラムダ式など)が IObservableや変更通知を受け取るメソッドに該当します。

オブザーバー パターンC# のイベント
SubjectEventHandler
IObservable or メソッド(実装方法による)メソッド

では、EventHandlerを生で使ってみましょう。

usingSystem;publicclassProgram{publicstaticvoidMain(){varh=newEventHandler(Handler1);h+=Handler2;h(null,EventArgs.Empty);// メソッドのように呼べる// h.Invoke(null, EventArgs.Empty); // Invoke メソッドでも上と同じ}privatestaticvoidHandler1(objectsender,EventArgse){Console.WriteLine("Handler1 が呼ばれました");}privatestaticvoidHandler2(objectsender,EventArgse){Console.WriteLine("Handler2 が呼ばれました");}}

EventHandler の実態は delegate と言ってメソッドを変数に代入するためのものです。

詳細は安定の ++C++ で。

デリゲート - C# によるプログラミング入門 | ++C++

メソッドとの違いは +=演算子を使って複数のメソッドをまとめるようなことができる点などがあります。上記プログラムを実行すると Handler1Handler2が呼び出されるので、以下のような結果になります。

Handler1 が呼ばれました
Handler2 が呼ばれました

EventHandler などの delegate を使うことでメソッドを複数保持しておいて、必要になったタイミングで一気に呼べる感じになってます。

イベントを定義してみよう

EventHandler 単品で使うと、それはただの delegate なのですが、クラスのメンバーとして定義するときに event という修飾子をつけることでイベントとして外部に公開できるようになります。
カウンタークラスをイベント付きで定義してみましょう。

usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;classCounter{// イベントを定義publiceventEventHandlerChanged;publicintValue{get;privateset;}publicvoidIncrement(){Value++;// イベントを呼び出す// イベントハンドラが登録されてないと Changed は null なので// null チェックをして呼び出す。(?. 演算子で一行で書けるChanged?.Invoke(this,EventArgs.Empty);// 第一引数が sender, 第二引数がイベント引数}}publicclassProgram{publicstaticvoidMain(){// カウンターを作ってイベントハンドラーを登録varcounter=newCounter();counter.Changed+=Print;// インクリメントしてイベントを発火してもらうcounter.Increment();counter.Increment();}// 第一引数が sender (この場合カウンター), 第二引数がイベント引数privatestaticvoidPrint(objectsender,EventArgse){Console.WriteLine(((Counter)sender).Value);}}

これが、シンプルなイベントの実装です。

イベントは第一引数に object 型の sender を受け取って、第二引数に EventArgs か EventArgs を継承したイベント引数を受け取るようになっています。イベントに対して付帯的な情報を付けたい場合は EventArgs を継承して値をイベントハンドラーに渡すことができます。上記のカウンターの例だと、例えばカウンターの値をイベント引数に入れて渡すということが考えられますね。やってみましょう。

usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;classCounterChangedEventArgs:EventArgs{publicintValue{get;}publicCounterChangedEventArgs(intvalue)=>Value=value;}classCounter{// イベントを定義。独自イベント引数を使う場合は型引数で指定するpubliceventEventHandler<CounterChangedEventArgs>Changed;publicintValue{get;privateset;}publicvoidIncrement(){Value++;// イベントを呼び出す// イベント引数は自分で定義したイベント引数ようの型にするChanged?.Invoke(this,newCounterChangedEventArgs(Value));}}publicclassProgram{publicstaticvoidMain(){// カウンターを作ってイベントハンドラーを登録varcounter=newCounter();counter.Changed+=Print;// インクリメントしてイベントを発火してもらうcounter.Increment();counter.Increment();}privatestaticvoidPrint(objectsender,CounterChangedEventArgse){// イベント引数に設定されている値を使うようにすることもできるConsole.WriteLine(e.Value);}}

実行結果は一緒です。sender をキャストして色々することもできますが、イベントハンドラーで一般的に必要とされる値をイベント引数に渡してやるというのが C# のイベントでは一般的です。

GUI ではどう使われてる?

GUI だとボタンのクリックとかテキストボックスのテキストが変更されたタイミングとか、リストボックスのリストの選択項目が変更されたときとか、そういうことがあったことをイベントとして定義しています。
こうすることでボタンはボタンとして振る舞うことに集中して、ボタンを使う人はボタンが押されたときの処理をイベントに追加することでアプリケーションの動作を作ることに集中できます。

例えば WPF でボタンを押したときにメッセージボックスを表示するような処理は以下のようになります。

publicclassMyWindow:Window{publicMyWindow(){varbutton=newButton{Content="押して"};button.Click+=(sender,args)=>MessageBox.Show("押したね!");Content=button;}}

画面いっぱいにボタンが表示されて押すとメッセージボックスが表示されます。

WPF で起きる問題

さて、ここまでの説明でイベントと GUI でのイベントの利用とか何も問題はなさそうなのですが、WPF ではちょっと問題があります。WPF は WPF が登場するまでの Windows Forms に比べて見た目を柔軟に表現できるようになっています。

今ではあまり考えられないことかもしれませんが、Twitter のツイートのような表示をリストボックスに表示させようとしたら、昔はオーナードローという仕組みを使う必要がありました。オーナードローというのは、要は自分で座標計算したりしてテキストや画像を自力で描画してねってことです。
このフォントで、この内容のテキストを表示すると何ピクセルだから、折り返しすることも考えると下にマージンがいくつ必要だから画像はこの位置に表示して…というのを自分で描いてました。控えめに言って地獄です。

WPF では、コントロールを簡単に組み合わせて表示することができます。ListBox の要素に Grid をおいて TextBlock や Image をレイアウトしてデータをはめ込んで完成。控えめに言ってオーナードローに比べたら天国です。

このような柔軟な見た目を定義可能にすると普通のイベントだと問題があります。
例えば以下のような XAML があるとします。これは WPF では有効なコントロールの置き方です。

<!-- ボタンの中に --><Button><!-- 縦並びでボタンを2個置く --><StackPanel><ButtonContent="Button1"/><ButtonContent="Button2"/></StackPanel></Button>

このとき一番外側のボタンのクリックイベントにイベントハンドラーを設定したら、内側のボタンをクリックしたときにどうなる?とか、それ以外にもボタンの中に画像をおいてるようなケースでも、ボタンの中の画像をクリックしたらボタンのクリックイベントはどうなる?という疑問が出てきます。

普通にコントロールが自分の管理する領域のマウスの動きやクリックを監視してイベントを処理していると画像上の操作はボタンにとっては別世界の話になってしまい、ボタンの上の画像をクリックするとボタンのクリックイベントは発生しないという問題が起きるかもしれません。

こう言った問題があるため、WPF は C# のイベントを拡張してルーティングイベントというものを作りました。簡単にいうと UI 部品で何かイベントがあったら親要素に対しても伝搬するような動きをするイベントです。
このような動きをする、ちょっと特殊なイベントの仕組みを作っておくと、例えばボタンの中のボタンがクリックされた時もイベントが処理されるまで親要素へ登っていって、内側のボタンのクリックを外側のボタンで処理すると言ったことができるようになっています。

詳細は以下の記事を見てみてください。

WPF4.5入門 その46 「WPF のイベントシステム」

まとめ

イベント色々あってとっかかり大変だなぁと思いました。


Viewing all articles
Browse latest Browse all 9739

Trending Articles