Showing posts with label programming. Show all posts
Showing posts with label programming. Show all posts

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!

Wednesday, August 12, 2009

Using Parameters in ADO.NET

As a general rule, parameters should always be used when inserting values into SQL statements. Many people, generally new developers, don't follow this rule because they either don't realise that parameters exist or they don't understand the issues that their use helps to avoid. When I was new to .NET programming, I fell into the former category. Not having read extensively on ADO.NET at the time, I didn't know that parameters could be used to insert field values into SQL code. I started out writing code like this:

C#

string sql = "INSERT INTO User (FirstName, LastName, DateOfBirth, ChildCount) " +
             "VALUES ('" + this.firstNameField.Text +
             "', '" + this.lastNameField.Text +
             "', '" + this.dateOfBirthPicker.Value.ToString("yyyy-MM-dd") +
             "', " + this.childrenSpinner.Value + ")";
SqlCommand myCommand = new SqlCommand(sql);

VB

Dim sql As String = "INSERT INTO User (FirstName, LastName, DateOfBirth, ChildCount) " & _
                    "VALUES ('" & Me.firstNameField.Text & _
                    "', '" & Me.lastNameField.Text & _
                    "', '" & Me.dateOfBirthPicker.Value.ToString("yyyy-MM-dd") & _
                    "', " & Me.childrenSpinner.Value & ")"
Dim myCommand As New SqlCommand(sql)

N.B. I will initially write all examples using the SqlClient ADO.NET provider for SQL Server and later I will discuss the adjustments required when using other providers and other data sources.

The previous example demonstrates various issues associated with this type of code but let’s start with the most obvious: it’s hard to read. With all the various literals and property values being concatenated it’s a bit hard to immediately pick out what’s what. As a result, it’s very easy to make mistakes in such code. It’s very easy to miss a single quote here or a comma there. Of course, it’s not too hard to actually view the end result to see if it is what you intended it to be. I’m constantly surprised, though, by the number of people who build such strings and still don’t actually look at their contents, even when an error occurs.

C#

string sql = "INSERT INTO User (FirstName, LastName, DateOfBirth, ChildCount) " +
             "VALUES ('" + this.firstNameField.Text +
             "', '" + this.lastNameField.Text +
             "', '" + this.dateOfBirthPicker.Value.ToString("yyyy-MM-dd") +
             "', " + this.childrenSpinner.Value + ")";
 
MessageBox.Show(sql);

VB

Dim sql As String = "INSERT INTO User (FirstName, LastName, DateOfBirth, ChildCount) " & _
                    "VALUES ('" & Me.firstNameField.Text & _
                    "', '" & Me.lastNameField.Text & _
                    "', '" & Me.dateOfBirthPicker.Value.ToString("yyyy-MM-dd") & _
                    "', " & Me.childrenSpinner.Value & ")"
 
MessageBox.Show(sql)

While there are other, possibly better ways, this is the first thing that should occur to a new developer: a simple message box to display the string that was constructed. This will make it much easier to see if you’ve simply missed some punctuation. This is a lesson that should be taken into all development: always view your data and don’t just assume that it is what it’s supposed to be. If an error occurs then you’ve done something wrong and invalid or incorrect data is always a likely candidate.

Now, I’ve always been a big fan of the String.Format method in preference to straight string concatenation when I need to join more than two or three substrings together. Building SQL statements was one of the first scenarios in which I put it to work. It makes such code easier to read and, therefore, less error-prone.

C#

string sql = string.Format("INSERT INTO User (FirstName, LastName, " +
                           "DateOfBirth, ChildCount) " +
                           "VALUES ('{0}', '{1}', '{2:yyyy-MM-dd}', {3})",
                           this.firstNameField.Text,
                           this.lastNameField.Text,
                           this.dateOfBirthPicker.Value,
                           this.childrenSpinner.Value);
SqlCommand myCommand = new SqlCommand(sql);

VB

Dim sql As String = String.Format("INSERT INTO User (FirstName, LastName, " & _
                                  "DateOfBirth, ChildCount) " & _
                                  "VALUES ('{0}', '{1}', '{2:yyyy-MM-dd}', {3})", _
                                  Me.firstNameField.Text, _
                                  Me.lastNameField.Text, _
                                  Me.dateOfBirthPicker.Value, _
                                  Me.childrenSpinner.Value)
Dim myCommand As New SqlCommand(sql)

As you can see, it’s much easier to tell where each single quote and comma is and, therefore, whether any are missing. It also makes formatting values such as dates a bit neater.

