Sunday, October 18, 2009

Manipulating GDI+ Drawings

I've seen many people ask how to manipulate the drawing they produce using GDI+. This is no trivial thing because, as you'll know if you've used GDI+ much at all, your drawing is simply pixels on the screen. There are no objects that you can get references to, set properties on or call methods of.

If you want to use GDI+ to draw on your form and/or controls and you want to be able to change what's drawn at all then you really only have one course of action available. You need to declare one or more member variables, store data in those variables that describe your drawing and then read that data on the Paint event. If you want to be able manipulate your drawing, e.g. drag items around, then you need to use that same data to determine how the mouse state relates to your drawing and then make changes accordingly.

For this example, we're going to create a simple Windows Forms application that behaves, in a rudimentary way, like the VS design window. Just as you can draw controls onto a form, drag them around and use their right-click menu to change the z-order on a Windows Form, so our app will allow you to draw boxes by clicking and dragging, move boxes around by dragging and dropping and also to change the z-order using a right-click.

The app will draw white rectangles with a black border, which will make it easy to see which is in front of which. The boxes will have an apparent z-order, much as controls do on a form. We will add right-click functionality that will allow us to send a box to the back, making it appear to be behind the other boxes, or to bring it to the front, making it appear to be in front of all others. We will also add functionality to enable dragging and dropping boxes around the form.

So, first things first, you'll need to open VS and create a new Windows Forms Application project. I've tested this in VS 2008 but the code I'll provide should also work in VS 2005. I'll also provide both C# and VB code so you can use whichever language you like.

Let's start by adding a PictureBox to the form and setting its Dock property to Fill, so it fills the entire form. We're going to need handle some events of that PictureBox so let's create those event handlers now. We'll only consider the drawing part for now so what events will we need to handle? The user is going to depress the left mouse button to start drawing, drag the mouse to the opposite corner of the box they want to draw, then release the mouse button. To be able to pick up all of that activity we will need to handle the MouseDown, MouseMove and MouseUp events of the PictureBox. Of course, we're using GDI+ to draw on the PictureBox so we'll obviously need to handle its Paint event too.

As I said earlier, we need to store data that describes our drawing in one or more member variables that we can edit and then read in the Paint event handler. In this case we will need to store the start point and the end point of the box we're currently drawing. Two Point fields will do for that. We'll also need to store data for each box we'vew previously drawn. We'll use instances of the Rectangle structure to represent a box and we'll use a generic List to store a Rectangle for each box we've previosuly drawn.

C#

private Point startPoint;
private Point endPoint;
 
private List<Rectangle> boxes = new List<Rectangle>();

VB

Private startPoint As Point
Private endPoint As Point
 
Private boxes As New List(Of Rectangle)

First of all, let’s think about how we want the drawing done. That way we can implement the body of our Paint event handler and then essentially forget about it. So, all previously drawn boxes will be recorded as a Rectangle in our generic list while, if we are currently drawing a box, it will be defined by the start point and end point values.

C#

private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
    Graphics g = e.Graphics;
 
    foreach (Rectangle box in this.boxes)
    {
        g.FillRectangle(Brushes.White, box);
        g.DrawRectangle(Pens.Black, box);
    }
 
    if (Control.MouseButtons == MouseButtons.Left)
    {
        Rectangle box = this.GetRectangle(this.startPoint, this.endPoint);
 
        g.FillRectangle(Brushes.White, box);
        g.DrawRectangle(Pens.Black, box);
    }
}

VB

Private Sub PictureBox1_Paint(ByVal sender As Object, _
                              ByVal e As PaintEventArgs) Handles PictureBox1.Paint
    With e.Graphics
        For Each box As Rectangle In Me.boxes
            .FillRectangle(Brushes.White, box)
            .DrawRectangle(Pens.Black, box)
        Next
 
        If Control.MouseButtons = Windows.Forms.MouseButtons.Left Then
            Dim box As Rectangle = Me.GetRectangle(Me.startPoint, Me.endPoint)
 
            .FillRectangle(Brushes.White, box)
            .DrawRectangle(Pens.Black, box)
        End If
    End With
End Sub

