vendredi 4 juillet 2008

Printing and Graphics

Drawing and Printing in C#

By James Foxall,Wendy Haro-Chun

Date: Feb 15, 2002

Sample Chapter is provided courtesy of Sams.

Return to the article


C# provides an amazingly powerful array of drawing capabilities. In this "one-hour" excerpt by James Foxall and Wendy Haro-Chun, you learn the basic skills for drawing shapes and text to a form or other graphical surface.

C# provides an amazingly powerful array of drawing capabilities. However, this power comes at the price of a steep learning curve. Drawing isn't intuitive; you can't sit down for a few minutes with the online Help text and start drawing graphics. After you learn the basic principles involved, however, you'll find that drawing isn't that complicated. In this hour, you'll learn the basic skills for drawing shapes and text to a form or other graphical surface. You'll learn about pens, colors, and brushes. In addition, you'll learn how to persist graphics on a form—and even how to create bitmaps that exist solely in memory.

The highlights of this hour include the following:

  • Understanding the Graphics object
  • Working with pens
  • Using system colors
  • Working with rectangles
  • Drawing shapes
  • Drawing text
  • Persisting graphics on a form

Understanding the Graphics Object

The code within the Windows operating system that handles drawing everything to the screen, including text, lines, and shapes, is called the Graphics Device Interface (GDI). The GDI processes all drawing instructions from applications as well as from Windows itself, and generates the proper output for the current display. Because the GDI generates what you see onscreen, it has the responsibility of dealing with the particular display driver installed on the computer and the settings of the driver, such as resolution and color depth. This means that applications don't have to worry about these details; you write code that tells the GDI what to output, and the GDI does whatever is necessary to produce that output. This behavior is called device independence because applications can instruct the GDI to display text and graphics using code that is independent of the particular display device.

C# code communicates with the GDI primarily via a Graphics object. The basic process is the following:

  • An object variable is created to hold a reference to a Graphics object.

  • The object variable is set to a valid Graphics object (new or existing).

  • To draw or print, you call methods of the Graphics object.

Creating a Graphics Object for a Form or Control

If you want to draw directly to a form or control, you can easily get a reference to the drawing surface by calling the CreateGraphics() method of the object in question. For example, to create a Graphics object that draws to a text box, you could use code such as this:

System.Drawing.Graphics objGraphics; objGraphics = this.textBox1.CreateGraphics();

When you call CreateGraphics(), you're setting the object variable to hold a reference to the Graphics object of the form or control's client area. The client area of a form is the gray area within the borders and title bar of the form. The client area of a control is usually the entire control. All drawing and printing done using the Graphics object is sent to the client area. In the code shown previously, the Graphics object references the client area of a text box, so all drawing methods called on the Graphics object would draw on the text box only.

NOTE

When you draw directly to a form or control, the object in question doesn't persist what is drawn on it. If the form is obscured in any way, such as by a window covering it or by minimizing the form, the next time the form is painted, it won't contain anything that was drawn on it. Later in this hour, I'll teach you how to persist graphics on a form.

Creating a Graphics Object for a New Bitmap

You don't have to set a Graphics object to the client area of a form or control; you can also set a Graphics object to a bitmap that exists only in memory. For performance reasons, you might want to use a memory bitmap to store temporary images or to use as a place to build complex graphics before sending them to a visible element. To do this, you first have to create a new bitmap. To create a new bitmap, you declare a variable to hold a reference to the new bitmap and then you create a new bitmap using the following syntax:

Bitmap variable = new Bitmap(width, height, pixelformat);

