Team LiB
Previous Section Next Section

The Mouse, GDI+, and You

OK, so now you know the basics of the keyboard and the mouse. As you saw from the last example, using graphics can make for a more visually descriptive data entry program. I can see this kind of ListBox/images screen in a hospital-supply program. The list could be the complicated Latin names of surgical instruments and the images could be pictures of the instruments. The images would be a verification of what the user thinks the word means. The Panel could represent the set of instruments a surgeon would need in an operating room. You could also use something like this in an online-store checkout screen.

Anyway, moving on to the GDI+ enhancement: marquis selection. The purpose of this enhancement is to allow the user to drag a rectangle around some of the flags shown in the Panel, thereby selecting them. This is a common enough thing to do in data entry programs that you should learn how to do it.

The method that you will use to draw a rectangle is (funnily enough) Graphics.DrawRectangle. There is one caveat with this method, though: The Rectangle data type argument needs its starting point to be at the top left of the rectangle. It also needs its ending point to be below and to the right of the starting point. Consider the following rectangles as defined in C#:

      Rectangle r1 = new Rectangle(100, 100, 100, 50);
      Rectangle r2 = new Rectangle(100, 100, -100, 50);
      Rectangle r3 = new Rectangle(100, 100, 100, -50);
      Rectangle r4 = new Rectangle(100, 100, -100, -50);

Each of these rectangles is perfectly legal; however, only the first one is valid for the DrawRectangle method. When the user drags the mouse, there is no guarantee that she will start at the top left and end at the bottom right. In fact, all things being equal, there is only a 25% chance of this happening. What you need to do as a programmer is convert the bottom three rectangles to the proper form so that all four values are positive. This is not so easy.

The best way I have found to accomplish this is to create a static class with a method that takes a rectangle and returns a corrected one. Now this class is quite small, as it has no constructor and the single method is short. However, I made it a class because I can see adding a few other static methods to it to handle such things as adding a method to return a rounded version of a sharp-cornered rectangle. Of course, in this case, the return data type would be a region, not a rectangle.

Add a new class to your MouseTrap project and call it "RectangleC." You will need to include the System.Drawing namespace at the top of this class. This class has no constructor and only one static (shared in VB) method. Listings 4-3a and 4-3b show the code for the whole class.

Listing 4-3a: C# Code for the Rectangle Converter Class
Start example

using System;
using System.Drawing;

namespace MouseTrap_c
{
  /// <summary>
  /// Converts a rectangle that starts at any corner into one that can be drawn by
  /// the Graphics object.
  /// </summary>
  public class RectangleC
  {
    public static Rectangle Convert(Rectangle rect)
    {
      rect.X = rect.X - (Math.Abs(rect.Width) - rect.Width)/2;
      rect.Y = rect.Y - (Math.Abs(rect.Height) - rect.Height)/2;
      rect.Size = new Size(Math.Abs(rect.Width), Math.Abs(rect.Height));

      return rect;
    }
  }
}
End example
Listing 4-3b: VB Code for the Rectangle Converter Class
Start example
Option Strict On

Imports System.Drawing

Public Class RectangleC

  Public Shared Function Convert(ByVal rect As Rectangle) As Rectangle

    rect.X = rect.X - CInt((Math.Abs(rect.Width) - rect.Width) / 2)
    rect.Y = rect.Y - CInt((Math.Abs(rect.Height) - rect.Height) / 2)
    rect.Size = New Size(Math.Abs(rect.Width), Math.Abs(rect.Height))

    Return rect
  End Function
End Class
End example

Not much here, is there? When I first made this class I had all kinds of math in here. With some patience and advice from friends, I managed to boil it down to what you see here.

Unless you have worked with the GDI+ namespaces before, this may be your first introduction to the Point and Size structures. They are extremely useful and necessary in just about all GDI+ code. It is beyond the scope of this book to fully explain the Point and Size structures and their uses, so I encourage you to look in the online help for more information.

Compile your project and correct any errors you may have. You need this class to compile cleanly before you go any further.

Add the following variable to your class local variables section in the main form.

C#

    //This is added for marquis selection of flags in the Panel
    Rectangle Marquis = Rectangle.Empty;

VB

  'This is added for marquis selection of flags in the Panel
  Dim Marquis As Rectangle = Rectangle.Empty

I use the static Empty method to create a rectangle of no size. When the Paint delegate sees this, it skips past trying to render this rectangle and choose flags.

Speaking of Paint delegates, you will need to create one for the Panel. Because this is a small example, I use a delegate for the Paint event rather than override it.

Include the following method in your main form's code.

C#

    private void PanelPaint(object sender, PaintEventArgs e)
    {
      Rectangle r = RectangleC.Convert(Marquis);

      if(Marquis != Rectangle.Empty)
      {
        e.Graphics.DrawRectangle(Pens.Red, r);
        foreach(PictureBox P in P1.Controls)
        {
          if(r.Contains(P.Bounds))
            P.BorderStyle = BorderStyle.FixedSingle;
          else
            P.BorderStyle = BorderStyle.None;
        }
      }
    }

VB

  Private Sub PanelPaint(ByVal sender As Object, ByVal e As PaintEventArgs)
    Dim P As PictureBox
    Dim r As Rectangle = RectangleC.Convert(Marquis)

    If Not Marquis.Equals(Rectangle.Empty) Then
      e.Graphics.DrawRectangle(Pens.Red, r)
      For Each P In P1.Controls
        If r.Contains(P.Bounds) Then
          P.BorderStyle = BorderStyle.FixedSingle
        Else
          P.BorderStyle = BorderStyle.None
        End If
      Next
    End If
  End Sub

The first thing I do here is to create a rectangle that has the correct starting and ending points. I use the new static RectangleC class to do this.