Let’s look at a few points of interest in that code. First, it uses a foreach/For Each loop to enumerate the existing boxes for drawing. As such it will draw the oldest first, thus placing newer boxes in front of older, which is the desired behaviour. If there is a box currently being drawn it gets drawn last of all, in front of all other boxes. Note that the test for whether or not a box is currently being drawn is that the left mouse button and only the left mouse button is depressed. Finally, note that that code calls a GetRectangle method that takes two Point arguments and returns a Rectangle. It’s our next job to write that method.

So, if you’re given two Points, how will you create a Rectangle from them? To create a Rectangle we need the coordinates of the top, left corner and the dimensions. We’ll obviously assume that our two Points are diagonally opposite but we don’t which diagonal or in which order. We don’t have to care though. We know that, whatever combination of corners we have, the top, left corner will be defined by the lower of the two X values and the lower of the two Y values. The dimensions will just be the difference between the X values and the Y values, although we’ll have to take the absolute value to allow for either order.

C#

private Rectangle GetRectangle(Point startPoint, Point endPoint)
{
    return new Rectangle(Math.Min(startPoint.X, endPoint.X),
                         Math.Min(startPoint.Y, endPoint.Y),
                         Math.Abs(startPoint.X - endPoint.X),
                         Math.Abs(startPoint.Y - endPoint.Y));
}

VB

Private Function GetRectangle(ByVal startPoint As Point, _
                              ByVal endPoint As Point) As Rectangle
    Return New Rectangle(Math.Min(startPoint.X, endPoint.X), _
                         Math.Min(startPoint.Y, endPoint.Y), _
                         Math.Abs(startPoint.X - endPoint.X), _
                         Math.Abs(startPoint.Y - endPoint.Y))
End Function

Next we need to look at what we’re going to do when the user depresses the mouse button. That location will become the start point for the new box, and it will also be the initial value for the end point, given that the mouse hasn’t moved from the start point yet.

C#

private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
    if (Control.MouseButtons == MouseButtons.Left)
    {
        Point location = e.Location;
 
        this.startPoint = location;
        this.endPoint = location;
    }
}

VB

Private Sub PictureBox1_MouseDown(ByVal sender As Object, _
                                  ByVal e As MouseEventArgs) Handles PictureBox1.MouseDown
    If Control.MouseButtons = Windows.Forms.MouseButtons.Left Then
        Dim location As Point = e.Location
 
        Me.startPoint = location
        Me.endPoint = location
    End If
End Sub

Note that the value tested is Control.MouseButtons rather than e.Button. This because we want to start drawing if the left mouse button and only the left button is depressed. If the user has already depressed the right mouse button and then depresses the left, e.Button would still have the value Left, while Control.MouseButtons would not.

Next, let’s consider what we want to do when the user releases the mouse button. Let’s keep it simple to begin with. We first need to create a Rectangle from the start point and end point. We can do that courtesy of our GetRectangle method. We then need to add that to our List of Rectangles so that it gets drawn. Finally, we need to invalidate the area occupied by that Rectagle and then tell the PictureBox to repaint it. We invalidate only the area that has changed, for efficiency.

C#

private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left &&
        Control.MouseButtons == MouseButtons.None)
    {
        this.endPoint = e.Location;
 
        Rectangle box = this.GetRectangle(this.startPoint, this.endPoint);
 
        this.boxes.Add(box);
        this.pictureBox1.Invalidate(box);
        this.pictureBox1.Update();
    }
}

VB

Private Sub PictureBox1_MouseUp(ByVal sender As Object, _
                                ByVal e As MouseEventArgs) Handles PictureBox1.MouseUp
    If e.Button = Windows.Forms.MouseButtons.Left AndAlso _
       Control.MouseButtons = Windows.Forms.MouseButtons.None Then
        Me.endPoint = e.Location
 
        Dim box As Rectangle = Me.GetRectangle(Me.startPoint, Me.endPoint)
 
        Me.boxes.Add(box)
        Me.PictureBox1.Invalidate(box)
        Me.PictureBox1.Update()
    End If
End Sub

We’re actually at the point where our application can do something useful, so let’s give it a try. Run your project and try creating a few boxes. Just push the left mouse button, drag the mouse and then release. You’ll see that nothing gets drawn while we’re dragging yet, but a box appears when we release the mouse.

