WPF draggable and clickable button

I have a borderless, transparent Window with only one Button.

My fancy button looks like this

The expected behaviour is:

  • When I click and drag the Button, the Button must follow the cursor.
  • When I only click on the Button, the MouseDown, PreviewMouseDown events or Command binding should raise.

At first I tried to call the DragMove() on PreviewMouseDown event, but that blocks the click events. Now my idea is: I set a 100ms delay after mouse down. If the time passed, than the button wil be dragging, otherwise it was just a click.

Code

private bool _dragging;
private Point startpos;
CancellationTokenSource cancellation;

private void Button_PreviewMouseMove(object sender, MouseEventArgs e)
{
    if (_dragging && e.LeftButton == MouseButtonState.Pressed)
    {
        var currentpos = e.GetPosition(this);
        Left += currentpos.X - startpos.X;
        Top += currentpos.Y - startpos.Y;
    }
}

private async void Button_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
    if (e.ChangedButton != MouseButton.Left)
        return;

    _dragging = false;
    startpos = e.GetPosition(this);

    cancellation?.Cancel();
    cancellation = new CancellationTokenSource();

    await Task.Delay(100, cancellation.Token).ContinueWith(task =>
    {
        _dragging = !task.IsCanceled;
    });
}

private void Button_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
    if (_dragging)
    {
        _dragging = false;
        e.Handled = true;
    }

    cancellation?.Cancel();
}

Basically it works, but has some bugs:

  • When I hold down the mouse button for longer time and release, then the click won't work, because after 100ms the dragging will be active.
  • After I dragged the button, and click anywhere outside the Button and Window control, the PreviewMouseDown and PreviewMouseUp events are raised. I don't know why??

Does somebody have any better solution?

1 answer

  • answered 2020-02-13 00:24 Jérémie Yardin

    For your first problem:
    You must handle the case when your button is pushed down but not moved. I think a better way to do this (instead of the 100ms delay) would be to specify a minimum threshold of movement above which the dragging will start.
    You could do it like this:

    private const double _dragThreshold = 1.0;
    private bool _dragging;
    private Point startpos;
    CancellationTokenSource cancellation;
    
    private void Button_PreviewMouseMove(object sender, MouseEventArgs e)
    {
        var currentpos = e.GetPosition(this);
        var delta = currentpos - startpos;
        if ((delta.Length > _dragThreshold || _dragging) && e.LeftButton == MouseButtonState.Pressed)
        {
            _dragging = true;
            Left += currentpos.X - startpos.X;
            Top += currentpos.Y - startpos.Y;
        }
    }
    
    private async void Button_PreviewMouseDown(object sender, MouseButtonEventArgs e)
    {
        if (e.ChangedButton != MouseButton.Left)
            return;
    
        _dragging = false;
        startpos = e.GetPosition(this);
    
        cancellation?.Cancel();
        cancellation = new CancellationTokenSource();
    }
    

    For your second problem: the button will capture the mouse on the mouse down event.
    You need to release the captured mouse with the ReleaseMouseCapture method when you are done with your drag.

    private void Button_PreviewMouseUp(object sender, MouseButtonEventArgs e)
    {
        if (_dragging)
        {
            _dragging = false;
            e.Handled = true;
            var button = sender as Button;
            button.ReleaseMouseCapture();
        }
    
        cancellation?.Cancel();
    }