The width and height arguments are exactly what they appear to be: the width and height of the new bitmap. The pixelformat argument, however, is less intuitive. This argument determines the color depth of the bitmap and may also specify whether the bitmap has an alpha layer (used for transparent portions of bitmaps). Table 1 lists a few of the common values for PixelFormat (see C#'s online Help for the complete list of values and their meanings). Note that the pixelformat parameter is referenced as System.Drawing.Imaging.PixelFormat.formatenumeration.

Table 1—Common Values for PixelFormat

Value Description
Format16bppGrayScale The pixel format is 16 bits per pixel. The color information specifies 65,536 shades of gray.
Format16bppRgb555 The pixel format is 16 bits per pixel. The color information specifies 32,768 shades of color, of which five bits are red, five bits are green, and five bits are blue.
Format24bppRgb The pixel format is 24 bits per pixel. The color information specifies 16,777,216 shades of color, of which eight bits are red, eight bits are green, and eight bits are blue.

For example, to create a new bitmap that is 640 pixels wide by 480 pixels tall and has a pixel depth of 24 bits, you could use the following statement:

objMyBitMap = new Bitmap(640, 480,     System.Drawing.Imaging.PixelFormat.Format24bppRgb);

After the bitmap is created, you can create a Graphics object that references the bitmap using the FromImage() method, like this:

objGraphics = Graphics.FromImage(objMyBitMap);

Now, any drawing or printing done using objGraphics would be performed on the memory bitmap. For the user to see the bitmap, you'd have to send the bitmap to a form or control. You'll do this in the section "Persisting Graphics on a Form."

Disposing of an Object

When you're finished with a Graphics object, you should call its Dispose() method to ensure that all resources used by the Graphics object are freed. Simply letting an object variable go out of scope doesn't ensure that the resources used by the object are freed. Graphics objects can use considerable resources, so you should always call Dispose() when you're finished with any Graphics object (including Pens and other types of objects).

C# also supports a way of automatically disposing object resources. This can be accomplished utilizing C#'s using statement. The using statement wraps a declared object or objects in a block and disposes of those objects once the block is done. As a result, after the code is executed in a block, the block is exited, and the resources are disposed of upon exit. Following is the syntax for the using statement and a small sample:

using (expression | type identifier = initializer) {   // Statements to execute  }  using (MyClass objClass = new MyClass()) {   objClass.Method1();   objClass.Method2(); }

One thing to keep in mind is that the using statement acts as a wrapper for an object within a specified block of code; therefore, it is only useful for declaring objects that are used and scoped within a method.

Working with Pens

A pen is an object that defines line-drawing characteristics. Pens are used to define color, line width, and line style (solid, dashed, and so on), and pens are used with almost all the drawing methods you'll learn about in this hour.

C# supplies a number of predefined pens, and you can also create your own. To create your own pen, use the following syntax:

Pen variable = new Pen(color, width);

After a pen is created, you can set its properties to adjust its appearance. For example, all Pen objects have a DashStyle property that determines the appearance of lines drawn with the pen. Table 2 lists the possible values for DashStyle.

Table 2—Possible Values for DashStyle

Value

Description

Dash

Specifies a line consisting of dashes.

DashDot

Specifies a line consisting of a pattern of dashes and dots.

DashDotDot

Specifies a line consisting of alternating dashes and double dots.

Dot

Specifies a line consisting of dots.

Solid

Specifies a solid line.

Custom

Specifies a custom dash style. The Pen object contains properties that can be used to define the custom line.


The enumeration for DashStyle is part of the Drawing.Drawing2D object. Therefore, to create a new pen and use it to draw an ellipse, for example, you could use the following code:

Pen objMyPen = new Pen(System.Drawing.Color.DarkBlue, 3); objMyPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;

C# includes many standard pens, which are available via the System.Drawing.Pens class, as in the following:

objPen = System.Drawing.Pens.DarkBlue;

When drawing using the techniques discussed shortly, you can use custom pens or system-defined pens—it's your choice.

Using System Colors

At some point, you may have changed your Windows theme, or perhaps you changed the image or color of your desktop. What you may not be aware of is that Windows enables you to customize the colors of almost all Windows interface elements. The colors that Windows allows you to change are called system colors. To change your system colors, right-click the desktop and choose Properties from the shortcut menu to display the Display Properties dialog box, and then click the Appearance tab (see Figure 1). To change the color for a specific item, you can select the item from the Item drop-down list, or you can click the element in the top half of the tab.

NOTE

On Windows XP, you'll need to click Advanced on the Appearance tab to change your system colors.

Figure 1 The Display Properties dialog box lets you select the colors of most Windows interface elements.

When you change a system color using the Display Properties dialog box, all loaded applications should change their appearance to match your selection. In addition, when you start any new applications, they should also match their appearance to your selection. If you had to write code to manage this behavior, you would have to write a lot of code, and you would be justified in avoiding the whole thing. However, making an application adjust its appearance to match the user's system color selections is actually quite trivial; therefore, there's no reason not to do it.

To designate that an interface color should stay in sync with a user's system colors, you assign a system color to a color property of the item in question (see Figure 2). Table 3 lists the system colors you can use. For example, if you wanted to ensure that the color of a button matches the user's corresponding system color, you would assign the system color named Control to the BackColor property of the Button control.

Figure 2 System colors are assigned using the System palette tab.

Fortunately, when you create new forms and when you add controls to forms, C# automatically assigns the proper system color to the appropriate properties. Another good thing is that when a user changes a system color using the Display Properties dialog box, C# automatically updates the appearance of objects that use system colors; you don't have to write a single line of code to do this.

Be aware that you aren't limited to assigning system colors to their logically associated properties. You can assign system colors to any color property you want, and you can also use system colors when drawing. This allows you, for example, to draw custom interface elements that match the user's system colors. Be aware, however, that if you do draw with system colors, C# won't update the colors automatically when the user changes system colors; you would have to redraw the elements with the new system color.

NOTE

Users don't just change their system colors for aesthetic purposes. I work with a programmer who is colorblind. He's modified his system colors so that he can see things better on the screen. If you don't allow your applications to adjust to the color preferences of the user, you may make using your program unnecessarily difficult, or even impossible, for someone with color blindness.

Table 3—Properties of the SystemColors Class

Enumeration

Description

ActiveBorder

The color of the filled area of an active window border.

ActiveCaption

The color of the background of an active caption bar (title bar).

ActiveCaptionText

The color of the text of the active caption bar (title bar).

AppWorkspace

The color of the application workspace. The application workspace is the area in a multiple-document view that is not being occupied by child windows.

Control

The color of the background of push buttons and other 3D elements.

ControlDark

The color of shadows on a 3D element.

ControlDarkDark

The color of darkest shadows on a 3D element.

ControlLight

The color of highlights on a 3D element.

ControlLightLight

The color of lightest highlights on a 3D element.

ControlText

The color of the text on buttons and other 3D elements.

Desktop

The color of the Windows desktop.

GrayText

The color of the text on a user-interface element when it's unavailable.

Highlight

The color of the background of highlighted text. This includes selected menu items as well as selected text.

HighlightText

The color of the foreground of highlighted text. This includes selected menu items as well as selected text.

HotTrack

The color used to represent hot tracking.

InactiveBorder

The color of an inactive window border.

InactiveCaption

The color of the background of an inactive caption bar.

InactiveCaptionText

The color of the text of an inactive caption bar.

Info

The color of the background of the ToolTip.

InfoText

The color of the text of the ToolTip.

Menu

The color of the menu background.

MenuText

The color of the menu text.

ScrollBar

The color of the scrollbar background.

Window

The color of the background in the client area of a window.

WindowFrame

The color of the frame around a window.

WindowText

The color of the text in the client area of a window.


Working with Rectangles

Before learning how to draw shapes, you need to understand the concept of a rectangle as it relates to C# programming. A rectangle isn't necessarily used to draw a rectangle (although it can be). Rather, a rectangle is a structure used to hold bounding coordinates used to draw a shape. Obviously, a square or rectangle can fit within a rectangle. However, so can circles and ellipses. Figure 3 illustrates how most shapes can be bound by a rectangle.

Figure 3 Rectangles are used to define the bounds of most shapes.

To draw most shapes, you must have a rectangle. The rectangle you pass to a drawing method is used as a bounding rectangle; the proper shape (circle, ellipse, and so on) is always drawn. Creating a rectangle is easy. First, you dimension a variable as Rectangle and then you set the X, Y, Width, and Height properties of the object variable. The X, Y value is the coordinate of the upper-left corner of the rectangle. For example, the following code creates a rectangle that has its upper-left corner at coordinate 0,0, has a width of 100, and a height of 50:

Rectangle rectBounding = new Rectangle(); rectBounding.X = 0; rectBounding.Y = 0; rectBounding.Width = 100; rectBounding.Height = 50;

The Rectangle object enables you to send the X, Y, Height, and Width values as part of its initialize construct. Using this technique, you could create the same rectangle with only a single line of code:

 Rectangle rectBounding = new Rectangle(0,0,100,50);

You can do a number of things with a rectangle after it's defined. Perhaps the most useful is the capability to enlarge or shrink the rectangle with a single statement. You enlarge or shrink a rectangle using the Inflate() method. The most common syntax of Inflate() is the following:

object.Inflate(changeinwidth, changeinheight);

When called this way, the rectangle width is enlarged (the left side of the rectangle remains in place) and the height is enlarged (the top of the rectangle stays in place). To leave the size of the height or width unchanged, pass 0 as the appropriate argument. To shrink a dimension, specify a negative number.

If you're going to do much with drawing, you'll use a lot of Rectangle objects, and I strongly suggest that you learn as much about them as you can.

Drawing Shapes

Now that you've learned about the Graphics object, pens, and rectangles, you'll probably find drawing shapes to be fairly simple. Shapes are drawn by calling methods of a Graphics object. Most methods require a rectangle, which is used as the bounding rectangle for the shape, as well as a pen. In this section, I'll show you what you need to do to draw different shapes.

NOTE

I've chosen to discuss only the most commonly drawn shapes. The Graphics object contains many methods for drawing additional shapes.

Drawing Lines

Drawing lines is accomplished with the DrawLine() method of the Graphics object. DrawLine() is one of the few drawing methods that doesn't require a rectangle. The syntax for DrawLine() is

object.DrawLine(pen, x1, y1, x2, y2);

Object refers to a Graphics object and pen refers to a Pen object, both of which have already been discussed. X1, Y1 is the coordinate of the starting point of the line, whereas X2, Y2 is the coordinate of the ending point; C# draws a line between the two points, using the specified pen.

Drawing Rectangles

Drawing rectangles (and squares for that matter) is accomplished using the DrawRectangle() method of a Graphics object. As you might expect, DrawRectangle() accepts a pen and a rectangle. Following is the syntax for calling DrawRectangle() in this way:

object.DrawRectangle(pen, rectangle);

If you don't have a Rectangle object (and you don't want to create one), you can call DrawRectangle() using the following format:

object.DrawRectangle(pen, X, Y, width, height);

Drawing Circles and Ellipses

Drawing circles and ellipses is accomplished by calling the DrawEllipse() method. If you're familiar with geometry, you'll note that a circle is simply an ellipse that has the same height as it does width. This is why no specific method exists for drawing circles; DrawEllipse() works perfectly. Like the DrawRectangle() method, DrawEllipse() accepts a Pen and a Rectangle. The rectangle is used as a bounding rectangle—the width of the ellipse is the width of the rectangle, whereas the height of the ellipse is the height of the rectangle. DrawEllipse() has the following syntax:

object.DrawEllipse(pen, rectangle);

In the event that you don't have a Rectangle object defined (and again you don't want to create one), you can call DrawEllipse() with this syntax:

object.DrawEllipse(pen, X, Y, Width, Height);

Clearing a Drawing Surface

To clear the surface of a Graphics object, call the Clear() method, passing it the color to paint the surface, like this:

objGraphics.Clear(Drawing.SystemColors.Control);

Drawing Text

Printing text on a Graphics object is very similar to drawing a shape, and the method name even contains the word Draw, in contrast to Print. To draw text on a Graphics object, call the DrawString() method. The basic format for DrawString() looks like this:

object.DrawString(stringoftext, font, brush, topX, leftY);

A few of these items are probably new to you. The argument stringoftext is fairly self-explanatory; it's the string you want to draw on the Graphics object. The topX and leftY arguments represent the coordinate at which drawing will take place; they represent the upper-left corner of the string, as illustrated in Figure 4.

Figure 4 The coordinate specified in DrawString() represents the upper-left corner of the printed text.

The arguments brush and font aren't so obvious. Both arguments accept objects. A brush is similar to a pen. However, whereas a pen describes the characteristics of a line, a brush describes the characteristics of a fill. For example, both pens and brushes have a color, but where pens have an attribute for defining a line style, such as dashed or solid, a brush has an attribute for a fill pattern, such as solid, hatched, weave, or trellis. When drawing text, a solid brush is usually sufficient. You can create brushes in much the same way as you create pens, or you can use one of the standard brushes available from the System.Drawing.Brushes class.

A Font object defines characteristics used to format text, including the character set (Times New Roman, Courier, for example), size (point size), and style (bold, italic, normal, underlined, and so on). To create a new Font object, you could use code such as the following:

Font objFont; objFont = new System.Drawing.Font("Arial", 30);

The text Arial in this code is the name of a font installed on my computer. In fact, Arial is one of the few fonts installed on all Windows computers. If you supply the name of a font that doesn't exist, C# will use a default font. The second parameter is the point size of the text. If you want to use a style other than normal, you can provide a style value as a third parameter, like this:

objFont = new System.Drawing.Font("Arial Black", 30,FontStyle.Bold);

or

objFont = new System.Drawing.Font("Arial Black", 30,FontStyle.Italic);

In addition to creating a Font object, you can also use the font of an existing object, such as a Form. For example, the following statement prints text to a Graphics object using the font of the current form:

objGraphics.DrawString("This is the text that prints!",              this.Font,System.Drawing.Brushes.Azure, 0, 0);

Persisting Graphics on a Form

You'll often use the techniques discussed in this hour to draw to a form. However, you may recall from earlier hours that when you draw to a form (actually, you draw to a Graphics object that references a form), the things that you draw aren't persisted; the next time the form paints itself, the drawn elements will disappear. For example, if the user minimizes the form or obscures the form with another window, the next time the form is painted, it will be missing any and all drawn elements that were obscured. You can use a couple of approaches to deal with this behavior:

  • Place all code that draws to the form in the form's Paint event.

  • Draw to a memory bitmap and copy the contents of the memory bitmap to the form in the form's Paint event.

If you're drawing only a few items, placing the drawing code in the Paint event might be a good approach. However, consider a situation in which you've got a lot of drawing code. Perhaps the graphics are drawn in response to user input, so you can't re-create them all at once. In these situations, the second approach is clearly better.

Build a Graphics Project Example

You're now going to build a project that uses the skills you've already learned to draw to a form. In this project, you'll use the technique of drawing to a memory bitmap to persist the graphics each time the form paints itself.

NOTE

The project you're about to build is perhaps the most difficult yet. I'll explain each step of the process of creating this project, but I won't spend any time explaining the objects and methods that I've already discussed.

To make things interesting, I've used random numbers to determine font size as well as the X, Y coordinate of the text you're going to draw to the form. The Random class and its Next() method will be used to generate pseudo-random numbers. To generate a random number within a specific range (such as a random number between 1 and 10), you use the following:

randomGenerator.Next(1,10);

I don't want you to dwell on the details of how the ranges of random numbers are created. However, at times, you may need to use a random number, so I thought I'd spice things up a bit and teach you something cool at the same time.

Start by creating a new Windows Application titled Persisting Graphics.

Change the name of the default form to fclsMain, set the form's Text property to Persisting Graphics, and change the entry point of the project to reference fclsMain instead of Form1. The interface of your form will consist of a text box and a button. When the user clicks the button, the contents of the text box will be drawn on the form in a random location and a random font size. Add a new text box to your form and set its properties as follows:

Property

Value

Name
txtInput
Location
56,184
Size
100,20
Text
(make blank)

Add a new button to the form and set its properties as follows:

Property

Value

Name
btnDrawText
Location
160,184
Text
Draw Text

Let the code fly!

As I mentioned earlier, all drawing is going to be performed using a memory bitmap, which will then be drawn on the form. You will reference this bitmap in multiple places, so you're going to make it a module-level variable by following these steps:

  1. Double-click the Form to access its Load event.

  2. Locate the statement public class fclsMain : System.Windows.Forms.Form and position your cursor immediately after the left bracket ({) on the next line.

  3. Press Enter to create a new line.

  4. Enter the following statement:

    private System.Drawing.Bitmap m_objDrawingSurface;

For the bitmap variable to be used, it must reference a Bitmap object. A good place to initialize things is in the form's Load event, so put your cursor back in the Load event now and enter the following code:

// Create a drawing surface with the same dimensions as the client // area of the form. m_objDrawingSurface = new Bitmap(this.ClientRectangle.Width,   this.ClientRectangle.Height,_   System.Drawing.Imaging.PixelFormat.Format24bppRgb); InitializeSurface();

Your procedure should now look like the one shown in Figure 5.

Figure 5 Make sure your code appears exactly as it does here.

The first statement creates a new bitmap in memory. Because the contents of the bitmap are to be sent to the form, it makes sense to use the dimensions of the client area of the form as the size of the new bitmap—which is exactly what you've done. The final statement calls a procedure that you haven't yet created.

Position the cursor after the closing bracket (}) of the fclsMain_Load event and press Enter to create a new line. You're now going to write code to initialize the bitmap. The code will clear the bitmap to the system color named Control and then draw an ellipse that has the dimensions of the bitmap. (I've added comments to the code so that you can follow along with what's happening; all the concepts in this code have been discussed already.) Enter the following in its entirety:

private void InitializeSurface() {   Graphics objGraphics;   Rectangle rectBounds;       // Create a Graphics object that references the bitmap and clear it.   objGraphics = Graphics.FromImage(m_objDrawingSurface); 		   objGraphics.Clear(SystemColors.Control);    //Create a rectangle the same size as the bitmap.   rectBounds = new Rectangle(0, 0,         m_objDrawingSurface.Width,m_objDrawingSurface.Height);   //Reduce the rectangle slightly so the ellipse won't appear on the border.   rectBounds.Inflate(-1, -1);    // Draw an ellipse that fills the form.   objGraphics.DrawEllipse(Pens.Orange, rectBounds);    // Free up resources.   objGraphics.Dispose(); }

Your procedure should now look like the one shown in Figure 6.

Figure 6 Again, verify that your code is entered correctly.

If you run your project now, you'll find that nothing is drawn to the form. This is because the drawing is being done to a bitmap in memory, and you haven't yet added the code to copy the bitmap to the form. The place to do this is in the form's Paint event so that the contents of the bitmap are sent to the form every time the form paints itself. This ensures that the items you draw always appear on the form.

Create an event handler for the form's Paint event by first returning to the form designer and selecting the form. Click the Event icon (the lightning bolt) in the Properties window and then double-click Paint to create a Paint event . Add the following code to the Paint event:

Graphics objGraphics ;  //You can't modify e.Graphics directly. objGraphics = e.Graphics; // Draw the contents of the bitmap on the form. objGraphics.DrawImage(m_objDrawingSurface, 0,0,   m_objDrawingSurface.Width,   m_objDrawingSurface.Height); objGraphics.Dispose();

NOTE

The previous code can be rewritten as follows when utilizing the using statement mentioned earlier in this chapter, notice how the Dispose() method is not required anymore.

   using (Graphics objGraphics = e.Graphics)    {    objGraphics.DrawImage(m_objDrawingSurface,0,0,       m_objDrawingSurface.Width,      m_objDrawingSurface.Height);    }

The e parameter of the Paint event has a property that references the Graphics object of the form. However, you can't modify the Graphics object using the e parameter (it's read-only), which is why you've created a new Graphics object with which to work and then set the object to reference the form's Graphics object. The method DrawImage() draws the image in a bitmap to the surface of a Graphics object, so the last statement is simply sending the contents of the bitmap to the form.

If you run the project now, you'll find that the ellipse appears on the form. Furthermore, you can cover the form with another window or even minimize it, and the ellipse will always appear on the form when it's displayed again.

The last thing you're going to do is write code that draws the contents entered into the text box on the form. The text will be drawn with a random size and location. Return to the form designer and double-click the button to access its Click event. Add the following code:

Graphics objGraphics;  Font objFont;  int intFontSize, intTextX, intTextY; 			 Random randomGenerator = new Random();  			 // If no text has been entered, get out. if (txtInput.Text == "") return;  // Create a graphics object using the memory bitmap. objGraphics = Graphics.FromImage(m_objDrawingSurface);  // Create a random number for the font size. Keep it between 8 and 48. intFontSize = randomGenerator.Next(8,48); // Create a random number for the X coordinate of the text. intTextX = randomGenerator.Next(0,this.ClientRectangle.Width); // Create a random number for the Y coordinate of the text. intTextY = randomGenerator.Next(0,this.ClientRectangle.Height);  // Create a new font object. objFont = new System.Drawing.Font("Arial", intFontSize, FontStyle.Bold); // Draw the user's text. objGraphics.DrawString(txtInput.Text, objFont, System.Drawing.Brushes.Red, intTextX, intTextY); // Clean up. objGraphics.Dispose(); // Force the form to paint itself. This triggers the Paint event. this.Invalidate();

The comments I've included should make the code fairly self-explanatory. However, the last statement bears discussing. The Invalidate() method of a form invalidates the client rectangle. This operation tells Windows that the appearance of the form is no longer accurate and that the form needs to be repainted. This, in turn, triggers the Paint event of the form. Because the Paint event contains the code that copies the contents of the memory bitmap to the form, invalidating the form causes the text to appear. If you don't call Invalidate() here, the text won't appear on the form (but it is still drawn on the memory bitmap).

NOTE

If you draw elements that are based on the size of the form, you'll need to call Invalidate() in the Resize event of the form; resizing a form doesn't trigger the form's Paint event.

The last thing you need to do is make sure you free up the resources used by your module-level Graphics object. Add an event handler for the Closed event of the form now using the Properties window, and enter the following statement:

m_objDrawingSurface.Dispose();
 

Your project is now complete! Click Save All on the toolbar to save your work, and then press F5 to run the project. You'll notice immediately that the ellipse is drawn on the form. Type something into the text box and click the button. Click it again. Each time you click the button, the text is drawn on the form using the same brush, but with a different size and location (see Figure 7).

Figure 7 Text is drawn on a form, much like ordinary shapes.

Summary

You won't need to add drawing capabilities to every project you create. However, when you need the capabilities, you need the capabilities. In this hour, you learned the basic skills for drawing to a graphics surface, which can be a form, control, memory bitmap, or one of many other types of surfaces. You learned that all drawing is done using a Graphics object, and you now know how to create a Graphics object for a form or control and even how to create a Graphics object for a bitmap that exists in memory.

Most drawing methods require a pen and a rectangle, and you can now create rectangles and pens using the techniques you learned in this hour. After learning pens and rectangles, you've found that the drawing methods themselves are pretty easy to use. Even drawing text is a pretty easy process when you've got a Graphics object to work with.

Persisting graphics on a form can be a bit complicated, and I suspect this will confuse a lot of new C# programmers who try to figure it out on their own. However, you've now built an example that persists graphics on a form, and you'll be able to leverage the techniques involved when you have to do this in your own projects.

I don't expect you to be able to sit down for an hour and create an Adobe Photoshop knock-off. However, you now have a solid foundation on which to build. If you're going to attempt a project that performs a lot of drawing, you'll want to dig deeper into the Graphics object.

Q&A

What if I need to draw a lot of lines, one starting where another ends? Do I need to call DrawLine() for each line?

The Graphics object has a method called DrawLines(), which accepts a series of points. The method draws lines connecting the sequence of points.

Is there a way to fill a shape?

The Graphics object includes methods that draw filled shapes, such as FillEllipse() and FillRectangle().

Workshop

Quiz

1. What object is used to draw to a surface?

2. To set a Graphics object to draw to a form directly, you call what method of the form?

3. What object defines the characteristics of a line? A fill pattern?

4. How do you make a color property adjust with the user's Windows settings?

5. What object is used to define the bounds of a shape to be drawn?

6. What method do you call to draw an irregular ellipse? A circle?

7. What method do you call to print text on a Graphics surface?

8. To ensure that graphics persist on a form, the graphics must be drawn on the form in what event?

