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


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

C# のイベント

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

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

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

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

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

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

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

// 監視する側が実装すべきインターフェース
interface IObserver
{
  // Subject に変更があったときに呼ばれるコールバック
  void Update(Subject s);
}

// 監視される側の基本クラス
class Subject
{
  private readonly List<IObserver> _observers = new List<IObserver>();
  // 監視してる人全員に何かがあったことを伝える
  protected void Notify()
  {
    foreach (var o in _observers)
    {
      o.Update(this);
    }
  }
  // 監視する人を追加する
  public void AddObserver(IObserver o) => _observers.Add(o);
}

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

class Counter : Subject
{
  public int Value { get; private set; }
  public void Increment()
  {
    Value++;
    // カウント値が変更があったことを通知する
    Notify();
  }
}

// カウンターの値が変化したらコンソールに出す人
class OutputToConsoleObserver : IObserver
{
  public void Update(Subject s)
  {
    Console.WriteLine(((Counter)s).Value);
  }
}

// 特に意味はないけどカウンターの値を2倍にして出す人
class DoubleObserver : IObserver
{
  public void Update(Subject s)
  {
    Console.WriteLine($"俺の方が数が大きいぞ:{((Counter)s).Value * 2}");
  }
}

public class Program
{
  public static void Main()
  {
    // カウンターを作って
    var counter = new Counter();
    // カウンターの値を監視して処理をする人を追加
    counter.AddObserver(new OutputToConsoleObserver());
    counter.AddObserver(new DoubleObserver());

    // カウンターの値を増やす
    counter.Increment();
    counter.Increment();
  }
}

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

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

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

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

using System;
using System.Collections.Generic;
using System.Linq;

// 監視される側の基本クラス
class Subject
{
  private readonly List<Action<Subject>> _observers = new List<Action<Subject>>();
  protected void Notify()
  {
    foreach (var o in _observers)
    {
      o(this);
    }
  }
  // 監視する人を追加する
  public void AddObserver(Action<Subject> o) => _observers.Add(o);
}

class Counter : Subject
{
  public int Value { get; private set; }
  public void Increment()
  {
    Value++;
    // カウント値が変更があったことを通知する
    Notify();
  }
}

public class Program
{
  public static void Main()
  {
    var counter = new Counter();
    counter.AddObserver(Print);
    counter.AddObserver(PrintDouble);

    counter.Increment();
    counter.Increment();
  }

  // 表示するだけの人
  private static void Print(Subject s)
  {
    Console.WriteLine(((Counter)s).Value);
  }

  private static void PrintDouble(Subject s)
  {
    Console.WriteLine($"俺の方が数が大きいぞ: {((Counter)s).Value * 2}");
  }
}

実行結果は同じです。

本題に戻る

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

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

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

using System;

public class Program
{
  public static void Main()
  {
    var h = new EventHandler(Handler1);
    h += Handler2;

    h(null, EventArgs.Empty); // メソッドのように呼べる
    // h.Invoke(null, EventArgs.Empty); // Invoke メソッドでも上と同じ
  }

  private static void Handler1(object sender, EventArgs e)
  {
    Console.WriteLine("Handler1 が呼ばれました");
  }
  private static void Handler2(object sender, EventArgs e)
  {
    Console.WriteLine("Handler2 が呼ばれました");
  }
}

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

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

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

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

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

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

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

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

using System;
using System.Collections.Generic;
using System.Linq;

class Counter
{
  // イベントを定義
  public event EventHandler Changed;
  public int Value { get; private set; }

  public void Increment()
  {
    Value++;
    // イベントを呼び出す
    // イベントハンドラが登録されてないと Changed は null なので
    // null チェックをして呼び出す。(?. 演算子で一行で書ける
    Changed?.Invoke(this, EventArgs.Empty); // 第一引数が sender, 第二引数がイベント引数
  }
}

public class Program
{
  public static void Main()
  {
    // カウンターを作ってイベントハンドラーを登録
    var counter = new Counter();
    counter.Changed += Print;

    // インクリメントしてイベントを発火してもらう
    counter.Increment();
    counter.Increment();
  }

  // 第一引数が sender (この場合カウンター), 第二引数がイベント引数
  private static void Print(object sender, EventArgs e)
  {
    Console.WriteLine(((Counter)sender).Value);
  }
}

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

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


using System;
using System.Collections.Generic;
using System.Linq;

class CounterChangedEventArgs : EventArgs
{
  public int Value { get; }
  public CounterChangedEventArgs(int value) => Value = value;
}
class Counter
{
  // イベントを定義。独自イベント引数を使う場合は型引数で指定する
  public event EventHandler<CounterChangedEventArgs> Changed;
  public int Value { get; private set; }

  public void Increment()
  {
    Value++;
    // イベントを呼び出す
    // イベント引数は自分で定義したイベント引数ようの型にする
    Changed?.Invoke(this, new CounterChangedEventArgs(Value));
  }
}

public class Program
{
  public static void Main()
  {
    // カウンターを作ってイベントハンドラーを登録
    var counter = new Counter();
    counter.Changed += Print;

    // インクリメントしてイベントを発火してもらう
    counter.Increment();
    counter.Increment();
  }

  private static void Print(object sender, CounterChangedEventArgs e)
  {
    // イベント引数に設定されている値を使うようにすることもできる
    Console.WriteLine(e.Value);
  }
}

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

GUI ではどう使われてる?

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

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

public class MyWindow : Window
{
  public MyWindow()
  {
    var button = new Button { 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>
    <Button Content="Button1" />
    <Button Content="Button2" />
  </StackPanel>
</Button>

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

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

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

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

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

まとめ

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