Dragging WPF elements

This is the first in a series of post exploring that beloved UI gesture: dragging and dropping things around.

In this first post, I'll take a look at the simplest, most striaghtforward approach: adding a bunch of event handlers to your window and updating layout properties. I'll be using VB.NET - converting to C# should not be too difficult.

First, we'll start with this code in Window1.xaml.

<Window x:Class="Window1"
xmlns="
https://schemas.microsoft.com/winfx/2006/xaml/presentation "
xmlns:x="
https://schemas.microsoft.com/winfx/2006/xaml "
Title="Drag Canvas" Height="300" Width="300">
<DockPanel>

  <Border DockPanel.Dock="Top" BorderBrush="DarkGray" BorderThickness="2" Padding="4">
<TextBlock FontSize="8pt" FontFamily="Tahoma" TextWrapping="Wrap">
<Bold>Drag Canvas Sample
</Bold><LineBreak /><Run>
This sample demonstrates the easiest way to drag shapes (or any other
elements) on a Canvas area.</Run>
</TextBlock>
</Border>

  <Canvas Name="MyCanvas">

   <Canvas.Background>
<LinearGradientBrush>
<LinearGradientBrush.GradientStops>
<GradientStop Color="White" Offset="0" />
<GradientStop Color="DarkBlue" Offset="1" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Canvas.Background>

   <Rectangle Canvas.Top="8" Canvas.Left="8"
Width="32" Height="32" Fill="LightPink" />
<Ellipse Canvas.Top="8" Canvas.Left="48"
Width="40" Height="16" Fill="Tan" />
<Rectangle Canvas.Top="48" Canvas.Left="8"
Width="32" Height="62" Fill="Green" />
<Ellipse Canvas.Top="48" Canvas.Left="48"
Width="62" Height="62" Fill="Orange" />

  </Canvas>

  </DockPanel>
</Window>

This creates a simple window that looks like this.

OK, now let's roll up our sleeves and build this bit by bit by adding code to Window1.xaml.vb.

First off, we'll start with some fields in Window1. Right under the constructor, we can add this bit of code.

Private m_StartPoint As Point ' Where did the mouse start off from?
Private m_OriginalLeft As Double ' What was the element's original x offset?
Private m_OriginalTop As Double ' What was the element's original y offset?
Private m_IsDown As Boolean ' Is the mouse down right now?
Private m_IsDragging As Boolean ' Are we actually dragging the shape around?
Private m_OriginalElement As UIElement ' What is it that we're dragging?
Private m_OverlayElement As Rectangle ' What is it that we're using to show where the shape will end up?

This is all information that we'll track while moving the shapes in the Canvas about.

Now, let's write the code to start the dragging. When the mouse goes down, we'll capture a some of the information we'll use, and then when the user begins to move the mouse, we'll add an element to show where the content will end up.

Private Sub MyCanvas_PreviewMouseLeftButtonDown(ByVal sender As Object, ByVal e As System.Windows.Input.MouseButtonEventArgs) Handles MyCanvas.PreviewMouseLeftButtonDown
If MyCanvas Is e.Source Then
Exit Sub
End If

 m_IsDown = True
m_StartPoint = e.GetPosition(MyCanvas)
m_OriginalElement = e.Source
MyCanvas.CaptureMouse()
e.Handled = True
End Sub

Private Sub MyCanvas_PreviewMouseMove(ByVal sender As Object, ByVal e As System.Windows.Input.MouseEventArgs) Handles MyCanvas.PreviewMouseMove
If m_IsDown Then
If Not m_IsDragging AndAlso _
Math.Abs(e.GetPosition(MyCanvas).X - m_StartPoint.X) > SystemParameters.MinimumHorizontalDragDistance AndAlso _
Math.Abs(e.GetPosition(MyCanvas).Y - m_StartPoint.Y) > SystemParameters.MinimumVerticalDragDistance Then
DragStarted()
End If
If m_IsDragging Then
DragMoved()
End If
End If
End Sub

