1/10/08

How to move WPF controls with mouse at runtime

I wanted to be able to drag or move controls around at runtime (using TranslateTransform) - by clicking the mouse on the desired control and moving it to a new location on the parent screen/control. This code sample uses the simplest WPF Elements - a Canvas and a couple of Rectangles.

I don't think this needs much explaination. So just copy/paste and run it. You should be able to see two rectangles on canvas and move them around when mouse is down.



L O G I C


  • Program maintains a variable "current" that keeps track of which control (or UI element) on canvas is under a possible dragging action.
  • Each Rectangle declares RenderTransform's TranslateTransform and subscribes to MouseLeftButtonDown events, where it is assigned to the current element.
  • Canvas subscribes to MouseDown, MouseUp and MouseMove. In MouseDown canvas gets the coordinates of the current control and captures the mouse on it. In MouseUp it releases the current element and the mouse. The MouseMove resets the location of the current element by adjusting the current element's RenderTransform with the new position.


C O D E
/* * * * * * * * * * * * * * * * * * * ** * * * ** * * * * * * * * *


. .xaml file:

* *  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

<Window x:Class="MoveObjectOnMouseDownUpMove.Window1"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

Title="Window1" Height="300" Width="300">

<Canvas x:Name="canvas"

MouseDown
="Canvas_MouseDown"

MouseUp="Canvas_MouseUp"

MouseMove="Canvas_MouseMove">

<Rectangle x:Name="rect2" Height="50" Width="50" Fill="Blue"

Canvas.Left="50" Canvas.Top="100"

MouseLeftButtonDown
="rect2_MouseLeftButtonDown">

<Rectangle.RenderTransform>

<TranslateTransform/>

</Rectangle.RenderTransform>

</Rectangle>

<Rectangle x:Name="rect1" Height="50" Width="50" Fill="Red" MouseLeftButtonDown="rect1_MouseLeftButtonDown">

<Rectangle.RenderTransform>

<TranslateTransform />

</Rectangle.RenderTransform>


</Rectangle>

</Canvas>

</Window>

    /* * * * * * * * * * * * * * * * * * * ** * * * ** * * * * * * * * *

    . xaml.cs file:
    * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 
    using System.Windows.Media.Imaging;

    using System.Windows.Navigation;

    using System.Windows.Shapes;



    namespace MoveObjectOnMouseDownUpMove

    {

    public partial class Window1 : Window

    {
       //see the Element class declaration at the bottom, using it for readablity 

    private Element current = new Element();


    public Window1()

    {

    InitializeComponent();

    }



    private void Canvas_MouseDown(object sender, MouseButtonEventArgs e)

    {

    this.current.X = Mouse.GetPosition((IInputElement)sender).X;

    this.current.Y = Mouse.GetPosition((IInputElement)sender).Y;



    // Ensure object receives all mouse events.

    this.current.InputElement.CaptureMouse();

    }



    private void Canvas_MouseUp(object sender, MouseButtonEventArgs e)

    {

    if (this.current.InputElement != null)

    this.current.InputElement.ReleaseMouseCapture();

    }



    private void Canvas_MouseMove(object sender, MouseEventArgs e)

    {

    // if mouse is down when its moving, then it's dragging current

    if (e.LeftButton == MouseButtonState.Pressed)

    this.current.IsDragging = true;



    if (this.current.IsDragging && current.InputElement != null)

    {

    // Retrieve the current position of the mouse.

    var newX = Mouse.GetPosition((IInputElement)sender).X;

    var newY = Mouse.GetPosition((IInputElement)sender).Y;


        // Reset the location of the object (add to sender's renderTransform

    // newPosition minus currentElement's position


    var rt = ((UIElement)this.current.InputElement).RenderTransform;

    var offsetX = rt.Value.OffsetX;

    var offsetY = rt.Value.OffsetY;

    rt.SetValue(TranslateTransform.XProperty, offsetX + newX - current.X);

    rt.SetValue(TranslateTransform.YProperty, offsetY + newY - current.Y);



    // Update position of the mouse

    current.X = newX;

    current.Y = newY;

    }

    }



    private void rect1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)

    {

    this.current.InputElement = (IInputElement)sender;

    }

    private void rect2_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)

    {

    this.current.InputElement = (IInputElement)sender;

    }

    }

    }
    /* * * * ** * * * * * * * * * * * * * * * * * * * * * * * * * * 
    helper class (don't need it, but makes things easier to read) 
    * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */



    public class Element

    {



    #region Fields

    bool isDragging = false;

    IInputElement inputElement = null;

    double x, y = 0;

    #endregion



    #region Constructor

    public Element() { }

    #endregion



    #region Properties

    public IInputElement InputElement

    {

    get { return this.inputElement; }

    set

    {

    this.inputElement = value;

    /* every time inputElement resets, the draggin stops (you actually don't even need to track it, but it made things easier in the begining, I'll change it next time I get to play with it. */


    this.isDragging = false;

    }

    }

    public double X

    {

    get { return this.x; }

    set { this.x = value; }

    }

    public double Y

    {

    get { return this.y; }

    set { this.y = value; }

    }

    public bool IsDragging

    {

    get { return this.isDragging; }

    set { this.isDragging = value; }

    }

    #endregion

    }

