How to drag a UserControl inside a Canvas
Asked Answered
S

8

16

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?

Sniggle answered 29/9, 2009 at 23:11 Comment(0)
S
42

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; };
Stabilize answered 29/9, 2009 at 23:42 Comment(10)
Great answer! Exactly what i searched for. Copy&Paste and it worked. And more elegant than my tries. I hope this helps loris too ;)Splinter
Well done! With some minor changes in mouse move handler it works perfectly. Thanks a lot!Sniggle
@loris: I am interested to hear what changes you needed to make and why you made them. I have used this exact code in a few projects already and it worked fine. Maybe there is something I overlooked.Stabilize
This really helped me. Thank you very much for posting this.Marozik
"This does not have to be in a canvas." ... Why this restriction?Sericeous
The question, however, referred to a Canvas like the parent... Is there complicated to change this logic for a Canvas parent?Sericeous
@Corey: No, it have some strange "delta", when mouseDown in a Canvas. You yourself wrote: "1. This does not have to be in a canvas."Sericeous
In Control_MouseLeftButtonDown, I found that I needed to replace clickPosition = e.GetPosition(this); with clickPosition = e.GetPosition(this.Parent as UIElement);. Before making that change, elements that I clicked on would jump down and right (since it was getting a starting position within the bounds of the element itself rather than the hosting canvas).Goldstone
@CoreySunwold: your example is working for me. I am using it in a tabcontrol. The user control can be dragged anywhere in the tab. Now what I want to know is how is it possible to make the user control boundary not cross the parent control? I mean some part of the user control become invisible if I drag it beyond the parent border. How to prevent it?Yorgo
@Yorgo If you know the dimensions of the parent control somehow from within Control_MouseMove, you may be able to do a check to ensure the transform X and Y coordinates are not outside the parent control.Stabilize
G
5

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.

Galsworthy answered 3/5, 2018 at 19:12 Comment(1)
This fails upon a third click on the object being moved. However, if the last IF condition in the MouseMove event is removed, it works properly.Gearard
C
4

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();
    }
  }
}
Canticle answered 9/10, 2019 at 18:23 Comment(2)
I cannot stress enough how sometimes the easiest solutions are the best. This works a mint! Thanks :DFrisco
Please use this only as quick hack and not in production code. This works smoothly with mouse, however I've noticed on my laptop, were I have double-tap touch pad function for the click, this fails horribly (50% o the time I grab a wrong UI element).Megalith
A
2

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)
{
   //...
}
Arsphenamine answered 30/4, 2015 at 17:50 Comment(3)
Cool, I was just wondering why my control kept jumping back to its original position!! You answered my question before I asked it.+1Peltry
actually how do you get the X and Y distance to move without the MouseDown event?Peltry
I down voted because you didn't provide the code. I was having the same problem as Duncan, and apparently you found the solution.... you just didn't show it.Patric
P
0

Updated minimal solution code by Themelis with preserving grab position:

private Point translation;
private bool isDragging;

void OnMouseMove(object sender, MouseEventArgs e)
{
    if (e.Source is Shape shape)
    {
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            Point p = e.GetPosition(canvas);

            if (!isDragging)
            {
                translation = new Point(p.X - Canvas.GetLeft(shape), p.Y - Canvas.GetTop(shape));
                isDragging = true;
            }

            Canvas.SetLeft(shape, p.X - translation.X);
            Canvas.SetTop(shape, p.Y - translation.Y);
            shape.CaptureMouse();
        }
        else
        {
            shape.ReleaseMouseCapture();
            isDragging = false;
        }
    }
}
Prut answered 28/10, 2022 at 15:49 Comment(0)
P
-2

I had some trouble with the given solutions and ended up with this:

    public partial class UserControlDraggable : UserControl
{
    public UserControlDraggable()
    {
        InitializeComponent();

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

    private void Control_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        _isDragging = true;
        _mouseLocationWithinMe = e.GetPosition(this);

        CaptureMouse();
    }

    private void Control_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        _isDragging = false;
        this.ReleaseMouseCapture();
    }

    private void Control_MouseMove(object sender, MouseEventArgs e)
    {
        if (_isDragging)
        {
            var mouseWithinParent = e.GetPosition(Parent as UIElement);

            Canvas.SetLeft(this, mouseWithinParent.X - _mouseLocationWithinMe.X);
            Canvas.SetTop(this, mouseWithinParent.Y - _mouseLocationWithinMe.Y);
        }
    }

    protected bool _isDragging;
    Point _mouseLocationWithinMe;
}

