LoginSignup
11
13

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-11-06

背景

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

実現したい動き

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

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

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

out.gif

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メソッドを使う必要があります。
使わないと高速でポインターを動かした際に、おいていかれることがあります。
out.gif

ただし、適切にReleaseMouseCaptureをしないと別のマウスイベントが反応しなくなります。

マウス移動ストリームにCaptureMouse と ReleaseMouseCaptureを組み込む

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

イベントを監視する要素に注意

注意点があります。

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

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

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

ソースコード全体

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>
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.SetLeftCanvas.SetTopを使って楕円を移動します。

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

リンク

11
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
13