I thought that this was an improvement, and it was, but it brings me to the next issue: inserting dates and times into SQL code. As you can see in the last two examples, I’ve had to explicitly format the date of birth. That’s because you cannot simply rely on a date and/or time value to be formatted by the .NET Framework in a way that is valid for your database. Even if the format is valid you may end up using a different value to the one you expected because of regional settings. If your month and day values get transposed between your application and your database then you may end up saving incorrect data, which is even worse than your application failing with an error message. We can always do as I’ve done above and use a format that will always work. It’s not a big deal to find out how each database you use formats date literals and then explicitly create that format, but wouldn’t it be better to not have to worry about format at all? When you use parameters you don’t have to.

Before I provide a code example that does use parameters I will touch on another common issue: strings containing apostrophes. This is a very common problem which, again, can be diagnosed by actually viewing the string you build. Doing so should make it obvious that you end up with mismatched single quotes. The question is, how do you prevent the issue. To include a single quote in a text literal you escape it with another single quote. As string concatenation is creating a SQL statement containing literals, that’s what needs to be done:

C#

string sql = string.Format("INSERT INTO User (FirstName, LastName, " +
                           "DateOfBirth, ChildCount) " +
                           "VALUES ('{0}', '{1}', '{2:yyyy-MM-dd}', {3})",
                           this.firstNameField.Text.Replace("'", "''"),
                           this.lastNameField.Text.Replace("'", "''"),
                           this.dateOfBirthPicker.Value,
                           this.childrenSpinner.Value);
SqlCommand myCommand = new SqlCommand(sql);

VB

Dim sql As String = String.Format("INSERT INTO User (FirstName, LastName, " & _
                                  "DateOfBirth, ChildCount) " & _
                                  "VALUES ('{0}', '{1}', '{2:yyyy-MM-dd}', {3})", _
                                  Me.firstNameField.Text.Replace("'", "''"), _
                                  Me.lastNameField.Text.Replace("'", "''"), _
                                  Me.dateOfBirthPicker.Value, _
                                  Me.childrenSpinner.Value)
Dim myCommand As New SqlCommand(sql)

Now, that’s not a big deal but why do it if it’s not necessary? Also, what about other unusual characters like line breaks? The simple fact is that, if you use parameters, all these considerations go away. The values never become part of the literal string so there’s no need to escape single quotes. Values remain in binary form rather than being converted to strings, so there’s no need to worry about format. There’s no need to worry about what values need to be wrapped in single quotes or other delimiters and which don’t. None of that is a concern.

C#

string sql = "INSERT INTO User (FirstName, LastName, DateOfBirth, ChildCount) " +
             "VALUES (@FirstName, @LastName, @DateOfBirth, @ChildCount)";
SqlCommand myCommand = new SqlCommand(sql);
SqlParameterCollection parameters = myCommand.Parameters;
 
parameters.AddWithValue("@FirstName", this.firstNameField.Text);
parameters.AddWithValue("@LastName", this.lastNameField.Text);
parameters.AddWithValue("@DateOfBirth", this.dateOfBirthPicker.Value.Date);
parameters.AddWithValue("@ChildCount", Convert.ToInt32(this.childrenSpinner.Value));

VB

Dim sql As String = "INSERT INTO User (FirstName, LastName, DateOfBirth, ChildCount) " & _
                    "VALUES (@FirstName, @LastName, @DateOfBirth, @ChildCount)"
Dim myCommand As New SqlCommand(sql)
 
With myCommand.Parameters
   .AddWithValue("@FirstName", Me.firstNameField.Text)
   .AddWithValue("@LastName", Me.lastNameField.Text)
   .AddWithValue("@DateOfBirth", Me.dateOfBirthPicker.Value.Date)
   .AddWithValue("@ChildCount", CInt(Me.childrenSpinner.Value))
End With

The first point to note here is how much easier the SQL code is to read. There’s no need for any string concatenation (other than what I’ve used to fit the code within the width of this page), punctuation or formatting, so the opportunity for errors is greatly reduced. In this case (but not all), each value has a name that makes its purpose obvious. Those names match up with the names of the parameters that are added to the command, so it’s easy to trace exactly what’s happening.

Now, let’s examine how the parameters are added to the command. In cases where the SQL code will be executed only once the parameters will only ever have one value. In such cases it’s advisable to call the AddWithValue method as it’s the simplest way to add a parameter and set its value.

The SqlClient ADO.NET provider supports named parameters, so notice that the parameter names in the SQL code match the parameter names passed to AddWithValue. We’ll look at positional parameters later but, in this case, the names must match or you’ll either send the wrong data to the wrong place or an exception will be thrown, which are obviously both poor outcomes.