There’s a bit of a problem though. Notice that the black border only gets drawn on the top and left sides, not on the right and bottom. That’s actually not an issue with the drawing itself but rather an issue with the invalidation. When you call Invalidate and pass a Rectangle, the right and bottom edges are exclusive. As such, if we want that last row and column of pixels to be drawn when we call Update, we need to enlarge the area we invalidate at least one pixel further right and one pixel further down. This is something that we’ll need to do again so let’s put it into its own method.

C#

private void InvalidateRectangle(Rectangle box)
{
    box.Inflate(1, 1);
    this.pictureBox1.Invalidate(box);
}

VB

Private Sub InvalidateRectangle(ByVal box As Rectangle)
    box.Inflate(1, 1)
    Me.PictureBox1.Invalidate(box)
End Sub

The Inflate method will increase the width and the height of the Rectangle by the specified amounts in both directions. As such, we’ll end up invalidating one row of pixels above and one column of pixels to the left that haven’t actually changed. That’s only a small number though and this code is more succinct than what we would write to get the size increase just to the right and down. It’s not a big deal though so, by all means, write that extra bit of code if want to be as efficient as possible. Note also that it’s safe to inflate the parameter directly because it’s just a copy of the Rectangle in the List, owing to Rectangle being a value type.

We can now call our InvalidateRectangle method from our MouseUp event handler.

C#

private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left &&
        Control.MouseButtons == MouseButtons.None)
    {
        this.endPoint = e.Location;
 
        Rectangle box = this.GetRectangle(this.startPoint, this.endPoint);
 
        this.boxes.Add(box);
        this.InvalidateRectangle(box);
        this.pictureBox1.Update();
    }
}

VB

Private Sub PictureBox1_MouseUp(ByVal sender As Object, _
                                ByVal e As MouseEventArgs) Handles PictureBox1.MouseUp
    If e.Button = Windows.Forms.MouseButtons.Left AndAlso _
       Control.MouseButtons = Windows.Forms.MouseButtons.None Then
        Me.endPoint = e.Location
 
        Dim box As Rectangle = Me.GetRectangle(Me.startPoint, Me.endPoint)
 
        Me.boxes.Add(box)
        Me.InvalidateRectangle(box)
        Me.PictureBox1.Update()
    End If
End Sub

Now let’s try running our project again. This time notice that the black border is drawn on all four sides. Try drawing boxes from using both diagonals in both directions and you’ll see it works correctly for all four cases. Also notice that new boxes are drawn in front of old ones.

The next order of business is getting the boxes to draw as we drag the mouse, rather than just appearing when we release the mouse button. For that to happen we need to tell the PictureBox to repaint from the MouseMove event handler. In this case we want to, again, check that the left mouse button and only the left mouse button is depressed. We’ll need to invalidate the area previously occupied by the box being drawn, in case it has shrunk, then invalidate the new area occupied by the box, then force a repaint.

C#

private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
    if (Control.MouseButtons == MouseButtons.Left)
    {
        this.InvalidateRectangle(this.GetRectangle(this.startPoint,
                                                   this.endPoint));
        this.endPoint = e.Location;
        this.InvalidateRectangle(this.GetRectangle(this.startPoint,
                                                   this.endPoint));
        this.pictureBox1.Update();
    }
}

VB

Private Sub PictureBox1_MouseMove(ByVal sender As Object, _
                                  ByVal e As MouseEventArgs) Handles PictureBox1.MouseMove
    If Control.MouseButtons = Windows.Forms.MouseButtons.Left Then
        Me.InvalidateRectangle(Me.GetRectangle(Me.startPoint, Me.endPoint))
        Me.endPoint = e.Location
        Me.InvalidateRectangle(Me.GetRectangle(Me.startPoint, Me.endPoint))
        Me.PictureBox1.Update()
    End If
End Sub

Try running the project and drawing some boxes again. You’ll see that now the box gets drawn on the form as we’re dragging the mouse. Try dragging the mouse in a circle around the start point and see how it gets drawn correctly in all four directions. Try doing the same when drawing a box over some others and see how those underneath get redrawn correctly as they reappear, thanks to our invalidating the old area of the box as well as the new.

That’s the end of the first stage of the project: drawing the boxes. The next stage is to be able to right-click a box and select an option to send it to the back of the z-order or bring it to the front. In order to do that, we’re obviously going to have to add a ContextMenuStrip to the form. We won’t be able to just assign it to the PictureBox’s ContextMenuStrip property though, because we’ll only want to show it when the user right-clicks on a box.