It is basically Corey's example but leverages hints from Hawlett. It works ONLY when the parent container is a Canvas. Also, it deserves to be dolled up with some limits to keep the user from dragging the control to places it really should not be.

Pronouncement answered 15/2, 2016 at 1:33 Comment(1)
Yeah, it was posted with some caveats. It worked fine for me in the defined environment.Pronouncement
C
-2

This code works perfectly!

Button newBtn = new Button();
newBtn.AddHandler(Button.ClickEvent, new RoutedEventHandler(BtTable_Click));
newBtn.AddHandler(Button.PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(BtTable_MouseLeftButtonDown));
newBtn.AddHandler(Button.PreviewMouseLeftButtonUpEvent, new MouseButtonEventHandler(BtTable_MouseLeftButtonUp));
newBtn.AddHandler(Button.PreviewMouseMoveEvent, new MouseEventHandler(BtTable_MouseMove));

Button Move

private object movingObject;
private double firstXPos, firstYPos;
private int ButtonSize = 50;

private void BtTable_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    Button newBtn = sender as Button;
    Canvas canvas = newBtn.Parent as Canvas;

    firstXPos = e.GetPosition(newBtn).X;
    firstYPos = e.GetPosition(newBtn).Y - ButtonSize;

    movingObject = sender;

    // Put the image currently being dragged on top of the others
    int top = Canvas.GetZIndex(newBtn);
    foreach (Button child in canvas.Children)
        if (top < Canvas.GetZIndex(child))
            top = Canvas.GetZIndex(child);
    Canvas.SetZIndex(newBtn, top + 1);
    Mouse.Capture(null);
}

private void BtTable_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    Button newBtn = sender as Button;
    Canvas canvas = newBtn.Parent as Canvas;

    movingObject = null;

    // Put the image currently being dragged on top of the others
    int top = Canvas.GetZIndex(newBtn);
    foreach (Button child in canvas.Children)
        if (top > Canvas.GetZIndex(child))
            top = Canvas.GetZIndex(child);
    Canvas.SetZIndex(newBtn, top + 1);
    Mouse.Capture(newBtn);
}

private void BtTable_MouseMove(object sender, MouseEventArgs e)
{
    if (e.LeftButton == MouseButtonState.Pressed && sender == movingObject)
    {
        Button newBtn = sender as Button;
        Canvas canvas = newBtn.Parent as Canvas;
        // Horizontal
        double newLeft = e.GetPosition(canvas).X - firstXPos - canvas.Margin.Left;
        // newLeft inside canvas right-border?
        if (newLeft > canvas.Margin.Left + canvas.ActualWidth - newBtn.ActualWidth)
            newLeft = canvas.Margin.Left + canvas.ActualWidth - newBtn.ActualWidth;
        // newLeft inside canvas left-border?
        else if (newLeft < canvas.Margin.Left)
            newLeft = canvas.Margin.Left;

        newBtn.SetValue(Canvas.LeftProperty, newLeft);

        //Vertical
        double newTop = e.GetPosition(canvas).Y - firstYPos - canvas.Margin.Top;
        // newTop inside canvas bottom-border?
        // -- Bottom --
        if (newTop > canvas.Margin.Top + canvas.ActualHeight - newBtn.ActualHeight - ButtonSize)
            newTop = canvas.Margin.Top + canvas.ActualHeight - newBtn.ActualHeight - ButtonSize;
        // newTop inside canvas top-border?
        // -- Top --
        else if (newTop < canvas.Margin.Top - ButtonSize)
            newTop = canvas.Margin.Top - ButtonSize;

        newBtn.SetValue(Canvas.TopProperty, newTop);
    }
}

Happy coding ;)

Cardsharp answered 15/9, 2017 at 15:10 Comment(0)
A
-3

I implemented this for both WPF and UWP store app. And added all the code in user control itself instead of the control which is using it, you can modify it as par your need.

WPF

public partial class DragUserControl : UserControl
{
    public DragUserControl()
    {
        InitializeComponent();
    }

    object MovingObject;
    double FirstXPos, FirstYPos;

    private void Button_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        this.MovingObject = this;
        FirstXPos = e.GetPosition(MovingObject as Control).X;
        FirstYPos = e.GetPosition(MovingObject as Control).Y;

