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 Button
s, Image
s, ComboBox
s, 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
.
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
.
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:
this.dragCanvas.BringToFront( someEllipse );
this.dragCanvas.SendToBack( someEllipse );
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:
- 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.
- 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.
- 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;
this.origCursorLocation = e.GetPosition( this );
this.ElementBeingDragged =this.FindCanvasChild(e.Source as DependencyObject);
if( this.ElementBeingDragged == null )
return;
double left = Canvas.GetLeft( this.ElementBeingDragged );
double right = Canvas.GetRight( this.ElementBeingDragged );
double top = Canvas.GetTop( this.ElementBeingDragged );
double bottom = Canvas.GetBottom( this.ElementBeingDragged );
this.origHorizOffset = ResolveOffset(left, right, out this.modifyLeftOffset);
this.origVertOffset = ResolveOffset( top, bottom, out this.modifyTopOffset );
e.Handled = true;
this.isDragInProgress = true;
}
Here is the FindCanvasChild
method:
public UIElement FindCanvasChild( DependencyObject depObj )
{
while( depObj != null )
{
UIElement elem = depObj as UIElement;
if( elem != null && base.Children.Contains( elem ) )
break;
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:
private static double ResolveOffset(
double side1, double side2, out bool useSide1 )
{
useSide1 = true;
double result;
if( Double.IsNaN( side1 ) )
{
if( Double.IsNaN( side2 ) )
{
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
.
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( this.ElementBeingDragged == null || !this.isDragInProgress )
return;
Point cursorLocation = e.GetPosition( this );
double newHorizontalOffset, newVerticalOffset;
#region Calculate Offsets
if( this.modifyLeftOffset )
newHorizontalOffset =
this.origHorizOffset + (cursorLocation.X - this.origCursorLocation.X);
else
newHorizontalOffset =
this.origHorizOffset - (cursorLocation.X - this.origCursorLocation.X);
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
Rect elemRect =
this.CalculateDragElementRect( newHorizontalOffset, newVerticalOffset );
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 );
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
.