Go ahead and add a ContextMenuStrip to your form. Add two items to the menu with text “Bring to Front” and “Send to Back”. You might as well create handlers for their Click events now, although we won’t add any code to them just yet. Before we do that we need to add a handler for the PictureBox’s MouseClick event, to detect tight-clicks. In order to determine whether the mouse pointer was in a box when the right-click occurred, we will need to loop through our List of Rectangles and check whether each one contains the mouse pointer. This is something we’ll have to do again so let’s put it into its own method.

C#

private int GetRectangleIndexAtPoint(Point location)
{
    Rectangle box;
    int result = -1;
 
    for (int index = this.boxes.Count - 1; index >= 0; index--)
    {
        box = this.boxes[index];
 
        if (box.Contains(location))
        {
            result = index;
            break;
        }
    }
 
    return result;
}

VB

Private Function GetRectangleIndexAtPoint(ByVal location As Point) As Integer
    Dim box As Rectangle
    Dim result As Integer = -1
 
    For index As Integer = Me.boxes.Count - 1 To 0 Step -1
        box = Me.boxes(index)
 
        If box.Contains(location) Then
            result = index
            Exit For
        End If
    Next
 
    Return result
End Function

Notice that that code loops through the List backwards. That’s because, if two boxes overlap, we want to detect the front-most one, which will have a higher index.

Now let’s use that method to detect whether or not we should display the ContextMenuStrip on a right-click.

C#

private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Right &&
        Control.MouseButtons == MouseButtons.None)
    {
        Point location = e.Location;
 
        this.selectedBoxIndex = this.GetRectangleIndexAtPoint(location);
 
        if (this.selectedBoxIndex != -1)
        {
            this.contextMenuStrip1.Show(this.pictureBox1, location);
        }
    }
}

VB

Private Sub PictureBox1_MouseClick(ByVal sender As Object, _
                                   ByVal e As MouseEventArgs) Handles PictureBox1.MouseClick
    If e.Button = Windows.Forms.MouseButtons.Right AndAlso _
       Control.MouseButtons = Windows.Forms.MouseButtons.None Then
        Dim location As Point = e.Location
 
        Me.selectedBoxIndex = Me.GetRectangleIndexAtPoint(location)
 
        If Me.selectedBoxIndex <> -1 Then
            Me.ContextMenuStrip1.Show(Me.PictureBox1, location)
        End If
    End If
End Sub

Notice that the result of the GetRectangleIndexAtPoint method is assigned to a field that we are yet to declare.

C#

private int selectedBoxIndex;

VB

Private selectedBoxIndex As Integer

That field is required because we’ll need to its value in order to identify which box to manipulate when the user selects a menu item.

The code first checks that the right mouse button was released and there are no other buttons currently depressed. In that case it gets the index of the front-most box that contains the mouse pointer. If there is such a box the menu is displayed at the mouse pointer location.

Now it’s time to implement the actions associated with the menu items. If you haven’t already, you should add Click event handlers for both menu items now. In each case we will want to move the box that was right-clicked, which is identified by the selectedBoxIndex field, from its current position to one end or the other. If we’re bringing the box to the front then it needs to be drawn last, so it should be placed at the end of the List, while if we’re sending it to the back it should be at the beginning of the List, to get drawn first.

C#

private void bringToFrontToolStripMenuItem_Click(object sender, EventArgs e)
{
    this.ChangeRectangleIndex(this.selectedBoxIndex, this.boxes.Count);
}
 
private void sendToBackToolStripMenuItem_Click(object sender, EventArgs e)
{
    this.ChangeRectangleIndex(this.selectedBoxIndex, 0);
}
 
private void ChangeRectangleIndex(int oldIndex, int newIndex)
{
    Rectangle box = this.boxes[oldIndex];
 
    if (oldIndex < newIndex)
    {
        newIndex--;
    }
 
    this.boxes.RemoveAt(oldIndex);
    this.boxes.Insert(newIndex, box);
 
    this.InvalidateRectangle(box);
    this.pictureBox1.Update();
}

VB

