DekGenius.com
[ Team LiB ] Previous Section Next Section

7.2 GDI+

GDI+ is the name of the drawing API in .NET. It is exposed through classes defined in the System.Drawing namespace and its descendants, System.Drawing.Drawing2D, System.Drawing.Imaging, and System.Drawing.Text. It provides a simple but powerful set of tools for drawing text, bitmaps, and vector graphics.

GDI+ Naming

None of the GDI+ classes have the name "GDI+" in them anywhere. The API is named after its predecessor, GDI (the Graphics Device Interface), which was the Win32 drawing API. GDI+ is not in fact a part of .NET—all the same facilities are available to Win32 programs on Windows XP (or older versions of Windows if GDI+ has been installed; GDI+ is put in automatically on these platforms when the .NET Framework is installed). The System.Drawing namespaces are the managed interface to GDI+.


There is a small group of classes that are crucial for drawing anything—some represent fundamental concepts such as colors, coordinates, and drawing styles, others represent entities that can be drawn into, such as bitmaps or windows. We will start by seeing what each of these classes is for, and how they relate to each other. Then, we will look at the various drawing facilities supplied by GDI+ and how to use them. Finally, we will look at some of the advanced support for changing the coordinate system used for drawing.

7.2.1 Essential GDI+ Classes

There are certain classes defined in the System.Drawing namespace that are used in almost all drawing code. This is because they represent concepts fundamental to all drawing operations such as coordinates or colors. We will examine this toolkit of drawing objects, looking at the purpose of each class and how it fits into the GDI+ framework.

Before we start though, there is an issue that concerns almost all the GDI+ classes. Because these classes are a wrapper on top of the underlying GDI+ facilities, they all represent unmanaged resources. When you create a GDI+ object, it consumes some OS resources, so it is vitally important that you free the object when you are done, because otherwise you could exhaust the system's resources, preventing your application (and maybe others) from running. Most classes in System.Drawing therefore implement IDisposable, the interface implemented by all classes that need to be freed in a timely fashion.

Drawing code therefore usually makes extensive use of the C# using keyword to free resources automatically. So most GDI+ code will look like this:

using (Brush foreBrush = new SolidBrush (ForeColor))
{
    g.FillRectangle (foreBrush, 0, 0, 100, 100);
}

VB lacks the convenience of the C# using construct, which the C# compiler translates into a call to the Dispose method within a finally block. As a result, you'll have to call Dispose yourself. The previous C# code fragment would be implemented as follows in VB:

Dim foreBrush As Brush = New SolidBrush(ForeColor)
Try
    g.FillRectangle(foreBrush, 0, 0, 100, 100)
Finally
    foreBrush.Dispose()
End Try

This creates a new Brush object (described below), uses it to draw a rectangle, and then frees it. You should use this approach whenever you create a GDI+ object. Of course, this only applies to classes. Value types are not allocated on the heap, so their lifetime is defined by their containing scope. Values therefore do not use this pattern. (The value types will be pointed out as we come to them, although if in doubt, try adding a using statement—the compiler will complain if you use one on the wrong kind of type. Likewise, in VB, if you try to call the Dispose method on an object that does not require disposal, it will not compile.)

You should only use this pattern if you created the object—for certain types of object (especially the Graphics class) the system will supply the object rather than requiring you to create it. In an OnPaint handler, for example, a Graphics object is supplied as a property of the PaintEventArgs parameter. In these cases, it is not your responsibility to dispose of the object—the framework will free it for you. But if you cause an object to be created, it becomes your job to call Dispose on it.

Occasionally, it is not possible to determine when an object has fallen out of use: if a control's Font property is changed, should that control dispose of the Font object that it was previously using? It should not, because it has no way of knowing whether any other controls are still using the same Font object. Discovering when there are no more controls using the Font object is a hard problem, and in this case we usually have to rely on the garbage collector. This strategy, which is effectively an admission of defeat, is not as bad as it would be for a resource such as a database connection, because most GDI+ objects are not all that expensive. If a few are leaked on a very occasional basis, it is not the end of the world if they don't release their resources until the garbage collector finally notices them. But you should not take this as a license not to bother disposing of your objects: if you fail to call Dispose on any of the objects you create in your OnPaint method, you can run into problems. OnPaint can be called frequently enough that you could exhaust your GDI+ resources before the garbage collector runs.

You should get into the habit of writing a using statement in C# or of calling IDisposable.Dispose from your code in VB whenever you use a GDI+ object. All the examples in this chapter will do this, and you should make sure that this practice becomes second nature. So let us now examine the objects we will be using to draw our controls.

7.2.1.1 Graphics

The Graphics class is the single most important type in GDI+. Without a Graphics object, we cannot draw anything because it is this class that provides the methods that perform drawing operations. A Graphics object represents something that can be drawn onto, usually either a window or a bitmap.

We do not normally need to create a Graphics object ourselves. This is because the most common place for drawing code is the OnPaint method, in which one is supplied for us as the Graphics property of the PaintEventArgs parameter, as shown in Example 7-1 for C# and Example 7-2 for VB. This Graphics object lets us draw things onto the control's window.

Example 7-1. Using a Graphics object in OnPaint with C#
protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;
    using (Brush b = new SolidBrush(ForeColor))
    {
        g.DrawString(text, Font, b, 0, 0);
        g.FillRectangle(b, 20, 20, 30, 30);
    }
        base.OnPaint(pe);
}
Example 7-2. Using a Graphics object in OnPaint with VB
Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)
   Dim g As Graphics = pe.Graphics
   Dim b As Brush = New SolidBrush(ForeColor)
   Try
      g.DrawString(Text, Font, b, 0, 0)
      g.FillRectangle(b, 20, 20, 30, 30)
   Finally
     brush.Dispose()
   End Try
   MyBase.OnPaint(pe)
End Sub

Examples Example 7-1 and Example 7-2 illustrate a common technique used for conciseness: because the Graphics property of the PaintEventArgs object needs to be used for every single drawing operation, a reference is typically held in a local variable with a short name (usually g). This avoids the rather verbose alternative of writing pe.Graphics in front of every single drawing method.

Example 7-1 also illustrates a couple of other important techniques. It has created a Brush object to control the appearance of the items that it draws (see below), but it has done so inside a using statement to make sure that the object is freed when it is no longer being used. It has also called the base class's OnPaint method—as mentioned above, you are required to do this whenever you override OnPaint.

The majority of methods supplied by the Graphics class begin with either Draw... or Fill.... The Draw... methods are typically used for drawing shape outlines; for example, DrawRectangle, DrawEllipse, and DrawPolygon will draw the outline of a rectangle, an ellipse, and a polygon, respectively. The corresponding Fill... methods draw the same shape, but they fill in the shape's interior rather than just drawing an outline. (There are exceptions to this. For example, DrawString draws a normal text string—it doesn't draw character outlines. DrawImage draws an image such as a bitmap; there isn't even a sensible interpretation of drawing an outline for a bitmap. But for shapes that support both filled and outline versions, there will be both Draw... and Fill... methods.)

To draw shapes, the Graphics object needs to know things like what color should be used, and what style should be used for an outline. These requirements are fulfilled by the Brush and Pen classes, described later. But GDI+ also needs to know where it should draw things, and how large they should be, so there are some types relating to size and position.

7.2.1.2 Point, Size, and Rectangle

Coordinates are fundamentally important to any drawing system, so GDI+ defines a few types to deal with location and size. The Point type represents a single two-dimensional point. The Size type represents something's dimensions (i.e., width and height). Rectangle is a combination of the two—it has both a location and a size. These are all value types, so you don't need to bother with using statements in C# or with calls to IDisposable.Dispose in VB.

GDI+ uses a two-dimensional Cartesian coordinate system. By default, increasing values of the x coordinate will move to the right, and increasing values of the y coordinate will move down. This is consistent with the Win32 GDI, although not with traditional graph orientation in mathematics, where increasing values of y move up, not down. The default units for the coordinate system are screen pixels for most Graphics objects. These are only the defaults—it is possible to change the orientation and units of the coordinate system, as we will see later.

The Point type has two properties, X and Y. Similarly, Size has Width and Height properties. These are all of type int or Integer. Both Point and Size define an IsEmpty property, which returns true if both dimensions' values are zero. Both define explicit conversion operators to convert from one to the other, so you can convert a Point to a Size and vice versa in C#, as shown in Example 7-3. These conversions map X onto Width and Y onto Height.

Example 7-3. Converting between Point and Size in C#
Point p = new Point (10, 10);
Size s = (Size) p;    // same as s = new Size (p.X, p.Y);
s.Width += 5;
Point p2 = (Point) s; // same as p2 =
            //   new Point(s.Width, s.Height);

Because VB doesn't directly support conversion operators, you have to take advantage of the fact that the conversion operators are translated into op_Explicit method calls, as shown in Example 7-4. (Alternatively, you could simply construct new Point or Size values directly.)

Example 7-4. Converting between Point and Size in VB
Dim p As New Point(10, 10)
Dim s As Size = p.op_Explicit(p)
s.Width += 5
Dim p2 As Point = s.op_Explicit(s)

The Point and Size types also overload the addition and subtraction operators. For Size this is straightforward—adding one Size to another creates a new Size whose Width and Height are the sum of the Width and Height properties of the originals. For the Point type, it is a little more complex—the only thing you can add to or subtract from a Point is a Size. (This is because conceptually a Point doesn't have a magnitude, so it's not clear what addition or subtraction of two Point values would mean.) Addition and subtraction move the position of the Point by the amount specified in the Size. Example 7-5 creates a Point and then moves it 5 pixels down and 2 pixels along by adding a Size, leaving the Point p at (12, 15).

Example 7-5. Moving a Point by adding a Size in C#
Point p = new Point (10, 10);
Size s = new Size (2, 5);
p = p + s;

Again, because VB doesn't support operator overloading, you can call the op_Addition method, the method that the overloaded addition operator is actually translated into at runtime. The VB that is equivalent to the C# code in Example 7-5 is shown in Example 7-6.

Example 7-6. Moving a Point by adding a Size in VB
Dim p As New Point(10, 10)
Dim s As New Size (2, 5)
p = Point.op_Addition(p, s)

The Rectangle class has both position and size. It can be constructed either from a Point and a Size, or with four integers, as shown in Example 7-7. Note that, except for slight syntactical differences, the C# and VB code are nearly identical.

Example 7-7. Creating a Rectangle
// C# code
Point p = new Point (10, 10);
Size s = new Size (20, 20);
Rectangle r = new Rectangle(p, s);
Rectangle r2 = new Rectangle(10, 10, 20, 20);

' VB code
Dim p As New Point(10, 10)
Dim s As New Size(20, 20)
Dim r As New Rectangle(p, s)
Dim r2 As New Rectangle(10, 10, 20, 20)

Rectangle provides a Location property of type Point, and a Size property of type Size. Rectangle also makes the same location and size information accessible through the X, Y, Width, and Height properties; this is just for convenience—these represent the same information as the Location and Size properties. This may seem pointless, but there is a subtle issue that means the following code will not compile:

myRectangle.Size.Width = 10;  // Won't compile

The compiler will complain that the Width property of a Size object can only be set if that Size is a variable, not a property. This is because the Size type is a value type, and Rectangle.Size is a property, not a field; this has the effect that the Size property can only be used to change the rectangle's size in its entirety, i.e., setting both the width and height in one operation. So the Width and Size properties supplied by the Rectangle are convenient because they make it possible to adjust the two dimensions independently, as shown in Example 7-8. The X and Y properties do the same job for the location, because Point is also a value type.

Example 7-8. The correct way to adjust a rectangle's width
myRectangle.Width = 10;

Rectangle also provides Top, Bottom, Left, and Right properties. These are read-only, because it is not obvious whether changing one of them should move or resize the rectangle. The rectangle's Location refers to its top left corner. So the Left property is synonymous with X, and Top with Y. These names presume a coordinate system where increasing values of x and y move to the right and down, respectively.

Rectangle does not overload the addition and subtraction operators, because it is ambiguous: should they move or resize the rectangle? Moving it is simple enough—just adding a Size to its Location property works:

myRectangle.Location += new Size (10, 20);

Here again, because VB does not support operator overloading (in this case, as we have seen earlier, the overloading of the Point type returned by the Location property), we have to call the Point type's op_Addition property, as follows:

myRectangle.Location = Point.op_Addition(myRectangle.Location, _
             New Size(10, 20))

To change the size, you can add a Size to the Size property in much the same way. Alternatively, you can call the Inflate method. This also adjusts the size, but in a slightly different way—inflating a rectangle by, say, (10, 10) actually moves all four edges of the rectangle out by 10. This makes each edge 20 units longer, and also adjusts the position so that the rectangle's center remains in the same place.

All these are value types, not reference types. This is partly because coordinates are used and modified extensively when using GDI+, so the overhead of putting them on the heap would be high. Also, coordinates are value-like entities—it doesn't really make any sense for a coordinate object to have its own identity. That would lead to the potential for bugs in which a programmer could set the Size of two rectangles to be the same, and then fail to realize that (because they are reference types) modifying the Size of one would also implicitly modify the other. This doesn't happen with value types, although they are not without their own complications, such as the issue with independent adjustment of width and height of a Rectangle, discussed above.

The three types discussed here all represent coordinates using int or Integer. GDI+ also supports the use of float or Single for all coordinates. This can allow applications much greater flexibility for their internal coordinate systems, and also makes it easier to apply certain kinds of transformations correctly. And because modern processors can manage floating-point arithmetic extremely quickly, there are no performance reasons not to use floating-point values. So each type discussed so far has a floating-point counterpart—the PointF, SizeF, and RectangleF types are similar to Point, Size, and Rectangle, respectively, except they use float or Single instead of int or Integer.

You may be wondering what a coordinate represented by a Point means in terms of position on the screen. By default, the units used by the coordinate system correspond to pixels—the Point whose value is (10, 20) represents a position 10 pixels to the right and 20 pixels down from the origin. The origin is usually the top lefthand corner of whatever is being drawn into (e.g., the window or a bitmap). But as you will see towards the end of this chapter, GDI+ lets you use different coordinate systems if you want to—it can automatically apply a transform to all coordinates to map them onto pixel positions.

Of course, position and size aren't everything. In order to draw something, GDI+ will need to know what color it should use, so we will now look at how colors are represented in this programming model.

7.2.1.3 Color

All drawing needs to be done in some color or other.[4] In GDI+ the Color type represents a color. It is used anywhere that a color needs to be specified. Color is a value type.

[4] For the purposes of this discussion, black and white are considered to be colors too.

Color can represent any color that can be expressed as a combination of red, green, and blue components, using 8-bit values for each. (This is the usual way of specifying colors in computing, because of how color displays work.) It can also support transparency with an alpha channel, another 8-bit value that indicates whether the color should be displayed as opaque, and if not, how transparent it should be—this is used when drawing one color on top of another. (The transparent drop shadows that Windows XP draws around menus use alpha blending, for example.)

Example 7-9 shows how to build a color value from its red, green, and blue components. This particular color will be orange (which is approximately two parts red to one part green when using additive primary colors).

Example 7-9. Building a color from RGB components
// C#
Color orange = Color.FromArgb(255, 128, 0);

' VB
Dim orange As Color = Color.FromArgb(255, 128, 0)

The Argb part of the method stands for "alpha, red, green, and blue." This is a little confusing because there are several overloads, not all of which take all the components. Example 7-9 just passes RGB but not A, for example. It is equivalent to this code:

// C#
Color orange = Color.FromArgb(255, 255, 128, 0);

' VB
Dim orange As Color = Color.FromArgb(255, 255, 128, 0)

This specifies an alpha value of 255, i.e., a completely non-transparent color. The three-component version shown in Example 7-9 always builds a non-transparent color. (It would have been less confusing if the RGB-only method was just called FromRgb, rather than being an overload of FromArgb.)

As well as building colors from their component parts, you can also use named colors. The Color type has static properties for each standard named web colors, so you can just specify colors such as Color.Teal or Color.AliceBlue. Alternatively you can use the SystemColors class, which provides static properties for each user-configurable system color, such as those used for window titles, menu items, etc. So to draw something in the color currently defined for control backgrounds, simply use SystemColors.Control.

If you are overriding the OnPaint method in a control, you should use the built-in ForeColor and BackColor properties where appropriate, rather than hard-wiring colors in. And if you need more colors in your control than a foreground and background color, consider adding extra properties to allow the user to edit these.

It is useful to be aware of the ControlPaint utility class. One of the facilities it provides is the ability to create a modified version of a color for drawing things such as shadows and highlights. For example, if you want to draw a bezel or similar 3D effect, you should always choose colors that are based on the background color; for example, use ControlPaint.Dark to get the "in shadow" version of a color, and ControlPaint.Light to get the pale version. Examples Example 7-10 and Example 7-11 draw a 3D dividing line at the top of the control, using whatever the control's background color is. (And because BackColor is an ambient property, by default, this will be whatever the background color of the containing form is.)

Example 7-10. Sensitivity to background color in C#
protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;

    using (Pen light = new Pen(ControlPaint.Light(BackColor)),
      dark = new Pen(ControlPaint.Dark(BackColor)))
    {
        g.DrawLine(dark, 0, 0, Width, 0);
        g.DrawLine(light, 0, 1, Width, 1);
    }
    base.OnPaint(pe)
}
Example 7-11. Sensitivity to background color in VB
   Protected Overrides Sub OnPaint(pe As PaintEventArgs)
      Dim g As  Graphics = pe.Graphics
      Dim light As New Pen(ControlPaint.Light(BackColor))
      Dim dark As New Pen(ControlPaint.Dark(BackColor))

      Try
         g.DrawLine(dark, 0, 0, Width, 0)
         g.DrawLine(light, 0, 1, Width, 1)
      Finally
         light.Dispose()
         dark.Dispose()
      End Try
      MyBase.OnPaint(pe)
   End Sub

Note that Color is a value type, not a class, for much the same reasons as the Point, Size, and Rectangle types—because colors are used extensively, and they make more sense as values than as objects. So Color values do not need to be freed with a using statement in C# or a call to Dispose in VB. (The using block in Example 7-10 frees the Pen objects, not the Color values.)

The Color type cannot be used in isolation when drawing. There is more to the way that GDI+ draws things than mere color, so GDI+ requires that all filled shapes be drawn with a Brush object, and outlines be drawn with a Pen object. So we will now look at these.

7.2.1.4 Brushes

GDI+ uses the Brush class to determine how it should paint areas of the screen. So you must pass a Brush of some kind to all the FillXxx methods of the Graphics class, and also to the DrawString method.

Brush is an abstract class. This is because there are several different ways GDI+ can fill in an area: it can use a single color, a pattern, a bitmap, or even a range of colors using so-called gradient fills. For each of these fill styles, there is a corresponding concrete class deriving from Brush.

The simplest type of brush is SolidBrush. When painting an area with this kind of brush, GDI+ will paint with a single color. This is the most common type of brush, particularly for text, where more complex textured or patterned brushes would be likely to make the text illegible. Examples Example 7-12 and Example 7-13 use two solid brushes. The first is based on the control's ForeColor property and is used to draw some text. The second is a pale shade of green used to draw a rectangle.

Example 7-12. Using SolidBrush in C#
protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;

    Color transparentGreen = Color.FromArgb(128, Color.PaleGreen);
    using (Brush fb = new SolidBrush(ForeColor),
       gb = new SolidBrush(transparentGreen))
    {
        g.DrawString("Hello!", Font, fb, 0, 5);
        g.FillRectangle(gb, 10, 0, 15, 25);
    }
        base.OnPaint(pe);
}
Example 7-13. Using SolidBrush in VB
Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)

   Dim g As Graphics = pe.Graphics
   Dim transparentGreen As Color = Color.FromArgb(128, _
                         Color.PaleGreen)
   Dim fb As Brush = New SolidBrush(ForeColor)
   Dim gb As Brush = New SolidBrush(transparentGreen)

   Try
      g.DrawString("Hello!", Font, fb, 0, 5)
      g.FillRectangle(gb, 10, 0, 15, 25)
   Finally
      fb.Dispose()
      fb.Dispose()
   End Try

   MyBase.OnPaint(pe)

End Sub

The output of this code, shown in Figure 7-1, illustrates that the SolidBrush class has a slightly misleading name. The transparentGreen color is see-through—even though the rectangle is drawn on top of the text, the "Hello!" string remains visible through the rectangle. If you try this code, you will see that the text is also tinted green underneath the rectangle. This is because the color has a partially transparent alpha value—it was built using the version of Color.FromArgb that takes an alpha value and a color as parameters and returns a transparent version of the color. So it turns out that the SolidBrush can be used to draw transparent colors. This is because the "Solid" name simply indicates that the same color (transparent or not) is used across the entire area of the fill.

Figure 7-1. A transparent SolidBrush
figs/winf_0701.gif

It is not always necessary to construct your own SolidBrush. If you require a brush that represents a system color (such as the control background or menu text color) GDI+ can provide ready-built brushes. There are two classes that supply Brush objects as static properties: Brushes and SystemBrushes. These provide solid brushes whose colors are those provided by the static members in Color and SystemColors. Because these are globally available brushes, it is not your responsibility to free them after using them—you are only required to call Dispose on objects that you created or caused to be created. In fact, you are not allowed to dispose of such brushes—doing so will cause an exception to be thrown. (Disposing of a brush obtained from the Brushes class does not currently throw an exception immediately. You will get an exception the next time you try to use a brush of the same color from the Brushes class.) As Example 7-14 shows, we can just use such a brush directly, and we don't need the using syntax in C#, nor do we need to call Dispose directly in VB. (In this case, we are painting the control with the background color of a ToolTip. ClientRectangle is a property of the Control class that returns a Rectangle indicating the area of the control that can be drawn on; for most controls, this is the control's size, but for a form, it is just the client area, without the borders or titlebar.)

Example 7-14. Using SystemBrushes
// C# code
protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;
    g.FillRectangle(SystemBrushes.Info, ClientRectangle);
    base.OnPaint(pe);
}

' VB code
Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)
    Dim g As Graphics = pe.Graphics
    g.FillRectangle(SystemBrushes.Info, ClientRectangle)
    MyBase.OnPaint(pe)
End Sub

Annoyingly, there are certain omissions from SystemBrushes. It only supplies the colors considered to be background colors. This mostly makes sense when you realize that there is a corresponding SystemPens class for the foreground colors, but it is unhelpful, because you sometimes need a brush for a foreground color, such as when displaying text. (So the absence of SystemBrushes.MenuText is particularly unhelpful for owner-drawn menus.) Fortunately, you can still get the system to supply you with an appropriate brush by calling the static SystemBrushes.FromSystemColor method. As with brushes returned by the static properties, you should not dispose of these brushes—they are cached by GDI+.

If you don't want to paint the entire fill area uniformly with one color, you might find the HatchBrush class to be more appropriate. (This class is defined in the System.Drawing.Drawing2D namespace, so you may need to add an extra using (in C#) or Imports (in VB) declaration at the top of your source file to use this brush.) This allows a repeating pattern to be drawn with two colors. The pattern must be one of those listed in the HatchStyle enumeration, which contains a list of patterns that will be familiar to long-term Windows developers, such as Trellis or Plaid. Although this requirement is fairly limiting—you can't define your own hatch styles—it can be useful for certain effects if a system style happens to suit your needs. Examples Example 7-15 and Example 7-16 show a HatchBrush being used to draw a half-transparent blue hatch pattern over a control. The result will look like Figure 7-2—Internet Explorer uses a similar effect to highlight bitmaps when you select parts of a web page by dragging the mouse.

Example 7-15. Using a HatchBrush in C#
protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;

    using (Brush tb = new SolidBrush (ForeColor),
       rb = new HatchBrush (HatchStyle.Percent50,
                      SystemColors.Highlight,
                      Color.Transparent))
    {
        g.DrawString ("Hello", Font, tb, 0, 0);
        g.FillRectangle(rb, ClientRectangle);
    }
    base.OnPaint(pe);
}
Example 7-16. Using a HatchBrush in VB
Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)
   Dim g As Graphics = pe.Graphics
   Dim tb As Brush = New SolidBrush(ForeColor)
   Dim rb As Brush = New HatchBrush(HatchStyle.Percent50, _
                  SystemColors.Highlight, _
                  Color.Transparent)
   Try
      g.DrawString("Hello", Font, tb, 0, 0)
      g.FillRectangle(rb, ClientRectangle)
   Finally
      tb.Dispose()
      rb.Dispose()
   End Try
   MyBase.OnPaint(pe)
End Sub
Figure 7-2. A HatchBrush in action
figs/winf_0702.gif

The HatchBrush draws in two colors, one for the foreground parts of the hatch pattern, and one for the background parts. This example has used Color.Transparent (a completely transparent color) for the background, which is why the text is visible through the hatched rectangle, even though the rectangle was drawn over it.

The HatchBrush class is very convenient to use because it comes with a set of built-in patterns. But this is also its weak point—it is not customizable. Fortunately, there is another type of brush that lets you use any fill pattern you like: TextureBrush. You construct a TextureBrush by supplying an Image object. The Image class represents pictures, typically bitmaps, and we will look at it shortly, but for now, we will just expose a property whose type is Image, which will enable users to supply a bitmap using the Forms Designer.

Examples Example 7-17 and Example 7-18 show how to create a TextureBrush based on an Image, and also how to pass the responsibility for creating the Image on to the user by making her supply one in the Forms Designer. The results can be seen in Figure 7-3—the text has been painted with a bitmap filling. TextureBrush also supports transparency, including bitmaps whose transparency is determined per-pixel.

Example 7-17. Creating and using a TextureBrush in C#
private Image fill;

[Category("Appearance")]
public Image FillImage
{
    get
    {
        return fill;
    }
    set
    {
        if (value != fill)
        {
  fill = value;
  Invalidate();
        }
    }
}

protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;

    if (fill != null)
    {
        using (Brush b = new TextureBrush(fill))
        {
  g.DrawString ("Hello", Font, b, 0, 0);
        }
    }
    base.OnPaint(pe);
}
Example 7-18. Creating and using a TextureBrush in VB
Private fill As Image

<Category("Appearance")> Public Property FillImage() As Image
    Get
        Return fill
    End Get
    Set(ByVal Value As Image)
        If Not fill Is Value Then
  fill = Value
  Invalidate()
        End If
    End Set
End Property

Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)

    Dim g As Graphics = pe.Graphics

    If Not fill Is Nothing Then
        Dim b As Brush = New TextureBrush(fill)
        Try
  g.DrawString("Hello", Font, b, 0, 0)
        Finally
  b.Dispose()
        End Try
    End If
    MyBase.OnPaint(pe)
End Sub
Figure 7-3. A TextureBrush in action
figs/winf_0703.gif

There is also a style of brush to support gradient fills. A gradient fill changes color from one place to another. These are used extensively in Windows XP to provide a less flat appearance to the UI. Examples Example 7-19 and Example 7-20 show how to paint the control's background with a gradient fill ranging from the foreground color at the top to the background color at the bottom. (Recall that Windows Forms will call OnPaintBackground to clear your control's background before calling OnPaint.) The results can be seen in Figure 7-4. (In a real application, you would normally want to pick a pair of colors that were more similar to get a less dramatic spread of colors for the background. Most of Windows XP's gradient background fills use only a very subtle change in color.)

Example 7-19. Using a LinearGradientBrush in C#
protected override void OnPaintBackground(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;

    using (Brush bg = new LinearGradientBrush(ClientRectangle,
                    ForeColor, BackColor,
                    LinearGradientMode.Vertical))
    {
        g.FillRectangle(bg, ClientRectangle);
    }
    // No need to call base for OnPaintBackground
}
Example 7-20. Using a LinearGradientBrush in VB
Protected Overrides Sub OnPaintBackground(ByVal pe As PaintEventArgs)
    Dim g As Graphics = pe.Graphics

    Dim bg As Brush = New LinearGradientBrush(ClientRectangle, _
            ForeColor, BackColor, _
            LinearGradientMode.Vertical)
    Try
        g.FillRectangle(bg, ClientRectangle)
    Finally
        bg.Dispose()
    End Try
    ' No need to call base for OnPaintBackground
End Sub

In Examples Example 7-19 and Example 7-20, we have used a LinearGradientBrush. Its constructor is overloaded, allowing the fill to be set up in various ways. The constructors all take a start color and an end color; what differs is the way the start and end coordinates of the fill can be set. In this case, we have passed a rectangle to specify the bounds of the fill and used the LinearGradientMode enumeration to indicate how the gradient should fill the rectangle. The options are self-explanatory—Horizontal, Vertical, ForwardDiagonal, and BackwardDiagonal. If you want more control over the angle of the fill, there is another constructor that takes a float or Single in place of a LinearGradientMode, specifying the fill angle in degrees. There is also a constructor that takes a pair of points, indicating the start and end points of the fill. It is even possible to specify multi-stage fills that use several different colors, using the InterpolationColors property—see the ColorBlend class in the reference section for details.

Figure 7-4. A linear gradient fill
figs/winf_0704.gif

GDI+ supports two different kinds of gradient fill brushes. As well as the simple linear gradient, there is the PathGradientBrush class. While the LinearGradientBrush can only draw gradients going in a single direction, the PathGradientBrush can handle any shape. Examples Example 7-21 and Example 7-22 show how to create and use a PathGradientBrush to draw an ellipse-shaped gradient fill. (The GraphicsPath class in the System.Drawing.Drawing2D namespace will be described later on in this chapter. For now it is enough to know that it can describe arbitrary shapes; in this case, we are using it to describe an ellipse.)

Example 7-21. Using a PathGradientBrush in C#
protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;
    using (GraphicsPath gp = new GraphicsPath())
    {
        gp.AddEllipse(ClientRectangle);
        using (PathGradientBrush b = new PathGradientBrush(gp))
        {
  b.CenterColor = Color.Cyan;
  Color[] outerColor = {Color.Navy};
  b.SurroundColors = outerColor;
  g.FillEllipse(b, ClientRectangle);
        }
    }
    base.OnPaint(pe)
}
Example 7-22. Using a PathGradientBrush in VB
Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)

    Dim g As Graphics = pe.Graphics
    Dim gp As New GraphicsPath()

    Try
       gp.AddEllipse(ClientRectangle)
       Dim b As PathGradientBrush = New PathGradientBrush(gp)
       Try
b.CenterColor = Color.Cyan
Dim outerColor() As Color = {Color.Navy}
b.SurroundColors = outerColor
g.FillEllipse(b, ClientRectangle)
       Finally
b.Dispose()
       End Try
    Finally
       gp.Dispose()
    End Try
    MyBase.OnPaint(pe)
End Sub

The result of this is shown in Figure 7-5. As you can see, the shading changes from the center in accordance with the shape of the path. In this case, we have drawn the object to be the same shape as the fill path, but this is not mandatory—we could equally have drawn some text with such a fill.

Figure 7-5. A path gradient fill
figs/winf_0705.gif

So we have seen how to control the way in which GDI+ fills in an area when painting to the screen. We can use a simple single color, a predefined hatch pattern, a bitmap, or a gradient fill. But many of the drawing operations provided by the Graphics class do not fill areas of the screen—they draw outlines instead. The options available for an outline's appearance are quite different from those for a filled area, so GDI+ defines a separate type to deal with this: Pen.

7.2.1.5 Pens

Just as the Brush class defines the way in which GDI+ will fill in areas of the screen, the Pen class determines how it draws outlines. However, Pen is not abstract; on the contrary, it is sealed or NonInheritable, which means that unlike the Brush family of classes, there is only one type of Pen. However, a Pen can use a Brush to control how it paints, so it supports all the same drawing techniques.

The Pen class provides features unique to outline drawing. For example, it allows a line thickness to be specified. Examples Example 7-23 and Example 7-24 draw 10 lines of varying thickness. Note that in this case, each Pen object is created based on a Color. We could also have supplied a SolidBrush of the appropriate color, but in this case it is easier to use the Pen constructor that takes a Color and a thickness (as a float or Single). Examples Example 7-23 and Example 7-24 also show the use of the StartCap and EndCap properties to set the style of the starts and ends of the lines.

Example 7-23. Selecting the line thickness in C#
protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;

    for (int i = 0; i < 10; ++i)
    {
        using (Pen p = new Pen(ForeColor, i))
        {
  p.StartCap = LineCap.Square;
  p.EndCap = LineCap.ArrowAnchor;
  g.DrawLine(p, i*15 + 10, 10, i*15 + 50, 50);
        }
    }

    base.OnPaint(pe);
}
Example 7-24. Selecting the line thickness in VB
Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)

    Dim g As Graphics = pe.Graphics
    Dim i As Integer

    For i = 0 to 9
        Dim p As New Pen(ForeColor, i)
        Try
  p.StartCap = LineCap.Square
  p.EndCap = LineCap.ArrowAnchor
  g.DrawLine(p, i*15 + 10, 10, i*15 + 50, 50)
        Finally
  p.Dispose()
        End Try
    Next
    MyBase.OnPaint(pe)
End Sub

The results are shown in Figure 7-6. Note that the first two lines appear to be the same width. This is because their widths are 0.0 and 1.0 respectively, and a line will always be drawn at least 1 pixel thick. (The default coordinate system when painting to the screen uses pixels as units, so the line whose width is 1.0 is also one pixel thick.)

Figure 7-6. Line thickness and caps
figs/winf_0706.gif

Drawing a thick line has the side effect that its bounding box on screen might be larger than the bounding box containing its endpoints. Figure 7-7 shows the same lines with their centers overlaid. This illustrates that both the width and the cap style can influence whether painting happens outside the bounds of the endpoints. This is a particularly important issue if your implementation of OnPaint uses the ClipRectangle property on the PaintEventArgs object to determine what does and doesn't need to be drawn. If your drawing test works on line coordinates alone you might decide not to draw a line that is in fact partially visible. You must always add sufficient leeway to take the width into account.

Figure 7-7. Line centers
figs/winf_0707.gif

By default, a Pen object will draw a solid line with no breaks. However, if you set the DashStyle property, you can draw dashed lines. This property's type is the DashStyle enumeration, which provides eponymous Dash, DashDot, DashDotDot, and Dot patterns. If these do not suit your needs, you can use the Custom style to define your own dash pattern. In this case, you must set the DashPattern property of the Pen to an array specifying the pattern. Each float or Single in this array alternately defines the length of a dash or a gap between two dashes.

Examples Example 7-25 and Example 7-26 draw a line with a custom dash pattern with alternating medium and long dashes, interspersed with short breaks, as defined by the pattern array.

Example 7-25. Creating a custom dash pattern in C#
protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;

    using (Pen p = new Pen(Color.Black))
    {
        p.DashStyle = DashStyle.Custom;
        float[] pattern = { 10, 2, 20, 2 };
        p.DashPattern = pattern;
        g.DrawLine(p, 2, 2, 100, 2);
    }

    base.OnPaint(pe);
}
Example 7-26. Creating a custom dash pattern in VB
Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)

    Dim g As Graphics = pe.Graphics

    Dim p AS New Pen(Color.Black)
    Try
        p.DashStyle = DashStyle.Custom
        Dim pattern() As Single = { 10, 2, 20, 2 }
        p.DashPattern = pattern
        g.DrawLine(p, 22, 22, 100, 22)
    Finally
        p.Dispose()
    End Try
    MyBase.OnPaint(pe)
End Sub

Figure 7-8 shows the result.

Figure 7-8. A custom dash pattern
figs/winf_0708.gif

When drawing a shape with corners (such as a rectangle), there are several different ways the corners can be displayed. You can set the Pen class's LineJoin property to be rounded off (LineJoin.Round), beveled (LineJoin.Bevel), or mitered (LineJoin.Miter), as shown in Figure 7-9.

Figure 7-9. Round, beveled, and mitered corners
figs/winf_0709.gif

A Pen can be constructed using either a Color or a Brush. But as with brushes, if you just need a simple pen to draw in either a well-known color or a system color, you can use the static properties in the Pens and SystemPens classes, respectively. Just as SystemBrushes only supplies brushes for the background-like colors, SystemPens only provides foreground-like colors, but again you can obtain a Pen for any of the missing system colors with the static FromSystemColor method. As with brushes, you must not call Dispose on pens obtained from the Pens or SystemPens classes, because they are cached, and are therefore considered to be owned by GDI+, not by you. Attempting to dispose of them will cause an exception to be thrown.

We have now seen the basic toolkit of objects used for drawing. Graphics represents a surface that we can draw onto, typically a window or a bitmap. Coordinates and sizes are represented by Point, Size, and Rectangle, and their floating-point equivalents, PointF, SizeF, and RectangleF. We tell the Graphics object how we would like it to paint areas and outlines using the Brush and Pen classes, and we use Color to specify the colors with which we would like to draw. So let us now look at how to use these to perform some specific drawing operations.

7.2.2 Shapes

The purpose of GDI+ is to allow us to draw images. GDI+ therefore provides us with a set of building blocks—primitive shapes from which we can construct drawings. We have already seen simple examples of this when looking at the brushes and pens, but we will now take a more detailed look at the available facilities.

We draw shapes by calling methods on a Graphics object, which represents the surface we are drawing on, be it a window or a bitmap or something else. These methods fall into two categories: those that paint filled areas, and those that draw outlines. With certain exceptions, the former all begin with Fill... and the latter begin with Draw.... In most cases, the same kinds of shapes can be drawn either filled or in outline; i.e., for any given shape, there is normally a Fill... and a Draw... method.

7.2.2.1 Rectangles and ellipses

The simplest shapes to draw are rectangles and ellipses. Although these obviously look very different, they turn out to be similar in use—when drawing an ellipse, you specify its size and position in exactly the same way as for a rectangle.

The Graphics class provides four methods for drawing these shapes. DrawRectangle and DrawEllipse draw the shapes in outline using a Pen, and FillRectangle and FillEllipse fill the shapes using a Brush. These methods are all overloaded, allowing you to specify the size and position in a variety of different ways. You can supply four numbers (either as int/Integer in VB—or float/Single in VB): the x and y coordinates and the width and height. The coordinates specify the top-left corner for rectangles, or the top-left corner of the bounding box for ellipses. Alternatively, you can pass a Rectangle value. Finally, you can also supply a RectangleF to all the methods apart from DrawRectangle.[5]

[5] This appears to be an accidental omission. Future versions of the framework may rectify this.

Sometimes you will want to draw several rectangles. For example, a control that draws a bar graph is likely to need to draw many. Instead of calling DrawRectangle or FillRectangle for each, it might be faster to pass an array of Rectangle or RectangleF values to the DrawRectangles and FillRectangles methods. (Despite the fact that you cannot pass a RectangleF to DrawRectangle, you can pass an array of them to DrawRectangles.)

These methods do not provide a direct way of rotating the shapes—their axes are always aligned with the horizontal and vertical drawing axis. However, it is still possible to draw a rotated ellipse or rectangle if necessary by using a transform—see Section 7.2.5 later on for details.

7.2.2.2 Lines and polygons

If you need to draw shapes that are more complex than rectangles and ellipses, you might be able to construct the picture you require out of straight lines. The Graphics class provides methods for drawing individual lines and groups of lines.

To draw a single line, use the DrawLine method, passing an appropriate Pen. You can specify the end points either by passing a pair of Point (or PointF) values, or you can pass the two coordinates as four numbers of type int (Integer in VB) or float (Single in VB), as shown previously in Example 7-23.

If you want to draw a series of connected lines, you can either call DrawLines or DrawPolygon. The difference between these is that the latter automatically draws a closed shape (i.e., it will draw an extra line connecting the final point back to the first one). Because a polygon is a closed shape, you can also draw a filled one with the FillPolygon method. Each of these methods takes an array of Point or PointF values.

When drawing a polygon, it is possible to specify points in such a way that some of its edges intersect each other. This presents FillPolygon with a problem—what should it do for areas that are enclosed by multiple edges? Figure 7-10 illustrates such a shape—the edge cuts in on itself, creating two squares in the middle of the shape. GDI+ can use two different rules to determine whether such regions should be filled. You can choose which rule is used by passing a value from the FillMode enumeration to FillPolygon.

The default is FillMode.Alternate, which means that each time a boundary is crossed, GDI+ will alternate between filling and not filling. For the first shape shown in Figure 7-10, this means that neither interior square is filled.

Figure 7-10. The Alternate and Winding fill modes
figs/winf_0710.gif

The other mode, FillMode.Winding, is a little more complex—it takes the direction that edges are pointing into account,[6] which means that interior regions may or may not be filled. This mode is rarely used—certain graphics systems have supported it historically, but unless you need compatibility with these, you will probably not use it.

[6] More precisely, when working across the shape, it maintains a count that is incremented every time an upward-facing edge is crossed and decremented every time a downward facing edge is encountered. It will fill the shape in any regions for which this count is nonzero. For this reason, this mode is also sometimes known as the nonzero winding rule.

Shapes made out of straight lines are all very well, but sometimes you will want to draw curved lines instead. GDI+ has full support for these too.

7.2.2.3 Curves

There are several different ways of drawing curved shapes. GDI+ lets you draw elliptical arcs, Bézier curves, and cardinal splines.

There are two ways of drawing sections of an ellipse: arcs and pies. An arc is a subsection of the perimeter of an ellipse, and as such can only be drawn in outline. So there is a DrawArc method, but no corresponding Fill... method. A pie is similar to an arc, but it defines a closed area by adding two lines joining the ends of the arc to the center of the ellipse—it is called a "pie" because you would use these to draw a segment of a pie chart. Because a pie is a closed area, there are both DrawPie and FillPie methods. Example output of each method is shown in Figure 7-11.[7]

[7] There is no direct support for drawing a chord, but this is easy enough to recreate using paths, which are described in the next section.

Figure 7-11. Output from DrawArc, DrawPie and FillPie
figs/winf_0711.gif

Because all three methods describe a segment of an ellipse, they all take similar sets of parameters. There are methods that take six numbers (either as int/Integer or float/Single), four of which describe the ellipse's x and y position and its width and height, and the other two of which describe the starting angle of the segment (in degrees, clockwise from the x axis) and the sweep angle. Alternatively, there are methods that use a Rectangle to describe the ellipse, with two float/Single parameters to describe the start and sweep angles. Finally, there are versions that take a RectangleF and two float/Single angles, although in another curious omission, FillPie only has three of these overloads and does not accept a RectangleF.[8]

[8] There is another more subtle anomaly: in the all-numbers versions, where the rectangle is specified in integer units, the angles are too, but in the methods that take a Rectangle, which uses integer units, the angles are specified as float/Single.

Elliptical segments are useful for certain applications, but you will often need a more flexible way of drawing curves. One of the other curve types offered by GDI+ is the cardinal spline, drawn by the DrawCurve method. A cardinal spline is a curve that passes through a set of points without any kinks—the line changes angle progressively to pass smoothly through each point. Figure 7-12 shows an example spline, with each of the five points that it passes through highlighted. (The points have been added for illustrative purposes. The DrawCurve method does not highlight the points like this.) There is also a DrawClosedCurve method, which draws a loop by joining the last point back to the starting point. Because this defines a closed shape, there is also a corresponding FillClosedCurve method.

Figure 7-12. A cardinal spline
figs/winf_0712.gif

Each of these methods can take an array of either Point or PointF values. They also take an optional float/Single representing the "tension" in the curve—this controls how close the curvature comes to the points. As the tension approaches zero, the curvature becomes tighter and happens closer to the points, with the lines becoming entirely straight at zero tension. As the tension increases, the lines become flatter around the control points, with the curvature being pushed out to the middle of the segments. The default tension is 0.5.

Tension in Cardinal Splines

This tension parameter exists because cardinal splines are meant to model the behavior of the wooden splines that were used for drafting in the days before CAD. Curves were drawn using flexible pieces of wood called splines; the bendiness of the wood influenced the shape of the curve, so several different thicknesses of spline were usually available to provide different effects. The tension parameter is designed to model this level of flexibility.


Another popular type of spline is a Bézier curve. With Bézier curves, each line segment is controlled by four points. As well as the start and end points, there are two other control points that determine the tangent and the rate of curvature at each end of the segment. This allows much more precise control of the shape than is possible with a cardinal spline's tension parameter, as the curvature can be adjusted on a per-segment basis.

Bézier curves are widely used in font design and for many graphic design applications because they offer such a high level of control. They do, however, require a little more effort to use than cardinal splines, on account of needing two control points to be defined for each segment, not just its endpoints. Because the control points define the tangent of the curve, you are also responsible for making sure that adjacent segments are cotangential if you want to avoid discontinuities in the curve, as Figure 7-13 shows. (The control points and the tangents that they form have been shown on this diagram. As you can see, the tangents on the point shared by the two segments at the top do not line up, so the curve has a kink.)

Figure 7-13. Bézier curves with discontinuity
figs/winf_0713.gif

Bézier curves are always drawn as open curves, so there are no Fill... methods for them on the Graphics class. (They can still be used to paint filled areas by building them into a path as described in the next section.) The DrawBezier method can be passed the four control points as Point or PointF values, or eight float values (but not int values for some reason). There is also DrawBeziers, which draws a connected series of curves. It takes an array of Point or PointF values. DrawBeziers presumes that each segment's endpoint will be the following segment's starting point, so although four points are required for the first segment, each subsequent segment only requires three more points. This is illustrated in Examples Example 7-27 and Example 7-28, which draw the curve shown previously in Figure 7-13.

Example 7-27. Using DrawBeziers in C#
protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;

    Point[] curvePoints =
        {
  // First segment
  new Point(10, 10), new Point (40, 40),
  new Point(50, 10), new Point (80, 10),

  // Second segment
  new Point(110, 40), new Point(150, 10),
  new Point(150, 40),

  // Third segment
  new Point(150, 70), new Point(70, 20),
  new Point(30, 60)
        };
    g.DrawBeziers(Pens.Black, curvePoints);

    base.OnPaint(pe);
}
Example 7-28. Using DrawBeziers in VB
Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)

    Dim g As Graphics = pe.Graphics
    Dim curvePoints() As Point = _
        { New Point(10, 10), New Point (40, 40), _
New Point(50, 10), New Point (80, 10), _
New Point(110, 40), New Point(150, 10), _
New Point(150, 40), _
New Point(150, 70), New Point(70, 20), _
New Point(30, 60) }
    g.DrawBeziers(Pens.Black, curvePoints)

    MyBase.OnPaint(pe)
End Sub

The DrawBeziers method requires the points to be specified in a certain order. It starts with the first point on the line, but the next two points are control points. So in Examples Example 7-27 and Example 7-28, the curve starts at (10, 10), with the tangent heading towards the first control point (40, 40). The next coordinate is also a control point—the tangent for the other end of the first segment heads towards (50, 10). The next coordinate specifies the next point on the line, i.e., the end of the first segment. It also doubles as the starting point of the next segment. For each following segment, the three points are the two control points (specifying the tangent at the start and end of the segment, respectively) and the end point of the segment. In each case, the end point of a segment doubles as the start point of the following segment, except for the very last segment.

So we have a powerful selection of different curve types at our disposal. But there are certain restrictions—what if we want to fill an area defined with Bézier curves rather than merely drawing an outline? Or what if we would like to draw or fill a shape that uses more than one of these curve styles, or even a mixture of curves and straight lines? We can do all these things by using graphics paths, which are described next.

7.2.2.4 Paths

GDI+ provides the System.Drawing.Drawing2D.GraphicsPath class, which allows any combination of the shapes defined so far to be combined into a single object. You can then get the Graphics object to draw this composite shape either filled or in outline, just as it would draw any of the built-in shapes. This allows you to paint areas using shapes that don't have their own Fill... method. You can also add text to a path, and paths may even be combined.

Using a GraphicsPath is a two-step process. First you must create the shape, then draw the shape that you have created. Creating a shape with the GraphicsPath class is very similar to drawing with the Graphics class—it provides a method for each of the primitive shapes described so far. But rather than calling, say, FillRectangle or DrawEllipse, you call methods beginning with Add.... Because a path defines a shape rather than a drawing operation, it does not distinguish between fills and outlines; you get to make that decision when you actually draw the path—the Graphics class has both DrawPath and FillPath methods. For example, there are AddRectangle, AddEllipse, AddBezier methods, and each is used in exactly the same way as the corresponding method on Graphics. None of these methods takes a Pen or a Brush, again because you get to specify that when you draw the shape, not when you create it.

Examples Example 7-29 and Example 7-30 show how to create and draw a closed path using both Bézier curves and straight line segments. It starts by adding three Bézier curves (using the same point data as in Examples Example 7-27 and Example 7-28) and then a straight line. Finally, it calls the CloseFigure method, which converts the path from open to closed, allowing us to use it for fills as well as outlines.

Example 7-29. Building a closed path in C#
protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;

    Point[] curvePoints =
        {
  new Point(10, 10), new Point (40, 40),
  new Point(50, 10), new Point (80, 10),
  new Point(110, 40), new Point(150, 10),
  new Point(150, 40), new Point(150, 70),
  new Point(70, 20), new Point(30, 60)
        };

    using (GraphicsPath gp = new GraphicsPath())
    using (Brush b = new HatchBrush(HatchStyle.Trellis,
      Color.Aqua, Color.Navy))
    using (Pen p = new Pen(Color.Black, 5))
    {
        gp.AddBeziers(curvePoints);
        gp.AddLine(30, 60, 10, 60);
        gp.CloseFigure();
        g.FillPath(b, gp);
        g.DrawPath(p, gp);
    }

    base.OnPaint(pe);
}
Example 7-30. Building a closed path in VB
Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)

    Dim g AS Graphics = pe.Graphics
    Dim gp As New GraphicsPath()
    Dim b As Brush = New HatchBrush(HatchStyle.Trellis, _
           Color.Aqua, Color.Navy)
    Dim p As New Pen(Color.Black, 5)
    Dim curvePoints As Point() = _
        { New Point(10, 10), New Point (40, 40), _
New Point(50, 10), New Point (80, 10), _
New Point(110, 40), New Point(150, 10), _
New Point(150, 40), New Point(150, 70), _
New Point(70, 20), New Point(30, 60) }

    Try
        gp.AddBeziers(curvePoints)
        gp.AddLine(30, 60, 10, 60)
        gp.CloseFigure()
        g.FillPath(b, gp)
        g.DrawPath(p, gp)
    Finally
        gp.Dispose()
        b.Dispose()
        p.Dispose()
    End Try
    MyBase.OnPaint(pe)
End Sub

As you can see from Figure 7-14, GraphicsPath has enabled us to use Bézier curves to paint both an outline and a fill despite the fact that the Graphics class has no FillBeziers method. Also note that although we only added one straight line to the path, this shape actually has two straight lines in it at the bottom left corner. The horizontal one is the line we added by calling AddLine. The vertical one was created as a result of calling ClosePath—GDI+ detected that our shape's first and last points were in different positions, so it added an extra line segment to close the loop.

Figure 7-14. A GraphicsPath in use
figs/winf_0714.gif

A path may contain multiple closed areas—once you have called CloseFigure, you can carry on adding more elements to the shape. Each closed area in a path is referred to as a figure. The ability to contain multiple figures is particularly useful when their areas overlap—this allows shapes with holes to be created. For example, if you wanted to create a path in the shape of the capital letter R, you can use one figure to define the letter's outline, and a second to define the shape of the hole in the loop of the R, as shown in Figure 7-15.

Figure 7-15. Creating holes with a multi-figure shape
figs/winf_0715.gif

These two figures can be combined into a single GraphicsPath. The exact behavior of a path when two figures overlap is determined by its FillMode property. This works in the same way as for DrawPolygon—the default is FillMode.Alternate, which for this example will have the expected behavior: the body of the R will be filled, but the hole in the loop will not be painted at all.

Paths with holes allow you to draw things in a way that would otherwise not be possible. If you could not create such paths, the only way to draw shapes like the letter R would be to fill the outline, and then to paint the hole in a different color. The problem with that is it obscures whatever was behind the letter in the first place. But as Figure 7-16 shows, when you paint a path with holes in it, the background shows through those holes.

Figure 7-16. A path with holes
figs/winf_0716.gif

The code that draws Figure 7-16 is shown in Examples Example 7-31 and Example 7-32. It illustrates another interesting point. Whenever you add a primitive shape that is intrinsically closed to a path, there is no need to call CloseFigure. This particular example builds an ellipse and then knocks a rectangular hole in it. Because ellipses and rectangles are always closed, we did not need to call CloseFigure at any point. CloseFigure is provided to enable you to construct closed shapes using primitives that are normally open, such as lines and curves.

Example 7-31. Drawing a shape with a hole in C#
protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;

    using (GraphicsPath gp = new GraphicsPath())
    using (Brush background = new HatchBrush(HatchStyle.Trellis,
      Color.AntiqueWhite, Color.DarkBlue))
    using (Brush foreground = new HatchBrush(HatchStyle.Weave,
      Color.Black, Color.Green))
    {
        g.FillRectangle(background, ClientRectangle);

        gp.AddEllipse(new Rectangle(10, 10, 60, 60));
        gp.AddRectangle(new Rectangle(30, 30, 20, 20));
        g.FillPath(foreground, gp);
    }

    base.OnPaint(pe);
}
Example 7-32. Drawing a shape with a hole in VB
Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)

    Dim g As Graphics = pe.Graphics
    Dim gp As New GraphicsPath()
    Dim background As Brush = New HatchBrush(HatchStyle.Trellis, _
      Color.AntiqueWhite, Color.DarkBlue)
    Dim foreground As Brush = New HatchBrush(HatchStyle.Weave, _
      Color.Black, Color.Green)

    Try
        g.FillRectangle(background, ClientRectangle)

        gp.AddEllipse(new Rectangle(10, 10, 60, 60))
        gp.AddRectangle(new Rectangle(30, 30, 20, 20))
        g.FillPath(foreground, gp)
    Finally
        gp.Dispose()
        background.Dispose()
        foreground.Dispose()
    End Try
    MyBase.OnPaint(pe)
End Sub

GraphicsPath also provides a solution to a problem mentioned earlier: when drawing an outline with a pen thickness greater than 1 pixel, the bounding box of the drawn line is usually slightly larger than the bounding box of all its points. (And in the case of splines, the bounding box can be considerably larger even with single-pixel-thick lines.) GraphicsPath has a Widen method that takes a Pen and converts the path to the shape its outline would have if it were drawn using that Pen. For example, calling Widen on a straight line with a thick pen converts it to a rectangle; calling Widen on an ellipse with a thick pen converts it into a pair of concentric ellipses. In general, calling Widen and then drawing the result with FillPath gives exactly the same results as drawing with DrawPath.

The Widen method is useful because it enables you to find out exactly what shape will be drawn on screen when you draw an outline. Calling GetBounds on a widened GraphicsPath will give you the true bounding box, taking things like line width and end cap styles into account. (Note that it is not necessary to use this for hit testing—GraphicsPath supplies two hit test functions, IsVisible and IsOutlineVisible. These will tell you whether a particular point lies under the shape when drawn filled and when drawn as an outline with a particular pen.)

Another interesting feature of GraphicsPath is that you can use it to clip other drawing operations. If you create a path and then pass it to the Graphics class's SetClip method, the path will be used as a stencil through which all further drawing is done. Examples Example 7-33 and Example 7-34 show how to do this in C# and VB, respectively—the code creates a GraphicsPath containing the text "Stencil" and then draws a series of concentric circles through it.

Example 7-33. Using a GraphicsPath as a stencil in C#
protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;

    using (GraphicsPath gp = new GraphicsPath())
    using (Pen p = new Pen(ForeColor, 3))
    {
        gp.AddString("Stencil",
  FontFamily.GenericSerif, (int) FontStyle.Bold, 48,
  new Point(10, 10), new StringFormat());
        g.SetClip(gp);

        Rectangle rect = new Rectangle(93, 35, 10, 10);
        for (int i = 0; i < 30; ++i)
        {
  g.DrawEllipse(p, rect);
  rect.Inflate(4, 4);
        }
    }

    base.OnPaint(pe);
}
Example 7-34. Using a GraphicsPath as a stencil in VB
Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)

    Dim g As Graphics = pe.Graphics
    Dim gp As New GraphicsPath()
    Dim p As New Pen(ForeColor, 3)

    Try
        Dim i As Integer

        gp.AddString("Stencil", _
  FontFamily.GenericSerif, FontStyle.Bold, 48, _
  New Point(10, 10), New StringFormat())
        g.SetClip(gp)

        Dim rect As New Rectangle(93, 35, 10, 10)
        For i = 0 to 29
  g.DrawEllipse(p, rect)
  rect.Inflate(4, 4)
        Next
    Finally
        gp.Dispose()
        p.Dispose()
    End Try
    MyBase.OnPaint(pe)
End Sub

The results are shown in Figure 7-17. It is possible to combine stencils—the SetClip method is overloaded. You can pass a member of the CombineMode enumeration as an optional second parameter. This supports various set operations, which will be described in the next section. The default is CombineMode.Replace, which just replaces the previous clip region with the new one.

Figure 7-17. Drawing through a GraphicsPath
figs/winf_0717.gif

The Graphics object allows the clip region to be specified in other ways—as well as passing a GraphicsPath, you can also provide a Rectangle or a Region. It turns out that the Graphics class uses Region as its fundamental clipping primitive, and it just converts other shapes into regions, so we will now look at the Region class.

7.2.2.5 Regions

A Region is similar to a GraphicsPath in that it can be used to define arbitrary shapes. But they are designed to be used in different contexts—paths are used for defining shapes that will normally be drawn, whereas regions tend to be used for pixel-related operations such as clipping or hit testing. For example, there is no way to draw a region. Likewise, a path must be converted to a region before it can be used for clipping (although passing a path to a Graphic object's SetClip method does this automatically).

A Region can be created from a Rectangle, a RectangleF, or a GraphicsPath. Regions can also be combined, and one of the differences between regions and paths is the way in which combination works. You can add as many graphics paths as you like together, but the result is always the sum of its parts, and the only control you have over the way that overlapping paths combine is with the FillMode you specify when drawing the path. Regions offer a little more flexibility—they can be combined using various set operations.

The simplest way of combining two regions is to use the Region class's Union method. This results in a region that contains all the areas from both regions. Alternatively you can use Intersect, which creates a region containing only those areas that were covered by both regions. Slightly more subtle is the Exclude method, which creates a region that contains only those parts of the original region that were not also in the second region. (In other words, it calculates the intersection, and then subtracts that from the original. This lets you use one region to take bites out of another.) Complement does much the same thing only in reverse—it calculates the intersection and subtracts that from the second region instead of the original region. Finally, there is the Xor method, which performs an exclusive or operation; it creates a region containing all areas that were either in the first or the second region, but not in both. (Xor is effectively equivalent to FillMode.Alternate. There is no equivalent to FillMode.Winding because the winding rule depends on path direction, but regions are only concerned with area.)

The CombineMode enumeration has entries representing each combination type. This can be passed to the Graphics class's SetClip method (described earlier) to describe exactly how the new clip region should be combined with the old one. The enumeration also defines a Replace value, allowing the new clip region to replace the old one instead of being combined with it.

Regions also allow for flexible hit testing. Although GraphicsPath supplies simple hit testing with its IsVisible and IsOutlineVisible methods, these are of limited use. If you wish to test whether the mouse pointer is over a particular object, it is usually necessary to give the user a few pixels of leeway. (Many users have their mouse configured to move so quickly that they cannot actually hit certain pixels at all.) The hit testing supported by GraphicsPath is unfortunately all single pixel. However, the Region class has an overloaded IsVisible method that allows a Rectangle to be passed, and it will test whether any part of the Rectangle intersects with any part of the region. This makes it straightforward to perform hit testing with support for an arbitrary degree of sloppiness—the larger the rectangle, the greater the margin you allow for user inaccuracy.

Regions can also be used to set the shape of a control. The Control class has a property called Region, which can be used to define a nonrectangular shape for a control. This works both for forms and normal controls. Examples Example 7-35 and Example 7-36 show how to create an elliptical window. (For this to be useful, more work would be required in practice—the titlebar is mostly obscured, as are the corners of the window, so such a form would need to provide alternate mechanisms for moving and resizing the window.)

Example 7-35. Creating an elliptical window in C#
public MyForm()
{
    InitializeComponent();

    using (GraphicsPath gp = new GraphicsPath())
    {
        gp.AddEllipse(ClientRectangle);
        Region = new Region(gp);
    }
}
Example 7-36. Creating an elliptical window in VB
Public Sub New()

    InitializeComponent()

    Dim gp As New GraphicsPath()

    Try
        gp.AddEllipse(ClientRectangle)
        Region = new Region(gp)
    Finally
        gp.Dispose()
    End Try
End Sub

So we have seen that GDI+ has extensive and flexible support for shapes. This ranges from simple constructs such as rectangles and ellipses, through lines and curves, to composite shapes represented either as paths or regions. But we have not yet looked at text. Although text could be considered as just another kind of shape, it has many unique features that require special consideration. So we will now look at the support for text in GDI+.

7.2.3 Text

Almost all applications need to display text. In many cases, this can be dealt with by using built-in controls. Even when parts of the display are custom-drawn, you can often get away with using the Label class to display text. But for some controls, you will need display text from within the OnPaint method.

To represent text strings, GDI+ simply uses the .NET runtime's intrinsic System.String type (or string, as it is usually abbreviated in C#, and String, as it is usually abbreviated in VB). The only types that GDI+ defines are for modifying the text's appearance. These types fall roughly into two categories: those used to choose the typeface in which the text will be drawn, and those used to control the formatting of the text. We will start by looking at the classes used to select a typeface and associated attributes.

7.2.3.1 Fonts

The Font class determines the style in which text will be drawn. It specifies the typeface (e.g., Times Roman, Univers, or Palatino), but it also controls details such as whether bold or italic are in use, and the size of the text.

If you are writing a control, the easiest way to obtain a Font object is to use the Control class's Font property. This will pick up the ambient font (which is usually the default font—8.25pt Microsoft Sans Serif) unless the property has been set explicitly by the user. The advantages of this are that your text will be in harmony with all other text on the form by default, and you don't need to create the Font object yourself. Drawing text can be as simple as this:

// C# code
protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;

    using (Brush b = new SolidBrush(ForeColor))
    {
        g.DrawString("Hello", Font, b, 0, 0);
    }
    base.OnPaint(pe);
}

' VB code
Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)

    Dim g As Graphics = pe.Graphics
    Dim b As Brush = New SolidBrush(ForeColor)
    Try
        g.DrawString("Hello", Font, b, 0, 0)
    Finally
        b.Dispose()
    End Try
    MyBase.OnPaint(pe)
End Sub

Here, we have simply provided a text string to be displayed, and we are painting it using the font and color specified by the control's Font and ForeColor properties. The last two parameters to DrawString specify the position at which to draw the text, so this text will appear at the top-left corner of the control.

You will not always be able to rely on the Control class's Font property to supply you with a Font object. Your control might need to use more than one font, in which case you will need to build your own Font objects. The first thing to be aware of is that Font objects are immutable—having created a font, you cannot modify properties such as size or boldness. If you want an emboldened version of a font, you must create a new one. Fortunately, the Font class provides constructors that make it easy to build a font that is a slight variation on an existing one.

To build a font by modifying the use of styles such as italic or bold, you can use the Font constructor, which takes a Font and a FontStyle. FontStyle is an enumeration containing Bold, Italic, Strikeout, and Underline members to determine the style in which the text will be drawn. You can use these in any combination; for example:

FontStyle.Bold|FontStyle.Italic

or:

FontStyle.Bold Or FontStyle.Italic

or you can specify FontStyle.Regular to indicate that you require a plain version of the font. Be aware that not all typefaces support all styles—for example, some typefaces are only available in bold, and attempting to create a regular version will cause an error — later on we will see how to anticipate and avoid such problems by using the FontFamily class.

There is also a constructor that takes an existing Font and a new size. The size is an em size, which is to say it specifies the width of the letter M in the typeface. (This is the standard way of defining typeface sizes.) This will be in units of points (i.e., 1/72 of an inch; for historical reasons typeface sizes are almost always measured in points), although there is another constructor that also takes a value from the GraphicsUnit enumeration, allowing you to specify other units such as pixels or millimeters.

But if you want to create a new font from scratch, rather than basing it on an existing font, you need to tell GDI+ which font family you would like it to use. You can do this either by specifying a family name as a string (e.g., "Arial") or a FontFamily object. Using a string, such as new Font("Arial", 12) is the most straightforward, but there are certain advantages to using the FontFamily class.

Certain typeface names are a fairly safe bet—Arial, for instance, is ubiquitous because it ships with Windows. But in general there is always the risk that the typeface name you specify will not always be available. To avoid the errors that this will cause, it is usually better to use a FontFamily object. FontFamily lets you enumerate all the available typefaces, which is helpful if you want to let the user pick a font from a list. Examples Example 7-37 and Example 7-38 show how to display a list of font family names in a listbox. The code first obtains an array of FontFamily objects by calling FontFamily.GetFamilies; this must be provided with a Graphics object because the selection of fonts available may sometimes be dictated by where the drawing is taking place. So in this case, we are using the Control class's CreateGraphics method to obtain a Graphics object for our control. We then pass the FontFamily array to the listbox (listFonts) as a data source, and tell it to display the font names as list entries.

Example 7-37. Showing font families in a listbox using C#
using (Graphics g = CreateGraphics())
{
    FontFamily[] families = FontFamily.GetFamilies(g);
    listFonts.DataSource = families;
    listFonts.DisplayMember = "Name";
}
Example 7-38. Showing font families in a listbox using VB
Dim g As Graphics = CreateGraphics()
Dim families() As FontFamily = FontFamily.GetFamilies(g)
listFonts.DataSource = families
listFonts.DisplayMember = "Name"
g.Dispose()

This technique guarantees that you are only using font families that you know are present. It also enables you to find out whether a particular style of font is available. As mentioned above, not all fonts support all styles—it is quite common for a typeface to be bold only. The FontFamily class lets you find out whether a particular style is available by passing the FontStyle you would like to its IsAvailable method. This returns a bool/Boolean to indicate whether the style is supported.

Having established that the typeface you require is available in the appropriate style, you can use the Font class constructor that takes a FontFamily, a size (float/Single) and a FontStyle. This will create a brand new Font object built to your specifications.

Sometimes you will simply require a font that looks approximately right—it might be sufficient to use any old sans-serif font without caring whether it's Arial or Linotype Helvetica. The FontFamily class therefore provides some static properties that return non-specific FontFamily objects with certain broad visual characteristics. It provides a GenericSansSerif property that will return a family such as Microsoft Sans Serif or Arial. There is a GenericSerif property, which will return something like Times New Roman. Finally there is GenericMonospace, which will return a monospaced font such as Courier New.

Having chosen a typeface, we need to be able to control how it is displayed, so we will now consider how to manage features such as alignment and cropping.

7.2.3.2 Formatting

The Graphics class provides several overloads of the DrawString method. The simplest just takes a string, a Font, a Brush, and a position, and will draw the text from left to right starting exactly at the position specified. For many applications this is sufficient, but sometimes a little more control is required to get the text to appear in exactly the right position.

The most obvious example of where the simple approach falls down is if you need to right-align your text—e.g., you need some text to appear up against the far right edge of your control. It is difficult to do this by specifying the position of the top-left corner of the string—you would need to find out how long the string will be and adjust the start position accordingly. And although you can do this, there is a much simpler way.

One of the overloads of the DrawString method lets you specify the position by supplying a rectangle rather than a point. You can then pass a parameter of type StringFormat, which controls, among other things, how the string is positioned within this rectangle. The StringFormat class has properties that control horizontal and vertical positioning: Alignment and LineAlignment. These both use the StringAlignment enumeration type, and can be one of Center, Far, or Near. Near means left or top for horizontal or vertical positioning, respectively, while Far means right or bottom.[9] So it is now simple to position text without measuring it. Examples Example 7-39 and Example 7-40 draw text that is vertically centered and aligned to the righthand side of the control.

[9] These can be inverted—the StringFormat class can be configured for right-to-left text for languages where this appropriate. In this case, Near would be the right and Far would be the left.

Example 7-39. Aligning text using C#
protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;

    using (Brush b = new SolidBrush(ForeColor))
    using (StringFormat sf = new StringFormat())
    {
        sf.Alignment = StringAlignment.Far;
        sf.LineAlignment = StringAlignment.Center;

        g.DrawString("Hello", Font, b, ClientRectangle, sf);
    }
    base.OnPaint(pe);
}
Example 7-40. Aligning text using VB
Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)

    Dim g As Graphics = pe.Graphics
    Dim b As Brush = New SolidBrush(ForeColor)
    Dim sf As New StringFormat()

    Try
        sf.Alignment = StringAlignment.Far
        sf.LineAlignment = StringAlignment.Center

        g.DrawString("Hello", Font, b, _
    RectangleF.op_Implicit(ClientRectangle), sf)
    Finally
        b.Dispose()
        sf.Dispose()
    End Try
    MyBase.OnPaint(pe)

End Sub

Note that we have used the Control class's ClientRectangle property, which defines the bounds of the control. This means that the text will automatically be aligned to the control's righthand edge. When using DrawString in this way, it will also break text over multiple lines if necessary. (It will do this when the rectangle is too narrow to hold the whole string, but tall enough to hold multiple lines. If the string is too long to fit even when split, it is simply truncated.)

Many controls choose to expose an alignment property of type ContentAlignment (e.g., the Button class's TextAlign property). This allows the horizontal and vertical alignment to be set through a single property. Because there are three possible positions for each dimension (Near, Center, and Far), the ContentAlignment enumeration has nine values. Unfortunately, the framework does not currently supply a way of creating a StringFormat object whose Alignment and LineAlignment properties match the positions specified in a ContentAlignment value. The only solution, presented in Examples Example 7-41 and Example 7-42, is somewhat ugly.

Example 7-41. Converting from ContentAlignment to StringFormat in C#
private StringFormat FormatFromContentAlignment(ContentAlignment align)
{
    StringFormat sf = new StringFormat();
    switch (align)
    {
        case ContentAlignment.BottomCenter:
        case ContentAlignment.MiddleCenter:
        case ContentAlignment.TopCenter:
  sf.Alignment = StringAlignment.Center;
  break;
        case ContentAlignment.BottomRight:
        case ContentAlignment.MiddleRight:
        case ContentAlignment.TopRight:
  sf.Alignment = StringAlignment.Far;
  break;
        default:
  sf.Alignment = StringAlignment.Near;
  break;
    }
    switch (align)
    {
        case ContentAlignment.BottomCenter:
        case ContentAlignment.BottomLeft:
        case ContentAlignment.BottomRight:
  sf.LineAlignment = StringAlignment.Far;
  break;
        case ContentAlignment.TopCenter:
        case ContentAlignment.TopLeft:
        case ContentAlignment.TopRight:
  sf.LineAlignment = StringAlignment.Near;
  break;
        default:
  sf.LineAlignment = StringAlignment.Center;
  break;
    }
    return sf;
}
Example 7-42. Converting from ContentAlignment to StringFormat in VB
Option Strict On

Imports System
Imports System.ComponentModel
Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.Windows.Forms

Public Class FormatLib

Private Function FormatFromContentAlignment(align As ContentAlignment) As StringFormat

    Dim sf As New StringFormat()

    Select Case align
        Case ContentAlignment.BottomCenter, _
   ContentAlignment.MiddleCenter, _
   ContentAlignment.TopCenter
  sf.Alignment = StringAlignment.Center
        Case ContentAlignment.BottomRight, _
   ContentAlignment.MiddleRight, _
   ContentAlignment.TopRight
  sf.Alignment = StringAlignment.Far
        Case Else
  sf.Alignment = StringAlignment.Near
    End Select
    Select Case align
        Case ContentAlignment.BottomCenter, _
   ContentAlignment.BottomLeft, _
   ContentAlignment.BottomRight
  sf.LineAlignment = StringAlignment.Far
        Case ContentAlignment.TopCenter, _
   ContentAlignment.TopLeft, _
   ContentAlignment.TopRight
  sf.LineAlignment = StringAlignment.Near
        Case Else
  sf.LineAlignment = StringAlignment.Center
    End Select
    
    Return sf

End Function

End Class

The StringFormat class also allows us to control other aspects of the text's appearance. For example, we can draw the text vertically by setting the StringFormatFlags.DirectionVertical flag on its FormatFlags property. The FormatFlags property can be set at construction time by passing in a StringFormatFlags value. See the reference section for other FormatFlags options.

There is a much more flexible way of rotating than using FormatFlags. See Section 7.2.5 later in this chapter.


The StringFormat class also supports drawing hot key underlines on your controls, such as those that appear on buttons and menu items if the Alt key is held down. This is particularly useful if you are drawing your own menu items. Simply set the StringFormat class's HotkeyPrefix member to HotkeyPrefix.Show, and GDI+ will add an underline on strings containing ampersands. For example, the string E&xit would be drawn with the x underlined. (The ampersand itself is just a marker and will not be displayed.) GDI+ will also strip the ampersands out without displaying the underlines if you specify HotkeyPrefix.Hide. You would use this in an owner-drawn menu when you are asked to draw a menu without accelerators (i.e., when the DrawItemEventArgs object's State member has the NoAccelerator flag set). The default is HotkeyPrefix.None, which means that ampersands don't get any special treatment—they are just displayed as normal characters.

Sometimes it will be necessary to measure a string before drawing it. This is particularly important if you are drawing anything that manages its layout dynamically. For example, owner-drawn menus need to calculate the size of their text to handle the MeasureItem event correctly. The Graphics class therefore provides the MeasureString method.

MeasureString is overloaded. At its simplest, it just takes a string and a Font, and returns the size of that string (i.e., how much space the string would take up if drawn with that font using DrawString). However, the DrawString methods that take a rectangle can change the size of the drawn string, due to issues such as cropping. So you can pass a SizeF value to MeasureString to indicate the size of the rectangle you will be using. Because a StringFormat object can also influence the size of the output, there are overloads that accept a StringFormat as well. All these methods return a SizeF indicating how much space the string will take up when displayed.

Examples Example 7-43 and Example 7-44 illustrate the use of MeasureString in the context of an owner-drawn menu. When drawing your own menu items, Windows Forms will raise the MeasureItem event to find out how wide your owner-drawn items are. It needs to know this to determine how large the menu should be. Menu width is normally determined by the text in the menu, so we use MeasureString to find this out. Note that we don't use the height as calculated by MeasureString; we use the nominal height given by the Font object's Height property. This is to make sure that all menu items come out the same height. (We also add in the size of the menu check—system-drawn menus always leave space for this on the left. It also doesn't look right unless you make the menu item 3 pixels higher and 8 pixels wider than necessary to hold the string—the system appears to add this much padding when drawing its own menus.)

Example 7-43. Using MeasureString for an owner-drawn menu item in C#
private void Menu_MeasureItem(object sender,
    MeasureItemEventArgs e)
{
    MenuItem item = (MenuItem) sender;

    Font menuFont = SystemInformation.MenuFont;
    e.ItemHeight = menuFont.Height + 3;

    StringFormat sf = new StringFormat(
  StringFormatFlags.DisplayFormatControl);
    sf.HotkeyPrefix = System.Drawing.Text.HotkeyPrefix.Hide;

    int textWidth = (int) e.Graphics.MeasureString(item.Text,
        menuFont, new PointF(0,0), sf).Width;

    Size checkSize = SystemInformation.MenuCheckSize;
    e.ItemWidth = textWidth + checkSize.Width + 8;
}
Example 7-44. Using MeasureString for an owner-drawn menu item in VB
Private Sub Menu_MeasureItem(sender As Object, _
    e As MeasureItemEventArgs) Handles menuFile.MeasureItem

    Dim item As MenuItem = DirectCast(sender, MenuItem)

    Dim  menuFont As Font = SystemInformation.MenuFont
    e.ItemHeight = menuFont.Height + 3

    Dim sf As New StringFormat( _
    StringFormatFlags.DisplayFormatControl)
    sf.HotkeyPrefix = System.Drawing.Text.HotkeyPrefix.Hide

    Dim textWidth As Integer = CInt(e.Graphics.MeasureString( _
    item.Text, menuFont, new PointF(0,0), sf).Width)

    Dim checkSize As Size = SystemInformation.MenuCheckSize
    e.ItemWidth = textWidth + checkSize.Width + 8
End Sub

So we have now seen how to draw images using either text or shapes. But sometimes we will not wish to construct pictures using these primitives—we might already have an image stored on disk that we wish to display as is. So we will now look at the GDI+ facilities for dealing with images.

7.2.4 Images

Pictures do not necessarily have to be drawn on the fly—it is possible to store a prebuilt image in a number of formats. GDI+ defines the Image class as an abstract representation of any such image.

GDI+ supports two different types of image, bitmaps and metafiles, and there is a class deriving from Image for each: Bitmap and Metafile. Bitmaps store information as raw pixel data—a bitmap image's size is always a fixed number of pixels, and displaying them at any other size requires a certain amount of image processing and can have mixed results. Conversely, metafiles store information as a series of primitive drawing operations. This means that they can be resized and rotated more easily than bitmaps, although they are usually slower to draw than bitmaps drawn at their natural size and orientation.

Regardless of their type, images are displayed by using the Graphics class's DrawImage method. There are several overloads of this method, but they all take an Image. Some just take the position at which to draw the image, while others take a position and a size, allowing the image to be scaled. Some also take a rectangle indicating which part of the image should be displayed, so that you can draw a subsection of the image.

Images can be created as well as displayed—it is possible to use GDI+ to build a new bitmap or metafile. This is made possible by the Graphics class's static FromImage method, which creates a Graphics object that lets you draw into an image.

7.2.4.1 Bitmaps

The Bitmap class represents an image stored as pixel data. You can create Bitmap objects from files. Several formats are supported, including BMP, JPEG, PNG, TIFF, and GIF. You can also create new images from scratch.

Be aware that there are licensing issues with GIF. It uses a data compression system that is subject to a patent owned by Unisys. If your application supports GIF files, you may need to obtain a license. Contact Unisys for further information.


The Bitmap class is often used to draw bitmaps that are stored in files. To create a new Bitmap object based on a file, simply pass the filename as a string to the constructor. (Or you can pass a Stream if that is more convenient; this can be useful if your bitmap file is stored as an embedded resource.) Examples Example 7-45 and Example 7-46 create and display a Bitmap object based on one of the standard Windows background bitmaps. (You would not use a hardcoded path like this in practice of course—this is just to keep the sample code simple.)

Example 7-45. Creating and drawing a Bitmap object in C#
private Image myImage;

public MyControl()
{
    myImage = new Bitmap("c:\\windows\\Prairie Wind.bmp");
}

protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;
    g.DrawImage(myImage, 0, 0);
    base.OnPaint(pe);
}
Example 7-46. Creating and drawing a Bitmap object in VB
Private myImage As Image

Public Sub New()
    myImage = New Bitmap("c:\\windows\\Prairie Wind.bmp")
End Sub

Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)
    Dim g As Graphics = pe.Graphics
    g.DrawImage(myImage, 0, 0)
    MyBase.OnPaint(pe)
End Sub

This code displays the whole image at full size. But we can easily change this by passing an extra two parameters to specify the size:

g.DrawImage(myImage, 0, 0, 50, 50);

This will reduce the image to a 50 x 50 pixel square. Scaling images is intrinsically tricky, and there is always a tradeoff between time taken to draw the image and the resulting image quality. The Graphics object lets you specify whether you want to favor quality or speed through its InterpolationMode property. This can be set to values from the InterpolationMode enumeration, which from fastest to slowest are NearestNeighbor, Bilinear (the default value), HighQualityBilinear, Bicubic, and HighQualityBicubic. While the ones at the front of that list are faster, they will produce lower-quality results. It is beyond the scope of this book to describe the image processing algorithms implied by each of these settings, which makes it hard to offer more specific advice than that you should pick the fastest value that produces results that are good enough for your application. However, NearestNeighbor is unlikely to provide satisfactory results unless you are either scaling pictures by integer factors, or scaling things up to be so large that individual pixels will be clearly visible.

Another way of using the Bitmap class is to build a new bitmap. There are two reasons you might want to do this. One is that your application requires a particularly complex piece of drawing to be done, and you want to draw it just once into a bitmap to make subsequent redraws work more quickly. The other reason is that you want to save a picture to disk as a bitmap.

To create a brand new Bitmap from scratch, you can simply specify the width and height you require as construction parameters. However, it is usually a good idea to pass in a reference to a Graphics object as well. This guarantees that the new bitmap has characteristics that are compatible with the Graphics object (such as color depth and resolution). Examples Example 7-47 and Example 7-48 create a new bitmap with the text "Hello" drawn into it.

If you want to create a bitmap that has attributes that are different from any available Graphics object (e.g., you want to create an image with a low color depth to conserve space), there is a constructor that takes a PixelFormat value, allowing you to specify the exact color format you require. You would normally only do this if you planned to save the bitmap to a file.


Example 7-47. Creating a Bitmap from scratch using C#
using (Graphics gOrig = CreateGraphics())
{
    myBitmap = new Bitmap(50, 50, gOrig);
    using (Graphics g = Graphics.FromImage(myBitmap))
    {
        g.FillRectangle(Brushes.White, 0, 0,
  myBitmap.Width, myBitmap.Height);
        g.DrawString("Hello",
  new Font (FontFamily.GenericSerif, 14),
  Brushes.Blue, 0, 0);
    }
}
Example 7-48. Creating a Bitmap from scratch using VB
Dim gOrig As Graphics = CreateGraphics()
Try
    myBitmap = new Bitmap(50, 50, gOrig)
    Dim g As Graphics = Graphics.FromImage(myBitmap)
    Try
        g.FillRectangle(Brushes.White, 0, 0, _
  myBitmap.Width, myBitmap.Height)
        g.DrawString("Hello", _
  new Font (FontFamily.GenericSerif, 14), _
  Brushes.Blue, 0, 0)
    Finally
        g.Dispose()
    End Try
Finally
    gOrig.Dispose()
End Try

Note how this code fills the entire bitmap with a white background before starting. This is important because by default bitmaps start out completely transparent. This can have some surprising effects if you paint text onto them with ClearType or font smoothing enabled.

Examples Example 7-47 and Example 7-48 are unusual in that they have a couple of using statements disposing of Graphics objects in the C# code and of calls to Graphics objects' Dispose methods in the VB code. Normally we do not need to call Dispose on a Graphics object. But remember, the rule is that you are responsible for disposing of any object that you create. Generally speaking, we don't create Graphics objects—we just use the ones supplied by the system. But here we are creating two, one to obtain a set of properties with which to initialize the Bitmap object, and another to let us draw on the bitmap. Because we created them, we must also call Dispose on them, which will be done automatically at the end of the using blocks in C#. (The CreateGraphics method being called on the first line is a method supplied by the Control class—as we saw in Examples Example 7-37 and Example 7-38, it lets you obtain a Graphics object for the control in contexts where you wouldn't otherwise have one, such as in its constructor.)

The bitmap created in Examples Example 7-47 and Example 7-48 could then be drawn using the same OnPaint method as in Examples Example 7-45 and Example 7-46. Alternatively it can be saved to disk. The Image class provides a Save method, allowing a filename and file format to be specified. Example 7-49 saves the bitmap in PNG format.

Example 7-49. Saving a Bitmap
myBitmap.Save("c:\\MyPic.png", ImageFormat.Png);

The Graphics class provides overloads of the DrawImage method that allow you to draw rotated and sheared bitmaps. These all work the same way—you can tell the method where to draw the bitmap by specifying three points. These are used as positions for three of the four corners of the bitmap, and the position of the fourth is inferred by forming a parallelogram. However, it is easier to achieve rotation by drawing with a transformation, which will be described later in this chapter.

The Bitmap class is great when you want to display a picture stored as a bitmap file, or when you wish to cache a fixed-size image for fast redraw. But if you require a little more flexibility when redrawing, a metafile might be a more appropriate choice, so we will now look at the support in GDI+ for these.

7.2.4.2 Metafiles

As with bitmaps, metafiles can be used in two ways. A metafile can be loaded from disk and displayed. Alternatively, a new metafile can be created, either for later display or to be saved to disk.

Creating a Metafile object based on a file works in exactly the same way as for bitmaps—you simply pass the filename as a constructor parameter (or a Stream if that is more convenient). Building a new Metafile from scratch turns out to be slightly more involved, because the Metafile object insists on having an HDC[10] (that is, a handle to a Win32 device context) to determine the characteristics of the metafile. (These characteristics include factors such as the resolution; although metafiles do not store raw pixel data, they are aware of the resolution of the device for which they were originally created.) This is easy enough to deal with because we can obtain an HDC from the Graphics class, but it means that the creation process is a little more long-winded than for a bitmap, as Examples Example 7-50 and Example 7-51 show.

[10] This is a curious anachronism. An HDC is Win32's nearest equivalent to a Graphics object. It is somewhat strange that the Metafile class insists on having one of these to create a new Metafile from scratch instead of just using a Graphics object.

Example 7-50. Creating a metafile from scratch in C#
using (Graphics og = CreateGraphics())
{
    IntPtr hdc = og.GetHdc();
    try
    {
        myImage = new Metafile(hdc, EmfType.EmfPlusOnly);
        using (Graphics g = Graphics.FromImage(myImage))
        {
  g.DrawString("Hello",
      new Font (FontFamily.GenericSerif, 14),
      Brushes.Blue, 0, 0);
        }
    }
    finally
    {
        og.ReleaseHdc(hdc);
    }
}
Example 7-51. Creating a metafile from scratch in VB
Dim og As Graphics = CreateGraphics()
Try
    Dim hdc As IntPtr = og.GetHdc()
    Try
        myImage = New Metafile(hdc, EmfType.EmfPlusOnly)
        Dim g As Graphics = Graphics.FromImage(myImage)
        Try
  g.DrawString("Hello", _
      New Font (FontFamily.GenericSerif, 14), _
      Brushes.Blue, 0, 0)
        Finally
 g.Dispose()
        End Try
    Finally
        og.ReleaseHdc(hdc)
    End Try
Finally
   og.Dispose()
End Try

Both examples use a try...finally block to make absolutely sure that the HDC is released. Because an HDC is an unmanaged type (i.e., a classic Win32 type, not a .NET type), we are responsible for making sure it is freed under all circumstances. If we forget, the garbage collector will not help us—an HDC is just an IntPtr, which is a value type large enough to hold either a pointer or an int/Integer; on 32-bit systems, this is a 32-bit value. Value types are not garbage collected, so if we forget to clean up this resource, it will be leaked. (This is much worse than forgetting to clean up GDI+ resources—with those, the garbage collector will eventually come to our aid.) The use of a try...finally block means that the call to ReleaseHdc will always occur even if an exception is thrown in the try block.

The metafile created in Examples Example 7-50 and Example 7-51 can be drawn using the same OnPaint method shown in Examples Example 7-45 and Example 7-46DrawImage works in exactly the same way for metafiles as for bitmaps. Note that when creating a metafile, we did not need to fill the background color to white before starting. This is because metafiles work differently—bitmaps work as a drawing surface that must be wiped clean before starting; metafiles simply list drawing operations to be applied, so the results are independent of background color. This means that it is much easier to create a transparent metafile than a transparent bitmap if you wish to use antialiasing.

7.2.4.3 Color transformations

When displaying either metafiles or bitmaps, it is possible to perform a limited amount of color processing on the images as they are drawn. Several of the overloads of the DrawImage method take an ImageAttributes parameter, which allows color transformations to be specified.

The ImageAttributes class is particularly useful for applying simple effects such as building a grayscale version of a color image or making a solid image partially transparent. The mechanism by which it achieves this is a color matrix. This is a matrix that can be applied to every color in the source image to transform it to a new image.

A description of the details of matrix multiplication is beyond the scope of this book, but if you are familiar with this branch of mathematics, here is how color matrixes are used. A color matrix is a 5 x 5 matrix. Each pixel in the source image (or each color, if the source image is a metafile) is represented as a 1 x 5 vector. The first four numbers represent red, green, blue, and alpha values with the float/Single type, where the values range from 0.0 to 1.0. The fifth value is a dummy column that is always 1.0—this is provided to allow the color matrix to perform translations as well as scaling operations. Each color is then multiplied by the color matrix, with the resulting 5 x 1 matrix used as the new color (with the fifth column ignored).

So what does this mean in practice? You can use a color matrix to perform global changes to color and transparency on an image. Examples Example 7-52 and Example 7-53 draw any image with a 40% alpha channel (i.e., see-through), regardless of whether that image has intrinsic transparency. It uses a DrawImage overload that takes an ImageAttributes object. (As it happens this particular overload can also scale the image; unfortunately, there aren't any overloads that use ImageAttributes that don't also do other operations like scaling or rotation, so there is a certain amount of unwanted complexity just to draw the image at its original size.)

Example 7-52. Drawing an image with transparency in C#
protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;

    g.DrawString("Behind image", Font, Brushes.LightGreen, 0, 0);
    using (ImageAttributes ia = new ImageAttributes())
    {
        ColorMatrix cm = BuildTransparencyMatrix(0.4f);
        ia.SetColorMatrix(cm);
        int w = myImage.Width;
        int h = myImage.Height;
        Rectangle dest = new Rectangle(0, 0, w, h);
        g.DrawImage(myImage, dest, 0, 0, w, h, GraphicsUnit.Pixel, ia);
    }
    base.OnPaint(pe);
}

private ColorMatrix BuildTransparencyMatrix(float alpha)
{
    ColorMatrix cm = new ColorMatrix();
    cm.Matrix33 = 0;
    cm.Matrix43 = alpha;
    return cm;
}
Example 7-53. Drawing an image with transparency in VB
Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)

    Dim g As Graphics = pe.Graphics

    g.DrawString("Behind image", Font, Brushes.LightGreen, _
       0, 0)
    Dim ia As New ImageAttributes()
    Try
       Dim cm As ColorMatrix = BuildTransparencyMatrix(0.4f)
        ia.SetColorMatrix(cm)
        Dim w As Integer = myImage.Width
        Dim h As Integer = myImage.Height
        Dim dest As New Rectangle(0, 0, w, h)
        g.DrawImage(myImage, dest, 0, 0, w, h, _
          GraphicsUnit.Pixel, ia)
    Finally
       ia.Dispose()
    End Try

    MyBase.OnPaint(pe)
End Sub

Private Function BuildTransparencyMatrix(alpha As Single) _
       As ColorMatrix
    Dim cm As New ColorMatrix()
    cm.Matrix33 = 0
    cm.Matrix43 = alpha
    Return cm
End Function

The results of this code can be seen in Figure 7-18. Observe that in Example 7-52 and Example 7-53, the bitmap is drawn after the text—i.e., it is drawn right over it. The text is only visible because the image was drawn transparently.

Figure 7-18. A bitmap drawn transparently with a ColorMatrix
figs/winf_0718.gif

The BuildTransparencyMatrix method in Examples Example 7-52 and Example 7-53 is just one example. It is easy enough to create other simple transforms. For instance, Examples Example 7-54 and Example 7-55 build a color matrix that will convert color images into grayscale (black and white) images.

Example 7-54. A color matrix to build grayscale images using C#
private ColorMatrix BuildGrayscaleMatrix()
{
    float[][] matrixValues =
        {
  new float[] { 0.3f, 0.3f, 0.3f, 0, 0 },
  new float[] { 0.5f, 0.5f, 0.5f, 0, 0 },
  new float[] { 0.2f, 0.2f, 0.2f, 0, 0 },
  new float[] { 0,      0,    0,  1, 0 },
  new float[] { 0,      0,    0,  0, 1 }
    };
    return new ColorMatrix(matrixValues);
}
Example 7-55. A color matrix to build grayscale images using VB
Private Function BuildGrayscaleMatrix() As ColorMatrix
    Dim matrixValues()() As Single = _
    { New Single() { 0.3f, 0.3f, 0.3f, 0, 0 }, _
      New Single() { 0.5f, 0.5f, 0.5f, 0, 0 }, _
      New Single() { 0.2f, 0.2f, 0.2f, 0, 0 }, _
      New Single() { 0,      0,    0,  1, 0 }, _
      New Single() { 0,      0,    0,  0, 1 }  _
    }
    MsgBox(matrixValues(3)(2))
    Return New ColorMatrix(matrixValues)
End Function

If you want grayed out versions of images for your user interface, the ControlPaint class provides a DrawImageDisabled method that will do this for you. It performs a slightly different color transformation from the one shown in Examples Example 7-54 and Example 7-55—it reduces the contrast so you will never see anything as dark as black, or as pale as white. (You could easily do this with a ColorMatrix by reducing the scale factors and adding in offsets on the fourth row of the matrix. But because the ControlPaint class can do this for you, there is usually no need.)


So we have seen how to draw images with or without various color transformations, text, and a wide variety of shapes. Finally, we will look at the facilities supplied by GDI+ for applying geometrical transformations to our output.

7.2.5 Coordinate Systems and Transformations

Whenever we draw something with GDI+, we specify its position and any relevant size information. For these numbers to mean anything, there must be some coordinate system in place. By default, coordinates are in terms of screen pixels, but we can actually modify the coordinate system to transform our output, allowing translations, rotations, and shearing to be applied automatically to everything we draw.

There are many reasons why this could be useful. For example, there are certain drawing primitives for which the relevant methods on the Graphics class do not provide a means of rotating or shearing the output. The only way to draw rotated or sheared versions of such objects is to draw with an appropriate transformation in place. Also, if you are writing a control that provides a view of a large area, transforms can make it simple to implement facilities such as scrolling and zooming. Likewise, if you have a piece of code that paints a particular drawing, a transformation is likely to be the easiest way to allow rotated views of that picture.

Example 7-56 shows a very common way of modifying the world transform (the transform applied by a Graphics object to every drawing operation). This is the OnPaint method inside a ScrollableControl. Any control deriving from ScrollableControl should make sure that it offsets everything it draws by the current scroll position. (The ScrollableControl class provides a property called AutoScrollPosition, which is a Point indicating the scroll position.) This example simply adjusts the world transform by adding in a translation based on the current scroll position. The rest of the drawing code could be written without needing to build in any awareness of the scroll position, because GDI+ is automatically offsetting everything we draw.

Example 7-56. Translating the transform for scrolling
// C#
protected override void OnPaint(PaintEventArgs pe)
{
    Graphics g = pe.Graphics;
    g.TranslateTransform(AutoScrollPosition.X,
        AutoScrollPosition.Y);
    . . .
}

' VB
Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)
    Dim g As Graphics = pe.Graphics
    g.TranslateTransform(AutoScrollPosition.X, _
               AutoScrollPosition.Y)
    . . .
End Sub

Example 7-57 shows how to rotate the transform to draw some text rotated by 45 degrees. This illustrates an important point. Sometimes you will want to apply a temporary transformation just to alter how one item is drawn; in this example, only the text string is to be drawn rotated. In such cases, you will want to take the transform back off again before continuing to draw. The Graphics class supplies a ResetTransform method, which removes any transform currently in place. This method will often be appropriate, but it does not work if the transform is also being used for other purposes such as scrolling because of the way that transforms are combined.

The nature of transformation matrixes is that you can apply as many different transformations as you like—a single matrix can be used to represent the combined effect of any number of individual matrixes. So it is definitely allowable to translate the transform for scrolling purposes and to then rotate it. The problem is that calling ResetTransform removes all current transforms, which would include the translation applied for scrolling purposes. Example 7-57 is sensitive to this: it retrieves the current transform, applies a rotation for its own drawing, and then puts the original transform back when it has finished. (The Transform property always returns a copy of the current Transform, so the Matrix it returns will not be modified when we call RotateTransform.) This means that any code that follows will not be affected by the rotation, but will still benefit from the translation that was applied for scrolling.

Example 7-57. Rotating the transform
// C#
Matrix origTx = g.Transform;
g.RotateTransform(45);
g.DrawString("Rotated", Font, Brushes.LightGreen, 20, 20);
g.Transform = origTx;

' VB
Dim origTx As Matrix = g.Transform
g.RotateTransform(45)
g.DrawString("Rotated", Font, Brushes.LightGreen, 20, 20)
g.Transform = origTx

The Graphics transform can be used to apply any affine transformation[11]—it just uses a 3 x 3 matrix, where the third row is used to apply translations. (Just as a dummy fifth column was added to colors for color matrixes, a dummy third column is added to each two-dimensional coordinate for transformation, to allow translations.) As well as being able to translate and rotate the transform, there is a ScaleTransform method, which can be useful for implementing a zoom feature.

[11] An affine transformation is any transformation that can be applied with a 2 x 2 matrix, optionally combined with a translation. This allows rotation, scaling, shearing, and translation.

There is no method for explicitly shearing the transformation. To do this, you will need to use the Matrix class directly. Matrix represents a 3 x 3 matrix used for two-dimensional transforms, and enables you to set each individual element if you need that level of control. (The Graphics.Transform property is of type Matrix.) You can also apply a Matrix object to a GraphicsPath using the Warp method to transform all the elements of a path without needing to go through a Graphics object.

    [ Team LiB ] Previous Section Next Section