Exercises

  1. Modify the example in this hour to use a font other than Arial. If you're not sure what fonts are installed on your computer, open the Start menu and choose Settings and then Control Panel. You'll have an option on the Control Panel for viewing your system fonts.

  2. Create a project that draws an ellipse that fills the form, much like the one you created in this hour. However, draw the ellipse directly to the form in the Paint event. Make sure that the ellipse is redrawn when the form is sized. (Hint: Invalidate the form in the form's Resize event.)

Answers

1. What object is used to draw to a surface?

		Graphics

2. To set a Graphics object to draw to a form directly, you call what method of the form?

		CreateGraphics()

3. What object defines the characteristics of a line? A fill pattern?

Pens define lines, brushes define fill patterns.

4. How do you make a color property adjust with the user's Windows settings?

Assign a system color to the property

5. What object is used to define the bounds of a shape to be drawn?

		Rectangle

6. What method do you call to draw an irregular ellipse? A circle?

Ellipses and circles are both drawn using the DrawEllipse() method.

7. What method do you call to print text on a Graphics surface?

		DrawString()

8. To ensure that graphics persist on a form, the graphics must be drawn on the form in what event?

The form's Paint event




--
Alain Lompo
Excelta - Conseils et services informatiques
MCT
MCSD For Microsoft .Net
MVP Windows Systems Server / Biztalk Server
Certifié ITIL et Microsoft Biztalk Server

Imprimer une facture

The other day, I decided to cruise ZDNET to look for a simple shareware program to make invoices. I have to say, I couldn't find what I was looking for, so I turned to C# and .NET.  This program can stand improvements but it will get you started in creating an invoice and printing it to the printer. You can customize the invoice by changing the bitmap supplied in the download to an invoice of your choice and then move the controls to fit into the proper locations on the Background bitmap.  This invoice layout was scanned in  from Intuit's, Quicken 99 and modified to add a few features.

Figure 1 - The Invoice in a Windows Form

Below is the design for the InvoiceMaker.NET program in UML:

Figure 2 - UML Diagram of InvoiceMaker.NET reverse engineered using WithClass 2000

The program has several pieces of code probably worth discussing.  In this article we'll concentrate on printing and serialization.  If you are curious about how the printing was designed, you'll want to check out my article on the W-2 Form in which I borrowed most of the printing code. The only section that differs radically is how I printed out the ListView.  Below is the code for printing a ListView called from the printDocument1_PrintPage event handler:

private void DrawAll(Graphics g)
{......
// handle List View Control printing
if (Controls[i].GetType() == this.listView1.GetType())
{
for (int row = 0; row < listView1.Items.Count; row++)
{
int nextColumnPosition = listView1.Bounds.X;
for (int col = 0; col < listView1.Items[row].SubItems.Count; col++)
{
g.DrawString(listView1.Items[row].SubItems[col].Text, listView1.Items[row].Font,Brushes.Black,
(nextColumnPosition + 3)*scalex,(listView1.Items[row].Bounds.Y + listView1.Bounds.Y)* scaley,
new StringFormat());
nextColumnPosition += listView1.Columns[col].Width;
}
}
}
...
}

This code simply cycles through the ListView Items and through each ListView Item's Subitem and prints them to the Graphics surface using DrawString.  The horizontal position of the string is determined using the widths in the Columns collection of the ListView. (Note that the position is scaled to the printing surface as in the W-2 Forms article).

Serialization

In order to serialize the form, I created a separate object that can serialize itself called InvoiceData and then filled and unfilled the form using this object.  (I tried to make the Form serializable, but .NET didn't like that).  The InvoiceData class also contains a collection of objects called InvoiceDataRows, which are also serializable.  To make an object serializable in .NET, simply stick the attribute Serializable above the class as shown below:

[Serializable]
public class InvoiceData
{
public InvoiceDataRow[] DataRows = new InvoiceDataRow[23];
public string LogoFile = "";
public string BillerAddress = "";
public string BillToAddress = "";
public string ShipToAddress = "";
public string InvoiceDate = "";
public string InvoiceNumber = "";
public string DueDate = "";
public string PONumber = "";
public string PercentTax = "";
public string Subtotal = "";
public string Total = "";
public int RowCount = 0;
....
}

If we want to save the data
in the form, the following method is used:

void SaveForm()
{
// find a suitable place to put the invoice output
if (saveFileDialog1.ShowDialog() == DialogResult.OK)
{
// Fill the InvoiceData Object with the Form Input
FillInvoiceDataWithForm();
// Create a BinaryFormatter object for streaming data out to a file
IFormatter formatter = new BinaryFormatter();
// Create a file stream for writing
Stream stream = new FileStream(saveFileDialog1.FileName, FileMode.Create,
FileAccess.Write, FileShare.None);
// Serialize the members of the InvoiceData class into the file
formatter.Serialize(stream, TheInvoiceData);
stream.Close();
}
}

The BinaryFormatter object spits out the data into a file containing all the members of the InvoiceData class.  Because InvoiceDataRow is also serializable and because its a member of the InvoiceData class as an array, the entire InvoiceDataRow[] array is also spit out to the file. (This is a serious time saver, still it would be nice if Forms were serializable).

To Deserialize the data, we use the method below:

void OpenForm()
{
// Get the name of the file you want to load into the form
if (openFileDialog2.ShowDialog() == DialogResult.OK)
{
// Create a BinaryFormatter object for reading in the data
IFormatter formatter = new BinaryFormatter();
// Open a stream to the file containing the data
Stream stream = new FileStream(openFileDialog2.FileName, FileMode.Open,FileAccess.Read, FileShare.None);
// Deserialize the Data into the InvoiceData Object
TheInvoiceData = (InvoiceData)formatter.Deserialize(stream);
stream.Close();
// Transfer the data from the InvoiceData object into the Form
FillFormWithInvoiceData();
// ** This section is for UI maintenance after reading the file, and unrelated to serialization **
// Initialize Itemized row for editing row 0
InitializeRowEditing(0);
// Dump the first row of the List View into the row of edit
boxes
DumpListViewToRow(0);
// ******************************************************
}
}
}

Deserialization is just as simple.  TheInvoiceData class knows how to populate itself from the BinaryFormatter since its Serializable.  To get the data from the file into the InvoiceData object, you simply call Deserialize on the file stream you want to populate from. (Of course the file must be one that you previously created using serialize on this object, otherwise it won't work).

DateTime Controls

The Invoice program uses DateTime Controls to populate the dates in the form.  Once nice featrue of these controls is you can create custom formats by setting the Format property of the control to Custom and the CustomFormat property to the format you wish to display (mine are set to MM/dd/yy).

String Formatting an EditBox

To get the amounts converted into currency format, I used a nice feature in .NET that allows you to specify how you want a double value to look in the ToString method.  Below is the line of code that will format the sum(a double type) of the Amount column to look like a dollar and cents value.

SubtotalTextBox.Text = sum.ToString("#,###.00");

Improvements

This program would be better, if it didn't use serialization, but dumped its contents into a database.  Then you could track customer information, marketing data, and other nice things a business cares about in their invoices.  Anyway, if you want to spit out a quick invoice to your printer, this program should do the job. Happy billing!


--
Alain Lompo
Excelta - Conseils et services informatiques
MCT
MCSD For Microsoft .Net
MVP Windows Systems Server / Biztalk Server
Certifié ITIL et Microsoft Biztalk Server

Impression d'un formulaire complexe


Fig 1.0 - PrintPreview of the W2 Form

If you are a C# programmer, before you run out and buy that latest version of your favorite tax program, you may want to consider this useful article on form creation.  This article covers a fairly practical aspect of using a computer - dealing with forms.  The concepts in this article can be used to create any Form Application so that you can design forms that you can Fill Out, Open, Save, Print and Print Preview.  Below is the simple UML design of our W2 Form Filler Application:

Fig 1.1 - UML Doc/View Design of W2 Form App reverse engineered using WithClass 2000

The first step to creating the form is to scan in the Form and convert it to a .gif file.  You can also get most forms these days electronically from the .gov sites that supply them.  Once you've got the form in a .gif (or jpeg or whatever),  you can apply it to your form as an image background.   Simply set the forms BackgroundImage property to the gif or jpeg file that you scanned in.  Now you are ready to set up the forms edit fields.  This form only uses TextBoxes and CheckBoxes.  The TextBoxes overlay the whitespaces for all the places you would normally fill in the form and have names that are appropriate for the particular field on the form. The property window for a TextBox is shown below:



Fig 1.2 - Property window of a TextBox in the Form

The BorderStyle for all textboxes are set to none and the background color(BackColor) is set to the color of the form.

With Checkboxes you need to do a bit of extra work.  Since you can't eliminate the border of a checkbox, we needed to go into the jpeg file of the form and remove the checkboxes, so we could place the real checkboxes in the form.  You can us MSPaint and the little eraser utility to do this easily enough.  The properties of the checkboxes are shown below:

Fig 1.3 - Property window of a Checkbox in the Form

You need to choose the FlatStyle to be Flat for the Checkbox, so the checkbox looks like a checkbox on a typical government form rather than that fun 3d look.

Once we've put all our window controls on the form, we need to set the tab order.  This is done by clicking on the form and going into the View menu, then choosing Tab Order.

Fig 1.4 - Choosing Tab Order from the View Menu

Once you've chosen the Tab Order menu item, you'll notice your form light up with a bunch of boxed numbers.  Click in each box in the order you want users to traverse your form.

Now we can get down to some good old-fashioned C# coding (well not that old-fashioned yet ;-).  This application handles many aspects of C# .NET coding (serialization, printing, print preview).  We are only going to talk about printing in detail in this article, because, well, it's the most interesting.  Printing requires that you have a PrintDocument object added to the form.  We've also added a PrintDialog object and a PrintPreview Dialog Object.  It's much less expensive and time consuming to test printing in the print preview window so you should try to get this working first.  The PrintPreview code is shown below:

private void PreviewMenu_Click(object sender, System.EventArgs e)
{
PrintPreviewDialog printPreviewDialog1 =
new PrintPreviewDialog();
printPreviewDialog1.Document =
this.printDocument1 ; // Attach PrintDocument to PrintPreview Dialog
printPreviewDialog1.FormBorderStyle = FormBorderStyle.Fixed3D ;
printPreviewDialog1.SetBounds(20, 20,
this.Width, this.Height); // enlarge dialog to
show the form
printPreviewDialog1.ShowDialog();
}

Listing 1 - Code for Print Preview

Both printing and print preview use the same event to print the form. They both use the PrintPage event from the print document.  All printing code is performed in this routine. The printing is done into a Graphics object which is passed into the print page event arguement:

private void printDocument1_PrintPage(object sender,
System.Drawing.Printing.PrintPageEventArgs e)
{
DrawAll(e.Graphics);
// Pass the Graphics Object (or better known as the Device Context) to the Draw routine
}

Listing 2 - Code For Printing a Page

The DrawAll routine is in two parts. The first part prints the image scaled to fit on the printed page. The second part cycles through all the controls and prints the text or check mark contained within each control. The program uses the position of the textboxes and checkboxes, along with the scaling factors, to position the filled-in information on the form:

private void DrawAll(Graphics g)
{
// Create the source rectangle from the BackgroundImage Bitmap Dimensions
RectangleF srcRect = new Rectangle(0, 0, this.BackgroundImage.Width,
BackgroundImage.Height);
// Create the destination rectangle from the printer settings holding printer page dimensions
int nWidth = printDocument1.PrinterSettings.DefaultPageSettings.PaperSize.Width;
int nHeight = printDocument1.PrinterSettings.DefaultPageSettings.PaperSize.Height;
RectangleF destRect =
new Rectangle(0, 0, nWidth, nHeight/2);
// Draw the image scaled to fit on a printed page
g.DrawImage(this.BackgroundImage, destRect, srcRect, GraphicsUnit.Pixel);
// Determine the scaling factors of each dimension based on the bitmap and the printed page dimensions
// These factors will be used to scale the positioning of the contro contents on the printed form
float scalex = destRect.Width/srcRect.Width;
float scaley = destRect.Height/srcRect.Height;
Pen aPen =
new Pen(Brushes.Black, 1);
// Cycle through each control. Determine if it's a checkbox or a textbox and draw the information inside
// in the correct position on the form
for (int i = 0; i < this.Controls.Count; i++)
{
// Check if its a TextBox type by comparing to the type of one of the textboxes
if (Controls[i].GetType() == this.Wages.GetType())
{
// Unbox the Textbox
TextBox theText = (TextBox)Controls[i];// Draw the textbox string at the position of the textbox on the form, scaled to the print page
g.DrawString(theText.Text, theText.Font, Brushes.Black,theText.Bounds.Left*scalex, theText.Bounds.Top * scaley,new StringFormat());
}
if (Controls[i].GetType() == this.RetirementPlanCheck.GetType())
{
// Unbox the Checkbox
CheckBox theCheck = (CheckBox)Controls[i];
// Draw the checkbox rectangle on the form scaled to the print page
Rectangle aRect = theCheck.Bounds;g.DrawRectangle(aPen, aRect.Left*scalex,aRect.Top*scaley, aRect.Width*scalex, aRect.Height*scaley);
// If the checkbox is checked, Draw the x inside the checkbox on the form scaled to the print page
if (theCheck.Checked)
{
g.DrawString("x", theCheck.Font, Brushes.Black,
theCheck.Left*scalex + 1, theCheck.Top*scaley + 1,
new StringFormat());
}
}
}
}

Listing 3 - Drawing routine for drawing the form to the printer or the print preview

The actual printing onto a printer begins in the routine below.  This routine brings up the print dialog and if the user accepts, it prints the form onto the printer using the PrintPage event previously discussed:

private void menuItem2_Click(object sender, System.EventArgs e)
{
// Attach the PrintDialog to the PrintDocument Object
printDialog1.Document = this.printDocument1;
// Show the Print Dialog before printing
if (printDialog1.ShowDialog() == DialogResult.OK)
{
this.printDocument1.Print(); // Print the Form
}
}

Listing 4 - Printing Menu Event for printing to the printer

Serialization

You may want to browse through the rest of the code to see how serialization is done. This project uses a Document/View archictecture.  The W2Document Class handles persistence for the form(reading/writing) and the W2Document is made serializable by the [Serializable()] attribute inside the class. The read and write routines use the BinaryFormatter Class  in combination with the File Class to serialize and deserialize the information extracted from the form.

Improvements

I think that the project could be made much more useful if the code was ported to the Web Form. Then someone could create an application with C# running code-behind that outputted the Web Form information into a database rather than a file.  Then maybe government, hospitals, insurance companies, law firms and all other businesses dealing with "form-bureaucracy" could get more easily organized through .NET.



--
Alain Lompo
Excelta - Conseils et services informatiques
MCT
MCSD For Microsoft .Net
MVP Windows Systems Server / Biztalk Server
Certifié ITIL et Microsoft Biztalk Server

Printing again and again

If you misplaced your ruler, here's an application that will create one for you on your printer! Unfortunately, you'll still need a ruler the first time using it so that you can calibrate the measurement for your particular printer, but once you know the calibration value, you are all set with a fairly accurate ruler.  Below is the simple design of our ruler.  The ruler itself is drawn in a Form.  The only other class used is the Calibration Dialog used to enter and retrieve a calibration value:

Figure 1 - Part of the ruler



This design was reverse engineered using the WithClass 2000 UML Design Tool for C#

The ruler is created by simply determining the resolution in pixels per inch.  This information can be retrieved from the graphics object itself:

void SizeFormToRuler()
{
// get resolution, most screens are 96 dpi, but you never know...
Graphics g = this.CreateGraphics();
this.HRes = g.DpiX; // Horizontal Resolution
this.VRes = g.DpiY; // Vertical Resolution
Width = (int)HRes;
Height = (
int)VRes * 11;
Left = 250;
Top = 5;
}

The actual tick marks and numbers are drawn
in the Form_Paint event handler method. This method establishes a Ruler Height in inches and then calls the method to draw the ruler:

private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
Graphics g = e.Graphics;
RulerHeight = 11;
DrawRuler(g);
}

The DrawRuler method
is the heart of the application. It draws the tick marks and numbers and spaces them according to the number of pixels in an inch (+ a calibration value to make the inch measurement accurate). The Modulus Function is used to determine when its appropriate to draw a particular tick mark. For example when i modulus PixelsPerInch is equal to 0, then we've found an inch marker.

private void DrawRuler(Graphics g)
{
g.DrawRectangle(Pens.Black, ClientRectangle);
g.FillRectangle(Brushes.White , 0, 0, Width, Height);
float PixelsPerInch = VRes + (float)Calibration;
int count = 1;
//cycle through every pixel in the ruler at 1/16 pixel intervals
// Mark the appropriate tick values
for (float i = 1; i < (float)PixelsPerInch*RulerHeight + 10; i += 0.0625f)
{
// use the modulus function to determine what type of tick to use
if ((i % (PixelsPerInch)) == 0)
{
g.DrawLine(Pens.Black, 0, i, 25, i);
// Draw a Number at every inch mark
g.DrawString(count.ToString(), TheFont, Brushes.Black, 25, i, new StringFormat));count++;
}
else if (((i*2) % ((PixelsPerInch))) == 0)
{
g.DrawLine(Pens.Black, 0, i, 20, i);
}
else if (((i*4) % ((PixelsPerInch))) == 0)
{
g.DrawLine(Pens.Black, 0, i, 15, i);
}
else if (((i*8) % ((PixelsPerInch))) == 0)
{
g.DrawLine(Pens.Black, 0, i, 10, i);
}
else if (((i*16) % ((PixelsPerInch))) == 0)
{
g.DrawLine(Pens.Black, 0, i, 5, i);
}
}
}