When calling AddWithValue, the data type of the parameter is inferred from the type of the specified value. That’s why, in the case of the parameter named @ChildCount, the Value property of the NumericUpDown control is converted to an Int32 before being assigned. If the Value property was used as is then the type of the parameter would be inferred from a Decimal instead of an Int32 and a data type mismatch would occur at the database.

Note also that, in the case of the parameter named @DateOfBirth, a DateTime is retrieved from the DateTimePicker’s Value property and then the Date property of that value is used. Because the code is not using formatting to create a date-only string, if we want only the date and not the time then we must explicitly assign only the date and not the time.

As mentioned previously, if the SQL code is being executed only once then the value of each parameter doesn’t need to change. In such cases it is easiest to specify the value when creating the parameter and let the data type be inferred. In cases where the SQL code will be executed multiple times with different parameter values each time we need to take a different approach. In such cases we need to specify the data type when creating the parameters, then set their Value properties each time we want to use new data.

C#

string sql = "INSERT INTO User (FirstName, LastName, DateOfBirth, ChildCount) " +
             "VALUES (@FirstName, @LastName, @DateOfBirth, @ChildCount)";
SqlCommand myCommand = new SqlCommand(sql, myConnection);
SqlParameterCollection parameters = myCommand.Parameters;
 
parameters.Add("@FirstName", SqlDbType.VarChar, 50);
parameters.Add("@LastName", SqlDbType.VarChar, 50);
parameters.Add("@DateOfBirth", SqlDbType.DateTime);
parameters.Add("@ChildCount", SqlDbType.Int);
 
foreach (ListItem item in myList)
{
    parameters["@FirstName"].Value = item.FirstName;
    parameters["@LastName"].Value = item.LastName;
    parameters["@DateOfBirth"].Value = item.DateOfBirth;
    parameters["@ChildCount"].Value = item.ChildCount;
 
    myCommand.ExecuteNonQuery();
}

VB

Dim sql As String = "INSERT INTO User (FirstName, LastName, DateOfBirth, ChildCount) " & _
                    "VALUES (@FirstName, @LastName, @DateOfBirth, @ChildCount)"
Dim myCommand As New SqlCommand(sql, myConnection)
 
With myCommand.Parameters
    .Add("@FirstName", SqlDbType.VarChar, 50)
    .Add("@LastName", SqlDbType.VarChar, 50)
    .Add("@DateOfBirth", SqlDbType.DateTime)
    .Add("@ChildCount", SqlDbType.Int)
 
    For Each item As ListItem In myList
        .Item("@FirstName").Value = item.FirstName
        .Item("@LastName").Value = item.LastName
        .Item("@DateOfBirth").Value = item.DateOfBirth
        .Item("@ChildCount").Value = item.ChildCount
 
        myCommand.ExecuteNonQuery()
    Next
End With

The parameters are created and added to the command once only and then their Value properties are set each time new values are needed. The data source-specific data types of the parameters are specified when they are created because they cannot be inferred. Finally, the size is also specified for those parameters whose type requires it. Scale and precision can also be set if required.

There is another way to set parameter values also. Using a DataAdapter you might save data directly from a DataTable. In that case you won’t be looping through the records and setting parameter values yourself. It still needs to be done but the DataAdapter will do it for you. In that case though, you need to tell it exactly where to get the value for each parameter. You do this by specifying the SourceColumn of each parameter, which is the name of the column from which the parameter’s Value will be taken for each DataRow.

C#

string sql = "INSERT INTO User (FirstName, LastName, DateOfBirth, ChildCount) " +
             "VALUES (@FirstName, @LastName, @DateOfBirth, @ChildCount)";
SqlCommand myCommand = new SqlCommand(sql);
SqlParameterCollection parameters = myCommand.Parameters;
 
parameters.Add("@FirstName", SqlDbType.VarChar, 50, "FirstName");
parameters.Add("@LastName", SqlDbType.VarChar, 50, "LastName");
parameters.Add("@DateOfBirth", SqlDbType.DateTime, 0, "DateOfBirth");
parameters.Add("@ChildCount", SqlDbType.Int, 0, "ChildCount");
 
SqlDataAdapter myAdapter = new SqlDataAdapter();
 
myAdapter.InsertCommand = myCommand;

VB

Dim sql As String = "INSERT INTO User (FirstName, LastName, DateOfBirth, ChildCount) " & _
                    "VALUES (@FirstName, @LastName, @DateOfBirth, @ChildCount)"
Dim myCommand As New SqlCommand(sql)
 