A few things to note are the fact that we capture the mouse when it goes down (in case the user moves the mouse out of the bounding area), and the DragStarted() and DragMoved() helper methods I'm using. Also, notice how we use the system paramters to respect the user's selection for how far the mouse needs to move before we start dragging. This is especially helpful for laptops and tablets, on which it's very common to have the pointer shake a bit when working with the mouse stick thingy or a pen.

Now, what is it that we do in our helper methods?

Private Sub DragStarted()
m_IsDragging = True

 m_OriginalLeft = Canvas.GetLeft(m_OriginalElement)
m_OriginalTop = Canvas.GetTop(m_OriginalElement)

  ' Add a rectangle to indicate where we'll end up.
' We'll just use an alpha-blended visual brush.
Dim brush As VisualBrush
brush = New VisualBrush(m_OriginalElement)
brush.Opacity = 0.5

 m_OverlayElement = New Rectangle()
m_OverlayElement.Width = m_OriginalElement.RenderSize.Width
m_OverlayElement.Height = m_OriginalElement.RenderSize.Height
m_OverlayElement.Fill = brush

 MyCanvas.Children.Add(m_OverlayElement)
End Sub

Private Sub DragMoved()
Dim currentPosition As Point = System.Windows.Input.Mouse.GetPosition(MyCanvas)
Dim elementLeft As Double = (currentPosition.X - m_StartPoint.X) + m_OriginalLeft
Dim elementTop As Double = (currentPosition.Y - m_StartPoint.Y) + m_OriginalTop
Canvas.SetLeft(m_OverlayElement, elementLeft)
Canvas.SetTop(m_OverlayElement, elementTop)
End Sub

Notice how I use a alpa-blended VisualBrush to show the user where the shape is going to end up. Also, because I'm working with a Canvas, I simply change the Left and Top attached properties. If this were a DockPanel, I might try seeing whether I would want to try snapping to an edge; or if this were a Grid, I might try to figure out in which cell the shape would drop. I'm just sticking to the simplest case in this sample, but you can take it as far as you want.

This is all well and good, but how do we actually drop the shape? Two typical gestures are releasing the mouse button, or pressing Escape (to cancel the operation). Let's code that up.

Private Sub MyCanvas_PreviewMouseLeftButtonUp(ByVal sender As Object, ByVal e As System.Windows.Input.MouseButtonEventArgs) Handles MyCanvas.PreviewMouseLeftButtonUp
If m_IsDown Then
DragFinished(False)
e.Handled = True
End If
End Sub

Private Sub Window1_PreviewKeyDown(ByVal sender As Object, ByVal e As System.Windows.Input.KeyEventArgs) Handles Me.PreviewKeyDown
' This is handled at the window level, because neither MyCanvas nor
' its children ever get keyboard focus.
If e.Key = Input.Key.Escape AndAlso m_IsDragging Then
DragFinished(True)
End If
End Sub

Again, a helper method to make it easy to isolate what happens when we actually finish dragging, and here's the implementation for it.

Private Sub DragFinished(ByVal canceled As Boolean)
System.Windows.Input.Mouse.Capture(Nothing)
If m_IsDragging Then
MyCanvas.Children.Remove(m_OverlayElement)
If Not canceled Then
Canvas.SetLeft(m_OriginalElement, Canvas.GetLeft(m_OverlayElement))
Canvas.SetTop(m_OriginalElement, Canvas.GetTop(m_OverlayElement))
End If
m_OverlayElement = Nothing
End If
m_IsDragging = False
m_IsDown = False
End Sub

And that, my dear readers, completes today's sample. Give it try, play around with it, marvel at the alpha-blended 'preview', and try extending it. Can you add other controls to drag around?

This posting is provided "AS IS" with no warranties, and confers no rights. Use of included script samples are subject to the terms specified at https://www.microsoft.com/info/cpyright.htm.