The calibration value is retrieved by using the calibration dialog.  This dialog brings up a NumericUpDown control for choosing the number of pixels to offset the inch.  The offset can be a negative or positive number of pixels depending upon how your printer is off.  I found my printer was off by 4 pixels too small, so my calibration was +4.  Below is the calibration dialog:

Fig 3  - Ruler Calibration Dialog

The Calibration Dialog uses ShowDialog to display the form and retrieves the Calibration Value in the CalibrationValue property of the dialog:

private void CalibrationMenu_Click(object sender, System.EventArgs e)
{
// Create Calibration Dialog
CalibrationForm dlg = new CalibrationForm();
// Set the initial Calibration Value
dlg.CalibrationValue = Calibration;
// Show the dialog and retrieve the Calibration Value
if (dlg.ShowDialog() == DialogResult.OK)
{
Calibration = dlg.CalibrationValue;
WriteCalibration();
// write the value out to a file to remember for next time
}
Invalidate();
}

Printing the ruler
is accomplished using 3 different print controls: The PrintDocument, The PrintDialog, and the PrintPreviewDialog control. Once you've dropped these controls on your form and assigned the PrintDocument Instance to the corresponding Document properties in the PrintDialog and the PrintPreviewDialog, it's fairly easy to set up printing. Below is the routine for PrintPreview. As you can see there is not really much to it.

private void PrintPreviewMenu_Click(object sender, System.EventArgs e)
{
this.printPreviewDialog1.ShowDialog();
}

Printing
is not much more complicated. Simply open the PrintDialog and once the user has assigned properties, call the Print method on the PrintDocument:

private void PrintMenu_Click(object sender, System.EventArgs e)
{
if (printDialog1.ShowDialog() == DialogResult.OK)
{
printDocument1.Print();
}
}

The actual printing occurs
in the PrintDocument's PrintPage Event Handler. This is where you put all the GDI routines for drawing the ruler. You can actually use the same drawing routines in your print handler as you do for your Paint Handler. Your just passing in a Printer Graphics Object instead of a Screen Graphics object:

private void printDocument1_PrintPage(object sender,
System.Drawing.Printing.PrintPageEventArgs e)
{
Graphics g = e.Graphics;
PageSettings ps = e.PageSettings;
// Set the ruler height based on the Page Settings of the printer
// That way you can have a larger ruler for a larger piece of paper
RulerHeight = (int)(ps.Bounds.Height/100.0);
DrawRuler(g);
}

We've also added persistence to this application to remember the calibration value. The two routines below read and write the calibration value using the StreamReader and StreamWriter classes:

void ReadCalibration()
{
// Determine the path of the file from the application itself
string calfile = Application.StartupPath + @"\cal.txt";
// If the calibration file exists, read in the initial calibration value
if (File.Exists(calfile))
{
StreamReader tr =
new StreamReader(calfile);
string num = tr.ReadLine();
Calibration = Convert.ToInt32(num);
tr.Close();
}
}
void WriteCalibration()
{
string calfile = Application.StartupPath + @"\cal.txt";
treamWriter tw =
new StreamWriter(calfile);
tw.Flush();
// Write out the calibration value
tw.WriteLine(Calibration.ToString());
tw.Close();
}

Improvements

This control could be improved by adding a metric ruler option.  This is easy enough to accomplish simply by figuring out the pixels per centimeter and then using modulus 10 to get the millimeters.  Also a large ruler can be created by printing to multiple pages.  I'm sure you can think of some other ideas with a little measured thought



--
Alain Lompo
Excelta - Conseils et services informatiques
MCT
MCSD For Microsoft .Net
MVP Windows Systems Server / Biztalk Server
Certifié ITIL et Microsoft Biztalk Server

DataGridView Printing

Introduction

Well it is that time of the year again. Time to get in shape for the summer months, and what better way to do it then through a rigid exercise program. I was never very good at tracking my progress in the gym, but thanks to .NET (and my wife),  I have a way to do just that. This program uses the DataGridView bound to a Microsoft Access Database to create a printed sheet with your latest work out plans. The workout chart includes the exercise, number of sets, number of reps, amount of weight, and most importantly, whether or not you completed the exercise. The best part of this program is that you can print out your exercise program and bring it with you to the weight room. 

 

Figure 1 - Exercise Program

New Form Capture Feature

.NET comes with a new form capture feature: the ability to copy the form as you see it on the screen into a bitmap. Although not useful for controls that you need to scroll, New Form Capture Feature works if you can fit all the contents you need to see on the screen into a bitmap. In this application, we stretched the DataGridView to allow for the full contents of the exercises, and we enabled AutoScrolling on the Form. This way the DataGridView will be much larger than the form, and the screen capture will print the entire DataGridView contents to the printer. The exercise application gives you two options: turning on the form capture feature to print out the form bitmap, or printing using a customized DataGridView printing. The customized printing is the default mode of the exercise program and it's more powerful because you can print the data in the grid view beyond any scrolling limitations of the DataGridView. Below is the code for capturing the form in a bitmap and drawing it to a graphics surface:
 

Listing 1 - Printing the Form Capture to a Graphics Surface

private void DrawFormSurface(System.Drawing.Printing.PrintPageEventArgs e)
        {

            // create a bitmap the size of the form
            Bitmap bmp = new Bitmap(gridExercise.Width, gridExercise.Height);

            // draw the form image to the bitmap
            gridExercise.DrawToBitmap(bmp, new Rectangle(0, 0, gridExercise.Width, gridExercise.Height));

            // draw the bitmap image of the form onto the graphics surface
            e.Graphics.DrawImage(bmp, new Point(0, 0));
        }

 

Using a Filename to Map an Image in the DataGridView