With myCommand.Parameters
   .Add("@FirstName", SqlDbType.VarChar, 50, "FirstName")
   .Add("@LastName", SqlDbType.VarChar, 50, "LastName")
   .Add("@DateOfBirth", SqlDbType.DateTime, 0, "DateOfBirth")
   .Add("@ChildCount", SqlDbType.Int, 0, "ChildCount")
End With
 
Dim myAdapter As New SqlDataAdapter
 
myAdapter.InsertCommand = myCommand

In this case the fourth parameter of the Add method is the parameter’s SourceColumn. Note that this is the name of a DataColumn in the DataTable that will be passed to the DataAdapter’s Update method, i.e. it is the name of the column the data is coming from, not the name of the column in the database the data is going to. In the majority of cases those two column names will be the same but certainly not in all.

It’s also important to note that, if we are passing a fourth argument to the Add method, we must be passing a third argument too. That third argument is the maximum size of the data. For variable-size data types, like VarChar, this value is important but for fixed-size data types, like Int, it is not. If you know the specific size of a fixed-size data type, e.g. 4 bytes for type Int, then you can specify that value but there’s really no need. All values of a fixed-size data type will be the same size so there’s no need to specify the maximum size. As such you can just pass zero for fixed-size data types.

Now, let’s look at how using parameters differs amongst ADO.NET providers and data sources. First up, let’s look using OleDb and an Access database. The code should look almost exactly the same, except using OleDb classes instead of SqlClient classes.

C#

string sql = "INSERT INTO User (FirstName, LastName, DateOfBirth, ChildCount) " +
             "VALUES (@FirstName, @LastName, @DateOfBirth, @ChildCount)";
OleDbCommand myCommand = new OleDbCommand(sql);
OleDbParameterCollection parameters = myCommand.Parameters;
 
parameters.AddWithValue("@FirstName", this.firstNameField.Text);
parameters.AddWithValue("@LastName", this.lastNameField.Text);
parameters.AddWithValue("@DateOfBirth", this.dateOfBirthPicker.Value.Date);
parameters.AddWithValue("@ChildCount", Convert.ToInt32(this.childrenSpinner.Value));

VB

Dim sql As String = "INSERT INTO User (FirstName, LastName, DateOfBirth, ChildCount) " & _
                    "VALUES (@FirstName, @LastName, @DateOfBirth, @ChildCount)"
Dim myCommand As New OleDbCommand(sql)
 
With myCommand.Parameters
    .AddWithValue("@FirstName", Me.firstNameField.Text)
    .AddWithValue("@LastName", Me.lastNameField.Text)
    .AddWithValue("@DateOfBirth", Me.dateOfBirthPicker.Value.Date)
    .AddWithValue("@ChildCount", CInt(Me.childrenSpinner.Value))
End With

It’s important to understand that there’s a subtle difference in the way the code behaves though. As I said earlier, SqlClient supports named parameters. That is to say that when you add each parameter to a SqlCommand its name is used as a key to identify it. When it comes time to insert the values into the SQL code, every instance of that name is replaced with the value of the parameter with that name. Even though the code in the preceding example uses the same parameter names in the SQL code and the same names when adding the parameters, those names are not actually related.

The Jet OLEDB provider uses the position of each parameter to decide what values to substitute into the SQL code. All the parameter names in the SQL code are numbered and all the parameters added to the OleDbCommand are numbered. Values are then substituted based on those positions, whether or not the names match. This leads to two important considerations when using OleDb and Access. Firstly, while it makes no difference what order the parameters are added to a SqlCommand, you must ensure that parameters are added to an OleDbCommand in the order they appear in the SQL code. Secondly, while the value of a single SqlParameter can be substituted into SQL code as many times as required, even if the same value is to be used more than once in an OleDbCommand, you must add a discrete parameter for each place it will be used.

Next, let’s look at using OleDb with a SQL Server database. This should rarely be done, given that SqlClient provides more features and better performance, but there may be occasions where it’s necessary. Like the Jet OLEDB provider, the SQL Server OLEDB provider supports only positional parameters. Unlike the Jet provider though, it also doesn’t support pseudo-names for the parameters in the SQL code. In this case you must use “?” symbols in the code to represent a parameter.

C#

string sql = "INSERT INTO User (FirstName, LastName, DateOfBirth, ChildCount) " +
             "VALUES (?, ?, ?, ?)";
OleDbCommand myCommand = new OleDbCommand(sql);
OleDbParameterCollection parameters = myCommand.Parameters;
 