Private Sub BringToFrontToolStripMenuItem_Click(ByVal sender As Object, _
                                                ByVal e As EventArgs) Handles BringToFrontToolStripMenuItem.Click
    Me.ChangeRectangleIndex(Me.selectedBoxIndex, Me.boxes.Count)
End Sub
 
Private Sub SendToBackToolStripMenuItem_Click(ByVal sender As Object, _
                                              ByVal e As EventArgs) Handles SendToBackToolStripMenuItem.Click
    Me.ChangeRectangleIndex(Me.selectedBoxIndex, 0)
End Sub
 
Private Sub ChangeRectangleIndex(ByVal oldIndex As Integer, ByVal newIndex As Integer)
    Dim box As Rectangle = Me.boxes(oldIndex)
 
    If oldIndex < newIndex Then
        newIndex -= 1
    End If
 
    Me.boxes.RemoveAt(oldIndex)
    Me.boxes.Insert(newIndex, box)
 
    Me.InvalidateRectangle(box)
    Me.PictureBox1.Update()
End Sub

While it would be possible for the bring-to-front functionality to use Add instead of Insert, I’ve used Insert in both cases for the sake of code reuse. The ChangeRectangleIndex function first gets the box that selected by the right-click. It then adjusts the new index if necessary, because removing an item will decrement the index of all subsequent items. The box is then removed from the list and re-inserted at the new index. Finally, the area occupied by the box in question is invalidated and a repaint is forced.

Try running the project again and drawing a few boxes. Now try right-clicking in various places, some on a box and some not. You’ll see that the menu is only displayed when the click is on a box, as it should be. Now try selecting the two menu options and see the apparent z-order of the boxes change accordingly. Also note that, if you right-click on an area where two boxes overlap, it’s always the one in front that receives the command from the menu item.

OK, that’s the setup over. Now let’s get down to the business at hand: implementing drag-and-drop functionality for our boxes. First of all, let’s add a ToolStrip to our form and then add one button. That button will be used for switching our app from drawing mode to dragging mode. Set the Text of the button to “Click to Drag” and set its DisplayStyle to “Text”. Now, at this point you’ll want to right-click on your PictureBox and select “Bring to Front”, to prevent it filling the entire form and going behind the ToolStrip. Finally, double-click the button to create a handler for its Click event. We now want to add a flag that will indicate to the app whether it is in drawing mode or dragging mode and have that flag toggled when the tool bar button is clicked.

C#

private bool drawMode = true;
 
private void toolStripButton1_Click(object sender, EventArgs e)
{
    this.drawMode = !this.drawMode;
 
    if (this.drawMode)
    {
        this.toolStripButton1.Text = "Click to Drag";
    }
    else
    {
        this.toolStripButton1.Text = "Click to Draw";
    }
}

VB

Private drawMode As Boolean = True
 
Private Sub ToolStripButton1_Click(ByVal sender As Object, _
                                   ByVal e As EventArgs) Handles ToolStripButton1.Click
    Me.drawMode = Not Me.drawMode
 
    If Me.drawMode Then
        Me.ToolStripButton1.Text = "Click to Drag"
    Else
        Me.ToolStripButton1.Text = "Click to Draw"
    End If
End Sub

Note that the flag is true by default, indicating that we are in draw mode to begin with. The text displayed on the button indicates what the next click will do, which is why we set the Text to “Click to Drag” in the designer.

At the moment, all the PictureBox’s event handlers assume that we are drawing a new box if the left mouse button is depressed. We’re going to have to go through all of them and change that behaviour so that the code will only draw a new box if we are in draw mode. In drag mode something new will happen.

Let’s start with the Paint event handler. At the moment the code is drawing all the boxes in the list and then, if the left mouse button is down, it draws the new box. Now, we will only want to draw the new box if the left mouse button is down AND we are in draw mode.

C#

private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
    Graphics g = e.Graphics;
 
    foreach (Rectangle box in this.boxes)
    {
        g.FillRectangle(Brushes.White, box);
        g.DrawRectangle(Pens.Black, box);
    }
 
    if (this.drawMode && Control.MouseButtons == MouseButtons.Left)
    {
        Rectangle box = this.GetRectangle(this.startPoint, this.endPoint);
 
        g.FillRectangle(Brushes.White, box);
        g.DrawRectangle(Pens.Black, box);
    }
}

VB

