Reactive Extensionsつまみ食い(1)

はじめまして

画像処理系のソフトウェアを開発しているプログラマです。
仕事でC#を使い始めて約2年ほど経ち、ようやく手になじんできました。
とはいえ、.Netの世界は広く深いので、まだまだほんの入り口に足を踏み入れた程度でしょう。

このブログでは、C#、.Net関連の話題を中心にプログラミングに関する記事を書く予定です。
仕事では試せないことや、日々気になったトピックなどを取り上げたいと思います。

仕事と家事と子守りの合間を縫って、1日1行でもコーディングを続けたいと思いこのブログを始めました。
記事の更新はマイペースでやっていきたいと思いますが、どうぞよろしくお願いします。

第1回目のテーマは - Reactive Extensions -

ブログの最初のテーマですが、
Twitterで度々話題にあがっていてずっと気になっていたReactive Extensions(Rx)を試してみたいと思います。
ちょうど良いタイミングで@ITに「Reactive Extensions(Rx)入門 」が連載中でしたので、この記事を参考にRxをつまみ食いしてみたいと思います。

今回は「第2回 イベント・プログラミングとRx」から。
http://www.atmarkit.co.jp/fdotnet/introrx/introrx_02/introrx_02_02.html

この記事で紹介されている、マウスイベントの合成について試してみたいと思います。
なお、作成するアプリケーションはWindows Formsアプリケーションとします。

イベントからIObservableを生成する

まずは、マウスイベントをObservableオブジェクトに変換します。

Observable.FromEventメソッドを使用して、MouseDown、MouseMove、MouseUpの各イベントをObservable化します。
今回はWindowsFormsアプリケーションですので、System.Windows.Forms.Controlクラスの拡張メソッドとして纏めます。

public static class ControlExtensions
{
  //マウスダウンイベント
  public static IObservable<MouseEventArgs> MouseDownAsObservable(this Control control)
  {
    return Observable.FromEvent<MouseEventHandler, MouseEventArgs>(
             h => (o, e) => h(e),
             h => control.MouseDown += h,
             h => control.MouseDown -= h);
  }
  //マウスムーブイベント
  public static IObservable<MouseEventArgs> MouseMoveAsObservable(this Control control)
  {
    return Observable.FromEvent<MouseEventHandler, MouseEventArgs>(
             h => (o, e) => h(e),
             h => control.MouseMove += h,
             h => control.MouseMove -= h);
  }
  //マウスアップイベント
  public static IObservable<MouseEventArgs> MouseUpAsObservable(this Control control)
  {
    return Observable.FromEvent<MouseEventHandler, MouseEventArgs>(
             h => (o, e) => h(e),
             h => control.MouseUp += h,
             h => control.MouseUp -= h);
  }
}

マウスドラッグイベントの合成

これらObservable化されたマウスイベントからマウスドラッグイベントを合成します。
ドラッグイベントを生成する処理も、他のマウスイベントと同様に拡張メソッドとして実装します。

//マウスドラッグイベント生成 拡張メソッド
public static IObservable<MouseDragEventArgs> MouseDragAsObservable(
                this Control control, MouseButtons mouseButton)

戻り値はドラッグ処理中に必要なデータをまとめた自作イベント引数のObservableオブジェクトです。

//マウスドラッグイベント引数
public class MouseDragEventArgs : EventArgs
{
  //マウスボタン押下位置
  public Point StartLocation { set; get; }
  //直前のマウス位置
  public Point LastLocation { set; get; }
  //現在のマウス位置
  public Point Location { set; get; }
  //コンストラクタ
  public MouseDragEventArgs(Point startLocation, Point lastLocation, Point Location)
  {
    this.StartLocation = startLocation;
    this.LastLocation = lastLocation;
    this.Location = Location;
  }
}

ドラッグ中の処理では、現在のマウス位置に加え、ドラッグ開始時のマウス位置や直前のマウス位置が必要となることが多々あります。これらがまとまってSubscribeへと流れるようにイベントデータを加工します。

また、拡張メソッドの引数 mouseButtonでドラッグイベントが発動するボタンを指定するようにしました。
これで右ボタンドラッグ、左ボタンドラッグ等、押下されたボタンに応じた処理を別々に記述することが可能となります。

合成処理 詳細

以上を踏まえて、ドラッグイベントを合成する処理の中身を実装してみます。