parameters.AddWithValue("FirstName", this.firstNameField.Text);
parameters.AddWithValue("LastName", this.lastNameField.Text);
parameters.AddWithValue("DateOfBirth", this.dateOfBirthPicker.Value.Date);
parameters.AddWithValue("ChildCount", Convert.ToInt32(this.childrenSpinner.Value));

VB

Dim sql As String = "INSERT INTO User (FirstName, LastName, DateOfBirth, ChildCount) " & _
                    "VALUES (?, ?, ?, ?)"
Dim myCommand As New OleDbCommand(sql)
 
With myCommand.Parameters
   .AddWithValue("FirstName", Me.firstNameField.Text)
   .AddWithValue("LastName", Me.lastNameField.Text)
   .AddWithValue("DateOfBirth", Me.dateOfBirthPicker.Value.Date)
   .AddWithValue("ChildCount", CInt(Me.childrenSpinner.Value))
End With

The Jet OLEDB provider also supports this notation, as do many others.

There are various other ADO.NET providers that support named parameters however. One such example is the Connector/Net provider available for MySQL. It supports a similar notation to the SqlClient provider except that parameter names must be prefixed with “?” instead of “@”.

C#

string sql = "INSERT INTO User (FirstName, LastName, DateOfBirth, ChildCount) " +
             "VALUES (?FirstName, ?LastName, ?DateOfBirth, ?ChildCount)";
MySqlCommand myCommand = new MySqlCommand(sql);
MySqlParameterCollection parameters = myCommand.Parameters;
 
parameters.AddWithValue("?FirstName", this.firstNameField.Text);
parameters.AddWithValue("?LastName", this.lastNameField.Text);
parameters.AddWithValue("?DateOfBirth", this.dateOfBirthPicker.Value.Date);
parameters.AddWithValue("?ChildCount", Convert.ToInt32(this.childrenSpinner.Value));

VB

Dim sql As String = "INSERT INTO User (FirstName, LastName, DateOfBirth, ChildCount) " & _
                    "VALUES (?FirstName, ?LastName, ?DateOfBirth, ?ChildCount)"
Dim myCommand As New MySqlCommand(sql)
 
With myCommand.Parameters
   .AddWithValue("?FirstName", Me.firstNameField.Text)
   .AddWithValue("?LastName", Me.lastNameField.Text)
   .AddWithValue("?DateOfBirth", Me.dateOfBirthPicker.Value.Date)
   .AddWithValue("?ChildCount", CInt(Me.childrenSpinner.Value))
End With

You should check the documentation for other providers and data sources to see what notation they support.

I should also point out that the use of parameters is no different when using stored procedures. Whether your CommandType is Text or StoredProcedure, if your SQL code contains parameters then you add them to your command in exactly the same way.

Finally, let’s look at one of the most important reasons to use parameters in the first place: security. When you use string concatenation to build SQL statements you can insert anything into the code at any position. You may intend to insert just a single value but, if the data is provided by the user, it’s often difficult or impossible to ensure that that’s all you’re getting. This can allow malicious users to perform what’s called “SQL injection”, where they inject a chunk of SQL code into your statement where you intended to use only a value. For example, let’s consider a situation where you want to allow the user to search for an employee record by payroll number:

C#

string sql = "SELECT * " +
             "FROM Employee " +
             "WHERE PayrollNumber = '" +
             this.payrollNumberField.Text +
             "'";

VB

Dim sql As String = "SELECT * " & _
                    "FROM Employee " & _
                    "WHERE PayrollNumber = '" & _
                    Me.payrollNumberField.Text & _
                    "'"

In that case you expect the user to enter a payroll number and the system will find the matching record if such a record exists. For instance, if the user enters the value “12345” then the SQL statement that will eventually be executed will be:

SELECT * FROM Employee WHERE PayrollNumber = ‘12345’

That’s exactly as you intended, but what happens if a malicious user enters the value “12345’; DELETE FROM Employee; SELECT * FROM Employee WHERE PayrollNumber = ‘12345”? The actual SQL code that eventually gets executed will be:

SELECT * FROM Employee WHERE PayrollNumber = ‘12345’
DELETE FROM Employee
SELECT * FROM Employee WHERE PayrollNumber = ‘12345’

Congratulations, you just deleted the entire contents of your Employee table. By using a parameter to insert the payroll number you ensure that whatever the user enters gets treated as a value and cannot be interpreted as SQL code.

There will be situations where SQL injection will not be possible but you should still use parameters all the time. Doing so helps you to avoid various issues and is just a good habit to get into. If you always use parameters then you never have to decide whether you should or not and you can never make the wrong choice.