We want to place an image of the exercise into the DataGridView without inserting the image in the database.  We prefer to place all the images in an images directory. Then we will read the images file names from the database, retrieve the images from the images directory, and put the images into the DataGridView. In order to accomplish all this, we'll need to create a custom grid view column and grid view cell that inherits from DataGridViewImageColumn and DataGridViewImageCell.  We then override the GetFormattedValue method in the custom DataGridViewImageCell class. In the GetFormatted value method, we create a mapping between the file names and the actual images. Below is the code for accomplishing the filename to image mapping in a DataGridView:

Listing 2 - Mapping Images into the DataGridView from file name Strings

using System;
using
System.IO;
using
System.Collections.Generic;
using
System.Text;
using
System.Drawing;
using
System.Windows.Forms;
using
System.ComponentModel;
using
System.Reflection;
 

namespace ExerciseProgram
{

 /// <summary>
 /// Create a Custom DataGridViewImageColumn
 /// </summary>

public class StringImageColumn : DataGridViewImageColumn
{

    public StringImageColumn()
    {
        this.CellTemplate = new CustomImageCell();
        this.ValueType = typeof(string);  // value of this column is a string, but it shows  images in the cells after formatting
    }

}

 

/// <summary>
///
Create a Custom DataGridViewImageCell
///
</summary>

public class CustomImageCell : DataGridViewImageCell
{

    // mapping between filename and image
    static System.Collections.Generic.Dictionary<string, Image> dotImages = new Dictionary<string, Image>(); // load up custom dot images

    static public System.Collections.Generic.Dictionary<string, Image> Images
    {
        get
        {
            return dotImages;
        }
    }

    public CustomImageCell()
    {
        this.ValueType = typeof(int);
    }

 

    protected override object GetFormattedValue(
        object value, int rowIndex, ref DataGridViewCellStyle cellStyle,
        TypeConverter valueTypeConverter,
        TypeConverter formattedValueTypeConverter,
        DataGridViewDataErrorContexts context)
    {

        if (value.GetType() != typeof(string))
            return null;

        // get the image from the string and return it
        LoadImage(value);

        // return the mapped image
        return dotImages[(string)value];
    }

    public static void LoadImage(object value)
    {

       
//  load the image from the images directory if it does not exist
        if (dotImages.ContainsKey((string)value) == false)
        {
            string path = Path.GetDirectoryName(Application.ExecutablePath) + "\\images\\" + (string)value;

            // read the image file
            Image theImage = Image.FromFile(path);

             //  assign the image mapping
            dotImages[(string)value] = theImage;
        }
    }

    public override object DefaultNewRowValue
    {
        get
        {

           
return 0;
       
}
    }

}

Updating the Grid

The DataGridView is bound to the Microsoft Access Database via a DataGridAdapter. We can edit the sets, reps, and weight values inside the grid while increasing body strength and the program will persist these values to the database. Sometimes it is useful to override the behavior of the DataGridView for updates and inserts in order to customize how the grid is updated. Below is the ADO.NET code that updates the grid in Microsoft Access without using the OleDb Adapter:
 

Listing 3 - Updating the Microsoft  Access Database after editing the GridView

/// <summary>

        /// Event Handler called when Cell is finished editing
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void gridExercise_CellEndEdit(object sender, DataGridViewCellEventArgs e)
        {
            UpdateCurrentRow(e);
        }

        private void UpdateCurrentRow(DataGridViewCellEventArgs e)
        {
            int index = e.RowIndex;
            DataRow dr = scheduleDataSet.Tables[0].Rows[e.RowIndex];
            string val = dr[gridExercise.Columns[e.ColumnIndex].DataPropertyName].ToString();
            OleDbCommand updateCommand = new OleDbCommand();

            // construct update command and update from the data set
            updateCommand.CommandText = "UPDATE `Schedule` SET `Completed` = {0},  `Reps` = {1},
                                             `Weight` = {2}, `Sets` = {3} WHERE `ID` = {4}"
;

            updateCommand.CommandText = String.Format(updateCommand.CommandText, dr["Completed"],
                                             dr["Reps"], dr["Weight"],
                                             dr["Sets"], dr["ID"]);

            updateCommand.CommandType = System.Data.CommandType.Text;
            updateCommand.Connection = scheduleTableAdapter.Connection;
            updateCommand.Connection.ConnectionString = string.Format(
                               @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0}\Schedule.mdb"
,
                               Path.GetDirectoryName(Application.ExecutablePath));

            updateCommand.Connection.Open();

            // execute the update on the database
            updateCommand.ExecuteNonQuery();
            updateCommand.Connection.Close();
        }

Printing the DataGridView by Brute Force.

Once we are satisfied with our exercise parameters, it is time to print out the exercise schedule and head to the gym. Printing is accomplished using the PrintDocument in conjuction with GDI+. Every row and column is painstakingly drawn to the print graphics surface. Listing 4 shows the code that draws the exercise rows below the header. The method loops through each row in the DataSet and draws the text corresponding to the column in the data row.  If the row contains a picture, rather than text, the image that is mapped to the text in the cell is printed instead. We also need to track the row and column position after placing each object onto the graphics surface so we know where the next object goes. We do this by incrementing the local variables: columnPosition and rowPosition.
 

Listing 4 - Printing out the Exercise Rows

        private void DrawGridBody(Graphics g, ref int columnPosition, ref int rowPosition)
        {
            // loop through each row and draw the data to the graphics
            // surface.

            foreach (DataRow dr in scheduleDataSet.Tables[0].Rows)
            {
                columnPosition = 0;
 

                // draw a line to separate the rows 

                g.DrawLine(Pens.Black, new Point(0, rowPosition), new Point(this.Width, rowPosition));

 

                // loop through each column in the row, and
                // draw the individual data item
                foreach (DataGridViewColumn dc in gridExercise.Columns)
                {
                    // if its a picture, draw a bitmap
                    if (dc.DataPropertyName == "Picture")
                    {
                        if (dr[dc.DataPropertyName].ToString().Length != 0)
                        {
                            if (CustomImageCell.Images.ContainsKey(dr[dc.DataPropertyName].ToString()))
                            {
                                g.DrawImage(CustomImageCell.Images[dr[dc.DataPropertyName].ToString()],
                                         new
Point(columnPosition, rowPosition));
                            }
                        }
                    }
                    else if (dc.ValueType == typeof(bool))
                    {
                        // draw a check box in the column

                        g.DrawRectangle(Pens.Black, new Rectangle(columnPosition, rowPosition + 20, 10, 10));
                    }
                    else
                    {
                        // just draw string in the column
                        string text = dr[dc.DataPropertyName].ToString();

                        if (dc.DefaultCellStyle.Font != null)
                            g.DrawString(text, dc.DefaultCellStyle.Font, Brushes.Black,
                                (float)columnPosition, (float)rowPosition + 20f); 
                       else
                            g.DrawString(text, this.Font, Brushes.Black, (float)columnPosition, (float)rowPosition + 20f);
                    }

 

                    // go to the next column position
                    columnPosition += dc.Width + 5;  

                } 

                // go to the next row position
                rowPosition = rowPosition + 65;
            }

        } 

Conclusion

The DataGridView is even more full featured than the DataGrid and provides a good interface for manipulating DataSets. This article explained two ways to print data inside the DataGridView. Hopefully this exercise will give you some insight into printing one of the more powerful controls inside the Windows Form in C# and .NET.
 



--
Alain Lompo
Excelta - Conseils et services informatiques
MCT
MCSD For Microsoft .Net
MVP Windows Systems Server / Biztalk Server
Certifié ITIL et Microsoft Biztalk Server