Private Sub PictureBox1_Paint(ByVal sender As Object, _
                              ByVal e As PaintEventArgs) Handles PictureBox1.Paint
    With e.Graphics
        For Each box As Rectangle In Me.boxes
            .FillRectangle(Brushes.White, box)
            .DrawRectangle(Pens.Black, box)
        Next
 
        If Me.drawMode AndAlso _
           Control.MouseButtons = Windows.Forms.MouseButtons.Left Then
            Dim box As Rectangle = Me.GetRectangle(Me.startPoint, Me.endPoint)
 
            .FillRectangle(Brushes.White, box)
            .DrawRectangle(Pens.Black, box)
        End If
    End With
End Sub

Next, let’s look at the MouseDown event. At the moment the code will store the drawing start point and the current drawing end point. If we’re dragging we are still going to want to remember the start point because we need to know where we’re dragging from, but the end point isn’t really relevant. What will need to remember though is which box, if any, we are dragging.

C#

private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
    if (Control.MouseButtons == MouseButtons.Left)
    {
        Point location = e.Location;
 
        this.startPoint = location;
 
        if (this.drawMode)
        {
            this.endPoint = location;
        }
        else
        {
            this.selectedBoxIndex = this.GetRectangleIndexAtPoint(location);
        }
    }
}

VB

Private Sub PictureBox1_MouseDown(ByVal sender As Object, _
                                  ByVal e As MouseEventArgs) Handles PictureBox1.MouseDown
    If Control.MouseButtons = Windows.Forms.MouseButtons.Left Then
        Dim location As Point = e.Location
 
        Me.startPoint = location
 
        If Me.drawMode Then
            Me.endPoint = location
        Else
            Me.selectedBoxIndex = Me.GetRectangleIndexAtPoint(location)
        End If
    End If
End Sub

When the mouse moves, we need to determine whether we’re dragging a box and, if we are, how far we’ve dragged it. We then need to move the box that distance and set the current location as the new starting point for the next drag. Because the box will have moved we need to tell the PictureBox to repaint. We’ll need to invalidate the area the box occupied before the move and the area it occupies afterwards.

C#

private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
    if (Control.MouseButtons == MouseButtons.Left)
    {
        if (this.drawMode)
        {
            this.InvalidateRectangle(this.GetRectangle(this.startPoint,
                                                       this.endPoint));
            this.endPoint = e.Location;
            this.InvalidateRectangle(this.GetRectangle(this.startPoint,
                                                       this.endPoint));
            this.pictureBox1.Update();
        }
        else if (this.selectedBoxIndex != -1)
        {
            Rectangle box = this.boxes[this.selectedBoxIndex];
            Point location = e.Location;
 
            this.InvalidateRectangle(box);
 
            box.Offset(location.X - this.startPoint.X,
                       location.Y - this.startPoint.Y);
            this.boxes[this.selectedBoxIndex] = box;
            this.startPoint = location;
 
            this.InvalidateRectangle(box);
            this.pictureBox1.Update();
        }
    }
}

VB

Private Sub PictureBox1_MouseMove(ByVal sender As Object, _
                                  ByVal e As MouseEventArgs) Handles PictureBox1.MouseMove
    If Control.MouseButtons = Windows.Forms.MouseButtons.Left Then
        If Me.drawMode Then
            Me.InvalidateRectangle(Me.GetRectangle(Me.startPoint, Me.endPoint))
            Me.endPoint = e.Location
            Me.InvalidateRectangle(Me.GetRectangle(Me.startPoint, Me.endPoint))
            Me.PictureBox1.Update()
        ElseIf Me.selectedBoxIndex <> -1 Then
            Dim box As Rectangle = Me.boxes(Me.selectedBoxIndex)
            Dim location As Point = e.Location
 
            Me.InvalidateRectangle(box)
 
            box.Offset(location.X - Me.startPoint.X, _
                       location.Y - Me.startPoint.Y)
            Me.boxes(Me.selectedBoxIndex) = box
            Me.startPoint = location
 
            Me.InvalidateRectangle(box)
            Me.PictureBox1.Update()
        End If
    End If
End Sub

Notice that, after offsetting the box the distance that the mouse pointer has moved, the box is assigned box to the List item. That’s because, again, Rectangle is a value type so the code retrieves and edits a copy of the Rectangle in the list. After making changes we must overwrite the original with that copy so the changes are persisted.

