共用方式為


User Interface

This chapter is excerpted from Programming Visual Basic 2008: Build .NET 3.5 Applications with Microsoft's RAD Tool for Business by Tim Patrick, published by O'Reilly Media

Programming Visual Basic 2008

Logo

Buy Now

A picture is worth a thousand words-or several thousand lines of source code, if you're generating a bitmap image of it. Writing code to manipulate images of varying color depths, or to trace out multilayer vector art, can be a nightmare of geometric contortions and linear algebra. It makes one yearn for those days of prescreen computers. The first programming class I took used a DECWriter, a printer-based terminal that had no screen, and included the graphics capabilities of a jellyfish. It was perfect for me. I couldn't draw a straight line anyway, and I didn't need some fancy schmancy "video display terminal" reminding me of it.

The graphics included in early display systems weren't much better. "Dumb terminals," such as the popular VT100, included some simple character graphics that displayed basic lines and blocks. Each graphic part was exactly one character in size, and any images you sought to display had to fit in a clunky 80 × 24 grid.

Fortunately for art aficionados everywhere, computers have come a long way in the graphics department. GDI+, the standard .NET drawing system, includes complex drawing features that would make a DECWriter cry. Built upon the older Windows "Graphics Device Interface" (GDI) technology, GDI+ includes commands for drawing lines, text, and images in the Picasso-enhanced world of 2D graphics.

Beyond GDI+, .NET also provides support for the newer Windows Presentation Foundation (WPF), a rich user interface and multimedia presentation system based in part on XML. WPF includes display and interaction features that go way beyond GDI+, although there are a few GDI+ features absent from WPF. Although I will give a brief overview of WPF in this chapter, most of the chapter (and all of the Library Project's user interface code) will focus on GDI+.

Overview of GDI+

Before .NET, Windows programmers depended on the GDI system to draw pretty much anything on the screen, even if they didn't know that GDI existed. In addition to bitmap images, all controls, labels, window borders, and icons appeared on the screen thanks to GDI. It was a giant step forward from character graphics. GDI presented a basic set of drawing features from which you could potentially output any type of complex image. But it wasn't easy. The graphics primitives were-well-primitive, and you had to build up complex systems from the parts. Most programmers weren't into making things beautiful, so they tried to avoid the complexities of GDI. But sometimes you had to draw a line or a circle, and there was no way around it.

GDI+, new with .NET, builds on GDI, providing the basic primitives of GDI, but also supplying some more complex groupings of graphics features into easy-to-use functions. This simplicity has brought about a renaissance of programmer-initiated graphics work. Take a look at Figure 18.1, "The marvel that is GDI+", which shows an image that was drawn using the older GDI, and that same image generated with just a few quick commands in GDI+.

Figure 18.1. The marvel that is GDI+

The marvel that is GDI+

The GDI+ system makes its home in the System.Drawing namespace, and includes multitudes of classes that represent the drawing objects, surfaces, and embellishment features that enable display graphics. But it's not just about display. GDI+ generalizes bitmap and vector drawing on all available output surfaces: bitmaps or line drawings on the screen (including form and control surfaces), report output on a printer, graffiti on the back wall of your local supermarket, image content destined for a JPEG file-they are all the same to GDI+. All destinations use the same drawing methods and objects, making it easier for you to generalize your drawing code.

GDI+'s features include surfaces, drawing inks, drawing elements, and transformations.

  • GDI+ generalizes drawing surfaces through the System.Drawing.Graphics class. This object represents a drawing canvas, with attributes for color depth and size (width and height). The canvas may link to a region of the workstation screen, an internal holding area for final output to the printer, or a general graphics canvas for manipulating content in-memory before outputting it to a display or a file. Another type of surface, the path (System.Drawing.Drawing2D.GraphicsPath), is like a macro recorder for vector (line) graphics. Drawing done within a path can be "replayed" on a standard drawing surface, or used to supply boundaries for other drawing commands.

  • Colors and inks appear in the form of colors (opaque or semitransparent color values), brushes (bitmap-based pseudo-pens used for fills and tiling), and pens (colored line-drawing objects with a specific thickness).

  • Drawing elements include rectangles, ellipses, lines, and other standard or custom-edge shapes. They also include fonts, both bitmapped and outline-based versions.

  • Transformations let you resize, rotate, and skew drawings as you generate them. When a transformation is applied to a surface, you can draw objects as though there were no transformation applied, and the changes will happen in real time.

The Windows Forms controls that you use in desktop applications generally take care of their own display features. However, some controls let you take over some or all of the drawing responsibilities. For instance, the ListBox control displays simple single-color text for each list item. However, you can override the drawing of each list item, providing your own custom content, which may include multicolor text or graphics. This ability to supply some of the drawing code to a control is known as owner draw, and it works through the same generalized Graphics object used for other drawing. We'll include some owner draw code in the Library Project.

In the interest of full disclosure, you should know that this chapter will cover probably only 1% of the available GDI+ features, if even that. GDI+ is complex and vast, and you could spend years delving into every little feature, just in time for your eventual switch over to WPF. I'll give you a brief overview of the GDI+ system so that you get a feel for some of the basics. If you need to manipulate images and text beyond what is listed here (and you probably will), try the MSDN documentation or another resource dedicated to deciphering GDI+.

Selecting a Canvas

Most drawing in .NET occurs in the context of a Graphics object. (For those familiar with pre-.NET development in Windows, this is similar to a device context.) Graphics objects provide a canvas on which you draw lines, shapes, bitmap images, and prerecorded drawing macros. Graphics object do not contain the graphics surface itself; they are simply generic conduits to the actual canvas. There is always some surface behind the Graphics object, whether it is a portion of the screen, a Bitmap object, or the simulated surface of a printed page. Any drawing that is done to the Graphics object immediately impacts the underlying surface.

The Graphics object includes dozens of methods that let you draw shapes and images on the graphics surface, and perform other magical 2D activities. We'll cover many of them in this chapter.

Obtaining and Creating Graphics Objects

Getting a Graphics object for an on-screen form or control is as easy as calling the form's or control's CreateGraphics method.

Dim wholeFormGraphics As Graphics = _
   Me.CreateGraphics(  )
Dim buttonOnlyGraphics As Graphics = _
   Button1.CreateGraphics(  )

Some events, most notably the Paint event for forms and controls, provide access to a Graphics object through the event arguments.

Private Sub PictureBox1_Paint(ByVal sender As Object, _
      ByVal e As System.Windows.Forms.PaintEventArgs) _
      Handles PictureBox1.Paint
   Dim paintCanvas As Graphics = e.Graphics
End Sub

You can also create a Graphics object that is unrelated to any existing display area by associating it to a bitmap.

Dim trueBitmap As New Bitmap(50, 50)
Dim canvas = Graphics.FromImage(trueBitmap)

Remember, all changes made to the canvas instance will impact the trueBitmap image.

Disposing of Graphics Objects Properly

When you are finished with a Graphics object that you create, you must dispose of it by calling its Dispose method. (This rule is true for many different GDI+ objects.) Don't keep it around for a rainy day because it won't be valid later. You must, must, must dispose of it when you are finished with it. If you don't, it could result in image corruption, memory usage issues, or worse yet, international armed conflict. So, please dispose of all Graphics objects properly.

canvas.Dispose(  )

If you create a Graphics object within an event, you really need to dispose of it before exiting that event handler. There is no guarantee that the Graphics object will still be valid in a later event. Besides, it's easy to re-create another Graphics object at any time.

If you use a Graphics object that is passed to you from another part of the program (like that e.Graphics reference in the preceding Paint event handler), you should not dispose of it. Each creator is responsible for disposing of its own objects.

Choosing Pens and Brushes

A lot of graphics work involves drawing primitives: using lines, ellipses, rectangles, and other regular and irregular shapes to build up a final display. As in real life, you draw these primitives using a Pen object. For those primitives that result in a fillable or semifillable shape, a Brush object specifies the color or pattern to use in that filled area. GDI+ includes many predefined pens and brushes, or you can create your own.

Pens

Pens are line-drawing tools used with the drawing commands of a Graphics object. A basic pen has a solid color and a thickness.

' ----- A red pen five units wide.
Dim redPen As New Pen(Color.Red, 5)

As with Graphics objects, any Pen you create using the New keyword must be disposed of properly when you are finished with it.

redPen.Dispose(  )

Several predefined pens are made available through the System.Drawing.Pens class, all named by their color, as in Pens.Red. If you use one of these pens, you don't have to dispose of it.

You can create a lot of interesting pens that vary by line styles, end decorations, and color variations. The following code generates the image displayed in Figure 18.2, "Yes sir, yes sir, three lines full":

Private Sub PictureBox1_Paint(ByVal sender As Object, _
      ByVal e As System.Windows.Forms.PaintEventArgs) _
      Handles PictureBox1.Paint
   ' ----- Draw some fancy lines.
   Dim usePen As Pen

   ' ----- Blank out the background.
   e.Graphics.Clear(Color.White)

   ' ----- Draw a basic 1-pixel line using the title
   '       bar color.
   usePen = New Pen(SystemColors.ActiveCaption, 1)
   e.Graphics.DrawLine(usePen, 10, 10, 200, 10)
   usePen.Dispose(  )

   ' ----- Draw a thicker dashed line with arrow and ball
   '       end caps. Each dashed segment has a triangle end.
   usePen = New Pen(Color.FromName("Red"), 5)
   usePen.DashCap = Drawing2D.DashCap.Triangle
   usePen.StartCap = Drawing2D.LineCap.ArrowAnchor
   usePen.EndCap = Drawing2D.LineCap.RoundAnchor
   usePen.DashStyle = Drawing2D.DashStyle.Dash
   e.Graphics.DrawLine(usePen, 10, 30, 200, 30)
   usePen.Dispose(  )

   ' ----- A semitransparent black pen with three line
   '       parts, two thin and one thick.
   usePen = New Pen(Color.FromArgb(128, 0, 0, 0), 10)
   usePen.CompoundArray = _
      New Single(  ) {0.0, 0.1, 0.4, 0.5, 0.8, 1.0}
   e.Graphics.DrawLine(usePen, 10, 55, 200, 55)
   usePen.Dispose(  )
End Sub

Figure 18.2. Yes sir, yes sir, three lines full

Yes sir, yes sir, three lines full

The code shows that there are a few different ways to specify a color, by either its predefined name (Color.White and SystemColors.ActiveCaption), a string name (using Color.FromName), or its Alpha-Red-Green-Blue value (Color.FromArgb). That last version lets you supply distinct values for the "alpha blend" (which sets the transparency level, from 0 for fully transparent to 255 for fully opaque), red, green, and blue components of the full color.

Most of the pen-specific properties I demonstrated here are somewhat self-explanatory. As with most of GDI+, the mind-numbing amount of available features makes it impossible to completely document in a small chapter, let alone provide a good night's sleep for authors designing such chapters. I will simply refer you to the online documentation for the Pen class to get all of the luscious details.

Brushes

Brushes are used for filling in spaces between drawn lines, even if you make those lines fully invisible. GDI+ includes a variety of brush types, including solid brushes (your basic single-color brush), hatch brushes (pattern brushes that are pleasant but general), texture brushes (where a custom bitmap is used for the brush), and gradient brushes (which slowly fade from one color to another across the brush). The System.Drawing.Brushes class includes some predefined solid brushes based on color name. As with pens, you must dispose of brushes that you create, but not the solid system-defined brushes.

The following block of code draws some simple rectangles with a variety of brush styles. The results appear in Figure 18.3, "Kind of square, if you ask me".

Private Sub PictureBox1_Paint(ByVal sender As Object, _
      ByVal e As System.Windows.Forms.PaintEventArgs) _
      Handles PictureBox1.Paint
   ' ----- Draw some fancy rectangles.
   Dim useBrush As Brush

   e.Graphics.Clear(Color.White)

   ' ---- Draw a filled rectangle with a solid color.
   e.Graphics.FillRectangle(Brushes.Cyan, 10, 10, 150, 50)

   ' ----- Draw a hatched rectangle. Use black for the
   '       background, and white for the pattern foreground.
   useBrush = New Drawing2D.HatchBrush( _
      Drawing2D.HatchStyle.LargeConfetti, _
      Color.White, Color.Black)
   e.Graphics.FillRectangle(useBrush, 10, 70, 150, 50)
   useBrush.Dispose(  )

   ' ----- Draw a left-to-right linear gradient rectangle.
   '       The gradient's own rectangle determines the
   '       starting offset, based on the Graphics surface
   '       origin.
   useBrush = New Drawing2D.LinearGradientBrush( _
      New Rectangle(200, 10, 75, 25), Color.Blue, _
      Color.Yellow, Drawing2D.LinearGradientMode.Horizontal)
   e.Graphics.FillRectangle(useBrush, 200, 10, 150, 50)
   useBrush.Dispose(  )

   ' ----- Use an image for the brush. I'm using the
   '       "LookupItem.bmp" graphic used in the Library
   '       Project.
   useBrush = New TextureBrush(Image.FromFile( _
      "LookupItem.bmp"))
   e.Graphics.FillRectangle(useBrush, 200, 70, 150, 50)
   useBrush.Dispose(  )
End Sub

Figure 18.3. Kind of square, if you ask me

Kind of square, if you ask me

Flowing Text from the Font

Circles and squares are OK, but they don't always communicate much, unless you are Joan Miró. Most of us depend on text to say what we mean. Fortunately, GDI+ has features galore that place text on your graphics surface.

Before graphical user interfaces were all the rage, text wasn't really an issue; either you used the characters built into the system, or you used nothing. On the screen, each letter of the alphabet was designed into the hardware of the computer or monitor, and any particular character could appear only within each square of the predefined 80 × 24 grid. Printers were a little better, since you could backspace and retype over previously typed positions to generate either bold or underscore text. Still, you were generally limited to one font, or just a small handful of basic fonts embedded in the printer's memory.

Such limitations are a thing of the past. All text in Microsoft Windows appears courtesy of fonts, descriptions of character shapes that can be resized or stretched or emphasized to meet any text need. And because the user can add fonts to the system at any time, and from any third-party source, the variety of these fonts is amazing. But you already know all this. Let's get on to the code.

To gain access to a font for use in your graphics, create an instance of the System.Drawing.Font class, passing it at least the font name and point size, and an optional style reference:

Dim basicFont As New Font("Arial", 14, FontStyle.Italic)

Naturally, the list of available fonts varies by system; if you're going to go beyond the basic preinstalled fonts supplied with Windows, you should confirm that a named font is really available, and have a fallback option if it is not. You can get a list of all fonts by asking GDI+ nicely. All fonts appear in "families," where each named family may have bold, italic, and other variations installed as separate font files. The following code block adds a list of all installed font families to a ListBox control:

Dim allFonts As New Drawing.Text.InstalledFontCollection(  )
For Each oneFamily As Drawing.FontFamily In allFonts.Families
   ListBox1.Items.Add(oneFamily.Name)
Next oneFamily

If the font you need isn't available and you aren't sure what to use, let GDI+ choose for you. It includes a few generic fonts for emergency use.

Drawing.FontFamily.GenericMonospace
Drawing.FontFamily.GenericSansSerif
Drawing.FontFamily.GenericSerif

Getting back to using fonts in actual drawing, the Graphics object includes a DrawString method that blasts out some text to the canvas.

Private Sub PictureBox1_Paint(ByVal sender As Object, _
      ByVal e As System.Windows.Forms.PaintEventArgs) _
      Handles PictureBox1.Paint
   Dim basicFont As New Font("Arial", 14, FontStyle.Italic)
   e.Graphics.DrawString("This is a test", basicFont, _
      Brushes.Black, 0, 0)
   basicFont.Dispose(  )
End Sub

Figure 18.4, "This is a test for sure" shows the output for this code block. In most of the sample code in this chapter, I'll be outputting content to a PictureBox control named PictureBox1 that I've placed on the form of a new Windows Forms application. I've also set that control's BorderStyle property to FixedSingle, and its BackColor property to White so that I can visualize the edges of the canvas. Drawing occurs in the Paint event handler, which gets called whenever the picture box needs to be refreshed, as when another window obscures it and then goes away. In the remaining code examples, I won't be including the Sub PictureBox1_Paint method definition, just the code that goes inside it.

Figure 18.4. This is a test for sure

This is a test for sure

Of course, you can mix and match fonts on a single output canvas. This code includes text using Arial 14 and Arial 18:

Dim basicFont As New Font("Arial", 14)
Dim strongFont As New Font("Arial", 18, FontStyle.Bold)
Dim offset As Single = 0.0
Dim showText As String
Dim textSize As Drawing.SizeF

showText = "This is some "
textSize = e.Graphics.MeasureString(showText, basicFont)
e.Graphics.DrawString(showText, basicFont, _
   Brushes.Black, offset, 0)
offset += textSize.Width

showText = "strong"
textSize = e.Graphics.MeasureString(showText, strongFont)
e.Graphics.DrawString(showText, strongFont, _
   Brushes.Black, offset, 0)
offset += textSize.Width

showText = "text."
textSize = e.Graphics.MeasureString(showText, basicFont)
e.Graphics.DrawString(showText, basicFont, _
   Brushes.Black, offset, 0)
offset += textSize.Width

strongFont.Dispose(  )
basicFont.Dispose(  )

The output of this code appears in the top box of Figure 18.5, "The good and the bad; both ugly", and it's OK. But I want the bottom edges of the main body parts of each text block-that is, the baselines of each block-to line up properly, as shown in the lower box of Figure 18.5, "The good and the bad; both ugly".

Figure 18.5. The good and the bad; both ugly

The good and the bad; both ugly

Doing all of the fancy font-lining-up stuff is kind of a pain in the neck. You have to do all sorts of measuring based on the original font design as extrapolated onto the pixel-based screen device. Then you connect the knee bone to the thigh bone, and so on. Here's the code I used to generate the second lined-up image:

Dim basicFont As New Font("Arial", 14)
Dim strongFont As New Font("Arial", 18, FontStyle.Bold)
Dim offset As Single = 0.0
Dim showText As String
Dim textSize As Drawing.SizeF
Dim basicTop As Single
Dim strongTop As Single
Dim strongFactor As Single
Dim basicFactor As Single

' ----- The Font Family uses design units, probably
'       specified by the original designer of the font.
'       Map these units to display units (points).
strongFactor = strongFont.FontFamily.GetLineSpacing( _
   FontStyle.Regular) / strongFont.Height
basicFactor = basicFont.FontFamily.GetLineSpacing( _
   FontStyle.Regular) / basicFont.Height

' ----- Determine the location of each font's baseline.
strongTop = (strongFont.FontFamily.GetLineSpacing( _
   FontStyle.Regular) - strongFont.FontFamily.GetCellDescent( _
   FontStyle.Regular)) / strongFactor
basicTop = (basicFont.FontFamily.GetLineSpacing( _
   FontStyle.Regular) - basicFont.FontFamily.GetCellDescent( _
   FontStyle.Regular)) / basicFactor

' ----- Draw a line that proves the text lines up.
e.Graphics.DrawLine(Pens.Red, 0, strongTop, _
   e.ClipRectangle.Width, strongTop)

' ----- Show each part of the text.
showText = "This is some "
textSize = e.Graphics.MeasureString(showText, basicFont)
e.Graphics.DrawString(showText, basicFont, _
   Brushes.Black, offset, strongTop - basicTop)
offset += textSize.Width

showText = "strong"
textSize = e.Graphics.MeasureString(showText, strongFont)
e.Graphics.DrawString(showText, strongFont, _
   Brushes.Black, offset, 0)
offset += textSize.Width

showText = "text."
textSize = e.Graphics.MeasureString(showText, basicFont)
e.Graphics.DrawString(showText, basicFont, _
   Brushes.Black, offset, strongTop - basicTop)
offset += textSize.Width

strongFont.Dispose(  )
basicFont.Dispose(  )

There's a lot more calculating going on in that code. And I didn't even try to tackle things like kerning, ligatures, or anything else having to do with typography. Anyway, if you need to perform complex font manipulation, GDI+ does expose all of the details so that you can do it properly. If you just want to output line after line of text using the same font, call the font's GetHeight method for each line displayed:

verticalOffset += useFont.GetHeight(e.Graphics)

Enough of that complex stuff. There are easy and cool things to do with text, too. Did you notice that text output uses brushes and not pens? This means you can draw text using any brush you can create. This block of code uses the Library Project's LookupItem bitmap brush to display some bitmap-based text.

Dim useBrush As Brush = New TextureBrush( _
   Image.FromFile("LookupItem.bmp"))
Dim useFont As New Font("Arial", 60, FontStyle.Bold)
e.Graphics.DrawString("Wow!", useFont, useBrush, 0, 0)
useFont.Dispose(  )
useBrush.Dispose(  )

The output appears in Figure 18.6, "The merger of text and graphics".

Figure 18.6. The merger of text and graphics

The merger of text and graphics

Imagining Images

Probably more than anything else, the Internet has fueled the average computer user's need for visual stimuli. Web sites are awash with GIF, JPG, TIFF, and a variety of other image formats. Even if you deal with non-web applications, it's likely that you, as a programmer, will come into more frequent contact with graphical images. Fortunately, GDI+ includes features that let you manage and manipulate these images with ease.

The "BMP" file format is the native bitmap format included in Microsoft Windows, but it's not all that common in the web world. But none of that matters to GDI+. It can load and manage files using the following graphics formats:

  • Windows "BMP" bitmap files of any color depth and size.

  • CompuServe Graphics Interchange Format ("GIF") files, commonly used for non-photo images on the Internet.

  • Joint Photographic Experts Group ("JPEG") files, commonly used for photos and other images on the Internet. JPEG files are compressed internally to reduce file size, but with the possible loss of image quality.

  • Exchangeable Image File ("EXIF") files, a variation of JPEG that stores professional photographs.

  • Portable Network Graphics ("PNG") files, which are similar to GIF files, but with some enhanced features.

  • Tag Image File Format ("TIFF") files, which are kind of a combination of all other file formats. Some government organizations store scanned images using TIFF.

  • Metafiles, which store vector line art instead of bitmap images.

  • Icon ("ICO") files, which are used for standard Microsoft Windows icons. You can load them as bitmaps, but there is also a distinct Icon class that lets you treat them in more icon-like ways.

Three primary classes are used for images: Image (an abstract base class for the other two classes), Bitmap, and Metafile. I'll discuss the Metafile class a little later.

Bitmaps represent an image as drawn on a grid of bits. When a bit in the grid is on, that grid cell is visible or filled. When the bit is off, the grid cell is invisible or empty. Figure 18.7, "An 8 × 8 monochrome bitmap containing great art" shows a simple image using such a bitmap grid.

Figure 18.7. An 8 × 8 monochrome bitmap containing great art

An 8 × 8 monochrome bitmap containing great art

Since a bit can support only two states, "1-bit bitmap files" are monochrome, displaying images using only black and white. To include more colors, bitmaps add additional "planes." The planes are stacked on each other so that a cell in one plane matches up with that same position cell in all other planes. A set of eight planes results in an "8-bit bitmap image," and supports 256 colors per cell (because 2planes = 28 = 256). Some images include as many as 32 or even 64 bits (planes), although some of these bits may be reserved for "alpha blending," which makes perceived transparency possible.

Unless you are a hardcore graphics junkie, manipulating all of those bits is a chore. Fortunately, you don't have to worry about it since it's all done for you by the Bitmap class. You just need to worry about loading and saving bitmaps (using simple Bitmap methods, of course), using a bitmap as a brush or drawing object (as we did in some sample code in this chapter already), or writing on the bitmap surface itself by attaching a Graphics object to it.

If you have a bitmap in a file, you can load it via the Bitmap class constructor.

Dim niceImage As New Bitmap("LookupItem.bmp")

To save a bitmap object to a file, use its Save method.

niceImage.Save("LookupItem.jpg", Imaging.ImageFormat.Jpeg)

Another constructor lets you create new bitmaps in a variety of formats.

' ---- Create a 50-50 pixel bitmap, using 32 bit-planes
'      (eight each for the amounts of red, green, and blue
'      in each pixel, and eight bits for the level of
'      transparency of each pixel, from 0 to 255).
Dim niceImage As New Bitmap(50, 50, _
   Drawing.Imaging.PixelFormat.Format32bppArgb)

To draw a bitmap on a graphics surface, use the Graphics object's DrawImage method.

e.Graphics.DrawImage(niceImage, leftOffset, topOffset)

That statement draws the image to the graphics surface as is, but that's kind of boring. You can stretch and crop the image as you draw it, or even generate a thumbnail. I'll try all these methods using the image from the Library Project's "splash" welcome form (SplashImage.jpg).

Dim splashImage As New Bitmap("SplashImage.jpg")

' ----- Draw it at half width and height.
e.Graphics.DrawImage(splashImage, New RectangleF(10, 50, _
   splashImage.Width / 2, splashImage.Height / 2))

' ----- Stretch it with fun!
e.Graphics.DrawImage(splashImage, New RectangleF(200, 10, _
   splashImage.Width * 1.25, splashImage.Height / 4))

' ----- Draw the middle portion.
e.Graphics.DrawImage(splashImage, 200, 100, New RectangleF( _
   0, splashImage.Height / 3, splashImage.Width, _
   splashImage.Height / 2), GraphicsUnit.Pixel)

Figure 18.8, "Three views of a reader: a masterpiece by the author" shows the output for the previous block of code. But that's not all the drawing you can do. The DrawImage method includes 30 overloads. That would keep me busy for 37 minutes at least!

Figure 18.8. Three views of a reader: a masterpiece by the author

Three views of a reader: a masterpiece by the author

Exposing Your True Artist

OK, we've covered most of the basic GDI+ features used to draw images. Now it's all just a matter of issuing the drawing commands for shapes, images, and text on a graphics surface. Most of the time, you'll stick with the methods included on the Graphics object, all 12 bazillion of them. Perhaps I overcounted, but there are quite a few. Here's just a sampling:

  • Clear method. Clear the background with a specific color.

  • CopyFromScreen method. If the Prnt Scrn button on your keyboard falls off, this is the method for you.

  • DrawArc method. Draw a portion of an arc along the edge of an ellipse. Zero degrees starts at three o'clock. Positive arc sweep values move in a clockwise direction; use negative sweep values to move counterclockwise.

  • DrawBezier and DrawBeziers methods. Draw a Bézier spline, a formula-based curve that uses a set of points, plus directionals that guide the curve through the points.

  • DrawCurve, DrawClosedCurve, and FillClosedCurve methods. Draw "cardinal" curves (where points define the path of the curve), with an optional brush fill.

  • DrawEllipse and FillEllipse methods. Draw an ellipse or a circle (which is a variation of an ellipse).

  • DrawIcon, DrawIconUnstretched, DrawImage, DrawImageUnscaled, and DrawImageUnscaledAndClipped methods. Different ways of drawing images and icons.

  • DrawLine and DrawLines methods. Draw one or more lines with lots of options for making the lines snazzy.

  • DrawPath and FillPath methods. I'll discuss "graphic paths" a little later.

  • DrawPie and FillPie methods. Draw a pie slice border along the edge of an ellipse.

  • DrawPolygon and FillPolygon methods. Draw a regular or irregular geometric shape based on a set of points.

  • DrawRectangle, DrawRectangles, FillRectangle, and FillRectangles methods. Draw squares and rectangles.

  • DrawString method. We used this before to output text to the canvas.

  • FillRegion method. I'll discuss regions later in the chapter.

Here's some sample drawing code:

' ----- Line from (10, 10) to (40, 40).
e.Graphics.DrawLine(Pens.Black, 10, 10, 40, 40)

' ----- 90degree clockwise arc for 40-pixel diameter circle.
e.Graphics.DrawArc(Pens.Black, 50, 10, 40, 40, 0, −90)

' ----- Filled 40x40 rectangle with a dashed line.
e.Graphics.FillRectangle(Brushes.Honeydew, 120, 10, 40, 40)
Using dashedPen As New Pen(Color.Black, 2)
   dashedPen.DashStyle = Drawing2D.DashStyle.Dash
   e.Graphics.DrawRectangle(dashedPen, 120, 10, 40, 40)
End Using

' ----- A slice of elliptical pie.
e.Graphics.FillPie(Brushes.BurlyWood, 180, 10, 80, 40, _
   180, 120)

And so on. You get the idea. Figure 18.9, "Some simple drawings" shows the output for this code.

Figure 18.9. Some simple drawings

Some simple drawings

Paths: Drawings on Macro-Vision

The GraphicsPath class lets you collect several of the more primitive drawing objects (such as lines and arcs, and even rectangles) into a single grouped unit. This full path can then be replayed onto a graphics surface as a macro.

Using thePath As New Drawing2D.GraphicsPath
   thePath.AddEllipse(0, 0, 50, 50)
   thePath.AddArc(10, 30, 30, 10, 10, 160)
   thePath.AddRectangle(New Rectangle(15, 15, 5, 5))
   thePath.AddRectangle(New Rectangle(30, 15, 5, 5))

   e.Graphics.DrawPath(Pens.Black, thePath)
End Using

This code block draws a smiley face on the canvas (see Figure 18.10, "Drawing with a GraphicsPath object").

Figure 18.10. Drawing with a GraphicsPath object

Drawing with a GraphicsPath object

That's cute. Fortunately, there are other uses for graphics paths, some of which I'll discuss in the next section.

Keeping It Regional

Usually, when you draw images, you have the entire visible canvas to work with. (You can draw images and shapes off the edge of the canvas if you want, but if a tree draws an image in the forest and no one is there to admire it, does it appear?) But there are times when you may want only a portion of what you draw to appear. Windows uses this method itself to save time. When you obscure a window with another one, and then expose the hidden window, the application has to redraw everything that appeared on the form or window. But if only a portion of that background window was hidden and then made visible again, why should the program go through the trouble of drawing everything again? It really has to redraw only the part that was hidden, the part that was in the hidden region.

A region specifies an area to be drawn on a surface. And regions aren't limited to boring rectangular shapes. You can design a region based on simple shapes, or you can combine existing regions into more complex regions. For instance, if you have two rectangular regions, you can overlap them and request a new combined region that contains (1) all of the original two regions; (2) the original regions but without the overlapped parts; or (3) just the overlapped parts. Figure 18.11, "Different combinations of regions" shows these combinations.

Figure 18.11. Different combinations of regions

Different combinations of regions

During drawing operations, regions are sometimes referred to as "clipping regions" because any content drawn outside the region is clipped off and thrown away. The following code draws an image, but masks out an ellipse in the middle by using (ta-da!) a graphics path to establish a custom clipping region:

' ----- Load the image. We'll show it smaller than normal.
Dim splashImage As New Bitmap("SplashImage.jpg")
Dim thePath As New Drawing2D.GraphicsPath(  )

' ----- Create an elliptical path that is the size of the
'       output image.
thePath.AddEllipse(20, 20, splashImage.Width \ 2, _
   splashImage.Height \ 2)

' ----- Replace the original clipping region that covers
'       the entire canvas with just the rectangular region.
e.Graphics.SetClip(thePath, Drawing2D.CombineMode.Replace)

' ----- Draw the image, which will be clipped.
e.Graphics.DrawImage(splashImage, 20, 20, _
   splashImage.Width \ 2, splashImage.Height \ 2)

' ----- Clean up.
thePath.Dispose(  )

The output for this code appears in Figure 18.12, "Ready to hang in your portrait gallery".

Figure 18.12. Ready to hang in your portrait gallery

Ready to hang in your portrait gallery

Regions are also useful for "hit testing." If you draw a non-rectangular image on a form, and you want to know when the user clicks on the image, but not on any pixel just off the image, you can use a region that is the exact shape of the image to test for mouse clicks.

Twisting and Turning with Transformations

Normally, anything you draw on the graphics canvas is laid down directly on the bitmap surface. It's like a giant grid, and your drawing commands are basically dropping colored inks directly into each grid cell. The Graphics object also gives you the ability to pass your drawing commands through a geometric transformation before their output goes to the canvas surface. For instance, a rotation transformation would rotate your lines, shapes, and text by the amount you specify (in degrees), and then apply the result to the surface. Figure 18.13, "Normal and rotated text" displays the results of the following code, which applies two translations: (1) moving the (0, 0) origin right by 100 pixels and down by 75 pixels; and (2) adding a clockwise rotation of 270 degrees.

e.Graphics.DrawString("Normal", _
   SystemFonts.DefaultFont, Brushes.Black, 10, 10)
e.Graphics.TranslateTransform(100, 75)
e.Graphics.RotateTransform(270)
e.Graphics.DrawString("Rotated", _
   SystemFonts.DefaultFont, Brushes.Black, 10, 10)
e.Graphics.ResetTransform(  )

Figure 18.13. Normal and rotated text

Normal and rotated text

Transformations are cumulative; if you apply multiple transformations to the canvas, any drawing commands will pass through all of the transformations before arriving at the canvas. The order in which transformations occur is important. If the code we just ran had reversed the TranslateTransform and RotateTransform statements, the rotation would have altered the x, y coordinates for the entire canvas world. The subsequent translation of (100, 75) would have moved up the origin 100 pixels and then to the right 75 pixels.

The Graphics class includes these methods that let you apply transformations to the "world view" of the canvas during drawing:

  • RotateTransform
    Rotates the world view in clockwise degrees, from 0 to 359. The rotation can be positive or negative.

  • ScaleTransform
    Sets a scaling factor for all drawing. Basically, this increases or decreases the size of the canvas grid when drawing. Changing the scale impacts the width of pens. If you scale the world by a factor of two, not only do distances appear to be twice as far apart, but all pens draw twice as thick as when unscaled.

  • TranslateTransform
    Repositions the origin based on an x and y offset.

  • MultiplyTransform
    A sort of master transformation method that lets you apply transforms through a Matrix object. It has more options than just the standard transforms included in the Graphics object. For instance, you can apply a shearing transformation that skews all output in a rectangle-to-parallelogram type of change.

  • ResetTransform
    Removes all applied transformations from a canvas.

  • Save
    Saves the current state of the transformed (or untransformed) graphics surface to an object for later restoration. This allows you to apply some transformations, save them, apply some more, and then restore the saved set, wiping out any transformations applied since that set was saved.

  • Restore
    Restores a saved set of transformations.

Enhancing Controls Through Owner Draw

A lot more GDI+ drawing features are included in .NET, but what we've seen here should be enough to whet your appetite. You can do a lot of fancy drawing with GDI+, but let's face it: you and I are programmers, not artists. If we were artists, we'd be raking in six figures using a floor mop to draw traditional abstract cubist Italian landscapes with Bauhausian accents.

Fortunately, there are practical semi-artistic things you can do with GDI+. One important drawing feature is owner draw, a sharing of drawing responsibilities between a control and you, the programmer. (You are the "owner.") The ComboBox control supports owner drawing of the individual items in the drop-down portion of the list. Let's create a ComboBox control that displays color names, including a small sample of the color to the left of the name. Create a new Windows Forms application, and add a ComboBox control named ComboBox1 to Form1. Make these changes to ComboBox1:

  1. Change its DropDownStyle property to DropDownList.

  2. Change its DrawMode property to OwnerDrawFixed.

  3. Alter its Items property, adding multiple color names as distinct text lines in the String Collection Editor window. I added Red, Green, and Blue.

Now, add the following code to the source code area of Form1's class:

Private Sub ComboBox1_DrawItem(ByVal sender As Object, _
      ByVal e As System.Windows.Forms.DrawItemEventArgs) _
      Handles ComboBox1.DrawItem
   ' ----- Ignore the unselelected state.
   If (e.Index = −1) Then Return

   ' ----- Create a brush for the display color, based on
   '       the name of the item.
   Dim colorBrush As New SolidBrush(Color.FromName( _
      CStr(ComboBox1.Items(e.Index))))

   ' ----- Create a text brush. The color varies based on
   '       whether this item is selected or not.
   Dim textBrush As Brush
   If ((e.State And DrawItemState.Selected) = _
         DrawItemState.Selected) Or _
         ((e.State And DrawItemState.HotLight) = _
         DrawItemState.HotLight) Then
      textBrush = New SolidBrush(SystemColors.HighlightText)
   Else
      textBrush = New SolidBrush(SystemColors.ControlText)
   End If

   ' ----- Get the shape of the color display area.
   Dim colorBox As New Rectangle(e.Bounds.Left + 4, _
      e.Bounds.Top + 2, (e.Bounds.Height - 4) * 2, _
      e.Bounds.Height − 4)

   ' ----- Draw the selected or unselected background.
   e.DrawBackground(  )

   ' ----- Draw the custom color area.
   e.Graphics.FillRectangle(colorBrush, colorBox)
   e.Graphics.DrawRectangle(Pens.Black, colorBox)

   ' ----- Draw the name of the color to the right of
   '       the color.
   e.Graphics.DrawString(CStr(ComboBox1.Items(e.Index)), _
      ComboBox1.Font, textBrush, 8 + colorBox.Width, _
      e.Bounds.Top + ((e.Bounds.Height - _
      ComboBox1.Font.Height) / 2))

   ' ----- Draw a selected rectangle around the item,
   '       if needed.
   e.DrawFocusRectangle(  )

   ' ----- Clean up.
   textBrush.Dispose(  )
   colorBrush.Dispose(  )
End Sub

Run the code and play with the combo box, as shown in Figure 18.14, "Our custom color combo box".

Figure 18.14. Our custom color combo box

Our custom color combo box

Windows Presentation Foundation

Graphical user interfaces are a relatively new phenomenon. The first programmers didn't have all of the glitzy bling-bling that adorns many a modern UI. They had to make do with naval semaphore flags and Morse code. Here in the 21st century, having passed through the epochs of text-based and stick-figure interfaces, computers are finally able to present information in a way that totally confuses the end-user, yet in a beautiful and highly interactive style.

Microsoft's latest tool for building active, next-generation user interfaces is Windows Presentation Foundation, or WPF. As in LINQ, WPF melds together many different technologies into a unified whole. Some of those technologies have been with us for many years, such as Microsoft's Direct3D system that displays and manipulates 3D elements. WPF condenses all of these technologies, and makes them available through an XML-based descriptive language known as XAML (eXtensible Application Markup Language).

WPF includes features and elements that deal with many areas of presentation, including on-screen controls, 2D drawings (like GDI), 3D graphics (from Direct3D), static images (such as JPEG pictures), interactive multimedia (video and audio), and WYSIWYG document presentation (similar to PDF documents). Individual elements and entire user interfaces can be animated automatically, or in response to user interactions.

When it comes time to display your WPF content, you can present it to the user in a few common ways. XAML files and related .NET code can be built into a standalone application, much like a typical .NET Windows Forms application, but with the amazing Cary Grant looks normally inaccessible to developers.

WPF can also be used to generate web-based applications hosted within a user's browser. In fact, elements that you design for use in desktop-style applications can be used on the Web generally without any modifications. As expected, security limitations put a damper on some of the things you can do when running in this type of host.

Browser-based WPF programs require that the .NET Framework and the WPF libraries be installed and accessible on the client workstation. Microsoft is bundling up much of that technology and packaging it in a product called Silverlight. Designed to compete with the likes of Adobe's Flash platform and Sun Microsystems' new JavaFX system, Silverlight will eventually allow XAML and .NET linked content to run on non-Windows platforms, such as the Macintosh.

A third variation uses a subset of XAML to define a PDF-like static document. Such documents are known as XPS (XML Paper Specification) documents, and are actually ZIP files that contain all of the XAML pages, graphics, and other page elements in distinct files within the archive.

WPF and XAML

One of the hallmarks of application design in WPF is the separation of logic from presentation. This is a common goal expressed in many newer technologies, including XML and ASP.NET (see Chapter 23, Web Development). All application logic-all event handlers triggered by user input and system actions-is written in standard .NET code. The user interface can also appear as .NET code, with objects created out of the WPF-specific System.Windows namespace. But it's more common to design user interface elements and derived controls through XAML, an XML schema that can be generated by you in Visual Studio or Notepad, or by third-party tools.

Because XAML content can be built outside its application, a user interface design specialist with limited programming knowledge can build the UI components independently from the developer's work on the application logic. Microsoft offers a tool for such designers, called Microsoft Expression Blend, one of a handful of Expression design products. Other vendors also offer tools to generate rich XAML content.

Visual Studio 2008 lets you create complete WPF applications based on XAML content. To build a WPF application, start Visual Studio, create a new project, and from the New Project dialog select the WPF Application template within the Visual Basic Project type. Then click OK. Windows immediately creates a new WPF Forms project, displaying the starting form, Window1 (see Figure 18.15, "Not too different from a Windows Forms application").

Figure 18.15. Not too different from a Windows Forms application

Not too different from a Windows Forms application

The user interface is defined entirely by the XML set displayed at the bottom of the form. Currently, it defines the eventual application window itself.

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

    </Grid>
</Window>

This code defines the window, Window1, which has as its real class System.Windows.Window. The attributes of the XAML Window tag, including Title, Height, and Width, map back to properties of the same name in the Window class.

Let's spice up this form a little more. I'll add a button that includes a rainbow on the button's face, plus add a yellow glow around the button. I'll also add an event handler that shows a message box. As with standard Windows Forms application, you use the controls in the toolbox to build your form. I'll drag a button onto the form's surface and use the XAML text area to bring new life to the window.

<Window x:Class="Window1"
   xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
   Title="Window1" Height="160" Width="411">
   <Grid>
      <Button Margin="95,26,99,35" Name="Button1">
         <Button.Foreground>White</Button.Foreground>
         <Button.FontSize>18</Button.FontSize>
         <Button.FontWeight>Bold</Button.FontWeight >
         <Button.Background>
            <LinearGradientBrush>
               <LinearGradientBrush.GradientStops>
               <GradientStopCollection>
                  <GradientStop Color="Red" Offset="0" />
                  <GradientStop Color="Orange" Offset="0.1425"/>
                  <GradientStop Color="Yellow" Offset="0.285"/>
                  <GradientStop Color="Green" Offset="0.4275"/>
                  <GradientStop Color="Blue" Offset="0.57"/>
                  <GradientStop Color="Indigo" Offset="0.7325"/>
                  <GradientStop Color="Violet" Offset="0.875"/>
               </GradientStopCollection>
            </LinearGradientBrush.GradientStops>
        </LinearGradientBrush>
     </Button.Background>
     <Button.BitmapEffect>
        <OuterGlowBitmapEffect />
     </Button.BitmapEffect> Click Me
  </Button>
  </Grid>
</Window>

I'll also add an event handler, using the same point-and-click simplicity that I've used in Windows Forms projects.

Private Sub Button1_Click(ByVal sender As Object, _
      ByVal e As System.Windows.RoutedEventArgs) _
      Handles Button1.Click
   MsgBox("Hello, World")
End Sub

Running this program with the F5 key gives us the expected form and button, but oh, what a complex-looking button it is (see Figure 18.16, "Click somewhere on the rainbow").

Figure 18.16. Click somewhere on the rainbow

Click somewhere on the rainbow

Although the colors don't appear in this grayscale book, the form does indeed have a rainbow button.

Enhancing Classes with Attributes

Class-modifying attributes are something we discussed way back in Chapter 1, Introducing .NET, and they have nothing to do with GDI+. I just wanted to refresh your memory since they will be used in this chapter's project code.

Class- or member-modifying attributes appear just before the definition of the class or member, and within angle brackets. This code attaches the ObsoleteAttribute attribute to the SomeOldClass class:

<ObsoleteAttribute> _
Class SomeOldClass
   ...class details here...
End Class

(You can leave the "Attribute" part of an attribute's name off if the first part of the name doesn't conflict with any Visual Basic keyword.) Attributes appear as metadata in the final compiled assembly, and they are used by classes and applications that, by design, extract meaning from specific attributes. In this chapter's code, we'll make use of the PropertyGrid control, the control that implements the Properties panel within the Visual Studio development environment, and is often used to modify Form and Control properties. This control is available for your use in your own applications. To use it, assign a class instance to the control's SelectedObject property. Then, magically, all of the object's properties appear in the control's list of properties.

Nice as this is, it's not always desirable. Your object may have properties that should not be displayed. The PropertyGrid control is designed to be generic; it doesn't know about your object's needs, so it doesn't know which properties to exclude. That is, it doesn't know until you tell it through attributes. By adding specific attributes to your class's properties, you tell the PropertyGrid control how to treat members of your object. For instance, the BrowsableAttribute attribute tells the PropertyGrid to include (True) or exclude (False) the property.

<Browsable(False)> _
Public Property SecretProperty(  ) As String...

I'll supply additional details about this when we use the PropertyGrid control later in this chapter.

Summary

Although many parts of GDI+ are pretty much wrappers around the old GDI system, GDI+ still manages to provide power and simplicity that go way beyond the original implementation. In business development, you won't always have a need to use the more interesting aspects of the System.Drawing namespace. But when you do, you'll encounter a simple and coherent system for displaying images, text, and custom vector elements on the screen or other output medium.

And speaking of simple and coherent, look for WPF at a computer near you. As more and more programmers cozy up to the new system, it may eventually supplant both the Windows Forms and the ASP.NET page system as the primary presentation layer in .NET desktop and web applications.

Project

The Library Project has used features of GDI+ since the moment the first form appeared in the newly created project, but it all came for free through code included in the framework. Now it's time for you, the programmer, to add your own GDI+ contribution to the application. In this chapter's project code, we'll use GDI+ to enhance the normal display of a control through owner draw features. Plus, we'll finally begin to implement some of the bar code features I tempted you with in earlier chapters. I'm sorry to say that we won't be using the cool XML drawing experience that is WPF.

Note

Load the Chapter 18, User Interface (Before) Code project, either through the New Project templates or by accessing the project directly from the installation directory. To see the code in its final form, load Chapter 18, User Interface (After) Code instead.

Install the Bar Code Font

If you haven't yet obtained a bar code font, now is the time to do it. The features included in this chapter's project code will require you to use such a font. The publisher web site for this book (listed in Appendix A, Installing the Software) includes suggested resources for obtaining a font at little or no cost for your personal use. You may also purchase a professional bar code font. Make sure the font you obtain is a TrueType font.

Using Owner Draw

In the previous chapter, we added the ItemLookup.vb form with its multiple views of library items. One of those views included the MatchingItems control, a multicolumn listbox displaying Author/Name, Call Number, and Media Type columns. Although we stored the column-specific data within each item already, we didn't actually display the individual columns to the user.

The thing about multicolumn lists and other limited-space text displays is that some of the text is bound to overrun its "official" area if you let it. For instance, the text in one list column may overlap into another column of text. In such cases, it has become the tradition to chop off the extended content and replace it with an ellipsis (" . . . "). So, we'll need a routine that will determine whether a string is too long for its display area, and perform the chopping and ellipsizing as needed. Add the FitTextToWidth method to the General.vb file's module code.

Note

Insert Chapter 18, User Interface, Snippet Item 1.

Public Function FitTextToWidth(ByVal origText As String, _
      ByVal pixelWidth As Integer, _
      ByVal canvas As System.Drawing.Graphics, _
      ByVal useFont As System.Drawing.Font) As String
   ' ----- Given a text string, make sure it fits in the
   '       specified pixel width. Truncate and add an
   '       ellipsis if needed.
   Dim newText As String

  newText = origText
   If (canvas.MeasureString(newText, useFont).Width(  ) > _
         pixelWidth) Then
      Do While (canvas.MeasureString(newText & "...", _
            useFont).Width(  ) > pixelWidth)
         newText = Left(newText, newText.Length - 1)
         If (newText = "") Then Exit Do
      Loop
      If (newText <> "") Then newText &= "..."
   End If
   Return newText
End Function

The ItemLookup.vb form has a web-browser-like Back button with a drop-down list of recent entries. The items added to this list may include long book titles and author names. Let's use the new FitTextToWidth method to limit the size of text items in this list. Open the source code for the ItemLookup form and locate the RefreshBackButtons method. About halfway through this routine is this line of code:

whichMenu.Text = scanHistory.HistoryDisplay

Replace this line with the following lines instead.

Note

Insert Chapter 18, User Interface, Snippet Item 2.

whichMenu.Text = FitTextToWidth(scanHistory.HistoryDisplay, _
   Me.Width \ 2, useCanvas, whichMenu.Font)

That will limit any menu item text to half the width of the form, which seems reasonable to me. That useCanvas variable is new, so add a declaration for it at the top of the RefreshBackButtons method.

Note

Insert Chapter 18, User Interface, Snippet Item 3.

Dim useCanvas As Drawing.Graphics = Me.CreateGraphics(  )

Also, we need to properly dispose of that created graphics canvas at the very end of the method.

Note

Insert Chapter 18, User Interface, Snippet Item 4.

useCanvas.Dispose(  )

Now let's tackle owner draw list items. ListBox controls allow you to use your own custom drawing code for each visible item in the list. You have two options when you are managing the item drawing by yourself: you can keep every item a consistent height, or you can make each list item a different height based on the content for that item. In the MatchingItems listbox, we'll use the same height for every list item.

To enable owner draw mode, open the ItemLookup form design editor, select the MatchingItems listbox on the form or through the Properties panel, and change its DrawMode property to OwnerDrawFixed.

Each matching list item will include two rows of data: (1) the title of the matching item, in bold; and (2) the three columns of author, call number, and media type data. Add the following code to the form's Load event handler that determines the entire height of each list item, and the position of the second line within each item.

Note

Insert Chapter 18, User Interface, Snippet Item 5.

' ----- Prepare the form.
Dim formGraphics As System.Drawing.Graphics

' ----- Set the default height of items in the matching
'       items listbox.
formGraphics = Me.CreateGraphics(  )
MatchingItems.ItemHeight = CInt(formGraphics.MeasureString( _
   "A" & vbCrLf & "g", MatchingItems.Font).Height(  )) + 3
SecondItemRow = CInt(formGraphics.MeasureString("Ag", _
   MatchingItems.Font).Height(  )) + 1
formGraphics.Dispose(  )

I used the text "Ag" to make sure that the height included all of the font's ascenders and descenders (the parts that stick up and stick down from most letters). I think the calculation would include those values even if I used "mm" for the string, but better safe than sorry, I always say. Setting the MatchingItems.ItemHeight property here indicates the size of all items in the list. If we had decided to use variable-height items instead of fixed-height items, we would have handled the control's MeasureItem event. With fixed items, we can ignore that event, and move on to the event that does the actual drawing: DrawItem.

Here is what the code is going to do for each list item: (1) create the necessary brushes and font objects we will use in drawing; (2) draw the text strings on the list item canvas; and (3) clean up. Since list items can also be selected or unselected, we'll call some framework-supplied methods to draw the proper background and foreground elements that indicate item selections.

When we draw the multiple columns of text, it's possible that one column of text will be too long, and intrude into the next column area. This was why we wrote the FitTextToWidth function earlier. But it turns out that GDI+ already includes a feature that adds ellipses to text at just the right place when it doesn't fit. It's found in a class called StringFormat, in its Trimming property. Setting this property to EllipsisCharacter and using it when drawing the string will trim the string when appropriate. When we draw the string on the canvas, we will provide a rectangle that tells the string what its limits are. Here is the basic code used to draw one column of truncated text:

Dim ellipsesText As New StringFormat
ellipsesText.Trimming = StringTrimming.EllipsisCharacter
e.Graphics.DrawString("Some Long Text", e.Font, someBrush, _
   New Rectangle(left, top, width, height), ellipsesText)

The code we'll use to draw each list item in the MatchingItems list will use code just like this. Let's add that code now to the MatchingItems.DrawItem event handler.

Note

Insert Chapter 18, User Interface, Snippet Item 6.

' ----- Draw the matching items on two lines.
Dim itemToDraw As MatchingItemData
Dim useBrush As System.Drawing.Brush
Dim boldFont As System.Drawing.Font
Dim ellipsesText As StringFormat

' ----- Draw the background of the item.
If (CBool(CInt(e.State) And CInt(DrawItemState.Selected))) _
   Then useBrush = SystemBrushes.HighlightText _
   Else useBrush = SystemBrushes.WindowText
e.DrawBackground(  )

' ----- The title will use a bold version of the main font.
boldFont = New System.Drawing.Font(e.Font, FontStyle.Bold)

' ----- Obtain the item to draw.
itemToDraw = CType(MatchingItems.Items(e.Index), _
   MatchingItemData)
ellipsesText = New StringFormat
ellipsesText.Trimming = StringTrimming.EllipsisCharacter

' ----- Draw the text of the item.
e.Graphics.DrawString(itemToDraw.Title, boldFont, useBrush, _
   New Rectangle(0, e.Bounds.Top, _
   ItemColEnd.Left - MatchingItems.Left, _
   boldFont.Height), ellipsesText)
e.Graphics.DrawString(itemToDraw.Author, e.Font, useBrush, _
   New Rectangle(ItemColAuthor.Left, _
   e.Bounds.Top + SecondItemRow, _
   ItemColCall.Left - ItemColAuthor.Left - 8, _
   e.Font.Height), ellipsesText)

e.Graphics.DrawString(itemToDraw.CallNumber, e.Font, _
   useBrush, New Rectangle(ItemColCall.Left, _
   e.Bounds.Top + SecondItemRow, _
   ItemColEnd.Left - ItemColType.Left, _
   e.Font.Height), ellipsesText)
e.Graphics.DrawString(itemToDraw.MediaType, e.Font, _
   useBrush, New Rectangle(ItemColType.Left, _
   e.Bounds.Top + SecondItemRow, _
   ItemColType.Left - ItemColCall.Left - 8, _
   e.Font.Height), ellipsesText)

' ----- If the ListBox has focus, draw a focus rectangle.
e.DrawFocusRectangle(  )
boldFont.Dispose(  )

See, it's amazingly easy to draw anything you want in a listbox item. In this code, the actual output to the canvas via GDI+ amounted to just the four DrawString statements. Although this library database doesn't support it, we could have included an image of each item in the database, and displayed it in this listbox, just to the left of the title. Also, the calls to e.DrawBackground and e.DrawFocusRectangle let the control deal with properly highlighting the right item (although I did have to choose the proper text brush). Figure 18.17, "A sample book with two lines and three columns" shows the results of our hard labor.

Figure 18.17. A sample book with two lines and three columns

A sample book with two lines and three columns

Bar Code Design

The Library Project includes generic support for bar code labels. I visited a few libraries in my area and compared the bar codes added to both their library items (such as books) and their patron ID cards. I found that the variety was too great to shoehorn into a single predefined solution. Therefore, the Library application allows an administrator or librarian to design sheets of bar code labels to meet their specific needs. (There are businesses that sell preprinted bar code labels and cards to libraries that don't want to print their own. The application also supports this method, since bar code generation and bar code assignment to items are two distinct steps.)

To support generic bar code design, we will add a set of design classes and two forms to the application:

  • BarcodeItemClass.vb
    This class file contains six distinct classes, one of which is a base class for the other five derived classes. The derived classes design the static text elements, bar code images, bar code numbers, lines, and rectangles that the user will add to the surface of a single bar code label.

  • BarcodePage.vb
    This is an editor form derived from BaseCodeForm, the same base form used for the various code editors in the application. This form specifies the arrangement of label sheets. The user will probably purchase label sheets from his local office supply store. By entering the number of label rows and columns, the size of each label, and any spacing between and around each label, the user can design on pretty much any regular sheet of labels.

  • BarcodeLabel.vb
    Another editor based on BaseCodeForm, this form lets the user design a single bar code label by adding text, bar codes, lines, and rectangles to a preview area.

In a future chapter, we'll add label printing, where labels and pages are joined together in one glorious print job.

Since these three files together include around 2,000 lines of source code, I will show you only key sections of each one. I've already added all three files to your project code, so let's start with BarcodeItemClass.vb. It defines each type of display item that the user will add to a label template in the BarcodeLabel.vb form. Here's the code for the abstract base class, BarcodeItemGeneric:

Imports System.ComponentModel
Public MustInherit Class BarcodeItemGeneric
   <Browsable(False)> Public MustOverride ReadOnly _
      Property ItemType(  ) As String
   Public MustOverride Overrides _
      Function ToString(  ) As String
End Class

Not much going on here. The class defines two required members: a read-only String property named ItemType, and a requirement that derived classes provide their own implementation for ToString. The other five derived classes in this file enhance the base class to support the distinct types of display elements included on a bar code label. Let's look briefly at one of the classes, BarcodeItemRect. It allows an optionally filled rectangle to appear on a bar code label, and includes private members that track the details of the rectangle.

Public Class BarcodeItemRect
   ' ----- Includes a basic rectangle element in a
   '       bar code label.
   Inherits BarcodeItemGeneric

   ' ----- Private store of attributes.
   Private StoredRectLeft As Single
   Private StoredRectTop As Single
   Private StoredRectWidth As Single
   Private StoredRectHeight As Single
   Private StoredRectColor As Drawing.Color
   Private StoredFillColor As Drawing.Color
   Private StoredRectAngle As Short

The rest of the class includes properties that provide the public interface to these private members. Here's the code for the public FillColor property:

<Browsable(True), DescriptionAttribute( _
"Sets the fill color of the rectangle.")> _
Public Property FillColor(  ) As Drawing.Color
   ' ----- The fill color.
   Get
      Return StoredFillColor
   End Get
   Set(ByVal Value As Drawing.Color)
      StoredFillColor = Value
   End Set
End Property

Like most of the other properties, it just sets and retrieves the related private value. Its declaration includes two attributes that will be read by the PropertyGrid control later on. The Browsable property says, "Yes, include this property in the grid," and DescriptionAttribute sets the text that appears in the bottom help area of the PropertyGrid control.

When you've used the Properties panel to edit your forms, you've been able to set colors for a color property using a special color selection tool built into the property. Just having a property defined using System.Drawing.Color is enough to enable this same functionality for your own class. How does it work? Just as the FillColor property has attributes recognized by the PropertyGrid control, the System.Drawing.Color class also has such properties, one of which defines a custom property editor class for colors. Its implementation is beyond the scope of this book, but it's cool anyway. If you're interested in doing this for your own classes, you can read an article I wrote about property grid editors a few years ago.[4]

Before we get to the editor forms, I need to let you know about four supporting functions I already added to the General.vb module file:

  • BuildFontStylefunction
    Font styles (such as bold and italic) are set in Font objects using members of the System.Drawing.FontStyle enumeration. But when storing font information in the database, I chose to store these style settings using letters (such as B for bold). This function converts the letters back to a FontStyle value.

  • ConvertPageUnitsfunction
    The label editors let you position items in a few different measurement systems, including inches and centimeters. This function converts measurements between the different systems.

  • DBFontStylefunction
    This is the opposite of the BuildFontStyle function, preparing a FontStyle value for insertion into a database record.

  • GetBarcodeFontfunction
    This returns the name of the bar code font, if configured.

The BarcodePage form lets the user define a full sheet of labels. Not the labels themselves, but the positions of multiple labels on the same printed page. Figure 18.18, "The BarcodePage form" shows the fields on the form with some sample data.

Figure 18.18. The BarcodePage form

The BarcodePage form

Collectively, the fields on the form describe the size of the page and the size of each label that appears on the page. As the user enters the values, the Page Preview area instantly refreshes with a preview of what the page will look like.

As a code editor derived from BaseCodeForm, the logic in the form is already familiar to you; it manages the data found in a single record from the BarcodeSheet table. What's different is the GDI+ code found in the PreviewArea.Paint event handler. Its first main block of code tries to determine how you scale down an 8.5 × 11 piece of paper to make it appear in a small rectangle that is only 216 × 272 pixels in size. It's a lot of gory calculations that, when complete, determine the big-to-small-paper ratio, and lead to the drawing of the on-screen piece of paper with a border and a drop shadow.

e.Graphics.FillRectangle(SystemBrushes.ControlDark, _
   pageLeft + 1, pageTop + 1, pageWidth + 2, pageHeight + 2)
e.Graphics.FillRectangle(SystemBrushes.ControlDark, _
   pageLeft + 2, pageTop + 2, pageWidth + 2, pageHeight + 2)
e.Graphics.FillRectangle(Brushes.Black, pageLeft - 1, _
   pageTop - 1, pageWidth + 2, pageHeight + 2)
e.Graphics.FillRectangle(Brushes.White, pageLeft, _
   pageTop, pageWidth, pageHeight)

Then, before drawing the preview outlines of each rectangular label, it repositions the grid origin to the upper-left corner of the on-screen piece of paper, and transforms the world scale based on the ratio of a real-world piece of paper and the on-screen image of it.

e.Graphics.TranslateTransform(pageLeft, pageTop)
e.Graphics.ScaleTransform(useRatio, useRatio)

There are a few more calculations for the size of each label, followed by a double loop (for both rows and columns of labels) that does the actual printing of the label boundaries (detail calculations omitted for brevity).

For rowScan = 1 To CInt(BCRows.Text)
   For colScan = 1 To CInt(BCColumns.Text)
      leftOffset = ...
      topOffset = ...
      e.Graphics.DrawRectangle(Pens.Cyan, _
         leftOffset, topOffset, _
         oneWidthTwips, oneHeightTwips)
   Next colScan
Next rowScan

The BarcodeLabel form is clearly the more interesting and complex of the two bar code editing forms. While the BarcodePage form defines an entire sheet of labels with nothing inside each label, BarcodeLabel defines what goes inside each of those labels. Figure 18.19, "The BarcodeLabel form" shows this form with a sample label.

The BarcodeLabel form does derive from BaseCodeForm, so much of its code deals with the loading and saving of records from the BarcodeLabel and BarcodeLabelItem database tables. Each bar code label is tied to a specific bar code page template (which we just defined through the BarcodePage form), and stores its primary record in the BarcodeLabel table. This table defines the basics of the label, such as its name and measurement system. The text and shape items placed on that label are stored as records in the related BarcodeLabelItem table.

Figure 18.19. The BarcodeLabel form

The BarcodeLabel form

The PrepareFormFields routine loads existing label records from the database, creating instances of classes from the new BarcodeItemClass.vb file, and adds them to the DisplayItems ListBox control. Here's the section of code that loads in a "bar code image" (the actual displayed bar code) from an entry in the BarcodeLabelItems table:

newBarcodeImage = New Library.BarcodeItemBarcodeImage
newBarcodeImage.Alignment = CType(CInt(dbInfo!Alignment), _
   System.Drawing.ContentAlignment)
newBarcodeImage.BarcodeColor = _
   System.Drawing.Color.FromArgb(CInt(dbInfo!Color1))
newBarcodeImage.BarcodeSize = CSng(dbInfo!FontSize)
newBarcodeImage.Left = CSng(dbInfo!PosLeft)
newBarcodeImage.Top = CSng(dbInfo!PosTop)
newBarcodeImage.Width = CSng(dbInfo!PosWidth)
newBarcodeImage.Height = CSng(dbInfo!PosHeight)
newBarcodeImage.RotationAngle = CShort(dbInfo!Rotation)
newBarcodeImage.PadDigits = _
   CByte(DBGetInteger(dbInfo!PadDigits))
DisplayItems.Items.Add(newBarcodeImage)

The user can add new shapes, text elements, and bar codes to the label by clicking on one of the five Add Items buttons that appear just below the DisplayItems control. Each button adds a default record to the label, which the user can then modify. As each label element is selected from the DisplayItems, its properties appear in the ItemProperties control, an instance of a PropertyGrid control. Modification of a label element is a matter of changing its properties. Figure 18.20, "Modifying a label element property" shows a color property being changed.

Figure 18.20. Modifying a label element property

Modifying a label element property

As with the BarcodePage form, the real fun in the BarcodeLabel form comes through the Paint event of the label preview control, PreviewArea. This 300+ line routine starts out drawing the blank surface of the label with a drop shadow. Then it processes each element in the DisplayItems list, one by one, transforming and drawing each element as its properties indicate. As it passes through the element list, the code applies transforms to the drawing area as needed. To keep things tidy for each element, the state of the surface is saved before changes are made, and restored once changes are complete.

For counter = 0 To DisplayItems.Items.Count - 1
   ' ----- Save the current state of the graphics area.
   holdState = e.Graphics.Save(  )

   ...main drawing code goes here, then...
   ' ----- Restore the original transformed state of
   '       the graphics surface.
   e.Graphics.Restore(holdState)
Next counter

Each element type's code performs the various size, position, and rotation transformations needed to properly display the element. Let's take a closer look at the code that displays static text elements (code that is also called to display bar code text). After scaling down the world view to the label surface preview area, any user-requested rotation is performed about the upper-left corner of the rectangle that holds the printed text.

e.Graphics.TranslateTransform(X1, Y1)
e.Graphics.RotateTransform(textAngle)

Next, a gray dashed line is drawn around the text object to show its selected state.

pixelPen = New System.Drawing.Pen(Color.LightGray, _
   1 / e.Graphics.DpiX)
pixelPen.DashStyle = Drawing2D.DashStyle.Dash
e.Graphics.DrawRectangle(pixelPen, X1, Y1, X2, Y2)
pixelPen.Dispose(  )

After setting some flags to properly align the text vertically and horizontally within its bounding box, the standard DrawString method thrusts the text onto the display.

e.Graphics.DrawString(textMessage, useFont, _
   New System.Drawing.SolidBrush(textColor), _
   New Drawing.RectangleF(X1, Y1, X2, Y2), textFormat)

We will somewhat duplicate the label drawing code included in the BarcodeLabel class when we print actual labels in a later chapter.

The only thing left to do is to link up these editors to the main form. Since I've had so much fun with these forms, I'll let you play for a while in the code. Open the code for MainForm, locate the event handler for the AdminLinkBarcodeLabel.LinkClicked event, and add the following code.

Note

Insert Chapter 18, User Interface, Snippet Item 7.

' ----- Let the user edit the list of bar code labels.
If (SecurityProfile( _
      LibrarySecurity.ManageBarcodeTemplates) = False) Then
   MsgBox(NotAuthorizedMessage, MsgBoxStyle.OkOnly Or _
   MsgBoxStyle.Exclamation, ProgramTitle)
   Return
End If
' ----- Edit the records.
ListEditRecords.ManageRecords(New Library.BarcodeLabel)
ListEditRecords = Nothing

Do the same for the AdminLinkBarcodePage.LinkClicked event handler. Its code is almost identical except for the class instance passed to ListEditRecords.

Note

Insert Chapter 18, User Interface, Snippet Item 8.

' ----- Let the user edit the list of bar code pages.
If (SecurityProfile( _
      LibrarySecurity.ManageBarcodeTemplates) = False) Then
   MsgBox(NotAuthorizedMessage, MsgBoxStyle.OkOnly Or _
   MsgBoxStyle.Exclamation, ProgramTitle)
   Return
End If

' ----- Edit the records.
ListEditRecords.ManageRecords(New Library.BarcodePage)
ListEditRecords = Nothing

Fun with Graphics

GDI+ isn't all about serious drawing stuff; you can also have some fun. Let's make a change to the AboutProgram.vb form so that it fades out when the user clicks its Close button. This involves altering the form's Opacity property to slowly increase the transparency of the form. From our code's point of view, no GDI+ is involved. But it's still involved through the hidden code that responds to the Opacity property.

Open the source code for the AboutProgram.vb file, and add the following code to the end of the AboutProgram.Load event handler.

Note

Insert Chapter 18, User Interface, Snippet Item 9.

' ----- Prepare the form for later fade-out.
Me.Opacity = 0.99

Although this statement isn't really necessary, I found that the form tended to blink a little on some systems when the opacity went from 100% (1.0) to anything else (99%, or 0.99, in this case). This blink was less noticeable when I made the transition during the load process.

In the event handler for the ActClose.Click event, include this code.

Note

Insert Chapter 18, User Interface, Snippet Item 10.

' ----- Fade the form out.
Dim counter As Integer

For counter = 90 To 10 Step −20
   Me.Opacity = counter / 100
   Me.Refresh(  )
   Threading.Thread.Sleep(50)
Next counter
Me.DialogResult = Windows.Forms.DialogResult.Cancel

This code slowly fades out the form over the course of 250 milliseconds, in five distinct steps. So that the form doesn't close abruptly before the cool fade-out, open the form designer, select the ActClose button, and change its DialogResult property to None.

Another thing we never did was to set the primary icon for the application. Although this isn't strictly GDI+, it does involve graphics display, which impacts the user's perception of the program's quality. I've included an icon named Book.ico in the project's file set. Open the project properties, select the Application tab, and use the Icon field to browse for the Book.ico file.

While testing out the icon, I noticed that the splash window appeared (with the default Visual Studio icon) in the Windows task bar. In fact, each opened form appeared in the task bar, right alongside the main form's entry. This is non-standard, and it's all due to the ShowInTaskbar property setting included in each form. I've taken the liberty of going through all the forms (except for MainForm) and setting this property to False. Most of the forms were already set properly, so I altered the dozen or so that were set improperly.

The Library application is really starting to bulk up with features. In fact, by the next chapter, we will have added more than 95% of its total code. I can see the excitement on your face. Go ahead, turn the page, and add to your coding enjoyment.

[4] You can find the article referenced on my web site, http://www.timaki.com, in the Articles section.