Chapter 8. .NET Graphics Using GDI+

Topics in This Chapter

  • Graphics Overview: The first step in working with GDI+ is to understand how to create and use a Graphics object. This section looks at how this object is created and used to handle the Paint event.

  • Using the Graphics Object to Create Shapes: .NET offers a variety of standard geometric shapes that can be drawn in outline form or filled in. The GraphicsPath class serves as a container that enables geometric shapes to be connected.

  • Using Pens and Brushes: Pens are used to draw shapes in outline form in different colors and widths; a brush is used to fill in shapes and create solid and gradient patterns.

  • Color: Colors may be defined according to red/green/blue (RGB) values or hue/saturation/brightness (HSB) values. Our project example illustrates RGB and HSB color spaces. By representing a color as an object, .NET permits it to be transformed by changing property values.

  • Images: .NET includes methods to load, display, and transform images. The most useful of these is the Graphics.DrawImage method that allows images to be magnified, reduced, and rotated.

Very few programmers are artists, and only a minority of developers is involved in the world of gaming where graphics have an obvious justification. Yet, there is something compelling about writing an application that draws on a computer screen. For one thing, it's not difficult. An array of built-in functions makes it easy to create geometric objects, color them, and even animate them. In this regard, .NET should satisfy the would-be artist that resides in many programmers.

To understand the .NET graphics model, it is useful to look at its predecessor—the Win32 Graphical Device Interface (GDI). This API introduced a large set of drawing objects that could be used to create device independent graphics. The idea was to draw to a logical coordinate system rather than a device specific coordinate system—freeing the developer to concentrate on the program logic and not device details. .NET essentially takes this API, wraps it up in classes that make it easier to work with, and adds a wealth of new features.

The graphics classes are collectively called GDI+. This chapter looks at the underlying principles that govern the use of the GDI+, and then examines the classes and the functionality they provide. Several programming examples are included that should provide the tools you will need to further explore the .NET graphics namespaces.

Keep in mind that GDI+ is not restricted to WinForms applications. Its members are also available to applications that need to create images dynamically for the Internet (Web Forms and Web Services).You should also recognize that GDI+ is useful for more than just games or graphics applications. Knowledge of its classes is essential if you want to design your own controls or modify the appearance of existing ones.

GDI+ Overview

The types that make up GDI+ are contained in the gdiplus.dll file. .NET neatly separates the classes and enumerations into logically named namespaces that reflect their use. As Figure 8-1 shows, the GDI+ functions fall into three broad categories: two-dimensional vector graphics, image manipulation, and typography (the combining of fonts and text strings to produce text output).

GDI+ namespaces

Figure 8-1. GDI+ namespaces

This figure does not depict inheritance, but a general hierarchical relationship between the GDI+ namespaces. System.Drawing is placed at the top of the chart because it contains the basic objects required for any graphic output: Pen, Brush, Color, and Font. But most importantly, it contains the Graphics class. This class is an abstract representation of the surface or canvas on which you draw. The first requirement for any drawing operation is to get an instance of this class, so the Graphics object is clearly a fruitful place to begin the discussion of .NET graphics.

The Graphics Class

Drawing requires a surface to draw on, a coordinate system for positioning and aligning shapes, and a tool to perform the drawing. GDI+ encapsulates this functionality in the System.Drawing.Graphics class. Its static methods allow a Graphics object to be created for images and controls; and its instance methods support the drawing of various shapes such as circles, triangles, and text. If you are familiar with using the Win32 API for graphics, you will recognize that this corresponds closely to a device context in GDI. But the Graphics object is of a simpler design. A device context is a structure that maintains state information about a drawing and is passed as an argument to drawing functions. The Graphics object represents the drawing surface and provides methods for drawing on it.

Let's see how code gains access to a Graphics object. Your application most likely will work with a Graphics object inside the scope of an event handler, where the object is passed as a member of an EventArgs parameter. The Paint event, which occurs each time a control is drawn, is by far the most common source of Graphics objects. Other events that have a Graphics object sent to their event handler include PaintValue, BeginPrint, EndPrint, and PrintDocument.PrintPage. The latter three are crucial to printing and are discussed in the next chapter.

Although you cannot directly instantiate an object from the Graphics class, you can use methods provided by the Graphics and Control classes to create an object. The most frequently used is Control.CreateGraphics—an instance method that returns a graphics object for the control calling the method. The Graphics class includes the FromHwnd method that relies on passing a control's Handle to obtain a Graphics object related to the control. Let's look at both approaches.

How to Obtain a Graphics Object from a Control Using CreateGraphics

The easiest way to create a Graphics object for a control is to use its CreateGraphics method. This method requires no parameters and is inherited from the Control class by all controls. To demonstrate, let's create an example that draws on a Panel control when the top button is clicked and refreshes all or part of the panel in response to another button click. The user interface to this program is shown in Figure 8-2 and will be used in subsequent examples.

Interface to demonstrate using Graphics object to draw on a control

Figure 8-2. Interface to demonstrate using Graphics object to draw on a control

Listing 8-1 contains the code for the Click event handlers associated with each button. When the Decorate Panel button (btnDecor) is clicked, a Graphics object is created and used to draw a rectangle around the edge of the panel as well as a horizontal line through the middle. When the Refresh button (btnRefresh) is clicked, the panel's Invalidate method is called to redraw all or half of the panel. (More on the Invalidate command is coming shortly.)

Example 8-1. Using Control.CreateGraphics to Obtain a Graphics Object

using System.Drawing;
//
private void btnDecor_Click(object sender, System.EventArgs e)
{
   // Create a graphics object to draw on panel1
   Graphics cg = this.panel1.CreateGraphics();
   try (
      int pWidth  = panel1.ClientRectangle.Width;
      int pHeight = panel1.ClientRectangle.Height;
      // Draw a rectangle around border
      cg.DrawRectangle(Pens.Black,2,2,pWidth-4, pHeight-4);
      // Draw a horizontal line through the middle
      cg.DrawLine(Pens.Red,2,(pHeight-4)/2,pWidth-4,
                  (pHeight-4)/2);
   }
   finally {
      cg.Dispose();  // You should always dispose of object
   }
}
private void btnRefresh_Click(object sender,
                              System.EventArgs e)
{
// Invokes Invalidate to repaint the panel control
   if (this.radAll.Checked)   // Radio button - All
   {
      // Redraw panel1
      this.panel1.Invalidate();
      } else {
      // Redraw left half of panel1
      Rectangle r = new
            Rectangle(0,0,panel1.ClientRectangle.Width/2,
            ClientRectangle.Height);
      this.panel1.Invalidate(r); // Repaint area r
      this.panel1.Update();      // Force Paint event
   }
}