11 comments:

  1. Anonymous2/11/2010

    Thanks a lot, but there is a small bug in the code above. When the element stopped dragging (by mouse up event), it's location still may be changed.
    To prevent this unintended behavior method Canvas_MouseUp should be modified by adding simple line of code under "this.current.InputElement!=null" condition:

    this.current.IsDragging = false;

    Now all works well.

    ReplyDelete
  2. hey, you're right - good catch, thank you!

    ReplyDelete
  3. For me it started really to work first after I added one more statement setting the current back to null. So all together it looks like this:

    if (this.current.InputElement != null)
    {
    this.current.IsDragging = false;
    this.current.InputElement.ReleaseMouseCapture();
    this.current.InputElement = null;
    }

    ReplyDelete
  4. Yeah, good call on "defensive coding" - gotta check for null :) thank you!

    ReplyDelete
  5. Anonymous3/28/2012

    Но с кнопками оно по прежнему не работает.

    ReplyDelete
  6. Anonymous12/24/2012

    I want to use "canvas" inside flowdocument, and also my elements such as textblocks & images must be moveable. When I use these code above, because of FlowDocumentPageViewer's feature I guess, for the first time I click, the element isn't moved. Is there any way to move elements in such a scenario?

    ReplyDelete
  7. Anonymous12/29/2012

    rect2_MouseLeftButtonDown is redundant. Both rectangles can use the same event handler.

    ReplyDelete
  8. Anonymous12/29/2012

    // if mouse is down when its moving, then it's dragging current
    if (e.LeftButton == MouseButtonState.Pressed ||
    e.RightButton == MouseButtonState.Pressed ||
    e.MiddleButton == MouseButtonState.Pressed)
    this.current.IsDragging = true;

    With this, buttons can be moved with the right or middle mouse button.

    ReplyDelete
  9. A little bit improve for avoid some "sticking" to cursor^

    this.current.IsDragging = e.LeftButton == MouseButtonState.Pressed;
    instead
    if (e.LeftButton == MouseButtonState.Pressed)
    this.current.IsDragging = true;

    ReplyDelete
  10. Anonymous8/18/2018

    //update for fixing bugs
    private void Grid_MouseDown(object sender, MouseButtonEventArgs e)

    {
    this.current.X = Mouse.GetPosition((IInputElement)sender).X;
    this.current.Y = Mouse.GetPosition((IInputElement)sender).Y;


    // Ensure object receives all mouse events.
    if (this.current.InputElement != null)
    this.current.InputElement.CaptureMouse();

    }

    private void Grid_MouseUp(object sender, MouseButtonEventArgs e)

    {
    if (this.current.InputElement != null)
    {
    this.current.IsDragging = false;
    this.current.InputElement.ReleaseMouseCapture();
    this.current.InputElement = null;
    }

    }

    ReplyDelete
  11. Final update, it took me hours to fix this.
    If you have multiple transformations for the same object e.g. Rotation + Transformation then the above code is useless. It also depends which one is first because these are not commutative.

    The fix is this (and you can put Rotation and Transformation in any order. Did not test it also for Skew, only for these 2):

    private void Grid_MouseMove(object sender, MouseEventArgs e)

    {
    this.current.IsDragging = e.LeftButton == MouseButtonState.Pressed;
    if (this.current.IsDragging && current.InputElement != null)

    {
    var newX = Mouse.GetPosition((IInputElement)sender).X;
    var newY = Mouse.GetPosition((IInputElement)sender).Y;
    var rt = ((UIElement)this.current.InputElement).RenderTransform;
    string whichIsFirst = string.Empty;
    if (rt is TransformGroup)
    {
    TranslateTransform tt = null;
    RotateTransform rr = null;
    foreach (Transform tsf in (rt as TransformGroup).Children)
    switch (tsf)
    {
    case TranslateTransform t1:
    tt = t1;
    if(string.IsNullOrEmpty(whichIsFirst))
    whichIsFirst = "TranslateTransform";
    break;
    case RotateTransform r1:
    rr = r1;
    if (string.IsNullOrEmpty(whichIsFirst))
    whichIsFirst = "RotateTransform";
    break;
    default:
    break;
    }
    if (whichIsFirst.Equals("TranslateTransform"))
    {
    rr.SetValue(RotateTransform.CenterXProperty, rr.CenterX + newX - current.X);
    rr.SetValue(RotateTransform.CenterYProperty, rr.CenterY + newY - current.Y);
    }
    tt.SetValue(TranslateTransform.XProperty, tt.X + newX - current.X);
    tt.SetValue(TranslateTransform.YProperty, tt.Y + newY - current.Y);
    }
    else
    {
    rt.SetValue(TranslateTransform.XProperty, rt.Value.OffsetX + newX - current.X);
    rt.SetValue(TranslateTransform.YProperty, rt.Value.OffsetY + newY - current.Y);
    }
    current.X = newX;
    current.Y = newY;
    }
    }

    ReplyDelete