Question

I'm writing my first WPF application. 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? Thanks.

Was it helpful?

Solution

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; };

OTHER TIPS

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)
{
   //...
}

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.

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.

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 ;)

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top