背景
Rxの例としてドラッグアンドドロップを良く見かけます。
ちょっとずつ例が違うので、何が違うのかまとめます。
実現したい動き
ドラッグアンドドロップで実現したい動きは幾つかあります。
- オブジェクトを移動する
- 線を引く
今回は前者を対象とします。
Rxを使う準備
VisualStudioを使う場合、NugetでRx-Mainをインストールする必要があります。
マウス移動イベントストリームの作り方
Rxでは複数のイベントストリームを組み合わせて、新しいイベントストリームを作成します。
ドラッグアンドドロップでは、マウスボタンをクリックしてから離すまでのマウス移動イベントが欲しいです。
素材となるイベントストリーム
RxではObservable.FromEvent
を使ってイベントをイベントストリームに変換します。
これはおまじないです。丸暗記してください。
例えば...
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
を使います。
var drag = mouseDown
// マウスダウンをマウスムーブに変換
.SelectMany(_ => mouseMove)
// マウスアップが行われるまでTake。マウスアップでマウスのキャプチャをリリース
.TakeUntil(mouseUp)
// これを繰り返す
.Repeat();
- ラムダ式の使わない引数を
_
で定義する風習が、Rx界にはあるようです - TakeUntilを使う場合はRepeatをつけないと、ドラッグアンドドロップが一度きりになります
StartWithの有無
インターネット上ではmouseMoveにstartWithをつける例1、例2もありました。
var drag = mouseDown
// マウスダウンをマウスムーブに変換
.SelectMany(e => mouseMove.StartWith(e))
// マウスアップが行われるまでTake
.TakeUntil(mouseUp)
.Repeat();
動きは変わらないので、StartWithは要らなさそうです。
方法2. SkipUntil
マウスダウン以前のマウスムーブを無視するためにSkipUntil
を使います。
var drag = mouseMove
// マウスムーブをマウスダウンまでスキップ
.SkipUntil(mouseDown)
// マウスアップが行われるまでTake
.TakeUntil(mouseUp)
.Repeat();
SelectManyより直感的に思えます。インターネット上では、SelectMany
を使う例が多いです。
CaptureMouse と ReleaseMouseCapture
WPFでドラッグアンドドロップを実現する際にCaptureMouseメソッドを使う必要があります。
使わないと高速でポインターを動かした際に、おいていかれることがあります。
ただし、適切にReleaseMouseCaptureをしないと別のマウスイベントが反応しなくなります。
マウス移動ストリームにCaptureMouse と ReleaseMouseCaptureを組み込む
var drag = mouseMove
// マウスダウン時にマウスをキャプチャ
.SkipUntil(mouseDown.Do(_ => CaptureMouse()))
// マウスアップでマウスのキャプチャをリリース
.TakeUntil(mouseUp.Do(_ => ReleaseMouseCapture()))
// これを繰り返す
.Repeat();
イベントを監視する要素に注意
注意点があります。
- mouseDownは、移動する楕円のイベントストリーム
- mouseMoveとmouseUp、はウインドウのイベントストリーム
これはポインタが楕円の外に出た時にイベントを拾うためです。
楕円のmouseUpイベントでは、楕円外でmouseUpした際に、ReleaseMouseCaptureされません。他の要素でマウスイベントを拾えなくなります。
また、ウィンドウのmouseDownイベントでは、楕円外をクリックしても移動が開始されます。不自然な挙動になります。
ソースコード全体
<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>
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
を調整します。
この調整をしないと、楕円はポインタの右下に移動します。