WPFでマウスドラッグイベント

WPFにはデフォルトでドラッグのイベントは用意されていません。

今回はドラッグ用のイベントを実装してみたいと思います。

記事内で利用しているObservableクラス、Disposeクラス、Subscribe関数は下記記事を参照してください。
C# eventをIObservableに変換 - 何でもプログラミング

MouseDragArgs

ドラッグイベント時に渡されるクラスを作成します。

public enum MouseDragPhase
{
    Begin,
    Move,
    End
}
public class MouseDragArgs
{
    public Point StartPoint { get; }
    public ModifierKeys StartModifier { get; }
    public Point Point { get; }
    public MouseDragPhase Phase { get; }
    public MouseDragArgs(Point startPoint, ModifierKeys startModifier, Point point, MouseDragPhase phase)
    {
        StartPoint = startPoint;
        StartModifier = startModifier;
        Point = point;
        Phase = phase;
    }
}


MouseLeftButtonDragObs

IObservable<MouseDragArgs>を返す拡張メソッドを作成します。

Down、Up等のIObservableとオペレータの組み合わせで記述できなくもないですが、副作用(CaptureMouse等)や状態(isDragging等)を伴うため、手続き的に記述したほうがわかりやすいです。

マウスDown時にMouseCaptureし、要素から外れてもドラッグが継続されるように実装しています。

LostFocusを監視することにより、Up以外(Alt + Tabや右クリックメニュー等)でドラッグが終了しても通知するようにしてあります。

イベントが並走するのを避けるため、他のボタンがDownの状態では開始しないよう実装してあります。

この辺の仕様は適宜カスタマイズしてください。

public static IObservable<MouseDragArgs> MouseLeftButtonDragObs(this FrameworkElement element) =>
    new Observable<MouseDragArgs>(observer =>
    {
        bool isDragging = false;
        Point startPoint = new Point(0, 0);
        ModifierKeys startModifier = ModifierKeys.None;

        // Down
        MouseButtonEventHandler down = (s, e) =>
        {
            if (e.RightButton == MouseButtonState.Pressed || e.MiddleButton == MouseButtonState.Pressed)
                return;
            element.CaptureMouse();
            isDragging = true;
            startPoint = e.GetPosition(element);
            startModifier = Keyboard.Modifiers;
            observer.OnNext(new MouseDragArgs(startPoint, startModifier, startPoint, MouseDragPhase.Begin));
        };

        // Move
        MouseEventHandler move = (s, e) =>
        {
            if (isDragging)
                observer.OnNext(new MouseDragArgs(startPoint, startModifier, e.GetPosition(element), MouseDragPhase.Move));
        };

        // Up
        MouseButtonEventHandler up = (s, e) => element.ReleaseMouseCapture();

        // LostFocus
        MouseEventHandler lostFocus = (s, e) => 
        {
            if (isDragging == false)
                return;
            isDragging = false;
            observer.OnNext(new MouseDragArgs(startPoint, startModifier, e.GetPosition(element), MouseDragPhase.End));
        };

        // イベント登録
        element.MouseLeftButtonDown += down;
        element.MouseMove += move;
        element.MouseLeftButtonUp += up;
        element.LostMouseCapture += lostFocus;

        return new Disposable(() =>
        {
            // イベント解除
            element.MouseLeftButtonDown -= down;
            element.MouseMove -= move;
            element.MouseLeftButtonUp -= up;
            element.LostMouseCapture -= lostFocus;
        });
    });


動作確認

長方形上でのLeftDrag情報をコンソールに表示するアプリケーションになります。

Disposeボタンを押すと、一切出力されなくなります。
f:id:any-programming:20170512172531p:plain
Xaml

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="175" Width="135">
    <Grid>
        <Rectangle x:Name="rect" Fill="#FFF4F4F5" Stroke="Black" Margin="10,10,10,35"/>
        <Button x:Name="buttonDispose" Content="Dispose" Margin="10,115,10,10" />
    </Grid>
</Window>

C#

public MainWindow()
{
    InitializeComponent();

    var subscription = rect.MouseLeftButtonDragObs().Subscribe(args =>
    {
        if (args.Phase == MouseDragPhase.Begin)
            Console.WriteLine($"begin {args.StartModifier} {args.Point}");
        else if (args.Phase == MouseDragPhase.Move)
            Console.WriteLine($"move {args.StartModifier} {args.StartPoint} {args.Point}");
        else
            Console.WriteLine($"end {args.StartModifier} {args.StartPoint} {args.Point}");
    });
    buttonDispose.Click += (s, e) => subscription.Dispose();
}