かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Reactive Extensions再入門 その9「Skip + Take + Repeat = ドラッグ」

はじめに

しばらく間があいてしまったReactive Extensions再入門ですが、飽きたり忘れたりしてるわけじゃぁないです。12/3の発表用の資料作りのほうを優先させてるのでこちらの更新が滞っている状態ですm(_ _)m
12/3のRIA アーキテクチャ研究会が終わったらまた、前のペースで書きたいな〜と思ってます。ということで今回は、GUIのプログラムで誰もが一度はやるであろうウィンドウ内をマウスでドラッグしたときの処理を、これまでやってきたこと+αで実装してみようと思います。

SkipUntilとTakeUntilを使ったドラッグの処理

ここでは、SkipUntilとTakeUntilを使ってWPFアプリケーションでマウスのドラッグを処理してみます。通常のアプリケーションでは恐らくMouseDown, MouseUp, MouseMoveのイベントを処理して実現すると思います。MouseDownでフラグ変数を立ててマウスをキャプチャし、MouseUpでフラグ変数を下げてマウスのキャプチャを解放します。そして、MouseMoveイベントではフラグ変数を見てマウスボタンが押されているかどうか判定してドラッグ中の処理を記述します。このように単純なドラッグの処理を行うだけでも、フラグを使用したプログラムになってしまいます。Reactive Extensionsを使うとイベントを組み合わせて下記のように記述出来ます。
まず、MouseDown, MouseUp, MouseMoveのイベントをFromEventメソッドを使ってIObservableに変換します。

// マウスダウン、マウスアップ、マウスムーブのIObservableを作る
var mouseDown = Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>(
    h => (s, e) => h(e),
    h => this.MouseDown += h,
    h => this.MouseDown -= h);

var mouseMove = Observable.FromEvent<MouseEventHandler, MouseEventArgs>(
    h => (s, e) => h(e),
    h => this.MouseMove += h,
    h => this.MouseMove -= h);

var mouseUp = Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>(
    h => (s, e) => h(e),
    h => this.MouseUp += h,
    h => this.MouseUp -= h);

ドラッグ処理は、マウスが押された状態でマウスが動いている間なので、SkipUntilでMouseMoveイベントをMouseDownが行われるまで読み飛ばし、TakeUntilでMouseUpが行われるまでTakeすることで表しています。これだけだと、一度きりの処理なのでRepeatメソッドを使って繰り返し処理をするようにしています。コードを下記に示します。

var drag = mouseMove
    // マウスムーブをマウスダウンまでスキップ。マウスダウン時にマウスをキャプチャ
    .SkipUntil(mouseDown.Do(_ => this.CaptureMouse()))
    // マウスアップが行われるまでTake。マウスアップでマウスのキャプチャをリリース
    .TakeUntil(mouseUp.Do(_ => this.ReleaseMouseCapture()))
    // ドラッグが終了したタイミングでCompletedを表示
    .Finally(() => textBlock.Text = "Completed")
    // これを繰り返す
    .Repeat();

TakeUntilの後にFinallyメソッドという使ったことの無いメソッドを使用していますが、このメソッドはIObservableのシーケンスが終了したタイミングでしたい処理を記載することが出来るメソッドです。こうすることで、一回のドラッグが終了したタイミングでTextBlockにCompletedと表示するようにしています。その他に、mouseDownとmouseUpで使用しているDoメソッドは、純粋にIObservableのシーケンスに値が流れてきた時に処理を行うためのメソッドです。ここで、マウスのキャプチャとキャプチャのリリースを行っています。
最後に、このdragをSubscribeしてドラッグ中の処理を記述します。

// ドラッグ中は、イベント引数から座標を取り出して表示用に整えてTextBlockに設定
drag.Select(e => e.GetPosition(null))
    .Select(p => string.Format("X: {0}, Y:{1}", p.X, p.Y))
    .Subscribe(s => textBlock.Text = s);

イベント引数から座標を取り出し、座標を表示用文字列に整形して、TextBlockに表示させています。因みに、上記の処理はWindow1.xaml.csのコンストラクタに記載しています。Window1.xamlXAMLは下記のようになっています。

<Window x:Class="DragSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid Name="layoutRoot">
        <TextBlock Name="textBlock" />
    </Grid>
</Window>

実行結果

このプログラムを実行すると、下記のように何も表示されないウィンドウが表示されます。

このウィンドウ上でドラッグを行うと下図のようにドラッグしている箇所の座標がリアルタイムで表示されます。

ドラッグを終了すると下図のようにCompletedと表示されます。


このようにSkipUntilとTakeUntilとRepeat, Selectなどのこれまで紹介してきたメソッドを組み合わせることで通常はフラグなどを使用して記載する処理を非常にシンプルに記載できました。
最後に、MainWindow.xaml.csのコードの全体を下記に示します。

namespace DragSample
{
    using System;
    using System.Reactive.Linq;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Shapes;

    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // マウスダウン、マウスアップ、マウスムーブのIObservableを作る
            var mouseDown = Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>(
                h => (s, e) => h(e),
                h => this.MouseDown += h,
                h => this.MouseDown -= h);

            var mouseMove = Observable.FromEvent<MouseEventHandler, MouseEventArgs>(
                h => (s, e) => h(e),
                h => this.MouseMove += h,
                h => this.MouseMove -= h);

            var mouseUp = Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>(
                h => (s, e) => h(e),
                h => this.MouseUp += h,
                h => this.MouseUp -= h);

            var drag = mouseMove
                // マウスムーブをマウスダウンまでスキップ。マウスダウン時にマウスをキャプチャ
                .SkipUntil(mouseDown.Do(_ => this.CaptureMouse()))
                // マウスアップが行われるまでTake。マウスアップでマウスのキャプチャをリリース
                .TakeUntil(mouseUp.Do(_ => this.ReleaseMouseCapture()))
                // ドラッグが終了したタイミングでCompletedを表示
                .Finally(() => textBlock.Text = "Completed")
                // これを繰り返す
                .Repeat();

            // ドラッグ中は、イベント引数から座標を取り出して表示用に整えてTextBlockに設定
            drag.Select(e => e.GetPosition(null))
                .Select(p => string.Format("X: {0}, Y:{1}", p.X, p.Y))
                .Subscribe(s => textBlock.Text = s);
                    
        }

    }
}

まとめ

ドラッグの処理を1つもフラグ変数を使うことなくすっきりと書くことが出来ました。個人的には、このドラッグを処理するプログラムを初めて見たときにReactive Extensionsすげ〜〜!!ってなったのですがどうでしょう。
次回は、この他のIObservableに定義された拡張メソッドを見ていこうと思います。