//マウスドラッグイベント生成 拡張メソッド
public static IObservable<MouseDragEventArgs> MouseDragAsObservable(
                this Control control, MouseButtons mouseButton)
{
  //マウスダウンイベント
  var down = control.MouseDownAsObservable()
               .Where(e => e.Button == mouseButton); //指定ボタンのみ通す
  //マウスムーブイベント
  var move = control.MouseMoveAsObservable();
  //マウスアップイベント
  var Up = control.MouseUpAsObservable()
             .Where(e => e.Button == mouseButton); //指定ボタンのみ通す

  return down      //指定ボタンが押下されたら
    .SelectMany(   //次のイベントを連結する
      e0 =>
      {
        return move
          .TakeUntil(Up)  //ボタンが放されるまでのマウスムーブを取得する。
          //イベントデータをMouseDragEventArgsに加工する
          .Select(e => new MouseDragEventArgs(e0.Location, e0.Location,e.Location))
          .Scan((e1, e2) => new MouseDragEventArgs(e1.StartLocation, e1.Location,e2.Location));
      });
}

合成の流れですが、基本的には@ITの連載記事で紹介されているマウスドラッグイベントと同様です。
記事では次のような流れとなっています。

  • MouseDownからSelectMenyメソッドでMouseMoveに接続
  • TakeUntilメソッドでMouseUpが発行されるまでMouseMoveを通す
  • SelectメソッドでMouseEventArgsからマウス位置(X,Y)を抽出

今回はSelectメソッド以降の処理を変更してイベントデータを前述のMouseDragEventArgsのかたちに加工します。

まず、Selectメソッド内でMouseDown時のマウス位置(e0.Location)と現在のマウス位置(e.Location)使用してMouseEventArgsを生成します。この時点では直前のマウス位置(LastLocation)は取得できませんので、MouseDown時のマウス位置を入れておきます。

直前のマウス位置の付加は、Scanメソッドで行います。
連載記事では、「Scanメソッドは1つ前の「結果」と現在の「値」を合成して値を流す。」と説明されています。
Scanメソッドに渡すデリゲートの引数には、「e1」に1つ前の(Scanの)結果、「e2」に現在の(Selectから流れてくる)値が渡されます。

そこで、以下の通りにMouseEventArgsを生成します。

  • StartLocation = 1つ前のボタン押下位置(e1.StartLocation)
  • LastLocation = 1つ前のマウス位置(e1.Location)
  • Location = 現在のマウス位置(e2.Location)

また、MouseDown(およびMouseUp)イベントでは、指定したボタンを押下した(放した)場合のみを通過させるよう、Whereメソッドでフィルタリングしています。

使用例

次のコードは、マウスドラッグイベントを使用した簡単なプログラムの例です。
左ボタンドラッグで直線、右ボタンドラッグで四角形を描画します。

//Form1コンストラクタ
private Form1()
{
  
  var line= new Point[2];       //直線座標
  var rect = new Rectangle();   //矩形(四角形)座標
  
  //左ボタンドラッグ
  this.MouseDragAsObservable(MouseButtons.Left)
     .Subscribe(
       e=>
       {
         //マウス押下位置から現在位置まで直線を引く
         line[0] = e.StartLocation;
         line[1] = e.Location;
         this.Refresh();  //描画面更新
       });
  
  //右ボタンドラッグ
  this.MouseDragAsObservable(MouseButtons.Right)
     .Subscribe(
       e=>
       {
         //マウス押下位置から現在位置までを対角線とする四角形を描画
         rect = new Rectangle(e.StartLocation, 
                              new Size(Math.Abs(e.Location.X-e.StartLocation.X),
                                       Math.Abs(e.Location.Y-e.StartLocation.Y)));
         this.Refresh();  //描画面更新
       });
       
  //ペイントイベント
  this.Paint += 
    (sender,e)=>
    {
      //図形の描画を実行
      e.Graphics.DrawLine(Pens.Red, line[0], line[1]);
      e.Graphics.DrawRectangle(Pens.Blue, rect);
    }
}

従来のイベント処理ではMouseDownでボタン押下時のマウス位置を始点に設定、MouseMoveで現在のマウス位置を終点に設定のようにイベント毎に分散していた処理が、Subscribe内にまとめて記述可能となりました。
また、押下されたボタンの種類によって異なる処理も、イベント内で条件分岐する必要なくスッキリと記述できました。

まとめ

今回は比較的単純な処理のサンプルでしたが、十分にRxの威力を感じとることができました。

Rxによるイベントの合成のポイント!
  • 複雑なイベントの組み合わせも、Observableのメソッドチェーンにまとめる事で処理の流れが追いやすくなる。
  • 実現したいロジックそのものと、イベントの状態管理部分とを分離することで、見通しが良くなるだけでなく、再利用もしやすくなる。

次回

次回も引き続きReactive Extensionsによるイベントの合成で行きたいと思います。
テーマは「Rxでマウスジェスチャ―処理を作る」です。