The btnDecor Click event handler uses the DrawRectangle and DrawLine methods to adorn panel1. Their parameters—the coordinates that define the shapes—are derived from the dimensions of the containing panel control. When the drawing is completed, the Dispose method is used to clean up system resources held by the object. (Refer to Chapter 4, “Working with Objects in C#,” for a discussion of the IDisposable interface.) You should always dispose of the Graphics object when finished with it. The try-finally construct ensures that Dispose is called even if an interrupt occurs. As shown in the next example, a using statement provides an equivalent alternative to try-finally.

The btnRefresh Click event handler is presented as a way to provide insight into how forms and controls are drawn and refreshed in a WinForms environment. A form and its child controls are drawn (displayed) in response to a Paint event. Each control has an associated method that is responsible for drawing the control when the event occurs. The Paint event is triggered when a form or control is uncovered, resized, or minimized and restored.

How to Obtain a Graphics Object Using Graphics Methods

The Graphics class has three static methods that provide a way to obtain a Graphics object:

  • Graphics.FromHdcCreates the Graphics object from a specified handle to a Win32 device context. This is used primarily for interoperating with GDI.

  • Graphics.FromImageCreates a Graphics object from an instance of a .NET graphic object such as a Bitmap or Image. It is often used in ASP.NET (Internet) applications to dynamically create images and graphs that can be served to a Web browser. This is done by creating an empty Bitmap object, obtaining a Graphics object using FromImage, drawing to the Bitmap, and then saving the Bitmap in one of the standard image formats.

  • Graphics.FromHwndCreates the Graphics object from a handle to a Window, Form, or control. This is similar to GDI programming that requires a handle to a device context in order to display output to a specific device.

Each control inherits the Handle property from the Control class. This property can be used with the FromHwnd method as an alternative to the Control.CreateGraphics method. The following routine uses this approach to draw lines on panel1 when a MouseDown event occurs on the panel (see Figure 8-3).

Output for MouseDown example

Figure 8-3. Output for MouseDown example

Note that the Graphics object is created inside a using statement. This statement generates the same code as a try-finally construct that includes a g.Dispose() statement in the finally block.

private void panel1OnMouseDown(object sender, MouseEventArgs e)
{
   // The using statement automatically calls g.Dispose()
   using( Graphics g= Graphics.FromHwnd(panel1.Handle))
   {
      g.DrawLine(Pens.Red,e.X,e.Y,20,20);
   }
}

The Paint Event

A Paint event is triggered in a WinForms application when a form or control needs to be partially or fully redrawn. This normally occurs during the natural use of a GUI application as the window is moved, resized, and hidden behind other windows. Importantly, a Paint event can also be triggered programatically by a call to a control's Invalidate method.

Using Invalidate() to Request a Paint Event

The Control.Invalidate method triggers a Paint event request. The btnRefresh_Click event handler in Listing 8-1 showed two overloads of the method. The parameterless version requests that the entire panel control be redrawn; the second specifies that only the portion of the control's region specified by a rectangle be redrawn.

Here are some of the overloads for this method:

public void Invalidate()
public void Invalidate(bool invalidatechildren)
public void Invalidate(Rectangle rc)
public void Invalidate(Rectangle rc, bool invalidatechildren)

Note: Passing a true value for the invalidatechildren parameter causes all child controls to be redrawn.

Invalidate requests a Paint event, but does not force one. It permits the operating system to take care of more important events before invoking the Paint event. To force immediate action on the paint request, follow the Invalidate statement with a call to Control.Update.

Let's look at what happens on panel1 after a Paint event occurs. Figure 8-4 shows the consequences of repainting the left half of the control. The results are probably not what you desire: half of the rectangle and line are now gone. This is because the control's paint event handler knows only how to redraw the control. It has no knowledge of any drawing that may occur outside of its scope. An easy solution in this case is to call a method to redraw the rectangle and line after calling Invalidate. But what happens if Windows invokes the Paint event because half of the form is covered and uncovered by another window? This clears the control and our code is unaware it needs to redraw the rectangle and line. The solution is to handle the drawing within the Paint event handler.

Effects of invalidating a region

Figure 8-4. Effects of invalidating a region

Core Note

Core Note

When a form is resized, regions within the original area are not redrawn. To force all of a control or form to be redrawn, pass the following arguments to its SetStyle method. Only use this when necessary, because it slows down the paint process.

this.SetStyle(ControlStyles.ResizeRedraw, true);

Implementing a Paint Event Handler

After the Paint event occurs, a data class PaintEventArgs is passed as a parameter to the Paint event handler. This class provides access to the Graphics object and to a rectangle ClipRectangle that defines the area where drawing may occur. Together, these properties make it a simple task to perform all the painting within the scope of the event handler.

Let's see how to rectify the problem in the preceding example, where our drawing on panel1 disappears each time the paint event occurs. The solution, of course, is to perform the drawing inside the paint event handler. To do this, first register our event handler with the PaintEventHandler delegate:

this.panel1.Paint += new PaintEventHandler(paint_Panel);

Next, set up the event handler with the code to draw a rectangle and horizontal line on the panel. The Graphics object is made available through the PaintEventArgs parameter.

private void paint_Panel( object sender, PaintEventArgs e)
{
   Graphics cg = e.Graphics;
   int pWidth  = panel1.ClientRectangle.Width;
   int pHeight = panel1.ClientRectangle.Height;
   cg.DrawRectangle(Pens.Black,2,2,pWidth-4, pHeight-4);
   cg.DrawLine(Pens.Red,2,(pHeight-4)/2,pWidth-4,
               (pHeight-4)/2);
   base.OnPaint(e);   // Call base class implementation
}

The Control.OnPaint method is called when a Paint event occurs. Its role is not to implement any functionality, but to invoke the delegates registered for the event. To ensure these delegates are called, you should normally invoke the OnPaint method within the event handler. The exception to this rule is: To avoid screen flickering, do not call this method if painting the entire surface of a control.

Painting is a slow and expensive operation. For this reason, PaintEventArgs provides the ClipRectangle property to define the area that is displayed when drawing occurs. Any drawing outside this area is automatically clipped. However, it is important to realize that clipping affects what is displayed—it does not prevent the drawing code from being executed. Thus, if you have a time-consuming custom paint routine, the entire painting process will occur each time the routine is called, unless you include logic to paint only what is needed.

The following example illustrates how to draw selectively. It paints a pattern of semi-randomly colored rectangles onto a form's panel (see Figure 8-5). Before each rectangle is drawn, a check is made to confirm that the rectangle is in the clipping area.

private void paint_Panel( object sender,  PaintEventArgs e)
{
   Graphics g = e.Graphics;
   for (int i = 0; i< this.panel1.Width;i+=20)
   {
      for (int j=0; j< his.panel1.Height;j+=20)
      {
         Rectangle r= new Rectangle(i,j,20,20);
         if (r.IntersectsWith(e.ClipRectangle))
         {
            // FromArgb is discussed in Color section
            Brush b = new SolidBrush(Color.FromArgb((i*j)%255,
                  (i+j)%255, ((i+j)*j)%255));
            g.FillRectangle(b,r);
            g.DrawRectangle(Pens.White,r);
         }
      }
   }
}
Pattern used in paint example

Figure 8-5. Pattern used in paint example

The key to this code is the Rectangle.IntersectsWith method that checks for the intersection of two rectangles. In this case, it tests for overlap between the rectangle to be drawn and the clip area. If the rectangle intersects the clip area, it needs to be drawn. Thus, the method can be used to limit the portion of the screen that has to be repainted. To test the effects, this code was run with and without the IntersectsWith method. When included, the event handler required 0 to 17 milliseconds—depending on the size of the area to be repainted. When run without IntersectsWith, the event handler required 17 milliseconds to redraw all the rectangles.

Another approach to providing custom painting for a form or control is to create a subclass that overrides the base class's OnPaint method. In this example, myPanel is derived from the Panel class and overrides the OnPaint method to draw a custom diagonal line through the center of the panel.

// New class myPanel inherits from base class Panel
public class myPanel: Panel
{
   protected override void OnPaint(PaintEventArgs e)
   {
      Graphics g = e.Graphics;
      g.DrawLine(Pens.Aqua,0,0,this.Width,this.Height);
      base.OnPaint(e);
   }
}

Unless the new subclass is added to a class library for use in other applications, it is simpler to write an event handler to provide custom painting.

Using the Graphics Object

Let's look at the details of actually drawing with the Graphics object. The Graphics class contains several methods for rendering basic geometric patterns. In addition to the DrawLine method used in previous examples, there are methods for drawing rectangles, polygons, ellipses, curves, and other basic shapes. In general, each shape can be drawn as an outline using the Pen class, or filled in using the Brush class. The DrawEllipse and FillEllipse methods are examples of this. Let's look at some examples.

Basic 2-D Graphics

Figure 8-6 demonstrates some of the basic shapes that can be drawn with the Graphics methods. The shapes vary, but the syntax for each method is quite similar. Each accepts a drawing object—either a pen or brush—to use for rendering the shape, and the coordinates that determine the size and positions of the shape. A Rectangle object is often used to provide a shape's boundary.

Basic 2-D shapes

Figure 8-6. Basic 2-D shapes

The following code segments draw the shapes shown in Figure 8-6. To keep things simple, the variables x and y are used to specify the location where the shape is drawn. These are set to the coordinates of the upper-left corner of a shape.

Pen blkPen = new Pen(Brushes.Black,2 );  // width=2
// Set this to draw smooth lines
g.SmoothingMode =
      System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
// (1) Draw Circle and Draw Filled Circle
Rectangle r = new Rectangle(new Point(x,y),new Size(40,40));
g.DrawEllipse(blkPen, r);
g.FillEllipse(Brushes.Black,x,y+60,40,40);
// (2) Draw Ellipse and Filled Ellipse
int w = 60;
int h= 40;
Rectangle r = new Rectangle(new Point(x,y),new Size(w,h));
g.DrawEllipse(blkPen, r);
r = new Rectangle(new Point(x,y+60), new Size(w,h));
g.FillEllipse(Brushes.Red, r);
// (3) Draw Polygon
Point pt1 = new Point(x, y);
Point pt2 = new Point(x+22, y+12);
Point pt3 = new Point(x+22, y+32);
Point pt4 = new Point(x, y+44);
Point pt5 = new Point(x-22, y+32);
Point pt6 = new Point(x-22, y+12);
Point[] myPoints = {pt1, pt2, pt3, pt4, pt5, pt6};
g.DrawPolygon(blkPen, myPoints);
// Points would be changed so as not to draw over
// original polygon
g.FillPolygon(Brushes.Black, myPoints);
// (4)Draw Pie Shape and filled pie
Rectangle r = new Rectangle( new Point(x,y),new Size(80,80));
// Create start and sweep angles
int startAngle =  0;    // Clockwise from x-axis
int sweepAngle = -60;   // Clockwise from start angle
g.DrawPie(blkPen, r, startAngle, sweepAngle);
g.FillPie(Brushes.Black, x,y+60,80,80,startAngle, sweepAngle);
// (5) Draw Rectangle and Rectangle with beveled edges
blkPen.Width=5;   // make pen thicker to show bevel
g.DrawRectangle(blkPen,x,y,50,40);
blkPen.LineJoin = LineJoin.Bevel;
g.DrawRectangle(blkPen,x,y+60,50,40);
// (6) Draw Arc and Filled Pie
startAngle=45;
sweepAngle=180;
g.DrawArc(blkPen, x,y,40,40,startAngle, sweepAngle);
g.FillPie(Brushes.Black, x,y+60,40,40,startAngle,sweepAngle);

These code segments illustrate how easy it is to create simple shapes with a minimum of code. .NET also makes it easy to create more complex shapes by combining primitive shapes using the GraphicsPath class.

Creating Shapes with the GraphicsPath Class

The GraphicsPath class, which is a member of the System.Drawing.Drawing2D namespace, is used to create a container for a collection of primitive shapes. Succinctly, it permits you to add basic shapes to its collection and then to treat the collection as a single entity for the purpose of drawing and filling the overall shape. Before looking at a code example, you should be aware of some of the basic features of the GraphicsPath class:

  • It automatically connects the last point of a line or arc to the first point of a succeeding line or arc.

  • Its CloseFigure method can be used to automatically close open shapes, such as an arc. The first and last points of the shape are connected.

  • Its StartFigure method prevents the previous line from being automatically connected to the next line.

  • Its Dispose method should always be called when the object is no longer in use.

The following code creates and displays the Infinity Cross shown in Figure 8-7. It is constructed by adding five polygons to the GraphicsPath container object. The Graphics object then draws the outline and fills in the cross.

// g is the Graphics object
g.SmoothingMode = SmoothingMode.AntiAlias;
// Define five polygons
Point[] ptsT= {new Point(120,20),new Point(160,20),
      new Point(140,50)};
Point[] ptsL= {new Point(90,50),new Point(90,90),
      new Point(120,70)};
Point[] ptsB= {new Point(120,120),new Point(160,120),
      new Point(140,90)};
Point[] ptsR= {new Point(190,90), new Point(190,50),
      new Point(160, 70)};
Point[] ptsCenter = {new Point(140,50), new Point(120,70),
      new Point(140,90), new Point(160,70)};
// Create the GraphicsPath object and add the polygons to it
GraphicsPath gp = new GraphicsPath();
gp.AddPolygon(ptsT);   // Add top polygon
gp.AddPolygon(ptsL);   // Add left polygon
gp.AddPolygon(ptsB);   // Add bottom polygon
gp.AddPolygon(ptsR);   // Add right polygon
gp.AddPolygon(ptsCenter);
g.DrawPath(new Pen(Color.Red,2),gp); // Draw GraphicsPath
g.FillPath(Brushes.Gold,gp); // Fill the polygons
Infinity Cross

Figure 8-7. Infinity Cross

Instead of drawing and filling each polygon separately, we use a single DrawPath and FillPath statement to do the job.

The GraphicsPath class has several methods worth exploring—AddCircle, AddArc, AddEllipse, AddString, Warp, and others—for applications that require the complex manipulation of shapes. One of the more interesting is the Transform method that can be used to rotate or shift the coordinates of a DrawPath object. This following code segment offers a taste of how it works. A transformation matrix is created with values that shift the x coordinates by 50 units and leave the y coordinates unchanged. This Transform method applies the matrix to the DrawPath and shifts the coordinates; the shape is then drawn 50 units to the right of the first shape.

Matrix translateMatrix = new Matrix();
translateMatrix.Translate(50, 0);  // Offset x coordinate by 50
gp.Transform(translateMatrix);     // Transform path
g.DrawPath(Pens.Orange,gp);        // Display at new location

Hit Testing with Shapes

One of the reasons for placing shapes on a form is to permit a user to trigger an action by clicking a shape—as if she had clicked a button. Unlike a control, you cannot associate an event with a shape. Instead, you associate a MouseDown event with the container that holds the shape(s). Recall that a MouseDown event handler receives the x and y coordinates where the event occurs. After it has these, it is a simple process to use the rectangle and GraphicsPath methods to verify whether a point falls within their area:

bool Rectangle.Contains(Point(x,y))
bool GraphicsPath.IsVisible(Point(x,y))

To illustrate, consider an application that displays a map of US states and responds to a click on a state by displaying the name of the state capital. The map image is placed in a PictureBox, and rectangles and polygons are drawn on the states to set up the hit areas that respond to a MouseDown event. Figure 8-8 shows how the picture box's paint handler routine draws rectangles on three states and a polygon on Florida. (Of course, the shapes would not be visible in the actual application.) To respond to a pressed mouse key, set up a delegate to call an event handler when the MouseDown event occurs:

this.pictureBox1.MouseDown += new
      MouseEventHandler(down_Picture);
Hit test example

Figure 8-8. Hit test example

The following code implements event handler logic to determine if the mouse down occurs within the boundary of any shape.

private void down_Picture( object sender, MouseEventArgs e)
{
   // Rectangles and GraphicsPath gp are defined
   // as class variables
   if (rectNC.Contains(e.X,e.Y) )
   { MessageBox.Show("Capital: Raleigh"); }
   else if(rectSC.Contains(e.X,e.Y))
   { MessageBox.Show("Capital: Columbia");}
   else if(rectGA.Contains(e.X,e.Y))
   { MessageBox.Show("Capital: Atlanta");}
   else if(gp.IsVisible(e.X,e.Y))
   {MessageBox.Show("Capital: Tallahassee");}
}

After you have a basic understanding of how to create and use shapes, the next step is to enhance these shapes with eye catching graphical effects such as gradients, textured colors, and different line styles and widths. This requires an understanding of the System.Drawing classes: Pen, Brush, and Color.

Pens

The Graphics object must receive an instance of the Pen class to draw a shape's outline. Our examples thus far have used a static property of the Pens class—Pens.Blue, for example—to create a Pen object that is passed to the Graphics object. This is convenient, but in many cases you will want to create your own Pen object in order to use non-standard colors and take advantage of the Pen properties.

Constructors:

public Pen (Color color);
public Pen (Color color, single width);
public Pen (Brush brush);
public Pen (Brush brush, single width);

Example:

Pen p1 = new Pen(Color.Red, 5);
Pen p2 = new Pen(Color.Red); // Default width of 1

The constructors allow you to create a Pen object of a specified color and width. You can also set its attributes based on a Brush object, which we cover later in this section. Note that the Pen class inherits the IDisposable interface, which means that you should always call the Pen object's Dispose method when finished with it.

Besides color and width, the Pen class offers a variety of properties that allow you to control the appearance of the lines and curves the Pen object draws. Table 8-1 contains a partial list of these properties.

Table 8-1. Selected Pen Properties

Member

Description

Alignment

Determines how a line is drawn for closed shapes. Specifically, it specifies whether the line is drawn on the bounding perimeter or inside it.

Color

Color used to draw the shape or text.

DashCap

The cap style used at the beginning and end of dashes in a dashed line. A cap style is a graphic shape such as an arrow.

DashOffset

Distance from start of a line to the beginning of its dash pattern.

DashStyle

The type of dashed lines used. This is based on the DashStyle enumeration.

PenType

Specifies how a line is filled—for example, textured, solid, or gradient. It is determined by the Brush property of the Pen.

StartCap EndCap

The cap style used at the beginning and end of lines. This comes from the LineCap enumeration that includes arrows, diamonds, and squares—for example, LineCap.Square.

Width

Floating point value used to set width of Pen.

Let's look at some of the more interesting properties in detail.

DashStyle

This property defines the line style, which can be Solid, Dash, Dot, DashDot, DashDotDot, or Custom (see Figure 8-9). The property's value comes from the DashStyle enumeration.

Pen p1 = new Pen(Color.Black, 3);
p1.DashStyle = DashStyle.Dash;
g.DrawLine(p1,20,20,180,20);
DashStyles and LineCaps

Figure 8-9. DashStyles and LineCaps

StartCap and EndCap

These properties define the shape used to begin and end a line. The value comes from the LineCap enumeration, which includes ArrowAnchor, DiamondAnchor, Round, RoundAnchor, Square, SquareAnchor, and Triangle. Examples of the DiamondAnchor and RoundAnchor are shown in Figure 8-9. The following code is used to create the lines in the figure:

Graphics g = pictureBox1.CreateGraphics();
Pen p1 = new Pen(Color.Black, 5);
p1.StartCap = LineCap.DiamondAnchor;
p1.EndCap   = LineCap.RoundAnchor;
int yLine = 20;
foreach(string ds in Enum.GetNames(typeof(DashStyle)))
{
   if (ds != "Custom") // Ignore Custom DashStyle type
   {
      // Parse creates an enum type from a string
      p1.DashStyle = (DashStyle)Enum.Parse(
                     typeof(DashStyle), ds);
      g.DrawLine(p1,20,yLine,120,yLine);
      g.DrawString(ds,new Font("Arial",10),Brushes.Black,
                   140,yLine-8);
      yLine += 20;
   }
}

The code loops through the DashStyle enumeration and draws a line for each enum value except Custom. It also uses the DrawString method to display the name of the enumeration values. This method is discussed in Chapter 9.

Brushes

Brush objects are used by these Graphics methods to create filled geometric shapes:

FillClosedCurve    FillEllipse      FillPath           FillPie
FillPolygon        FillRectangle    FillRectangles     FillRegion

All of these receive a Brush object as their first argument. As with the Pen class, the easiest way to provide a brush is to use a predefined object that represents one of the standard colors—for example, Brushes.AntiqueWhite. To create more interesting effects, such as fills with patterns and gradients, it is necessary to instantiate your own Brush type. Unlike the Pen class, you cannot create an instance of the abstract Brush class; instead, you use one of its inheriting classes summarized in Table 8-2.

Table 8-2. Brush Types That Derive from the Brush Class

Brush Type

Description

SolidBrush

Defines a brush of a single color. It has a single constructor:

Brush b = new SolidBrush(Color.Red);

TextureBrush

Uses a preexisting image (*.gif, *.bmp, or *.jpg) to fill a shape.

Image img = Image.FromFile("c:\flower.jpg");
Brush b = new TextureBrush(img);

HatchBrush

Defines a rectangular brush with a foreground color, background color, and hatch style. Located in the System.Drawing.Drawing2D namespace.

LinearGradientBrush

Supports either a two-color or multi-color gradient. All linear gradients occur along a line defined by two points or a rectangle. Located in the Drawing2D namespace.

PathGradientBrush

Fills the interior of a GraphicsPath object with a gradient. Located in the Drawing2D namespace.

Note that all Brush classes have a Dispose method that should be called to destroy the Brush object when it is no longer needed.

The two most popular of these classes are HatchBrush, which is handy for creating charts, and LinearGradientBrush, for customizing the background of controls. Let's take a closer look at both of these.

The HatchBrush Class

As the name implies, this class fills the interior of a shape with a hatched appearance.

Constructors:

public HatchBrush(HatchStyle hStyle, Color forecolor)
public HatchBrush(HatchStyle hstyle, Color forecolor,
                  Color backcolor)

Parameters:

hStyle

HatchStyle enumeration that specifies the hatch pattern.

forecolor

The color of the lines that are drawn.

backcolor

Color of the space between the lines (black is default).

The predefined HatchStyle patterns make it a simple process to create elaborate, multi-color fill patterns. The following code is used to create the DarkVertical and DottedDiamond rectangles at the top of each column in Figure 8-10.

Graphics g = pictureBox1.CreateGraphics();
// Fill Rectangle with DarkVertical pattern
Brush b = new HatchBrush(HatchStyle.DarkVertical,
                         Color.Blue,Color.LightGray);
g.FillRectangle(b,20,20,80,60);
// Fill Rectangle with DottedDiamond pattern
b = new HatchBrush(HatchStyle.DottedDiamond,
                   Color.Blue,Color.LightGray);
g.FillRectangle(b,120,20,80,60);
Using HatchBrush with some of the available hatch styles

Figure 8-10. Using HatchBrush with some of the available hatch styles

The LinearGradientBrush Class

In its simplest form, this class creates a transition that takes one color and gradually blends it into a second color. The direction of the transition can be set to horizontal, vertical, or any specified angle. The location where the transition begins can be set to a focal point other than the beginning of the area to be filled in. In cases where the gradient must be tiled to completely fill an area, options are available to control how each repeat is displayed. These options can be confusing, so let's begin with how to create a gradient brush and then work with examples that demonstrate the more useful properties and methods of the LinearGradientBrush class.

Constructors:

public LinearGradientBrush(Rectangle rect, Color color1,
   Color color2, LinearGradientMode linearGradientMode)

public LinearGradientBrush(Rectangle rect, Color color1,
   Color color2, float angle)

Parameters:

rect

Rectangle specifying the bounds of the gradient.

color1

The start color in the gradient.

color2

The end color in the gradient.

angle

The angle in degrees moving clockwise from the x axis.

LinearGradientMode

A LinearGradientMode enum value: Horizontal, Vertical, BackwardDiagonal, ForwardDiagonal

There is no substitute for experimentation when it comes to understanding graphics related concepts. Figure 8-11 shows the output from filling a rectangle with various configurations of a LinearGradientBrush object. Here is the code that creates these examples:

// Draw rectangles filled with gradient in a pictureBox
Graphics g = pictureBox1.CreateGraphics();
Size sz = new Size(100,80);
Rectangle rb = new Rectangle(new Point(20,20),sz);
// (1) Vertical Gradient (90 degrees)
LinearGradientBrush b = new
      LinearGradientBrush(rb,Color.DarkBlue,Color.LightBlue,90);
g.FillRectangle(b,rb);
rb.X=140;
// (2) Horizontal Gradient
b = new LinearGradientBrush(rb,Color.DarkBlue,
                            Color.LightBlue,0);
g.FillRectangle(b,rb);
rb.Y = 120;
rb.X = 20;
// (3) Horizontal with center focal point
b = new LinearGradientBrush(rb,Color.DarkBlue,
                            Color.LightBlue,0);
// Place end color at position (0-1) within brush
b.SetBlendTriangularShape(.5f);
g.FillRectangle(b,rb);
LinearGradientBrush examples: (1) Vertical, (2) Horizontal, (3) Focus Point, (4) Tiling

Figure 8-11. LinearGradientBrush examples: (1) Vertical, (2) Horizontal, (3) Focus Point, (4) Tiling

The main point of interest in this code is the use of the SetBlendTriangularShape method to create the blending effect shown in the third rectangle in Figure 8-11. This method takes an argument between 0 and 1.0 that specifies a relative focus point where the end color is displayed. The gradient then “falls off” on either side of this point to the start color.

The fourth rectangle in the figure is created by repeating the original brush pattern. The following code defines a small gradient brush that is used to fill a larger rectangle:

// Tiling Example – create small rectangle for gradient brush
Rectangle rb1 = new Rectangle(new Point(0,0),new Size(20,20));
b = new LinearGradientBrush(rb,Color.DarkBlue,
                            Color.LightBlue,0);
b.WrapMode = WrapMode.TileFlipX;
// Fill larger rectangle with repeats of small gradient rectangle
g.FillRectangle(b,rb);

Notice how the light and dark colors are reversed horizontally before each repeat occurs: [light-dark][dark-light]. The WrapMode property determines how the repeated gradient is displayed. In this example, it is set to the WrapMode enum value of TileFlipX, which causes the gradient to be reversed horizontally before repeating. The most useful enum values include the following:

Tile

Repeats the gradient.

TileFlipX

Reverses the gradient horizontally before repeating.

TileFlipXY

Reverses the gradient horizontally and vertically before repeating.

TileFlipY

Reverses the gradient vertically before repeating.

Creating a Multi-Color Gradient

It takes only a few lines of code to create a LinearGradientBrush object that creates a multi-color gradient. The key is to set the LinearGradientBrush.InterpolationColors property to an instance of a ColorBlend class that specifies the colors to be used. As the following code shows, the ColorBlend class contains an array of colors and an array of values that indicate the relative position (0–1) of each color on the gradient line. This example creates a gradient with a transition from red to white to blue—with white in the middle.

b = new LinearGradientBrush(rb,Color.Empty,Color.Empty,0);
ColorBlend myBlend = new ColorBlend();
// Specify colors to include in gradient
myBlend.Colors = new Color[]
      {Color.Red, Color.White, Color.Blue,};
// Position of colors in gradient
myBlend.Positions = new float[] {0f, .5f, 1f};
b.InterpolationColors = myBlend;  // Overrides constructor colors

Colors

.NET implements the Color object as a structure that includes a large number of colors predefined as static properties. For example, when a reference is made to Color.Indigo, the returned value is simply the Indigo property. However, there is more to the structure than just a list of color properties. Other properties and methods permit you to deconstruct a color value into its internal byte representation or build a color from numeric values. To appreciate this, let's look at how colors are represented.

Computers—as opposed to the world of printing—use the RGB (red/green/blue) color system to create a 32-bit unsigned integer value that represents a color. Think of RGB as a three-dimensional space with the red, green, and blue values along each axis. Any point within that space represents a unique RGB coordinate value. Throw in a fourth component, the alpha value—that specifies the color's transparency—and you have the 4-byte AlphaRGB (ARGB) value that defines a color. For example, Indigo has RGB values of 75, 0, 130, and an alpha value of 255 (no transparency). This is represented by the hex value 4B0082FF.

Colors can also be represented by the HSL (hue/saturation/luminosity) and HSB (hue/saturation/brightness) color spaces. While RGB values follow no easily discernible pattern, HSL and HSB are based on the standard color wheel (see Figure 8-12) that presents colors in an orderly progression that makes it easy to visualize the sequence. Hue is represented as an angle going counterclockwise around the wheel. The saturation is the distance from the center of the wheel toward the outer edge. Colors on the outer edge have full saturation. Brightness measures the intensity of a color. Colors shown on the wheel have 100% brightness, which decreases as they are darkened (black has 0% brightness). There is no standard for assigning HSB/HSL values. Programs often use values between 0 and 255 to correspond to the RGB numbering scheme. As we will see, .NET assigns the actual angle to the hue, and values between 0 and 1 to the saturation and brightness.

Color wheel

Figure 8-12. Color wheel

How to Create a Color Object

The Color structure provides three static methods for creating a Color object: FromName, FromKnownColor, and FromArgb. FromName takes the name of a color as a string argument and creates a new struct: Color magenta = Color.FromName("Magenta"). The name must match one in the KnownColor enumeration values, which is an enumeration of all the colors represented as properties in the Color and SystemColor structures.

FromKnownColor takes a KnownColor enumeration value as its argument and produces a struct for that color:

Color magenta = Color.FromKnownColor(KnownColor.Magenta);

FromArgb allows you to specify a color by RGB and alpha values, which makes it easy to change the transparency of an existing color. Here are some of its overloads:

// (r, g, b)
Color slate1 = Color.FromArgb (112, 128, 144);
// (alpha, r, g, b)
Color slate2 = Color.FromArgb (255, 112, 128, 144);
// (alpha, Color)
Color lightslate = Color.FromArgb(50, slate2 );

Examining the Characteristics of a Color Object

The Color structure has four properties that return the ARGB values of a color: Color.A, Color.R, Color.G, and Color.B. All of these properties have a value in the range 0 to 255.

Color slateGray = Color.FromArgb(255,112,128,144);
byte a = slateGray.A;              // 255
byte r = slateGray.R;              // 112
byte g = slateGray.G;              // 128
byte b = slateGray.B;              // 144

The individual HSB values of a color can be extracted using the Color.GetHue, GetSaturation, and GetBrightness methods. The hue is measured in degrees as a value from 0.0 to 360.0. Saturation and brightness have values between 0 and 1.

Color slateGray = Color.FromArgb(255,112,128,144);
float hue = slateGray.GetHue();            //  210 degrees
float sat = slateGray.GetSaturation();     // .125
float brt = slateGray.GetBrightness();     // .501

Observe in Figure 8-12 that the hue of 210 degrees (moving clockwise from 0) falls between cyan and blue on the circle—which is where you would expect to find a slate gray color.

A Sample Project: Building a Color Viewer

The best way to grasp how the color spaces relate to actual .NET colors is to visually associate the colors with their RGB and HSB values. .NET offers a ColorDialog class that can be used to display available colors; however, it does not identify colors by the system-defined names that most developers work with. So, let's build a simple color viewer that displays colors by name and at the same time demonstrates how to use the GDI+ types discussed in this section.

Figure 8-13 shows the user interface for this application. It consists of a TreeView on the right that contains all the colors in the KnownColor enumeration organized into 12 color groups. These groups correspond to the primary, secondary, and tertiary[1] colors of the color wheel. In terms of hues, each section is 30 degrees on the circle. The interface also contains two panels, a larger one in which a selected color is displayed, and a smaller one that displays a brightness gradient for the selected color. This is created using a multi-color gradient comprising black and white at each end, and the color at a focus point determined by its Brightness value. The remainder of the screen displays RGB and HSB values obtained using the properties and methods discussed earlier.

Example: Color viewer demonstrates working with colors and gradients

Figure 8-13. Example: Color viewer demonstrates working with colors and gradients

The code for this application is shown in Listings 8-2 and 8-3. The former contains code to populate the TreeNode structure with the color nodes; Listing 8-3 shows the methods used to display the selected color and its color space values. The routine code for laying out controls on a form is excluded.

Example 8-2. Color Viewer: Populate TreeNode with All Colors

using System.Drawing;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
public class Form1 : Form
{
   public Form1()
   {
      InitializeComponent();  // Lay out controls on Form1
      // Set up event handler to be fired when node is selected
      colorTree.AfterSelect += new
            TreeViewEventHandler(ColorTree_AfterSelect);
      BuildWheel();   // Create Parent Nodes for 12 categories
   }
   [STAThread]
   static void Main()
   {
      Application.Run(new Form1());
   }
   // Parent Nodes in TreeView for each segment of color wheel
   private void BuildWheel()
   {
      TreeNode tNode;
      tNode = colorTree.Nodes.Add("Red");
      tNode = colorTree.Nodes.Add("Orange");
      tNode = colorTree.Nodes.Add("Yellow");
      // Remainder of nodes are added here ....
   }
   private void button1_Click(object sender, System.EventArgs e)
   {
      // Add Colors to TreeNode Structure
      // Loop through KnownColor enum values
      Array objColor = Enum.GetValues(typeof(KnownColor));
      for (int i=0; i < objColor.Length; i++)
      {
         KnownColor kc = (KnownColor) objColor.GetValue(i);
         Color c = Color.FromKnownColor(kc);
         if (!c.IsSystemColor) // Exclude System UI colors
         {
            InsertColor(c, c.GetHue());
         }
      }
   }
   private void InsertColor(Color myColor, float hue)
   {
      TreeNode tNode;
      TreeNode cNode = new TreeNode();
      // Offset is used to start color categories at 345 degrees
      float hueOffset = hue + 15;
      if (hueOffset >360) hueOffset -= 360;
      // Get one of 12 color categories
      int colorCat = (int)(hueOffset -.1)/30;
      tNode = colorTree.Nodes[colorCat];  // Get parent node
      // Add HSB values to node's Tag
      HSB nodeHSB = new HSB(hue, myColor.GetSaturation(),
            myColor.GetBrightness());
      cNode.Tag  = nodeHSB;      // Tag contains HSB values
      cNode.Text = myColor.Name;
      int nodeCt = tNode.Nodes.Count;
      bool insert=false;
      // Insert colors in ascending hue value
      for (int i=0; i< nodeCt && insert==false ;i++)
      {
         nodeHSB = (HSB)tNode.Nodes[i].Tag;
         if (hue < nodeHSB.Hue)
         {
            tNode.Nodes.Insert(i,cNode);
            insert = true;
         }
      }
      if (!insert) tNode.Nodes.Add(cNode);
   }
   public struct HSB
   {
      public float Hue;
      public float Saturation;
      public float Brightness;
      public HSB(float H, float S, float B)
      {
         Hue = H;
         Saturation = S;
         Brightness = B;
      }
   }
   // ---> Methods to Display Colors go here
}

When the application is executed, the form's constructor calls BuildWheel to create the tree structure of parent nodes that represent the 12 color categories. Then, when the Build Color Tree button is clicked, the Click event handler loops through the KnownColor enum value (excluding system colors) and calls InsertColor to insert the color under the correct parent node. The Tag field of the added node (color) is set to an HSB struct that contains the hue, saturation, and brightness for the color. Nodes are stored in ascending order of Hue value. (See Chapter 7, “Windows Forms Controls,” for a discussion of the TreeNode control.)

Listing 8-3 contains the code for both the node selection event handler and for ShowColor, the method that displays the color, draws the brightness scale, and fills all the text boxes with RGB and HSB values.

Example 8-3. Color Viewer: Display Selected Color and Values

private void ColorTree_AfterSelect(Object sender,
      TreeViewEventArgs e)
// Event handler for AfterSelect event
{
   // Call method to display color and info
   if (e.Node.Parent != null) ShowColor(e.Node);
}
private void ShowColor(TreeNode viewNode)
{
   Graphics g  = panel1.CreateGraphics();  // Color panel
   Graphics g2 = panel2.CreateGraphics();  // Brightness panel
   try
   {
      // Convert node's text value to Color object
      Color myColor = Color.FromName(viewNode.Text);
      Brush b = new SolidBrush(myColor);
      // Display selected color
      g.FillRectangle(b, 0,0,panel1.Width,panel1.Height);
      HSB hsbVal= (HSB) viewNode.Tag;
      // Convert hue to value between 0 and 255 for displaying
      int huescaled = (int) (hsbVal.Hue / 360 * 255);
      hText.Text = huescaled.ToString();
      sText.Text = hsbVal.Saturation.ToString();
      lText.Text = hsbVal.Brightness.ToString();
      rText.Text = myColor.R.ToString();
      gText.Text = myColor.G.ToString();
      bText.Text = myColor.B.ToString();
      // Draw Brightness scale
      Rectangle rect = new Rectangle(new Point(0,0),
            new Size(panel2.Width, panel2.Height));
      // Create multi-color brush gradient for brightness scale
      LinearGradientBrush bg = new LinearGradientBrush(rect,
            Color.Empty, Color.Empty,90);
      ColorBlend myBlend = new ColorBlend();
      myBlend.Colors = new Color[] {Color.White, myColor,
             Color.Black};
      myBlend.Positions = new float[]{0f,
             1-hsbVal.Brightness,1f};
      bg.InterpolationColors = myBlend;
      g2.FillRectangle(bg, rect);
      // Draw marker on brightness scale showing current color
      int colorPt = (int)((1–hsbVal.Brightness)* panel1.Height);
      g2.FillRectangle(Brushes.White,0,colorPt,10,2);
      b.Dispose();
      bg.Dispose();
   }
   finally
   {
      g.Dispose();
      g2.Dispose();
   }
}

The code incorporates several of the concepts already discussed in this section. Its main purpose is to demonstrate how Color, Graphics, and Brush objects work together. A SolidBrush is used to fill panel1 with a color sample, a gradient brush creates the brightness scale, and Color properties provide the RGB values displayed on the screen.

Images

GDI+ provides a wide range of functionality for working with images in a runtime environment. It includes support for the following:

  • The standard image formats such as GIF and JPG files.

  • Creating images dynamically in memory that can then be displayed in a WinForms environment or served up as images on a Web server or Web Service.

  • Using images as a surface to draw on.

The two most important classes for handling images are the Image and Bitmap class. Image is an abstract class that serves as a base class for the derived Bitmap class. It provides useful methods for loading and storing images, as well as gleaning information about an image, such as its height and width. But for the most part, working with images requires the creation of objects that represent raster images. This responsibility devolves to the Bitmap class, and we use it exclusively in this section.

Tasks associated with using images in applications fall into three general categories:

  • Loading and storing images. Images can be retrieved from files, from a stream of bytes, from the system clipboard, or from resource files. After you have loaded or created an image, it is easily saved into a specified image format using the Save method of the Image or Bitmap class.

  • Displaying an image. Images are dynamically displayed by writing them to the surface area of a form or control encapsulated by a Graphics object.

  • Manipulating an image. An image is represented by an array of bits in memory. These bits can be manipulated in an unlimited number of ways to transform the image. Traditional image operations include resizing, cropping, rotating, and skewing. GDI+ also supports changing an image's overall transparency or resolution and altering individual bits within an image.

Loading and Storing Images

The easiest way to bring an image into memory is to pass the name of the file to a Bitmap constructor. Alternatively, you can use the FromFile method inherited from the Image class.

string fname = "c:\globe.gif";
Bitmap bmp = new Bitmap(fname);
bmp = (Bitmap)Bitmap.FromFile(fname); // Cast to convert Image

In both cases, the image stored in bmp is the same size as the image in the file. Another Bitmap constructor can be used to scale the image as it is loaded. This code loads and scales an image to half its size:

int w = Image.FromFile(fname).Width;
int h = Image.FromFile(fname).Height;
Size sz= new Size(w/2,h/2);
bmp = new Bitmap(Image.FromFile(fname), sz); //Scales

GDI+ support images in several standard formats: bitmaps (BMP), Graphics Interchange Format (GIF), Joint Photographic Experts Group (JPEG), Portable Network Graphics (PNG), and the Tag Image File Format (TIFF). These are used for both loading and storing images and, in fact, make it quite simple to convert one format to another. Here is an example that loads a GIF file and stores it in JPEG format:

string fname = "c:\globe.gif";
bmp = new Bitmap(Image.FromFile(fname));
bmp.Save("c:\globe.jpg",
         System.Drawing.Imaging.ImageFormat.Jpeg);
// Compare size of old and new file
FileInfo fi= new FileInfo(fname);
int old = (int) fi.Length;
fi = new FileInfo("c:\globe.jpg");
string msg = String.Format("Original: {0} New: {1}",old,fi.Length);
MessageBox.Show(msg);  // ---> Original: 28996 New: 6736

The Save method has five overloads; its simplest forms take the name of the file to be written to as its first parameter and an optional ImageFormat type as its second. The ImageFormat class has several properties that specify the format of the image output file. If you have any experience with image files, you already know that the format plays a key role in the size of the file. In this example, the new JPEG file is less than one-fourth the size of the original GIF file.

To support multiple file formats, GDI+ uses encoders to save images to a file and decoders to load images. These are referred to generically as codecs (code-decode). An advantage of using codecs is that new image formats can be supported by writing a decoder and encoder for them.

.NET provides the ImageCodecInfo class to provide information about installed image codecs. Most applications allow GDI+ to control all aspects of loading and storing image files, and have no need for the codecs information. However, you may want to use it to discover what codecs are available on your machine. The following code loops through and displays the list of installed encoders (see Figure 8-14):

// Using System.Drawing.Imaging
string myList="";
foreach(ImageCodecInfo co in ImageCodecInfo.GetImageEncoders())
   myList = myList +"
"+co.CodecName;
Console.WriteLine(myList);
Codecs

Figure 8-14. Codecs

The DrawImage method of the Graphics object is used to display an image on the Graphics object's surface. These two statements load an image and draw it full size at the upper-left corner of the graphics surface (0,0). If the Graphics object surface is smaller than the image, the image is cropped (see the first figure in the following example).

Bitmap bmp = new Bitmap("C:\globe.gif");
g.DrawImage(bmp,0,0);    // Draw at coordinates 0,0

DrawImage has some 30 overloaded versions that give you a range of control over sizing, placement, and image selection. Many of these include a destination rectangle, which forces the source image to be resized to fit the rectangle. Other variations include a source rectangle that permits you to specify a portion of the source image to be displayed; and some include both a destination and source rectangle.

The following examples capture most of the basic effects that can be achieved. Note that the source image is 192×160 pixels for all examples, and the destination panel is 96×80 pixels.

  1. The source image is drawn at its full size on the target surface. Cropping occurs because the destination panel is smaller than the source image.

    Graphics g = panel1.CreateGraphics();
    g.DrawImage(bmp,0,0);
    
    Codecs
  2. The source image is scaled to fit the destination rectangle.

    Rectangle dRect = new
       Rectangle(new Point(0,0),
       new Size(panel1.Width,panel1.Height));
    g.DrawImage(bmp, dRect);
    //Or panel1.ClientRectangle
    
    Codecs
  3. Part of the source rectangle (left corner = 100,0) is selected.

    Rectangle sRect = new
       Rectangle(new Point(100,0),
       new Size(192,160));
    g.DrawImage(bmp,0,0,sRect,GraphicsUnit.Pixel);
    
    Codecs
  4. Combines examples 2 and 3: A portion of the source rectangle is scaled to fit dimensions of destination rectangle.

    g.DrawImage(bmp,dRect,sRect,
                GraphicsUnit.Pixel);
    
    Codecs
  5. The destination points specify where the upper-left, upper-right, and lower-left point of the original are placed. The fourth point is determined automatically in order to create a parallelogram.

    Point[]dp = {new Point(10,0),new Point(80,10),
        new Point(0,70)};   // ul, ur, ll
    g.DrawImage(bmp,dp);
    
    Codecs

The DrawImage variations shown here illustrate many familiar image effects: zoom in and zoom out are achieved by defining a destination rectangle larger (zoom in) or smaller (zoom out) than the source image; image skewing and rotation are products of mapping the corners of the original image to three destination points, as shown in the figure to the left of Example 5.

A Note on Displaying Icons

Icons (.ico files) do not inherit from the Image class and cannot be rendered by the DrawImage method; instead, they use the Graphics.DrawIcon method. To display one, create an Icon object and pass the file name or Stream object containing the image to the constructor. Then, use DrawIcon to display the image at a desired location.

Icon icon = new Icon("c:\clock.ico");
g.DrawIcon(icon,120,220);  // Display at x=120, y=220
icon.Dispose();            // Always dispose of object
g.Dispose();

Manipulating Images

.NET also supports some more advanced image manipulation techniques that allow an application to rotate, mirror, flip, and change individual pixels in an image. We'll look at these techniques and also examine the advantage of building an image in memory before displaying it to a physical device.

Rotating and Mirroring

Operations that rotate or skew an image normally rely on the DrawImage overload that maps three corners of the original image to destination points that define a parallelogram.

void DrawImage(Image image, Point destPoints[])

Recall from an earlier example that the destination points are the new coordinates of the upper-left, upper-right, and lower-left corners of the source image. Figure 8-15 illustrates the effects that can be achieved by altering the destination points.

Manipulation using DrawImage

Figure 8-15. Manipulation using DrawImage

The following code is used to create a mirrored image from the original image. Think of the image as a page that has been turned over from left to right: points a and b are switched, and point c is now the lower-right edge.

Bitmap bmp = new Bitmap(fname);               // Get image
// Mirror Image
Point ptA = new Point(bmp.Width,0);           // Upper left
Point ptB = new Point(0,0);                   // Upper right
Point ptC = new Point(bmp.Width, bmp.Height); // Lower left
Point[]dp = {ptA,ptB,ptC};
g.DrawImage(bmp,dp);

Many of these same effects can be achieved using the Bitmap.RotateFlip method, which has this signature:

Public void RotateFlip(RotateFlipType rft)

RotateFlipType is an enumeration that indicates how many degrees to rotate the image and whether to “flip” it after rotating (available rotations are 90, 180, and 270 degrees). Here are a couple of examples:

// Rotate 90 degrees
bmp.RotateFlip(RotateFlipType.Rotate90FlipNone);
// Rotate 90 degrees and flip along the vertical axis
bmp.RotateFlip(RotateFlipType.Rotate90FlipY);
// Flip horizontally (mirror)
bmp.RotateFlip(RotateFlipType.RotateNoneFlipX);

The most important thing to recognize about this method is that it changes the actual image in memory—as opposed to DrawImage, which simply changes it on the drawing surface. For example, if you rotate an image 90 degrees and then rotate it 90 degrees again, the image will be rotated a total of 180 degrees in memory.

Working with a Buffered Image

All of the preceding examples are based on drawing directly to a visible panel control on a form. It is also possible to load an image into, or draw your own image onto, an internal Bitmap object before displaying it. This can offer several advantages:

  • It permits you to create images such as graphs dynamically and display them in the application or load them into Web pages for display.

  • It improves performance by permitting the application to respond to a Paint event by redrawing a stored image, rather than having to reconstruct the image from scratch.

  • It permits you to keep the current “state” of the image. As long as all transformations, such as rotating or changing colors, are made first to the Bitmap object in memory, it will always represent the current state of the image.

To demonstrate, let's input a two-color image, place it in memory, change pixels in it, and write it to a panel. Figure 8-16 shows the initial image and the final image after pixels are swapped.

Use GetPixel() and SetPixel() to swap pixels

Figure 8-16. Use GetPixel() and SetPixel() to swap pixels

The following code creates a Bitmap object bmpMem that serves as a buffer where the pixels are swapped on the flag before it is displayed. We use the Graphics.FromImage method to obtain a Graphics object that can write to the image in memory. Other new features to note are the use of GetPixel and SetPixel to read and write pixels on the image.

Graphics g = pan.CreateGraphics();  // Create from a panel
Bitmap bmp = new Bitmap("c:\flag.gif");
g.DrawImage(bmp,0,0);         // Draw flag to panel
Bitmap bmpMem = new Bitmap(bmp.Width,bmp.Height);
Graphics gMem = Graphics.FromImage(bmpMem);
gMem.DrawImage(bmp,0,0);      // Draw flag to memory
// Define a color object for the red pixels
Color cnRed = Color.FromArgb(255,214,41,33);  // a,r,g,b
// Loop through all pixels in image and swap them
for (int y=0; y<bmpMem.Height; y++)
{
   for (int x=0; x<bmpMem.Width; x++)
   {
      Color px = bmpMem.GetPixel(x,y);
      if(px.G > 240)
         bmpMem.SetPixel(x,y, cnRed);         // Set white to red
      else  bmpMem.SetPixel(x,y,Color.White); // Set red to white
   }
}
g.DrawImage(bmpMem,0,0);        // Display reversed flag on panel
gMem.Dispose();
g.Dispose();

Core Note

Core Note

Applications that dynamically create images for display should draw them offscreen onto a Bitmap and then render them to the screen when completed.

Sample Project: Working with Images

This application brings together many of the concepts presented in this chapter: handling the Paint event; using Invalidation to clear portions of a screen; and using DrawImage to rotate, flip, and zoom in and out on an image.

The screen for the program is shown in Figure 8-17. It consists of a menu with three main selections: File is used to load an image; Image has options to mirror, flip, or copy an image; and Screen refreshes the screen. The panel control on the left serves as the main viewing window into which an image is loaded. The Image menu options are applied to this panel. The smaller panel is where part of the main image is copied. The + and – buttons zoom in and out, respectively.

User interface for Image Viewer [Source: Lourve, Paris]

Figure 8-17. User interface for Image Viewer [Source: Lourve, Paris]

The copying process is the most interesting part of the application. A user selects a rectangular area of the image by pressing the mouse button and dragging the mouse over the image. When the mouse button is raised, the selected area can be copied to the smaller panel by choosing Image-Copy from the menu.

The code for this project is presented in three sections: the menu operations, drawing the selection rectangle, and the functions associated with the small panel (panel2).

Implementing Menu Operations

The following fields are defined at the form level to make them available for menu operations:

private Bitmap bmp;          // Holds original loaded image
private Bitmap newbmp;       // Holds latest version of image
private bool imageStatus = false; // Indicates image is loaded
private int resizeLevel;     // Level image magnified/reduced

Listing 8-4 contains the code for the menu options to load an image into the viewing panel, flip an image, mirror an image, and refresh the viewing panel. The Image-Copy option is discussed in the code related to manipulating the image on panel2.

Example 8-4. Image Viewer: Menu Items

Private void menuItem6_Click(object sender, System.EventArgs e)
{
   // Load image from file
   OpenFileDialog fd = new OpenFileDialog();
   fd.InitialDirectory = "c:\" ;
   fd.Filter = "Image Files | *.JPG;*.GIF";
   if (fd.ShowDialog() == DialogResult.OK)
   {
      string fname= fd.FileName;
      using(Graphics g = panel1.CreateGraphics())
      {
         bmp = new Bitmap(fname); // Load image from file
         newBmp = bmp;            // Save copy of image
         // Clear main panel before drawing to it
         g.FillRectangle(Brushes.White,0,0,
               panel1.Width,panel1.Height );
         Rectangle r = new Rectangle(0,0,bmp.Width,
               bmp.Height);
         g.DrawImage(bmp,r);      // Draw image on panel
         ImageStatus = true;      // Indicates image exists
         // Clear small panel
         Graphics gClear= panel2.CreateGraphics();
         g.FillRectangle(Brushes.White,0,0,
               panel2.Width,panel2.Height );
         gClear.Dispose();
      }
   }
}
private void menuItem4_Click(object sender, System.EventArgs e)
{
   // Mirror image
   Graphics g= panel1.CreateGraphics();
   int h = newBmp.Height;
   int w = newBmp.Width;
   Point[] destPts = {
               new Point(w,0),
               new Point(0,0),
               new Point(w,h) };
   Bitmap tempBmp = new Bitmap(w,h);
   Graphics gr= Graphics.FromImage(tempBmp);
   gr.DrawImage(newBmp, destPts);  // Mirror temporary image
   g.DrawImage(tempBmp,0,0);       // Draw image on panel
   newBmp = tempBmp;               // Set to mirrored image
   g.Dispose();
   gr.Dispose();
}
private void menuItem3_Click(object sender, System.EventArgs e)
{
   // Flip image vertically
   newBmp.RotateFlip(RotateFlipType.RotateNoneFlipY);
   Graphics g = panel1.CreateGraphics();
   g.DrawImage(newBmp,0,0);
   g.Dispose();
}
private void menuItem9_Click(object sender, System.EventArgs e)
{
   // Refresh Screen
   panel1.Invalidate();    // Redraw entire panel
   panel1.Update();
   selectStatus = false;   // Refreshing removes selected area
}

The file loading routine displays a dialog box for the user to enter the image file name. If this image file exists, it is opened and displayed in panel1. If the image is larger than the panel, it is cropped.

The method that mirrors an image first creates a temporary Bitmap and uses DrawImage, as described earlier, to mirror the image to its surface. The mirrored image is displayed in the panel and saved in newBmp. Flipping could be done in a similar way, but for demonstration purposes, we use the RotateFlip method to directly transform newBmp before it is displayed.

The screen refresh routine simply calls Invalidate and Update to redraw the image on panel1. The main effect of this is to remove any selection rectangle (discussed next) that has been drawn on the image.

Drawing a Selection Rectangle on the Image

Listing 8-5 contains event handlers for Paint, MouseDown, MouseUp, and MouseMove. These routines permit the user to select a rectangular area on the image that can then be copied to panel2. The event handling routines are associated with the events using this code in the constructor:

panel1.MouseDown += new MouseEventHandler(Mouse_Down);
panel1.MouseUp   += new MouseEventHandler(Mouse_Up);
panel1.MouseMove += new MouseEventHandler(Mouse_Move);
panel1.Paint     += new PaintEventHandler(RePaint);

The following fields, defined at the form level, are used to keep status information:

private Point lastPoint = Point.Empty; // Tracks mouse movement
private Point origPoint = Point.Empty; // Mouse down coordinates
private Rectangle rectSel;             // Selected area
private bool selectStatus = false;     // True if area selected

When a MouseDown occurs, origPoint is set to the x,y coordinates and serves as the origin of the rectangle that is to be drawn. Dragging the mouse results in a rectangle being displayed that tracks the mouse movement. The MouseMove event handler must draw the rectangle at the new position and erase the previous rectangle. It uses lastPoint and origPoint to determine the part of the image to redraw in order to erase the previous rectangle. The new rectangle is determined by the current mouse coordinates and origPoint. When the MouseUp event occurs, rectSel is set to the final rectangle.

Example 8-5. Image Viewer: Select Area of Image to Copy

private void RePaint(object sender, PaintEventArgs e)
{
   Graphics g = e.Graphics;
   // Redraw part of current image to panel
   if (ImageStatus) g.DrawImage(newBmp,
       e.ClipRectangle,e.ClipRectangle, GraphicsUnit.Pixel);
   base.OnPaint(e);
}
private void Mouse_Down(object sender, MouseEventArgs e)
{
   if (lastPoint != Point.Empty)
   {
      panel1.Invalidate(rectSel); // Clear previous rect.
      panel1.Update();
   }
   lastPoint.X= e.X;
   lastPoint.Y= e.Y;
   origPoint = lastPoint;   // Save origin of selected area
   selectStatus=true;
}
private void Mouse_Up(object sender, MouseEventArgs e)
{
   // Selected area complete. Define it as a rectangle.
   rectSel.X = e.X;
   if (e.X > origPoint.X) rectSel.X = origPoint.X;
   rectSel.Y = origPoint.Y;
   rectSel.Width = Math.Abs(e.X- origPoint.X)+1;
   rectSel.Height= Math.Abs(e.Y - origPoint.Y)+1;
   origPoint = Point.Empty;
   if (rectSel.Width < 2) selectStatus=false;
}
private void Mouse_Move(object sender, MouseEventArgs e)
{
   // Tracks mouse movement to draw bounding rectangle
   if (origPoint != Point.Empty)
   {
      Rectangle r;
      Rectangle rd;
      // Get rectangle area to invalidate
      int xop = origPoint.X;
         if (xop > lastPoint.X) xop= lastPoint.X;
         int w = Math.Abs(origPoint.X - lastPoint.X)+1;
         int h = lastPoint.Y - origPoint.Y+1;
         r = new Rectangle(xop,origPoint.Y,w,h);
         // Get rectangle area to draw
         xop = e.X >= origPoint.X ? origPoint.X:e.X;
         w = Math.Abs(origPoint.X - e.X);
         h = e.Y - origPoint.Y;
         rd = new Rectangle(xop, origPoint.Y,w,h);
         Graphics g = panel1.CreateGraphics();
         // Redraw image over previous rectangle
         g.DrawImage(newBmp,r,r);
         // Draw rectangle around selected area
         g.DrawRectangle(Pens.Red,rd);
         g.Dispose();
         lastPoint.X= e.X;
         lastPoint.Y= e.Y;
   }
}

The logic for creating the rectangles is based on establishing a point of origin where the first MouseDown occurs. The subsequent rectangles then attempt to use that point's coordinates for the upper left corner. However, if the mouse is moved to the left of the origin, the upper left corner must be based on this x value. This is why the MouseUp and MouseMove routines check to see if the current x coordinate e.x is less than that of the origin.

Copying and Manipulating the Image on the Small Panel

The following code is executed when Image – Copy is selected from the menu. The selected area is defined by the rectangle rectSel. The image bounded by this rectangle is drawn to a temporary Bitmap, which is then drawn to panel2. A copy of the contents of panel2 is always maintained in the Bitmap smallBmp.

if (selectStatus)
{
   Graphics g = panel2.CreateGraphics();
   g.FillRectangle(Brushes.White,panel2.ClientRectangle);
   Rectangle rd = new
         Rectangle(0,0,rectSel.Width,rectSel.Height);
   Bitmap temp = new Bitmap(rectSel.Width,rectSel.Height);
   Graphics gi = Graphics.FromImage(temp);
   // Draw selected portion of image onto temp
   gi.DrawImage(newBmp,rd,rectSel,GraphicsUnit.Pixel);
   smallBmp = temp;   // save image displayed on panel2
   // Draw image onto panel2
   g.DrawImage(smallBmp,rd);
   g.Dispose();
    resizeLevel = 0;  // Keeps track of magnification/reduction
}

The plus (+) and minus (–) buttons are used to enlarge or reduce the image on panel2. The actual enlargement or reduction is performed in memory on smallBmp, which holds the original copied image. This is then drawn to the small panel. As shown in the code here, the magnification algorithm is quite simple: The width and height of the original image are increased in increments of .25 and used as the dimensions of the target rectangle.

// Enlarge image
Graphics g = panel2.CreateGraphics();
if (smallBmp != null)
{
   resizeLevel= resizeLevel+1;
   float fac= (float) (1.0+(resizeLevel*.25));
   int w = (int)(smallBmp.Width*fac);
   int h = (int)(smallBmp.Height*fac);
   Rectangle rd= new Rectangle(0,0,w,h);  // Destination rect.
   Bitmap tempBmp = new Bitmap(w,h);
   Graphics gi = Graphics.FromImage(tempBmp);
   // Draw enlarged image to tempBmp Bitmap
   gi.DrawImage(smallBmp,rd);
   g.DrawImage(tempBmp,rd);   // Display enlarged image
   gi.Dispose();
}
g.Dispose();

The code to reduce the image is similar, except that the width and height of the target rectangle are decremented by a factor of .25:

resizeLevel= (resizeLevel>-3)?resizeLevel-1:resizeLevel;
float fac= (float) (1.0+(resizeLevel*.25));
int w = (int)(smallBmp.Width*fac);
int h =(int) (smallBmp.Height*fac);

A Note on GDI and BitBlt for the Microsoft Windows Platform

As we have seen in the preceding examples, Graphics.DrawImage is an easy-to-use method for drawing to a visible external device or to a Bitmap object in memory. As a rule, it meets the graphics demands of most programs. However, there are situations where a more flexible or faster method is required. One of the more common graphics requirements is to perform a screen capture of an entire display or a portion of a form used as a drawing area. Unfortunately, GDI+ does not provide a direct way to copy bits from the screen memory. You may also have a graphics-intensive application that requires the constant redrawing of complex images. In both cases, the solution may well be to use GDI—specifically the BitBlt function.

If you have worked with the Win32 API, you are undoubtedly familiar with BitBlt. If not, BitBlt, which is short for Bit Block Transfer, is a very fast method for copying bits to and from a screen's memory, usually with the support of the graphics card. In fact, the DrawImage method uses BitBlt underneath to perform its operations.

Even though it is part of the Win32 API, .NET makes it easy to use the BitBlt function. The first step is to use the System.Runtime.InteropServices namespace to provide the DllImportAttribute for the function. This attribute makes the Win32 API available to managed code.

[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern int BitBlt(
   IntPtr hDestDC,     // Handle to target device context
   int xDest,          // x coordinate of destination
   int yDest,          // y coordinate of destination
   int nWidth,         // Width of memory being copied
   int nHeight,        // Height of memory being copied
   IntPtr hSrcDC,      // Handle to source device context
   int xSrc,           // x coordinate of image source
   int ySrc,           // y coordinate of image source
   System.Int32 dwRop  // Copy is specified by 0x00CC0020
);

This function copies a rectangular bitmap from a source to a destination. The source and destination are designated by handles to their device context. (In Windows, a device context is a data structure that describes the object's drawing surface and where to locate it in memory.) The type of bit transfer performed is determined by the value of the dwRop parameter. A simple copy takes the value shown in the declaration. By changing this value, you can specify that the source and target bits be combined by AND, OR, XOR, and other logical operators.

Using bitBlt is straightforward. In this example, the contents of a panel are copied to a Bitmap object in memory. Creating the Graphics object for the panel and Bitmap should be familiar territory. Next, use the Graphics object's GetHdc method to obtain a handle for the device context for the panel and Bitmap. These are then passed to the bitBlt function along with a ropType argument that tells the function to perform a straight copy operation.

// Draw an image on to a panel
Graphics g = panel1.CreateGraphics();
g.DrawLine(Pens.Black,10,10,50,50);
g.DrawEllipse(Pens.Blue,0,30,40,30);
// Create a memory Bitmap object to be the destination
Bitmap fxMem = new Bitmap(panel1.Width,panel1.Height);
Graphics gfxMem = Graphics.FromImage(fxMem);
int ropType= 0x00CC0020;   // perform a copy operation
// Get references to the device context for the source and target
IntPtr HDCSrc= g.GetHdc();
IntPtr HDCMem= gfxMem.GetHdc();
// Copy a rectangular area from the panel of size 100 x 100
bitBlt(HDCMem,0,0,100,100,HDCSrc,0,0,ropType);
// Release resources when finished
g.ReleaseHdc(HDCSrc);
gfxMem.ReleaseHdc(HDCMem);
g.Dispose();
gfxMem.Dispose();

Always pair each GetHdc with a ReleaseHdc, and only place calls to GDI functions within their scope. GDI+ operations between the statements are ignored.

Summary

GDI+ supports a wide range of graphics-related operations ranging from drawing to image manipulation. All require use of the Graphics object that encapsulates the drawing surface and provides methods for drawing geometric shapes and images to the abstract surface. The graphic is typically rendered to a form or control on the form to be displayed in a user interface.

The task of drawing is simplified by the availability of methods to create predefined geometric shapes such as lines, ellipses, and rectangles. These shapes are outlined using a Pen object that can take virtually any color or width, and can be drawn as a solid line or broken into a series of dashes, dots, and combinations of these. The shapes are filled with special brush objects such as SolidBrush, TextureBrush, LinearGradientBrush, and HatchBrush.

The Image and Bitmap classes are used in .NET to represent raster-based images. Most of the standard image formats—BMP, GIF, JPG, and PNG—are supported. After an image is loaded or drawn, the Graphics.DrawImage method may be used to rotate, skew, mirror, resize, and crop images as it displays them to the Graphics object's surface.

When custom graphics are included in a form or control, it is necessary to respond to the Paint event, to redraw all or part of the graphics on demand. A program can force this event for a control by invoking the Control.Invalidate method.

Test Your Understanding

1:

What two properties does PaintEventArgs provide a Paint handler routine? Describe the role of each.

2:

What method is called to trigger a Paint event for a control?

3:

Which image is drawn by the following code?

GraphicsPath gp = new GraphicsPath();
gp.AddLine(0,0,60,0);
gp.AddLine(0,20,60,20);
g.DrawPath(new Pen(Color.Black,2),gp);
Test Your Understanding

4:

Which of these statements will cause a compile-time error?

  1. Brush sb = new SolidBrush(Color.Chartreuse);
    
  2. Brush b = new Brush(Color.Red);
    
  3. Brush h = new HatchBrush(HatchStyle.DottedDiamond,
                             Color.Blue,Color.Red);
    

5:

Which of these colors is more transparent?

Color a = FromArgb(255,112,128,144);
Color b = FromArgb(200,212,128,200);

6:

You are drawing an image that is 200×200 pixels onto a panel that is 100×100 pixels. The image is contained in the Bitmap bmp, and the following statement is used:

g.DrawImage(bmp, panel1.ClientRectangle);

What percent of the image is displayed?

  1. 25%

  2. 50%

  3. 100%

7:

The Russian artist Wassily Kandinsky translated the dance movements of Gret Palucca into a series of schematic diagrams consisting of simple geometric shapes. The following is a computer generated schematic (approximating Kandinsky's) that corresponds to the accompanying dance position. The schematic is created with a GraphicsPath object and the statements that follow. However, the statements have been rearranged, and your task is to place them in a sequence to draw the schematic. Recall that a GraphicsPath object automatically connects objects.

Test Your Understanding
Graphics g = panel1.CreateGraphics();
g.SmoothingMode = SmoothingMode.AntiAlias;
GraphicsPath gp = new GraphicsPath();
gp.AddLine(10,170,30,170);
gp.AddLine(40,50,50,20);
gp.StartFigure();
gp.AddLine(16,100,100,100);
gp.AddLine(50,20,145,100);
gp.AddLine(100,100,190,180);
gp.StartFigure();
gp.AddArc(65,10,120,180,180,80);
g.DrawPath(new Pen(Color.Black,2),gp); gp.StartFigure();
gp.AddArc(65,5,120,100,200,70);

8:

The following statements are applied to the original image A:

Point ptA = new Point(bmp.Height,0);
Point ptB = new Point(bmp.Height,bmp.Width);
Point ptC = new Point(0,0);
Point[]dp = {ptA,ptB,ptC};
g.DrawImage(bmp,dp);

Which image is drawn by the last statement?

Test Your Understanding


[1] Tertiary colors are red-orange, yellow-orange, yellow-green, blue-green, blue-violet, and red-violet.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset