Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Dragging Elements in a Canvas

0.00/5 (No votes)
2 Sep 2006 5  
Discusses a class which provides automated dragging of elements in a WPF Canvas.

Introduction

Someone asked me recently how to create a WPF application where the user can drag a blue rectangle around the screen. I solved that puzzle, but kept generalizing the solution until it became possible to drag any UIElement (including Buttons, Images, ComboBoxs, Grids, etc.). This article explores a class called DragCanvas, which derives from Canvas. It enables the user to drag around the objects placed inside of it. The DragCanvas class also provides support for modifying the z-order of the elements it contains (such as ‘bring to front’ and ‘send to back’ operations).

Practical applications for this class might involve a Visio-like scratchpad work area, wherein the user should have complete freedom regarding where the visual objects belong relative to one another. A custom visual designer scenario might also benefit from this functionality.

This code was written and tested against the June 2006 CTP of the .NET Framework 3.0.

Background

While this article is not intended to be a review of the WPF layout system, it is important to review what the standard Canvas class exposes and how it can be used. Most panels in WPF provide automatic layout support, such as the docking behavior of the DockPanel, or the item wrapping behavior of the WrapPanel. This functionality is a welcome relief to many WinForms and MFC developers, who have had very limited support for automatic layout management in the past.

The Canvas is the only panel in the WPF layout system which does not provide any automatic layout support. The purpose of the Canvas panel is to provide absolute positioning, similar to putting controls on a WinForms Panel. The elements in a Canvas are never moved or resized by the Canvas. Developers find the Canvas useful in situations where the exact size and location of visual objects must not change due to, for example, the window resizing.

An element in a Canvas can specify its offsets from the Canvas’ sides via four attached properties: Left, Right, Top, and Bottom. If you are not familiar with attached properties, you might want to read my blog entry which discusses them in detail. The value of these properties indicate the position of an element relative to two sides of the Canvas. For example, the following XAML declares a Button that is 10 logical pixels away from the left edge and 20 logical pixels away from the top edge of its containing Canvas:

<Button Canvas.Left="10" Canvas.Top="20">Click Me</Button>

If the P property is set to N for an element, the element will always be N logical pixels away from the P edge of the Canvas. It is important to note that if both the Top and Bottom or Left and Right attached properties are specified for the same element, the Top and Left values will be honored, and the Bottom and Right values will be ignored.

The following image depicts how the four attached properties influence elements in a Canvas. The blue boxes seen below are contained within a Canvas, and their positions are specified within them. Notice that if one of the attached properties is not specified on an element, the default value is Double.NaN.

canvas offsets

Using the DragCanvas

It is easy to use the DragCanvas. All that you have to do is create an instance of the class in XAML and then place elements within it, just like you would do for a normal Canvas.

<jas:DragCanvas>
  <Button Canvas.Left="25" Canvas.Top="50">Click Me!</Button>
  <Rectangle 
    Fill="Blue" 
    Width="50" Height="50" 
    Canvas.Right="30" 
    Canvas.Bottom="40" />
</jas:DragCanvas>

By default, the DragCanvas manages the dragging of every element in its Children collection. If your application requires that the elements within the DragCanvas should not be draggable, you can set the AllowDragging property to false.

// Prevent the user from dragging elements in the DragCanvas.
this.dragCanvas.AllowDragging = false;

If you want to prohibit the user from being able to drag a specific element in the DragCanvas, you can use the CanBeDragged attached property to express this:

<jas:DragCanvas>
  <Button Canvas.Left="25" Canvas.Top="50">Click Me!</Button>
  <Rectangle 
    jas:DragCanvas.CanBeDragged="False"
    Fill="Blue" 
    Width="50" Height="50" 
    Canvas.Right="30" Canvas.Bottom="40" />
</jas:DragCanvas>

The DragCanvas prohibits the user from dragging any element out of its viewable area, so that the user cannot “lose” a visual item by accident. If your application logic requires that the user can drag elements out of the viewable area of the DragCanvas, set the AllowDragOutOfView property to true.

<jas:DragCanvas AllowDragOutOfView="True">
  ...
</jas:DragCanvas>

In applications where the user has complete freedom over the positions of items within a work area, it is often necessary to provide a way for objects within the work area to be brought to the front or sent to the back of the z-order. The DragCanvas provides two methods you can call to accomplish this task:

// Bring the Ellipse to the top of the z-order 
this.dragCanvas.BringToFront( someEllipse );

bring to front

 // Send the Ellipse to the back of the z-order.
 this.dragCanvas.SendToBack( someEllipse );

send to back

How it Works

The rest of the article discusses how the DragCanvas works. It is not necessary to read this section in order to use the class.

The element dragging logic is comprised of three steps:

  1. When the left mouse button is depressed, the DragCanvas looks for a child UIElement at the current mouse cursor location. If it finds one, a reference to that element is stored, information about the element’s location is saved, and the cursor location is cached.
  2. When the mouse moves, if there is an element being dragged (i.e., an element was found when the mouse button was depressed), then that element will be relocated by the distance between the old and current cursor locations, relative to the original element location.
  3. Eventually, when a mouse button is released, the drag element reference is nullified, so that when the mouse moves, there is no element to relocate.

It seems simple enough but, of course, the devil’s in the details!

Step One

The first step is implemented as follows:

protected override void OnPreviewMouseLeftButtonDown( MouseButtonEventArgs e )
{
 base.OnPreviewMouseLeftButtonDown( e );

 this.isDragInProgress = false;

 // Cache the mouse cursor location.
 this.origCursorLocation = e.GetPosition( this );

 // Walk up the visual tree from the element that was clicked, 
 // looking for an element that is a direct child of the Canvas.
 this.ElementBeingDragged =this.FindCanvasChild(e.Source as DependencyObject);
 if( this.ElementBeingDragged == null )
  return;

 // Get the element's offsets from the four sides of the Canvas.
 double left = Canvas.GetLeft( this.ElementBeingDragged );
 double right = Canvas.GetRight( this.ElementBeingDragged );
 double top = Canvas.GetTop( this.ElementBeingDragged );
 double bottom = Canvas.GetBottom( this.ElementBeingDragged );

 // Calculate the offset deltas and determine for which sides
 // of the Canvas to adjust the offsets.
 this.origHorizOffset = ResolveOffset(left, right, out this.modifyLeftOffset);
 this.origVertOffset = ResolveOffset( top, bottom, out this.modifyTopOffset );

 // Set the Handled flag so that a control being dragged 
 // does not react to the mouse input.
 e.Handled = true;

 this.isDragInProgress = true;
}

Here is the FindCanvasChild method:

/// <summary>
/// Walks up the visual tree starting with the specified DependencyObject, 
/// looking for a UIElement which is a child of the Canvas. If a suitable 
/// element is not found, null is returned. If the 'depObj' object is a 
/// UIElement in the Canvas's Children collection, it will be returned.
/// </summary>
/// <param name="depObj">
/// A DependencyObject from which the search begins.
/// </param>
public UIElement FindCanvasChild( DependencyObject depObj )
{
 while( depObj != null )
 {
  // If the current object is a UIElement which is a child of the
  // Canvas, exit the loop and return it.
  UIElement elem = depObj as UIElement;
  if( elem != null && base.Children.Contains( elem ) )
   break;

  // VisualTreeHelper works with objects of type Visual or Visual3D.
  // If the current object is not derived from Visual or Visual3D,
  // then use the LogicalTreeHelper to find the parent element.
  if( depObj is Visual || depObj is Visual3D )
   depObj = VisualTreeHelper.GetParent( depObj );
  else
   depObj = LogicalTreeHelper.GetParent( depObj );
 }
 return depObj as UIElement;
}

This method is responsible for walking up the visual and logical trees to find a UIElement which is a child of the DragCanvas. Since the object passed to this method might be deeply embedded inside of a child of the DragCanvas, it is necessary to walk up the ancestor chain of the argument value. For example, the element which was clicked on by the user could be a Run inside of a Hyperlink, which is in a TextBlock, which is in a StackPanel, which is contained within a UniformGrid (which is a child of the DragCanvas). In that situation, we need to walk from the Run object up to the UniformGrid because only direct descendants of a DragCanvas can be dragged.

The ResolveOffset method is used to determine which sides of the DragCanvas the drag element’s position is based off of. As mentioned in the ‘Background’ section of this article, the location of an element in a Canvas is determined by two offsets: horizontal and vertical. The horizontal offset can be relative to the left edge or the right edge of the Canvas, and the vertical offset can be relative to the top edge or bottom edge. The method that determines which edges of the Canvas the drag element’s location is relative to is shown below:

/// <summary>
/// Determines one component of a UIElement's location 
/// within a Canvas (either the horizontal or vertical offset).
/// </summary>
/// <param name="side1">
/// The value of an offset relative to a default side of the 
/// Canvas (i.e. top or left).
/// </param>
/// <param name="side2">
/// The value of the offset relative to the other side of the 
/// Canvas (i.e. bottom or right).
/// </param>
/// <param name="useSide1">
/// Will be set to true if the returned value should be used 
/// for the offset from the side represented by the 'side1' 
/// parameter. Otherwise, it will be set to false.
/// </param>
private static double ResolveOffset( 
        double side1, double side2, out bool useSide1 )
{
 // If the Canvas.Left and Canvas.Right attached properties 
 // are specified for an element, the 'Left' value is honored.
 // The 'Top' value is honored if both Canvas.Top and 
 // Canvas.Bottom are set on the same element. If one 
 // of those attached properties is not set on an element, 
 // the default value is Double.NaN.
 useSide1 = true;
 double result;
 if( Double.IsNaN( side1 ) )
 {
  if( Double.IsNaN( side2 ) )
  {
   // Both sides have no value, so set the
   // first side to a value of zero.
   result = 0;
  }
  else
  {
   result = side2;
   useSide1 = false;
  }
 }
 else
 {
  result = side1;
 }
 return result;
}

Lastly, we have the ElementBeingDragged property. The setter is of primary interest at this point. As you can see, when the drag element is established, it is given mouse capture. Mouse capture ensures that all mouse messages are immediately directed to the drag element, which allows the drag logic to work when the mouse is moved extremely fast, and also allows for the element to receive mouse messages when the cursor has left the client area of the DragCanvas.

