Previous Section  < Day Day Up >  Next Section

8.1. 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).

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梐n 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.

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.)

Listing 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梩he coordinates that define the shapes梐re 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.FromHdc. Creates the Graphics object from a specified handle to a Win32 device context. This is used primarily for interoperating with GDI.

  • Graphics.FromImage. Creates 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.FromHwnd. Creates 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).

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.

Figure 8-4. Effects of invalidating a region


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梚t 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);

         }

      }

   }

}


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梔epending 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.

    Previous Section  < Day Day Up >  Next Section