How to Move a Shape control with Mouse Events?

The Shape control in Microsoft.VisualBasic.PowerPacks provides several useful events, including MouseDown, MouseMove, MouseUp. Some useful actions you can work on a shape is to use the mouse to move or resize the shape. For example, you may want to drag an end point of a line and move the end point. You may also want to drag a side of a rectangle to resize it. Or you may just want to drag the whole shape to another position.

People tend to start with Shape’s MouseDown, MouseMove, MouseUp events but find out that it does not work well later: the mouse seems very slippery, or the drag stop in the halfway. This is because the shape control (or any control) has a limited region and will not fire the events when the mouse moves out of that region.

image

 

In the left picture, assume the small box is a RectangleShape control in a form, if you put mouse down at point 1 and then move the mouse along the line to the southeast, it will stop firing mouse events once it pass point 2.

One correct way to move a shape is to use the Shape’s MouseDown event to start the drag, and use its parent control to handle the MouseMove and MouseUp events. All shape controls in a form shares an invisible parent control called ShapeContainer. In the following code sample, I have a form contains an OvalShape1 and its parent control is ShapeContainer1. I added MouseDown handler to OvalShape and MouseMove and MouseUp to ShapeContainer1.

Public Class Form1

    Dim dragging As Boolean = False

    Dim oldShapePosition As Point

    Dim mouseDownX As Integer 'Mouse position based on ShapeContainer

    Dim mouseDownY As Integer

Private Sub OvalShape1_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) _

  Handles OvalShape1.MouseDown

        dragging = True

        oldShapePosition = OvalShape1.Location

        mouseDownX = e.X + OvalShape1.Location.X

        mouseDownY = e.Y + OvalShape1.Location.Y

    End Sub

Private Sub ShapeContainer1_MouseMove(ByVal sender As Object, ByVal e As MouseEventArgs) _

  Handles ShapeContainer1.MouseMove

   If (dragging) Then

            Dim deltaX As Integer = e.X - mouseDownX

            Dim deltaY As Integer = e.Y - mouseDownY

            Me.OvalShape1.Location = New Point(oldShapePosition.X + deltaX, oldShapePosition.Y + deltaY)

        End If

    End Sub

Private Sub ShapeContainer1_MouseUp(ByVal sender As Object, ByVal e As MouseEventArgs) _

  Handles ShapeContainer1.MouseUp

        dragging = False

    End Sub

End Class

Because MouseEventArgs has the relative mouse position to the underline control, at mouse down, I need to record the mouse down position as below so it converts to the coordinates of the ShapeContainer1.

        mouseDownX = e.X + OvalShape1.Location.X

        mouseDownY = e.Y + OvalShape1.Location.Y

 

We can also let ShapeContainer to handle MouseDown event, this will relieve you from converting the mouse coordinate above. However, you will need to use the Shape.HitTest to tell if the mouse down is on a shape. The above sample for mouse down event handling can then be like this:

Private Sub ShapeContainer1_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) _

  Handles ShapeContainer1.MouseDown

        If (OvalShape1.HitTest(MousePosition.X, MousePosition.Y)) Then

            dragging = True

            oldShapePosition = Me.OvalShape1.Location

            mouseDownX = e.X

            mouseDownY = e.Y

        End If

    End Sub

The above code works fine for moving RectangleShape and OvalShape. To resize them, you will need some more work to hit test on the side and change the Size and/or Location.

Now let’s take a look at the LineShape, which is kind of special because it does not have the Location or Size properties. Instead, it has StartPoint and EndPoint properties. To move the whole line, we can move both StatPoint and EndPoint. To drag only one end, we just move either StartPoint or EndPoint. To test mouse down near an end, I add this help function:

    Private Function MouseIsNearBy(ByVal testPoint As Point) As Boolean

        testPoint = Me.PointToScreen(testPoint)

        Return Math.Abs(testPoint.X - MousePosition.X) <= HitTestDelta _

            AndAlso Math.Abs(testPoint.Y - MousePosition.Y) <= HitTestDelta

    End Function

 

Here is the complete code that works for a Form with the LineShape control called lineShape1 and its parent container control called ShapeContainer1.

 Public Class Form1

    Const HitTestDelta As Integer = 3

    ' The mouse position when mouse down

    Dim oldMouseX As Integer

    Dim oldMouseY As Integer

    ' The line position when mouse down.

    Dim oldStartPoint As Point

    Dim oldEndPoint As Point

    Dim dragStartPoint As Boolean = False

    Dim dragEndPoint As Boolean = False

Private Sub ShapeContainer1_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) _

   Handles ShapeContainer1.MouseDown

        If (LineShape1.HitTest(MousePosition.X, MousePosition.Y)) Then

            oldMouseX = e.X

            oldMouseY = e.Y

            oldStartPoint = LineShape1.StartPoint

     oldEndPoint = LineShape1.EndPoint

            dragStartPoint = MouseIsNearBy(oldStartPoint)

            dragEndPoint = MouseIsNearBy(oldEndPoint)

            If (Not dragStartPoint AndAlso Not dragEndPoint) Then

                'If not drag either end, then drag both.

                dragStartPoint = True

                dragEndPoint = True

            End If

        End If

    End Sub

Private Sub ShapeContainer1_MouseMove(ByVal sender As Object, ByVal e As MouseEventArgs) _

  Handles ShapeContainer1.MouseMove

        If (dragStartPoint) Then

            LineShape1.StartPoint = New Point(oldStartPoint.X + e.X - oldMouseX, _

                   oldStartPoint.Y + e.Y - oldMouseY)

        End If

        If (dragEndPoint) Then

            LineShape1.EndPoint = New Point(oldEndPoint.X + e.X - oldMouseX, _

                                            oldEndPoint.Y + e.Y - oldMouseY)

        End If

    End Sub

Private Sub ShapeContainer1_MouseUp(ByVal sender As Object, ByVal e As MouseEventArgs) _

   Handles ShapeContainer1.MouseUp

        dragStartPoint = False

        dragEndPoint = False

    End Sub

    Private Function MouseIsNearBy(ByVal testPoint As Point) As Boolean

        testPoint = Me.PointToScreen(testPoint)

        Return Math.Abs(testPoint.X - MousePosition.X) <= HitTestDelta _

               AndAlso Math.Abs(testPoint.Y - MousePosition.Y) <= HitTestDelta

    End Function

End Class

The blog is meant to answer these MSDN forum questions:

https://social.msdn.microsoft.com/Forums/en-US/vbpowerpacks/thread/ae39de20-d95e-497c-9f71-2c7426bc4635

https://social.msdn.microsoft.com/Forums/en-US/vbpowerpacks/thread/5c29b36a-a74b-4477-903c-90af7a43a8eb

Thanks!