/// <summary>
/// Returns the UIElement currently being dragged, or null.
/// </summary>
/// <remarks>
/// Note to inheritors: This property exposes a protected 
/// setter which should be used to modify the drag element.
/// </remarks>
public UIElement ElementBeingDragged
{
 get
 {
  if( !this.AllowDragging )
   return null;
  else
   return this.elementBeingDragged;
 }
 protected set
 {
  if( this.elementBeingDragged != null )
   this.elementBeingDragged.ReleaseMouseCapture();

  if( !this.AllowDragging )
   this.elementBeingDragged = null;
  else
  {
   if( DragCanvas.GetCanBeDragged( value ) )
   {
    this.elementBeingDragged = value;
    this.elementBeingDragged.CaptureMouse();
   }
   else
    this.elementBeingDragged = null;
  }
 }

Step Two

Once a drag element has been established (i.e., the ElementBeingDragged property is non-null), it is possible to move that object when the mouse moves. The following method moves the drag element when the mouse moves:

protected override void OnPreviewMouseMove( MouseEventArgs e )
{
 base.OnPreviewMouseMove( e );

 // If no element is being dragged, there is nothing to do.
 if( this.ElementBeingDragged == null || !this.isDragInProgress )
  return;

 // Get the position of the mouse cursor, relative to the Canvas.
 Point cursorLocation = e.GetPosition( this );

 // These values will store the new offsets of the drag element.
 double newHorizontalOffset, newVerticalOffset;

 #region Calculate Offsets

 // Determine the horizontal offset.
 if( this.modifyLeftOffset )
  newHorizontalOffset = 
    this.origHorizOffset + (cursorLocation.X - this.origCursorLocation.X);
 else
  newHorizontalOffset = 
    this.origHorizOffset - (cursorLocation.X - this.origCursorLocation.X);

 // Determine the vertical offset.
 if( this.modifyTopOffset )
  newVerticalOffset = 
    this.origVertOffset + (cursorLocation.Y - this.origCursorLocation.Y);
 else
  newVerticalOffset = 
    this.origVertOffset - (cursorLocation.Y - this.origCursorLocation.Y);

 #endregion // Calculate Offsets

 if( ! this.AllowDragOutOfView )
 {
  #region Verify Drag Element Location

  // Get the bounding rect of the drag element.
  Rect elemRect = 
    this.CalculateDragElementRect( newHorizontalOffset, newVerticalOffset );

  //
  // If the element is being dragged out of the viewable area, 
  // determine the ideal rect location, so that the element is 
  // within the edge(s) of the canvas.
  //
  bool leftAlign = elemRect.Left < 0;
  bool rightAlign = elemRect.Right > this.ActualWidth;

  if( leftAlign )
   newHorizontalOffset = 
    modifyLeftOffset ? 0 : this.ActualWidth - elemRect.Width;
  else if( rightAlign )
   newHorizontalOffset = 
    modifyLeftOffset ? this.ActualWidth - elemRect.Width : 0;

  bool topAlign = elemRect.Top < 0;
  bool bottomAlign = elemRect.Bottom > this.ActualHeight;

  if( topAlign )
   newVerticalOffset = 
    modifyTopOffset ? 0 : this.ActualHeight - elemRect.Height;
  else if( bottomAlign )
   newVerticalOffset = 
    modifyTopOffset ? this.ActualHeight - elemRect.Height : 0;

  #endregion // Verify Drag Element Location
 }

 #region Move Drag Element

 if( this.modifyLeftOffset )
  Canvas.SetLeft( this.ElementBeingDragged, newHorizontalOffset );
 else
  Canvas.SetRight( this.ElementBeingDragged, newHorizontalOffset );

 if( this.modifyTopOffset )
  Canvas.SetTop( this.ElementBeingDragged, newVerticalOffset );
 else
  Canvas.SetBottom( this.ElementBeingDragged, newVerticalOffset );

 #endregion // Move Drag Element
}

That method first calculates the new offsets of the drag element, based on the current location of the mouse cursor. If the DragCanvas is not supposed to allow elements to be dragged out of view and the element’s new location would put some or all of the element out of view, the new offsets are modified so that the drag element is flushed against the edge(s) of the DragCanvas. After the new offsets are computed, the element location is updated by modifying the values of the appropriate attached properties.

Step Three

When either mouse button is released, the drag element is nullified.

protected override void OnPreviewMouseUp( MouseButtonEventArgs e )
{
 base.OnPreviewMouseUp( e );

 // Reset the field whether the left or right mouse button was 
 // released, in case a context menu was opened on the drag element.
 this.ElementBeingDragged = null;
}

Z-Order Methods

If you are interested in seeing how the methods which affect the z-order are implemented, look at the private UpdateZOrder method in the DragCanvas class. That helper method is used by both the public BringToFront and SendToBack methods. The full source code, plus a demo, is available for download at the top of this article.

Possible Improvements

Perhaps, exposing some drag-related events might be helpful in some scenarios, such as a cancelable BeforeElementDrag event, an ElementDrag event which provides info about the drag element and its location, and an AfterElementDrag event.

Article History Log

  • August 27, 2006 – Created article.
  • September 2, 2006 – Rewrote most of the article, and changed the source code download. Originally, this article discussed a class called CanvasDragManager, which was attached to a Canvas. With the help of some experts on the WPF Forum, I managed to overcome some problems and put all of the drag logic into a Canvas-derived class: the DragCanvas.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here