How to drag a UserControl inside a Canvas

I have a Canvas in which user can add UserControl subclasses containing a form. User should be able to drag these UserControl around the Canvas.

What's the best practice to do this with WPF?


Solution 1:

This is done in silverlight and not in WPF, but it should work the same.

Create two private properties on the control:

protected bool isDragging;  
private Point clickPosition;

Then attatch some event handlers in the constructor of the control:

this.MouseLeftButtonDown += new MouseButtonEventHandler(Control_MouseLeftButtonDown);
this.MouseLeftButtonUp += new MouseButtonEventHandler(Control_MouseLeftButtonUp);
this.MouseMove += new MouseEventHandler(Control_MouseMove);

Now create those methods:

private void Control_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    isDragging = true;
    var draggableControl = sender as UserControl;
    clickPosition = e.GetPosition(this);
    draggableControl.CaptureMouse();
}

private void Control_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    isDragging = false;
    var draggable = sender as UserControl;
    draggable.ReleaseMouseCapture();
}

private void Control_MouseMove(object sender, MouseEventArgs e)
{
    var draggableControl = sender as UserControl;

    if (isDragging && draggableControl != null)
    {
        Point currentPosition = e.GetPosition(this.Parent as UIElement);

        var transform = draggableControl.RenderTransform as TranslateTransform;
        if (transform == null)
        {
            transform = new TranslateTransform();
            draggableControl.RenderTransform = transform;
        }

        transform.X = currentPosition.X - clickPosition.X;
        transform.Y = currentPosition.Y - clickPosition.Y;
    }
}

A few things to note here:
1. This does not have to be in a canvas. It can be in a stackpanel, or grid as well.
2. This makes the entire control draggable, that means if you click anywhere in the control and drag it will drag the whole control. Not sure if thats exactly what you want.

Edit-
Expanding on some of the specifics in your question: The best way that I would implement this is to create a class that inherits from UserControl, maybe called DraggableControl that is built with this code, then all draggable controls should extend the DraggableControl.

Edit 2 - There is small issue when you have a datagrid in this control. If you sort a column in the datagrid the MouseLeftButtonUp event never fires. I have updated the code so that isDragging is protected. I found the best solution is to tie this anonymous method to the LostMouseCapture event of the datagrid:

this.MyDataGrid.LostMouseCapture += (sender, e) => { this.isDragging = false; };

Solution 2:

Corey's answer is mostly correct, but it's missing one crucial element: memory of what the last transform was. Otherwise, when you move an item, release the mouse button, and then click that item again, the transform resets to (0,0) and the control jumps back to its origin.

Here's a slightly modified version that works for me:

public partial class DragItem : UserControl
{
    protected Boolean isDragging;
    private Point mousePosition;
    private Double prevX, prevY;

    public DragItem()
    {
        InitializeComponent();
    }

    private void UserControl_MouseLeftButtonDown(Object sender, MouseButtonEventArgs e)
    {
        isDragging = true;
        var draggableControl = (sender as UserControl);
        mousePosition = e.GetPosition(Parent as UIElement);
        draggableControl.CaptureMouse();
    }

    private void UserControl_MouseLeftButtonUp(Object sender, MouseButtonEventArgs e)
    {
        isDragging = false;
        var draggable = (sender as UserControl);
        var transform = (draggable.RenderTransform as TranslateTransform);
        if (transform != null)
        {
            prevX = transform.X;
            prevY = transform.Y;
        }
        draggable.ReleaseMouseCapture();
    }

    private void UserControl_MouseMove(Object sender, MouseEventArgs e)
    {
        var draggableControl = (sender as UserControl);
        if (isDragging && draggableControl != null)
        {
            var currentPosition = e.GetPosition(Parent as UIElement);
            var transform = (draggableControl.RenderTransform as TranslateTransform);
            if (transform == null)
            {
                transform = new TranslateTransform();
                draggableControl.RenderTransform = transform;
            }
            transform.X = (currentPosition.X - mousePosition.X);
            transform.Y = (currentPosition.Y - mousePosition.Y);
            if (prevX > 0)
            {
                transform.X += prevX;
                transform.Y += prevY;
            }
        }
    }
}

The key is storing the previous X and Y offsets, and then using them to augment the current movement's offset in order to arrive at the correct aggregate offset.

Solution 3:

In case someone wants try out a minimal solution here is one using the MouseMove event.

The Layout

<Canvas 
  Background='Beige'
  Name='canvas'>

  <Rectangle 
    Width='50'
    Height='50'
    Fill='LightPink'
    Canvas.Left='350'
    Canvas.Top='175'
    MouseMove='Rectangle_MouseMove' />

</Canvas>

Code behind

void OnMouseMove(object sender, MouseEventArgs e)
{
  if (e.Source is Shape shape)
  {
    if (e.LeftButton == MouseButtonState.Pressed)
    {
      Point p = e.GetPosition(canvas);
      Canvas.SetLeft(shape, p.X - shape.ActualWidth / 2);
      Canvas.SetTop(shape, p.Y - shape.ActualHeight / 2);
      shape.CaptureMouse();
    }
    else
    {
      shape.ReleaseMouseCapture();
    }
  }
}

Solution 4:

Regarding Corey Sunwold solution - I got rid of MouseUp and MouseDown events and I simplified MouseMove method using MouseButtonState as below :) I'm using Canvas.SetLeft() and Canvas.SetTop() instead RenderTransform so I don't need to store old position from MouseDown event.

if (e.LeftButton == MouseButtonState.Pressed && draggableControl != null)
{
   //...
}