        Canvas canvas = this.Parent as Canvas;
        if (canvas != null)
        {
            canvas.PreviewMouseMove += this.MouseMove;
        }
    }

    private void MouseMove(object sender, MouseEventArgs e)
    {
        /*
         * In this event, at first we check the mouse left button state. If it is pressed and 
         * event sender object is similar with our moving object, we can move our control with
         * some effects.
         */
        Canvas canvas = sender as Canvas;

        Point canvasPoint = e.GetPosition(canvas);
        Point objPosition = e.GetPosition((MovingObject as FrameworkElement));
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            if (MovingObject != null)
            {
//This condition will take care that control should not go outside the canvas.
                    if ((e.GetPosition((MovingObject as FrameworkElement).Parent as FrameworkElement).X - FirstXPos > 0) && (e.GetPosition((MovingObject as FrameworkElement).Parent as FrameworkElement).X - FirstXPos < canvas.ActualWidth - (MovingObject as FrameworkElement).ActualWidth))
                    {
                        (MovingObject as FrameworkElement).SetValue(Canvas.LeftProperty, e.GetPosition((MovingObject as FrameworkElement).Parent as FrameworkElement).X - FirstXPos);
                    }

//This condition will take care that control should not go outside the canvas.
                    if ((e.GetPosition((MovingObject as FrameworkElement).Parent as FrameworkElement).Y - FirstYPos > 0) && (e.GetPosition((MovingObject as FrameworkElement).Parent as FrameworkElement).Y - FirstYPos < canvas.ActualHeight - (MovingObject as FrameworkElement).ActualHeight))
                    {
                        (MovingObject as FrameworkElement).SetValue(Canvas.TopProperty, e.GetPosition((MovingObject as FrameworkElement).Parent as FrameworkElement).Y - FirstYPos);
                    }
                }
            }
        }

        private void Ellipse_PreviewMouseLeftButtonUp_1(object sender, MouseButtonEventArgs e)
        {
            MovingObject = null;
        }
    }

Button_MouseLeftButtonDown is the click event of button through which you want to drag the control.

UWP

 public sealed partial class DragUserControl : UserControl
    {
        MovingObject;
        double FirstXPos, FirstYPos;

        public DragUserControl()
        {
            InitializeComponent();
        }

       private void Ellipse_PointerPressed(object sender, PointerRoutedEventArgs e)
        {
            this.MovingObject = this;

            FirstXPos = e.GetCurrentPoint(MovingObject as Control).Position.X;
            FirstYPos = e.GetCurrentPoint(MovingObject as Control).Position.Y;

            Canvas canvas = this.Parent as Canvas;
            if (canvas != null)
            {
                canvas.PointerMoved += Canvas_PointerMoved;
            }
        }

        private void Canvas_PointerMoved(object sender, PointerRoutedEventArgs e)
        {
            if (MovingObject != null)
            {
                Canvas canvas = sender as Canvas;

                Point canvasPoint = e.GetCurrentPoint(canvas).Position;
                Point objPosition = e.GetCurrentPoint((MovingObject as FrameworkElement)).Position;
                if (e.GetCurrentPoint(MovingObject as Control).Properties.IsLeftButtonPressed) //e.Pointer.IsInContact ==true)
                {
//This condition will take care that control should not go outside the canvas
                    if ((e.GetCurrentPoint((MovingObject as FrameworkElement).Parent as FrameworkElement).Position.X - FirstXPos > 0) && (e.GetCurrentPoint((MovingObject as FrameworkElement).Parent as FrameworkElement).Position.X - FirstXPos < canvas.ActualWidth - (MovingObject as FrameworkElement).ActualWidth))
                    {
                        (MovingObject as FrameworkElement).SetValue(Canvas.LeftProperty, e.GetCurrentPoint((MovingObject as FrameworkElement).Parent as FrameworkElement).Position.X - FirstXPos);
                    }

//This condition will take care that control should not go outside the canvas
                    if ((e.GetCurrentPoint((MovingObject as FrameworkElement).Parent as FrameworkElement).Position.Y - FirstYPos > 0) && (e.GetCurrentPoint((MovingObject as FrameworkElement).Parent as FrameworkElement).Position.Y - FirstYPos < canvas.ActualHeight - (MovingObject as FrameworkElement).ActualHeight))
                    {
                        (MovingObject as FrameworkElement).SetValue(Canvas.TopProperty, e.GetCurrentPoint((MovingObject as FrameworkElement).Parent as FrameworkElement).Position.Y - FirstYPos);
                    }
                }
            }
        }

        private void Ellipse_PointerReleased(object sender, PointerRoutedEventArgs e)
        {
            MovingObject = null;
        }
}

Ellipse_PointerPressed is the click event of ellipse through which you want to drag the control.

Avocation answered 5/10, 2016 at 4:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.