Reactive Extensionsを使ったWPFのドラッグアンドドロップをまとめる


# 背景
Rxの例としてドラッグアンドドロップを良く見かけます。
ちょっとずつ例が違うので、何が違うのかまとめます。

# 実現したい動き
ドラッグアンドドロップで実現したい動きは幾つかあります。

- オブジェクトを移動する
- 線を引く

今回は前者を対象とします。

![out.gif](https://qiita-image-store.s3.amazonaws.com/0/15964/090c37fd-20d8-196e-9f0b-f3c9ed916b91.gif "out.gif")

# Rxを使う準備

VisualStudioを使う場合、Nugetで[Rx-Main](https://www.nuget.org/packages/Rx-Main/)をインストールする必要があります。

# マウス移動イベントストリームの作り方
Rxでは複数のイベントストリームを組み合わせて、新しいイベントストリームを作成します。
ドラッグアンドドロップでは、マウスボタンをクリックしてから離すまでのマウス移動イベントが欲しいです。

## 素材となるイベントストリーム
Rxでは`Observable.FromEvent`を使ってイベントをイベントストリームに変換します。
これはおまじないです。丸暗記してください。

例えば...

```csharp
var mouseDown = Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>(
    h => (s, e) => h(e),
    h => Eli.MouseDown += h,
    h => Eli.MouseDown -= h);

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

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

## イベントストリームを組み合わせる

二つの方法があります。

### 方法1. SelectMany
一つのイベント（マウスダウン）を複数のイベント（マウスムーブ）に変換するために`SelectMany`を使います。

```csharp
var drag = mouseDown
    // マウスダウンをマウスムーブに変換
    .SelectMany(_ => mouseMove)
    // マウスアップが行われるまでTake。マウスアップでマウスのキャプチャをリリース
    .TakeUntil(mouseUp)
    // これを繰り返す
    .Repeat();
```

- ラムダ式の使わない引数を`_`で定義する風習が、Rx界にはあるようです
- TakeUntilを使う場合はRepeatをつけないと、ドラッグアンドドロップが一度きりになります

#### StartWithの有無
インターネット上ではmouseMoveにstartWithをつける[例1](http://webcache.googleusercontent.com/search?q=cache:JY3ygGtLuAMJ:jesseliberty.com/2011/02/11/reactive-drag-and-drop-part-1/+&cd=2&hl=ja&ct=clnk&gl=jp
)、[例2](http://theburningmonk.com/2010/02/linq-over-events-playing-with-the-rx-framework/)もありました。

```csharp
var drag = mouseDown
    // マウスダウンをマウスムーブに変換
    .SelectMany(e => mouseMove.StartWith(e))
    // マウスアップが行われるまでTake
    .TakeUntil(mouseUp)
    .Repeat();
```

動きは変わらないので、StartWithは要らなさそうです。

### 方法2. SkipUntil
マウスダウン以前のマウスムーブを無視するために`SkipUntil`を使います。

```csharp
var drag = mouseMove
    // マウスムーブをマウスダウンまでスキップ
    .SkipUntil(mouseDown)
    // マウスアップが行われるまでTake
    .TakeUntil(mouseUp)
    .Repeat();
```

SelectManyより直感的に思えます。インターネット上では、`SelectMany`を使う例が多いです。


# CaptureMouse と ReleaseMouseCapture

WPFでドラッグアンドドロップを実現する際にCaptureMouseメソッドを使う必要があります。
使わないと高速でポインターを動かした際に、おいていかれることがあります。
![out.gif](https://qiita-image-store.s3.amazonaws.com/0/15964/4f0daef6-8b38-2faf-24b3-d87fc0e273c4.gif "out.gif")

ただし、適切にReleaseMouseCaptureをしないと別のマウスイベントが反応しなくなります。
 
## マウス移動ストリームにCaptureMouse と ReleaseMouseCaptureを組み込む

```csharp
var drag = mouseMove
    // マウスダウン時にマウスをキャプチャ
    .SkipUntil(mouseDown.Do(_ => CaptureMouse()))
    // マウスアップでマウスのキャプチャをリリース
    .TakeUntil(mouseUp.Do(_ => ReleaseMouseCapture()))
    // これを繰り返す
    .Repeat();
```

## イベントを監視する要素に注意
注意点があります。

- mouseDownは、移動する楕円のイベントストリーム
- mouseMoveとmouseUp、はウインドウのイベントストリーム

これはポインタが楕円の外に出た時にイベントを拾うためです。
楕円のmouseUpイベントでは、楕円外でmouseUpした際に、ReleaseMouseCaptureされません。他の要素でマウスイベントを拾えなくなります。

また、ウィンドウのmouseDownイベントでは、楕円外をクリックしても移動が開始されます。不自然な挙動になります。

# ソースコード全体
```xaml:MainWindow.xaml
<Window x:Class="DandD.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DandD"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Canvas>
        <Ellipse
            x:Name="Eli"
            Width="30" Height="30"
            Fill="Gray" Stroke="Black" StrokeThickness="5"/>
    </Canvas>
</Window>
```

```csharp:MainWindow.xaml.cs
using System;
using System.Linq;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace DandD
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            var mouseDown = Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>(
                h => (s, e) => h(e),
                h => Eli.MouseDown += h,
                h => Eli.MouseDown -= h);

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

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

            var dragOffset = new Point();
            var drag = mouseMove
                .SkipUntil(mouseDown.Do(e => {
                    CaptureMouse();
                    dragOffset = e.GetPosition(Eli);
                }))
                .TakeUntil(mouseUp.Do(_ => ReleaseMouseCapture()))
                .Repeat();

            drag.Select(e => e.GetPosition(null))
                .Subscribe(p => {
                    UIElement el = Eli as UIElement;
                    Canvas.SetLeft(el, p.X - dragOffset.X);
                    Canvas.SetTop(el, p.Y - dragOffset.Y);
                });
        }
    }
}
```

`Canvas.SetLeft`と`Canvas.SetTop`を使って楕円を移動します。

楕円をポインタの下に移動するために、`dragOffset`を調整します。
この調整をしないと、楕円はポインタの右下に移動します。



# リンク
- [WPFでコントロールをドラッグ(1) | Moonmile Solutions Blog](http://www.moonmile.net/blog/archives/709)
- [Reactive Extensions再入門 その９「Skip + Take + Repeat = ドラッグ」 - かずきのBlog@hatena](http://blog.okazuki.jp/entry/20111124/1322145011)
- [Rx入門 (20) - Drag & Dropでの落書き - xin9le.net](http://blog.xin9le.net/entry/2012/02/17/235247)
- [Amazedsaint's Tech Journal: Rx Framework Part II - Observables From Events and ‘LINQ to Events’ – With a WPF Drawing demo](http://www.amazedsaint.com/2009/11/linq-to-events-more-on-net-reactive.html)