Nothing needs to happen on the MouseUp event when dragging so the only change we need to make to that handler is a check for draw mode.

C#

private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
    if (this.drawMode &&
        e.Button == MouseButtons.Left &&
        Control.MouseButtons == MouseButtons.None)
    {
        this.endPoint = e.Location;
 
        Rectangle box = this.GetRectangle(this.startPoint, this.endPoint);
 
        this.boxes.Add(box);
        this.InvalidateRectangle(box);
        this.pictureBox1.Update();
    }
}

VB

Private Sub PictureBox1_MouseUp(ByVal sender As Object, _
                            ByVal e As MouseEventArgs) Handles PictureBox1.MouseUp
    If Me.drawMode AndAlso _
       e.Button = Windows.Forms.MouseButtons.Left AndAlso _
       Control.MouseButtons = Windows.Forms.MouseButtons.None Then
        Me.endPoint = e.Location
 
        Dim box As Rectangle = Me.GetRectangle(Me.startPoint, Me.endPoint)
 
        Me.boxes.Add(box)
        Me.InvalidateRectangle(box)
        Me.PictureBox1.Update()
    End If
End Sub

Believe it or not, that’s it! Run the project and draw a few boxes, as you did before, and then click the button on the tool bar to switch to drag mode. Try clicking and dragging in some empty space and notice that no new box gets drawn. Now try clicking and dragging on a box and notice that the box follows the mouse pointer. You should also note that the z-order is always maintained during drag operations. You can still use the context menu to change the z-order while in drag mode so you might like to play around with that to confirm that it works with dragging too. If you want to draw a few more boxes then just click the tool button again to switch back to draw mode.

So, that’s all there is to it. There may not be any actual objects on-screen to manipulate directly, but there are always objects. With GDI+ we must have data stored somewhere that represents the drawing in some way, so we can simply manipulate those objects appropriately and force the UI to repaint to see the new drawing. We just need to do a little bit more work to calculate where we are and what we’re moving. We’re developers though: we love that stuff!

8 comments:

Anonymous said...

i without a doubt adore your own posting kind, very helpful,
don't quit as well as keep creating mainly because it just nicely to follow it,
impatient to looked over far more of your content articles, goodbye!

Anonymous said...

Very Very Useful Article.

I am new to Graphics and trying lot to google it that topic.

You are good.

DennisP said...

I am trying to follow your excellent article by creating the application in VS
C#, but have run into a problem with the "Bring to Front"/"Send to Back" menu Items click events not working. I have put in breakpoints in the code but ttey are not invoked when I click on the menus. Can you suggest what may be wrong ??

jmcilhinney said...

@DennisP, when you add the VB code to a form, the event handlers are automatically attached to the appropriate events courtesy of the Handles clauses. With the C# code, you need to wire up the event handlers yourself. You can do that in code but more likely you'd do it in the designer. To do that, select the desired control or component in the designer, open the Properties window, click the Events button, select the appropriate event and then select the desired method in the adjacent drop-down.

Dj Frank said...

Thanks for your pedagogy ! Great document. My problem is to do the same (drag drop) with DRAWSTRING (Iwant to insert text on an image and to be able to drag drop)... Could you help me please ???

jmcilhinney said...

@Dj Frank, I'm glad that you found this useful. If you ant to do it with text then you would do it in pretty much exactly the same way. Firstly, you can't really do it if you're drawing directly on an Image rather than on a control because drawing on controls gets erased every repaint but drawing on Images is permanent. As for how to do it, when you call DrawString you can specify a Rectangle within which to draw the text. You can use that Rectangle in pretty much exactly the same way I have. You just need a List of some data structure that contains the Rectangle and the text and any other information you need to redraw the text, rather than just the Rectangle.

Dj Frank said...

Please, could you contact me throught email ? I wanna send you my code...franklinan*gmail.com.

Thanks for youPlease, could you contact me throught email ? I wanna send you my code...franklinan*gmail.com.

Thanks for your timer time

jmcilhinney said...

@Dj Frank, I don't really do direct contact. Feel free to create a thread on any of these forums and send me a Private Message on that site to direct me to your thread. That way you can potentially get help from lots of other people too.

http://www.vbforums.com
http://www.vbdotnetforums.com
http://www.csharpforums.net