If the rectangle is not empty, I draw it on the Panel. I then iterate through each of the PictureBoxes in the Panel, looking for those that are completely contained in the bounding rectangle. If I find any, I mark them for deletion. If there are any outside this bounding rectangle, they are unmarked.

You need to know that GDI+ erases the drawing surface before it draws the rectangle. This is one reason that GDI+ is so much easier to use than the direct Windows GDI. You have to do far less to accomplish much more.

Well, you now have a Paint delegate. The trick now is to create the bounding rectangle and make sure this Paint delegate gets called when needed. Add the following three delegates below the Paint delegate.

C#

    private void PanelMouseDown(object sender, MouseEventArgs e)
    {
      if (e.Button != MouseButtons.Left)
        return;

      Marquis = new Rectangle(new Point(e.X, e.Y), Size.Empty);
    }
    private void PanelMouseMove(object sender, MouseEventArgs e)
    {
      if (e.Button != MouseButtons.Left)
        return;

      Marquis.Size = new Size(e.X-Marquis.X, e.Y-Marquis.Y);
      P1.Invalidate();
    }

    private void PanelMouseUp(object sender, MouseEventArgs e)
    {
      Marquis = Rectangle.Empty;
      P1.Invalidate();
    }

VB

  Private Sub PanelMouseDown(ByVal sender As Object, ByVal e As MouseEventArgs)

    If e.Button <> MouseButtons.Left Then Return
    Marquis = New Rectangle(New Point(e.X, e.Y), Size.Empty)
  End Sub

  Private Sub PanelMouseMove(ByVal sender As Object, ByVal e As MouseEventArgs)

    If e.Button <> MouseButtons.Left Then Return

    Marquis.Size = New Size(e.X - Marquis.X, e.Y - Marquis.Y)
    P1.Invalidate()
  End Sub

  Private Sub PanelMouseUp(ByVal sender As Object, ByVal e As MouseEventArgs)

    Marquis = Rectangle.Empty
    P1.Invalidate()
  End Sub

Notice that I make use of the MouseEventArgs argument to determine if I am currently painting. I can't tell you how many examples I've seen where the programmer used a global variable to determine if painting was going on.

The MouseDown delegate creates a new rectangle that begins where the user pressed the left mouse button. It has no size to start with.

The MouseMove delegate changes the size of the rectangle according to the current X and Y position of the mouse. There is no need for a variable to tell me if I am drawing; the fact that the left mouse button is down while I am moving it implies this. At the end of this method I invalidate the Panel. This fires the Paint event, which gets handled by the Paint delegate.

The MouseUp delegate simply resets the rectangle and invalidates the screen, which makes the rectangle disappear. All that is left are the flags, some of which may be marked for deletion.

Now to wire the mouse delegates up to the Panel. Add the following code to the constructor.

C#

      //These delegates are added to facilitate marquis selection
      P1.MouseMove += new MouseEventHandler(this.PanelMouseMove);
      P1.MouseDown += new MouseEventHandler(this.PanelMouseDown);
      P1.MouseUp += new MouseEventHandler(this.PanelMouseUp);
      P1.Paint += new PaintEventHandler(this.PanelPaint);

VB

    'These delegates are added to facilitate marquis selection
    AddHandler P1.MouseMove, New MouseEventHandler(AddressOf PanelMouseMove)
    AddHandler P1.MouseDown, New MouseEventHandler(AddressOf PanelMouseDown)
    AddHandler P1.MouseUp, New MouseEventHandler(AddressOf PanelMouseUp)
    AddHandler P1.MouseMove, New MouseEventHandler(AddressOf PanelMouseMove)
    AddHandler P1.Paint, New PaintEventHandler(AddressOf PanelPaint)

That's it. Compile and run the program. Figure 4-8 shows marquis selection in action.

Click To expand
Figure 4-8: Marking flags for deletion via marquis selection

Deleting the Flags

The other mouse-handling delegates for the images are simple enough. As the user left-clicks the picture, I change its border to make it stand out as being selected. When the mouse is run over the picture, I show the name of the flag on the StatusBar. This name comes from the Tag property of the PictureBox. This is also shown in Figure 4-7.

Figure 4-9 shows the screen with four of the flags marked for deletion.

Click To expand
Figure 4-9: Four flags marked for deletion

The user presses the Delete key and the marked flags are deleted. After the deletion, the rest of the flags are rearranged to fill the voids. Here is the C# code for this KeyDown delegate:

    private void DeleteFlags(object sender, KeyEventArgs e)
    {
      if(e.KeyCode == Keys.Delete)
      {

        //Try this shortcut. It will not work. Do you know why?
//        foreach(PictureBox p in P1.Controls)
//        {
//          if(p.BorderStyle == BorderStyle.FixedSingle)
//            P1.Controls.Remove(p);
//        }

        PictureBox p;
        bool deleted = true;
        while (deleted)
        {
          deleted = false;
          for(int k=0; k<P1.Controls.Count; k++)
          {
            if(P1.Controls[k] is PictureBox)
            {
              p = (PictureBox)P1.Controls[k];
              if(p.BorderStyle == BorderStyle.FixedSingle)
              {
                P1.Controls.RemoveAt(k);
                deleted = true;
                //Controls.count has changed. Reinitialize the "for" loop
                break;
              }
            }
          }
        }
        ArrangeImages();
      }
    }

I left in some code that I commented out. This code (if it worked) would be more elegant than the code below it. However, it does not work. Your homework is to read up on collections and find out why.

Tip 

Collections are a big part of the .NET Framework. It would behoove you to learn as much about them as you can.

The ability to remove an object from a collection while iterating over that collection would sure be nice. However, sometimes the brute-force way is best.


Team LiB
Previous Section Next Section