xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

Rx入門 (20) - Drag & Dropでの落書き

今回はDrag & Dropでキャンバスに落書きを行うサンプルを紹介します。通常のDrag & Drop処理と比較して、どれほど簡単に書けるのか比較してみましょう。

Rxを利用しないDrag & Drop処理

まず、Rxを利用しない場合のサンプルコードを示します。以下のように、ドラッグ中のフラグと前点記憶用のフィールドを用意する形になると思います。

using System.Drawing;
using System.Windows.Forms;
 
namespace WindowsFormsApplication
{
    public partial class MainForm : Form
    {
        //--- ドラッグ中のフラグと前の点を覚えるプロパティ
        private bool IsDragging{ get; set; }
        private Point PreviousLocation{ get; set; }
 
        public MainForm()
        {
            this.InitializeComponent();
        }
 
        protected override void OnMouseDown(MouseEventArgs e)
        {
            //--- 開始点を記憶
            this.PreviousLocation = e.Location;
            base.OnMouseDown(e);
        }
 
        protected override void OnMouseMove(MouseEventArgs e)
        {
            //--- 左ボタンが押されていればドラッグ開始
            if (!this.IsDragging)
            if (e.Button.HasFlag(MouseButtons.Left))
                this.IsDragging = true;
 
            //--- ドラッグ中は前の点と今の点を結ぶ
            if (this.IsDragging)
            {
                using (var graphic = this.CreateGraphics())
                using (var pen = new Pen(Color.Red, 1))
                    graphic.DrawLine(pen, this.PreviousLocation, e.Location);
                this.PreviousLocation = e.Location; //--- 今の点を覚えておく
            }
            base.OnMouseMove(e);
        }
 
        protected override void OnMouseUp(MouseEventArgs e)
        {
            //--- ドラッグ状態をクリア
            this.IsDragging       = false;
            this.PreviousLocation = Point.Empty;
            base.OnMouseUp(e);
        }
    }
}

ひとつひとつ分解すれば理解できますが、Drag & Drop開始から終了までと実際の処理が入り混じっていて分かりやすいコードとは言い難いです。

サンプルコード

以下にサンプルコードを示します。ポイントが幾つかあるので順番に説明します。

まず、特筆すべきなのはDrag & Dropの処理をひとつのストリームとして書けることです。イベントハンドラ方式ではフラグを使って書いていたあの (忌々しい) Drag & Dropが、たった1文で、しかもフラグもなく書けるなんて!Rx恐るべし!イベントをIObservable<T>に変換するコードは長くなりがちなので、必要に応じてメソッドに退避することをオススメします。

ふたつ目は、前後の値をまとめる書き方ができることです。Observable.ZipメソッドとObservable.Skipメソッドを使ったイディオム表現です (知ってるから書いているだけで、ソラでは書けません...)。Observable.Zipメソッドの動きについては@ITの記事で図解されているので、ぜひ参考にしてください。

そして最後に、Observable.RepeatメソッドでDrag & Dropの処理を繰り返すように指定します。これがないと1ストロークの線しか描けません。

using System;
using System.Drawing;
using System.Reactive.Linq;
using System.Windows.Forms;
 
namespace Sample39_Scribble
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            this.InitializeComponent();
 
            //--- ドラッグ処理を規定
            var drag    = this.MouseDownAsObservable()
                        .SelectMany(_ => this.MouseMoveAsObservable())
                        .TakeUntil(this.MouseUpAsObservable());
            drag.Zip //--- 前後の値でパッケージ化
            (
                drag.Skip(1),
                (x, y) => new { Prev = x.Location, Next = y.Location }
            )
            .Repeat() //--- 1ストロークで終わらず、何度も繰り返す
            .Subscribe(location =>
            {
                //--- 前の点と次の点を直線で結ぶ
                using (var graphic = this.CreateGraphics())
                using (var pen = new Pen(Color.Red, 1))
                    graphic.DrawLine(pen, location.Prev, location.Next);
            });
        }
 
        #region イベントからの変換はメソッドに退避
        IObservable<MouseEventArgs> MouseDownAsObservable()
        {
            return Observable.FromEvent<MouseEventHandler, MouseEventArgs>
            (
                handler => (sender, e) => handler(e),
                handler => this.MouseDown += handler,
                handler => this.MouseDown -= handler
            );
        }
 
        IObservable<MouseEventArgs> MouseMoveAsObservable()
        {
            return Observable.FromEvent<MouseEventHandler, MouseEventArgs>
            (
                handler => (sender, e) => handler(e),
                handler => this.MouseMove += handler,
                handler => this.MouseMove -= handler
            );
        }
 
        IObservable<MouseEventArgs> MouseUpAsObservable()
        {
            return Observable.FromEvent<MouseEventHandler, MouseEventArgs>
            (
                handler => (sender, e) => handler(e),
                handler => this.MouseUp += handler,
                handler => this.MouseUp -= handler
            );
        }
        #endregion
    }
}

上記からもわかるように、「ドラッグ処理の前の点と今の点を抽出し、その2点で線を結ぶ。その処理はずっと繰り返す」というように意味的に記述できています。宣言的に書けるって、素敵ですね!

実行例

実行例は以下の通りです。ちゃんとお絵かきできますね。

Scribble

参考記事

今回のサンプルは以下を参考にしています。ぜひ一緒にお読みください。