Chapter 9. Fonts, Text, and Printing

Topics in This Chapter

  • Fonts: .NET classes support the creation of individual font objects and font families. Treating a font as an object allows an application to set a font's style and size through properties.

  • Text: The Graphics.DrawString method and StringFormat class are used together to draw and format text on a drawing surface.

  • Printing: The PrintDocument class provides the methods, properties, and events used to control .NET printing. It exposes properties that allow printer selection, page setup, and choice of pages to be printed.

  • Creating a Custom PrintDocument Class: A custom PrintDocument class provides an application greater control over the printing process and can be used to ensure a consistent format for reports.

Chapter 8, “.NET Graphics Using GDI+,” focused on the use of GDI+ for creating graphics and working with images. This chapter continues the exploration of GDI+ with the focus shifting to its use for “drawing text.” One typically thinks of text as being printed—rather than drawn. In the .NET world, however, the display and rendering of text relies on techniques similar to those required to display any graphic: a Graphics object must be created, and its methods used to position and render text strings—the shape of which is determined by the font used.

This chapter begins with a look at fonts and an explanation of the relationship between fonts, font families, and typefaces. It then looks at the classes used to create fonts and the options for sizing them and selecting font styles. The Graphics.DrawString method is the primary tool for rendering text and has several overloads that support a number of formatting features: text alignment, text wrapping, the creation of columnar text, and trimming, to name a few.

The chapter concludes with a detailed look at printing. An incremental approach is taken. A simple example that illustrates the basic classes is presented. Building on these fundamentals, a more complex and realistic multi-page report with headers, multiple columns, and end-of-page handling is presented. Throughout, the emphasis is on understanding the GDI+ classes that support printer selection, page layout, and page creation.

Fonts

GDI+ supports only OpenType and TrueType fonts. Both of these font types are defined by mathematical representations that allow them to be scaled and rotated easily. This is in contrast to raster fonts that represent characters by a bitmap of a predetermined size. Although prevalent a few years ago, these font types are now only a historical footnote in the .NET world.

The term font is often used as a catchall term to describe what are in fact typefaces and font families. In .NET, it is important to have a precise definition for these terms because both Font and FontFamily are .NET classes. Before discussing how to implement these classes, let's look at the proper definition of these terms.

  • A font family is at the top of the hierarchy and consists of a group of typefaces that may be of any style, but share the same basic appearance. Tahoma is a font family.

  • A typeface is one step up the hierarchy and refers to a set of fonts that share the same family and style but can be any size. Tahoma bold is a typeface.

  • A font defines how a character is represented in terms of font family, style, and size. For example (see Figure 9-1), Tahoma Regular10 describes a font in the Tahoma font family having a regular style—as opposed to bold or italic—and a size of 10 points.

    Relationship of fonts, typefaces, and font families

    Figure 9-1. Relationship of fonts, typefaces, and font families

Font Families

The FontFamily class has two primary purposes: to create a FontFamily object that is later used to create an instance of a Font, or to provide the names of all font families available on the user's computer system.

Creating a FontFamily Object

Constructors:

public FontFamily (string name);
public FontFamily (GenericFontFamilies genfamilies);

Parameters:

name

A font family name such as Arial or Tahoma.

FontFamily ff = new FontFamily("Arial");  // Arial family

GenericFontFamilies

An enumeration (in the System.Drawing.Text namespace) that has three values: Monospace, Serif, and SansSerif. This constructor is useful when you are interested in a generic font family rather than a specific one. The following constructor specifies a font family in which each character takes an equal amount of space horizontally. (Courier New is a common monospace family.)

FontFamily ff = new FontFamily(GenericFontFamilies.Monospace);

As a rule, the more specific you are about the font family, the more control you have over the appearance of the application, which makes the first constructor preferable in most cases. However, use a generic font family if you are unsure about the availability of a specific font on a user's computer.

Listing Font Families

The FontFamily.Families property returns an array of available font families. This is useful for allowing a program to select a font internally to use for displaying fonts to users, so they can select one. The following code enumerates the array of font families and displays the name of the family using the Name property; it also uses the IsStyleAvailable method to determine if the family supports a bold style font.

string txt = "";
foreach (FontFamily ff in FontFamily.Families)
{
   txt += ff.Name;
   if (ff.IsStyleAvailable(FontStyle.Bold))txt= += " (B)";
   txt += "
"; // Line feed
}
textBox1.Text = txt;   // Place font family names in textbox

The Font Class

Although several constructors are available for creating fonts, they fall into two categories based on whether their first parameter is a FontFamily type or a string containing the name of a FontFamily. Other parameters specify the size of the font, its style, and the units to be used for sizing. Here are the most commonly used constructors:

Constructors:

public Font(FontFamily ff, Float emSize);
public Font(FontFamily ff, Float emSize, FontStyle style);
public Font(FontFamily ff, Float emSize, GraphicsUnit unit);
public Font(FontFamily ff, Float emSize, FontStyle style,
            GraphicsUnit unit);

public Font(String famname, Float emSize);
public Font(String famname, Float emSize, FontStyle style);
public Font(String famname, Float emSize, GraphicsUnit unit);
public Font(String famname, Float emSize, FontStyle style,
            GraphicsUnit unit); 

Parameters:

emSize

The size of the font in terms of the GraphicsUnit. The default GraphicsUnit is Point.

style

A FontStyle enumeration value: Bold, Italic, Regular, Strikeout, or Underline. All font families do not support all styles.

unit

The units in which the font is measured. This is one of the GraphicsUnit enumeration values:

Point—1/72nd inch

Inch

Display1/96th inch

Millimeter

Document—1/300th inch

Pixel (based on device resolution)

GraphicsUnits and sizing are discussed later in this section.

Creating a Font

Creating fonts is easy; the difficulty lies in deciding which of the many constructors to use. If you have already created a font family, the simplest constructor is

FontFamily ff = new FontFamily("Arial");
Font normFont = new Font(ff,10);      // Arial Regular 10 pt.

The simplest approach, without using a font family, is to pass the typeface name and size to a constructor:

Font normFont = new Font("Arial",10)  // Arial Regular 10 point

By default, a Regular font style is provided. To override this, pass a FontStyle enumeration value to the constructor:

Font boldFont = new Font("Arial",10,FontStyle.Bold);
Font bldItFont = new Font("Arial",10,
      FontStyle.Bold | FontStyle.Italic); // Arial bold italic 10

The second example illustrates how to combine styles. Note that if a style is specified that is not supported by the font family, an exception will occur.

The Font class implements the IDisposable interface, which means that a font's Dispose method should be called when the font is no longer needed. As we did in the previous chapter with the Graphics object, we can create the Font object inside a using construct, to ensure the font resources are freed even if an exception occurs.

using (Font normFont = new Font("Arial",12)) {

Using Font Metrics to Determine Font Height

The term font metrics refers to the characteristics that define the height of a font family. .NET provides both Font and FontClass methods to expose these values. To best understand them, it is useful to look at the legacy of typesetting and the terms that have been carried forward from the days of Gutenberg's printing press to the .NET Framework Class Library.

In typography, the square grid (imagine graph paper) used to lay out the outline (glyph) of a font is referred to as the em square. It consists of thousands of cells—each measured as one design unit. The outline of each character is created in an em square and consists of three parts: a descent, which is the part of the character below the established baseline for all characters; the ascent, which is the part above the baseline; and the leading, which provides the vertical spacing between characters on adjacent lines (see Figure 9-2).

Font metrics: the components of a font

Figure 9-2. Font metrics: the components of a font

Table 9-1 lists the methods and properties used to retrieve the metrics associated with the em height, descent, ascent, and total line space for a font family. Most of the values are returned as design units, which are quite easy to convert to a GraphicsUnit. The key is to remember that the total number of design units in the em is equivalent to the base size argument passed to the Font constructor. Here is an example of retrieving metrics for an Arial 20-point font:

FontFamily ff = new FontFamily("Arial");
Font myFont   = new Font(ff,20);   // Height is 20 points
int emHeight  = ff.GetEmHeight(FontStyle.Regular);     // 2048
int ascHeight = ff.GetCellAscent(FontStyle.Regular);   // 1854
int desHeight = ff.GetCellDescent(FontStyle.Regular);  // 434
int lineSpace = ff.GetLineSpacing(FontStyle.Regular);  // 2355
// Get Line Height in Points (20 x (2355/2048))
float guHeight = myFont.Size * (lineSpace / emHeight); // 22.99
float guHeight2 = myFont.GetHeight(); // 30.66 pixels

Table 9-1. Using Font and FontFamily to Obtain Font Metrics

Member

Units

Description

FontFamily.GetEmHeight

Design units

The height of the em square used to design the font family. TrueType fonts usually have a value of 2,048.

FontFamily.GetCellAscent

Design units

The height of a character above the base line.

FontFamily.GetCellDescent

Design units

The height of a character below the base line.

FontFamily.GetLineSpacing

Design units

The total height reserved for a character plus line spacing. The sum of CellAscent, CellDescent, and Leading (see Figure 9-2). This value is usually 12 to 15 percent greater than the em height.

Font.Size

Graphics unit

The base size (size passed to constructor).

Font.SizeInPoints

Points

The base size in points.

Font.GetHeight

Pixels

The total height of a line. Calculated by converting LineSpacing value to pixels.

The primary value of this exercise is to establish familiarity with the terms and units used to express font metrics. Most applications that print or display lines of text are interested primarily in the height of a line. This value is returned by the Font.GetHeight method and is also available through the Graphics.MeasureString method described in the next section.

Definitions:

cell height

=

ascent + descent

em height

=

cell height – internal leading

line spacing

=

cell height + external leading

Drawing Text Strings

The Graphics.DrawString method is the most straightforward way to place text on a drawing surface. All of its overloaded forms take a string to be printed, a font to represent the text, and a brush object to paint the text. The location where the text is to be printed is specified by a Point object, x and y coordinates, or a Rectangle object. The most interesting parameter is an optional StringFormat object that provides the formatting attributes for the DrawString method. We'll examine it in detail in the discussion on formatting.

Here are the overloads for DrawString. Note that StringFormat is optional in each.

Overloads:

public DrawString(string, font, brush, PointF      
   [,StringFormat]);
public DrawString(string, font, brush, float, float
   [,StringFormat]);
public DrawString(string, font, brush, RectangleF  
   [,StringFormat]);
 

Example:

Font regFont = new Font("Tahoma",12);
String s = "ice mast high came floating by as green as emerald.";
// Draw text beginning at coordinates  (20,5)
g.DrawString(s, regFont, Brushes.Black, 20,5);
regFont.Dispose(); 

In this example, the upper-left corner of the text string is located at the x,y coordinate 20 pixels from the left edge and 5 pixels from the top of the drawing surface. If the printed text extends beyond the boundary of the drawing surface, the text is truncated. You may want this in some cases, but more often you'll prefer that long lines be broken and printed as multiple lines.

Drawing Multi-Line Text

Several Drawstring overloads receive a rectangle to define where the output string is drawn. Text drawn into these rectangular areas is automatically wrapped to the next line if it reaches beyond the rectangle's boundary. The following code displays the fragment of poetry in an area 200 pixels wide and 50 pixels high.

String s = "and ice mast high came floating by as green 
   as emerald."
// All units in pixels
RectangleF rf = new RectangleF(20,5,200,50); 
// Fit text in rectangle
g.Drawstring(s,regFont,Brushes.Black, rf); 

Word wrapping is often preferable to line truncation, but raises the problem of determining how many lines of text must be accommodated. If there are more lines of text than can fit into the rectangle, they are truncated. To avoid truncation, you could calculate the height of the required rectangle by taking into account the font (f), total string length(s), and rectangle width (w). It turns out that .NET Graphics.MeasureString method performs this exact operation. One of its overloads takes the string, font, and desired line width as arguments, and returns a SizeF object whose Width and Height properties provide pixel values that define the required rectangle.

SizeF sf = g.MeasureString(String s, Font f, int w);

Using this method, the preceding code can be rewritten to handle the dynamic creation of the bounding rectangle:

Font regFont = new Font("Tahoma",12);
String s = "and ice mast high came floating by as green 
   as emerald."
int lineWidth = 200;
SizeF sf = g.MeasureString(s, regFont, lineWidth);
// Create rectangular drawing area based on return 
// height and width
RectangleF rf = new RectangleF(20,5,sf.Width, sf.Height); 
// Draw text in rectangle
g.Drawstring(s,regFont,Brushes.Black, rf); 
// Draw rectangle around text
g.DrawRectangle(Pens.Red,20F,5F,rf.Width, rf.Height);

Note that DrawString recognizes newline ( ) characters and creates a line break when one is encountered.

Formatting Strings with the StringFormat Class

When passed as an argument to the DrawString method, a StringFormat object can provide a number of formatting effects. It can be used to define tab positions, set column widths, apply right or left justification, and control text wrapping and truncation. As we will see in the next section, it is the primary tool for creating formatted reports. The members that we will make heaviest use of are shown in Table 9-2.

Table 9-2. Important StringFormat Members

Member

Description

Alignment

A StringAlignment enumeration value:

StringAlignment.Center—. Text is centered in layout rectangle.

StringAlignment.Far—. Text is aligned to the right for left-to-right text.

StringAlignment.Near—. Text is aligned to the left for left-to-right text.

Trimming

A StringTrimming enumeration value that specifies how to trim characters that do not completely fit in the layout rectangle:

StringTrimming.Character—. Text is trimmed to the nearest character.

StringTrimming.EllipsisCharacter—. Text is trimmed to the nearest

character and an ellipsis (...) is placed at the end of the line.

StringTrimming.Word—. Text is trimmed to the nearest word.

SetTabStops

Takes two parameters: SetTabStops(firstTabOffset, tabStops)

FirstTabOffset—. Number of spaces between beginning of line and first tab stop.

TabStops—. Array of distances between tab stops.

FormatFlags

This bit-coded property provides a variety of options for controlling print layout when printing within a rectangle.

StringFormatFlags.DirectionVertical—. Draws text from top-to-bottom.

StringFormatFlags.LineLimit—. Only entire lines are displayed within the rectangle.

StringFormatFlags.NoWrap—. Disables text wrapping. The result is that text is printed on one line only, irrespective of the rectangle's height

Using Tab Stops

Tab stops provide a way to align proportionate-spaced font characters into columns. To set up tab stops, you create a StringFormat object, use its SetTabStops method to define an array of tab positions, and then pass this object to the DrawString method along with the text string containing tab characters ( ).

Core Note

Core Note

If no tab stops are specified, default tab stops are set up at intervals equal to four times the size of the font. A 10-point font would have default tabs every 40 points.

As shown in Table 9-2, the SetTabStops method takes two arguments: the offset from the beginning of the line and an array of floating point values that specify the distance between tab stops. Here is an example that demonstrates various ways to define tab stops:

float[] tStops = {50f, 100f, 100f};  //Stops at: 50, 150, and 250
float[] tStops = {50f};        // Stops at: 50, 100, 150

You can see that it is not necessary to specify a tab stop for every tab. If a string contains a tab for which there is no corresponding tab stop, the last tab stop in the array is repeated. Listing 9-1 demonstrates using tabs to set column headers.

Example 9-1. Using Tab Stops to Display Columns of Data

private void RePaint(object sender, PaintEventArgs e)
{
   Graphics g = e.Graphics;
   Font hdrFont = new Font("Arial", 10,FontStyle.Bold);
   Font bdyFont = new Font("Arial", 10); 
   // (1) Create StringFormat Object
   StringFormat strFmt = new StringFormat();
   // (2) Define Tab stops
   float[] ts = {140,60,40};
   strFmt.SetTabStops(0, ts);
   // (3) Define column header text to be printed with tabs
   string header = "Artist	Country	Born	Died";
   // (4) Print column headers
   g.DrawString(header, hdrFont, Brushes.Black,10,10,strFmt);
   // Print one line below header
   string artist = "Edouard Manet	England	1832	1892";
   g.DrawString(artist,bdyFont,Brushes.Black,10,
                10 + bdyFont.GetHeight(), strFmt); 
   bdyFont.Dispose();
   hdrFont.Dispose();
}

Figure 9-3 shows the four-column output from this code. Note that the second column begins at the x coordinate 150, which is the first tab stop (140) plus the x coordinate (10) specified in DrawString.

Printing with tab stops

Figure 9-3. Printing with tab stops

The unit of measurement in this example is a pixel. This unit of measurement is determined by the Graphics.PageUnit property. To override the default (pixels), set the property to a GraphicsUnit enumeration value—for example, g.PageUnit = GraphicsUnit.Inch. Be aware that all subsequent drawing done with the Graphics object will use these units.

Core Note

Core Note

The use of tab spaces only supports left justification for proportionate fonts. If you need right justification—a virtual necessity for displaying financial data—pass a rectangle that has the appropriate coordinates to the DrawString method. Then, set the Alignment property of StringFormat to StringAlignment.Far.

String Trimming, Alignment, and Wrapping

The StringFormat Trimming and Alignment properties dictate how text is placed within a RectangleF object. Alignment works as you would expect, allowing you to center, right justify, or left justify a string. Trimming specifies how to truncate a string that extends beyond the boundaries of a rectangle when wrapping is not in effect. The basic options are to truncate on a word or character.

The following code segments demonstrate some of the common ways these properties can be used to format text strings.

Example 1: Printing Without a StringFormat Object

Font fnt = new Font("Tahoma",10,FontStyle.Bold);
RectangleF r = new RectangleF(5,5,220,60);
string txt = "dew drops are the gems of morning";
g.DrawString(txt,fnt,Brushes.Black,r);
g.DrawRectangle(Pens.Red,r.X,r.Y,r.Width,r.Height);
Example 1: Printing Without a StringFormat Object

Example 2: Printing with NoWrap Option

StringFormat strFmt = new StringFormat();
strFmt.FormatFlags = StringFormatFlags.NoWrap;
g.DrawString(txt,fnt,Brushes.Black,r,strFmt);
Example 2: Printing with NoWrap Option

Example 3: Printing with NoWrap and Clipping on a Word

StringFormat strFmt = new StringFormat();
strFmt.FormatFlags = StringFormatFlags.NoWrap;
strFmt.Trimming = StringTrimming.Word;
g.DrawString(txt,fnt,Brushes.Black,r,strFmt);
Example 3: Printing with NoWrap and Clipping on a Word

Example 4: Printing with NoWrap, Clipping on Word, and Right Justification

StringFormat strFmt = new StringFormat();
strFmt.FormatFlags = StringFormatFlags.NoWrap;
strFmt.Trimming = StringTrimming.Word;
strFmt.Alignment = StringAlignment.Far;
g.DrawString(txt,fnt,Brushes.Black,r,strFmt);
Example 4: Printing with NoWrap, Clipping on Word, and Right Justification

StringFormat also has a LineAlignment property that permits a text string to be centered vertically within a rectangle. To demonstrate, let's add two statements to Example 4:

strFmt.Alignment = StringAlignment.Center;
strFmt.LineAlignment = StringAlignment.Center;
Example 4: Printing with NoWrap, Clipping on Word, and Right Justification

Printing

The techniques discussed in Sections 9.1 and 9.2 are device independent, which means they can be used for drawing to a printer as well as a screen. This section deals specifically with the task of creating reports intended for output to a printer. For complex reporting needs, you may well turn to Crystal Reports—which has special support in Visual Studio.NET—or SQL Server Reports. However, standard reports can be handled quite nicely using the native .NET classes available. Moreover, this approach enables you to understand the fundamentals of .NET printing and apply your knowledge of event handling and inheritance to customize the printing process.

Overview

The PrintDocument class—a member of the System.Drawing.Printing namespace—provides the methods, properties, and events that control the print process. Consequently, the first step in setting up a program that sends output to a printer is to create a PrintDocument object.

PrintDocument pd = new PrintDocument();

The printing process in initiated when the PrintDocument.Print method is invoked. As shown in Figure 9-4, this triggers the BeginPrint and PrintPage events of the PrintDocument class.

PrintDocument events that occur during the printing process

Figure 9-4. PrintDocument events that occur during the printing process

An event handler wired to the PrintPage event contains the logic and statements that produce the output. This routine typically determines how many lines can be printed—based on the page size—and contains the DrawString statements that generate the output. It is also responsible for notifying the underlying print controller whether there are more pages to print. It does this by setting the HasMorePages property, which is passed as an argument to the event handler, to true or false.

The basic PrintDocument events can be integrated with print dialog boxes that enable a user to preview output, select a printer, and specify page options. Listing 9-2 displays a simple model for printing that incorporates the essential elements required. Printing is initiated when btnPrint is clicked.

Example 9-2. Basic Steps in Printing

using System.Drawing;
using System.Drawing.Printing;
using System.Windows.Forms;
// Code for Form class goes here
   // Respond to button click
   private void btnPrint_Click(object sender, 
                               System.EventArgs e)
   { PrintReport();   }
   // Set up overhead for printing
   private void PrintReport() 
   {
      // (1) Create PrintDocument object
      PrintDocument pd = new PrintDocument();
      // (2) Create PrintDialog
      PrintDialog pDialog = new PrintDialog();
      pDialog.Document = pd;
      // (3) Create PrintPreviewDialog
      PrintPreviewDialog prevDialog = new PrintPreviewDialog();
      prevDialog.Document = pd;
      // (4) Tie event handler to PrintPage event
      pd.PrintPage += new PrintPageEventHandler(Inven_Report);
      // (5) Display Print Dialog and print if OK received
      if (pDialog.ShowDialog()== DialogResult.OK) 
      {
         pd.Print(); // Invoke PrintPage event
      }
   }
   private void Inven_Report(object sender, 
                             PrintPageEventArgs e)
   {
      Graphics g = e.Graphics;
      Font myFont = new Font("Arial",10);
      g.DrawString("Sample Output",myFont,Brushes.Black,10,10);
      myFont.Dispose();
   }
}

This simple example illustrates the rudiments of printing, but does not address issues such as handling multiple pages or fitting multiple lines within the boundaries of a page. To extend the code to handle these real-world issues, we need to take a closer look at the PrintDocument class.

PrintDocument Class

Figure 9-5 should make it clear that the PrintDocument object is involved in just about all aspects of printing: It provides access to paper size, orientation, and document margins through the DefaultPageSettings class; PrinterSettings allows selection of a printer as well as the number of copies and range of pages to be printed; and event handlers associated with the PrintDocument events take care of initialization and cleanup chores related to the printing process. The PrintController class works at a level closer to the printer. It is used behind the scenes to control the print preview process and tell the printer exactly how to print a document.

Selected PrintDocument properties and events

Figure 9-5. Selected PrintDocument properties and events

An understanding of these classes and events is essential to implementing a robust and flexible printing application that provides users full access to printing options.

Printer Settings

The PrinterSettings object maintains properties that specify the printer to be used and how the document is to be printed—page range, number of copies, and whether collating or duplexing is used. These values can be set programmatically or by allowing the user to select them from the Windows PrintDialog component. Note that when a user selects an option on the PrintDialog dialog box, he is actually setting a property on the underlying PrinterSettings object.

Selecting a Printer

The simplest approach is to display the PrintDialog window that contains a drop-down list of printers:

PrintDocument pd = new PrintDocument();
PrintDialog pDialog = new PrintDialog();
pDialog.Document = pd;
if (pDialog.ShowDialog()== DialogResult.OK) 
{
   pd.Print(); // Invoke PrintPage event
}

You can also create your own printer selection list by enumerating the InstalledPrinters collection:

// Place names of printer in printerList ListBox
foreach(string pName in PrinterSettings.InstalledPrinters)
printerList.Items.Add(pName);

After the printer is selected, it is assigned to the PrinterName property:

string printer= 
      printerList.Items[printerList.SelectedIndex].ToString();
pd.PrinterSettings.PrinterName = printer;

Selecting Pages to Print

The PrinterSettings.PrintRange property indicates the range of pages to be printed. Its value is a PrintRange enumeration value—AllPages, Selection, or SomePagesthat corresponds to the All, Pages, and Selection print range options on the PrintDialog form. If Pages is selected, the PrinterSettings.FromPage and ToPage properties specify the first and last page to print. There are several things to take into consideration when working with a print range:

  • To make the Selection and Pages radio buttons available on the PrintDialog form, set PrintDialog.AllowSomePages and PrintDialog.AllowSelection to true.

  • The program must set the FromPage and ToPage values before displaying the Print dialog box. In addition, it's a good practice to set the MinimumPage and MaximumPage values to ensure the user does not enter an invalid page number.

  • Keep in mind that the values entered on a PrintDialog form do nothing more than provide parameters that are available to the application. It is the responsibility of the PrintPage event handler to implement the logic that ensures the selected pages are printed.

The following segment includes logic to recognize a page range selection:

pDialog.AllowSomePages = true;
pd.PrinterSettings.FromPage =1;
pd.PrinterSettings.ToPage = maxPg;
pd.PrinterSettings.MinimumPage=page 1;
pd.PrinterSettings.MaximumPage= maxPg;
if (pDialog.ShowDialog()== DialogResult.OK) 
{
   maxPg = 5;    // Last page to print
   currPg= 1;    // Current page to print
   if (pDialog.PrinterSettings.PrintRange == 
       PrintRange.SomePages)
   {
       currPg = pd.PrinterSettings.FromPage;
       maxPg  = pd.PrinterSettings.ToPage;
   }
   pd.Print(); // Invoke PrintPage event
}

This code assigns the first and last page to be printed to currPg and maxPg. These both have class-wide scope and are used by the PrintPage event handler to determine which pages to print.

Setting Printer Resolution

The PrinterSettings class exposes all of the print resolutions available to the printer through its PrinterResolutions collection. You can loop through this collection and list or select a resolution by examining the Kind property of the contained PrinterResolution objects. This property takes one of five PrinterResolutionKind enumeration values: Custom, Draft, High, Low, or Medium. The following code searches the PrinterResolutions collection for a High resolution and assigns that as a PrinterSettings value:

foreach (PrinterResolution pr in 
         pd.PrinterSettings.PrinterResolutions)
{
   if (pr.Kind == PrinterResolutionKind.High)
   {
      pd.PageSettings.PrinterResolution = pr;
      break;
   }
}

Page Settings

The properties of the PageSettings class define the layout and orientation of the page being printed to. Just as the PrinterSettings properties correspond to the PrintDialog, the PageSettings properties reflect the values of the PageSetupDialog.

PageSetupDialog ps = new PageSetupDialog();
ps.Document = pd;   // Assign reference to PrinterDocument 
ps.ShowDialog();

This dialog box lets the user set all the margins, choose landscape or portrait orientation, select a paper type, and set printer resolution. These values are exposed through the DefaultPageSettings properties listed in Figure 9-5. As we will see, they are also made available to the PrintPage event handler through the PrintPageEventArgs parameter and to the QueryPageSettingsEvent through its QueryPageSettingsEventArgs parameter. The latter can update the values, whereas PrintPage has read-only access.

Figure 9-6 illustrates the layout of a page that has the following DefaultPageSettings values:

Bounds.X = 0;
Bounds.Y = 0;
Bounds.Width = 850;
Bounds.Height = 1100;
PaperSize.PaperName = "Letter";
PaperSize.Height = 1100;
PaperSize.Width = 850;
Margins.Left = 100;
Margins.Right = 100;
Margins.Top = 100;
Margins.Bottom = 100;
Page settings layout

Figure 9-6. Page settings layout

All measurements are in hundredths of an inch. The MarginBounds rectangle shown in the figure represents the area inside the margins. It is not a PrinterSettings property and is made available only to the PrintPage event handler.

Core Note

Core Note

Many printers preserve an edge around a form where printing cannot occur. On many laser printers, for example, this is one-quarter of an inch. In practical terms, this means that all horizontal coordinates used for printing are shifted; thus, if DrawString is passed an x coordinate of 100, it actually prints at 125. It is particularly important to be aware of this when printing on preprinted forms where measurements must be exact.

PrintDocument Events

Four PrintDocument events are triggered during the printing process: BeginPrint, QueryPageSettingsEvent, PrintPage, and EndPrint. As we've already seen, PrintPage is the most important of these from the standpoint of code development because it contains the logic and statements used to generate the printed output. It is not necessary to handle the other three events, but they do provide a handy way to deal with the overhead of initialization and disposing of resources when the printing is complete.

BeginPrint Event

This event occurs when the PrintDocument.Print method is called and is a useful place to create font objects and open data connections. The PrintEventHandler delegate is used to register the event handler routine for the event:

pd.BeginPrint += new PrintEventHandler(Rpt_BeginPrint);

This simple event handler creates the font to be used in the report. The font must be declared to have scope throughout the class.

private void Rpt_BeginPrint(object sender, PrintEventArgs e)
{
   rptFont = new Font("Arial",10);
   lineHeight= (int)rptFont.GetHeight(); // Line height
}

EndPrint Event

This event occurs after all printing is completed and can be used to destroy resources no longer needed. Associate an event handler with the event using

pd.EndPrint += new PrintEventHandler(Rpt_EndPrint);

This simple event handler disposes of the font created in the BeginPrint handler:

private void Rpt_EndPrint(object sender, PrintEventArgs e)
{
   rptFont.Dispose();
}

QueryPageSettingsEvent Event

This event occurs before each page is printed and provides an opportunity to adjust the page settings on a page-by-page basis. Its event handler is associated with the event using the following code:

pd.QueryPageSettings += new 
      QueryPageSettingsEventHandler(Rpt_Query);

The second argument to this event handler exposes a PageSettings object and a Cancel property that can be set to true to cancel printing. This is the last opportunity before printing to set any PageSettings properties, because they are read-only in the PrintPage event. This code sets special margins for the first page of the report:

private void Rpt_Query(object sender,
                       QueryPageSettingsEventArgs e)
{
   // This is the last chance to change page settings 
   // If first page, change margins for title
   if (currPg ==1) e.PageSettings.Margins = 
                       new Margins(200,200,200,200);
   else e.PageSettings.Margins = new Margins(100,100,100,100);
}

This event handler should be implemented only if there is a need to change page settings for specific pages in a report. Otherwise, the DefaultPageSettings properties will prevail throughout.

PrintPage Event

The steps required to create and print a report fall into two categories: setting up the print environment and actually printing the report. The PrinterSettings and PageSettings classes that have been discussed are central to defining how the report will look. After their values are set, it's the responsibility of the PrintPage event handler to print the report to the selected printer, while being cognizant of the paper type, margins, and page orientation.

Figure 9-7 lists some of the generic tasks that an event handler must deal with in generating a report. Although the specific implementation of each task varies by application, it's a useful outline to follow in designing the event handler code. We will see an example shortly that uses this outline to implement a simple report application.

Tasks required to print a report

Figure 9-7. Tasks required to print a report

Defining the PrintPage Event Handler

The event handler method matches the signature of the PrintPageEventHandler delegate (refer to Listing 9-2):

public delegate void PrintPageEventHandler(
      object sender, PrintPageEventArgs e);

The PrintPageEventArgs argument provides the system data necessary for printing. As shown in Table 9-3, its properties include the Graphics object, PageSettings object, and a MarginBounds rectangle—mentioned earlier—that defines the area within the margins. These properties, along with variables defined at a class level, provide all the information used for printing.

Table 9-3. PrintPageEventArgs Members

Property

Description

Cancel

Boolean value that can be set to true to cancel the printing.

Graphics

The Graphics object used to write to printer.

HasMorePages

Boolean value indicating whether more pages are to be printed. Default is false.

MarginBounds

Rectangular area representing the area within the margins.

PageBounds

Rectangular area representing the entire page.

PageSettings

Page settings for the page to be printed.

Previewing a Printed Report

The capability to preview a report onscreen prior to printing—or as a substitute for printing—is a powerful feature. It is particularly useful during the development process where a screen preview can reduce debugging time, as well as your investment in print cartridges.

To preview the printer output, you must set up a PrintPreviewDialog object and set its Document property to an instance of the PrintDocument:

PrintPreviewDialog prevDialog = new PrintPreviewDialog();
      prevDialog.Document = pd;

The preview process is invoked by calling the ShowDialog method:

prevDialog.ShowDialog();

After this method is called, the same steps are followed as in actually printing the document. The difference is that the output is displayed in a special preview window (see Figure 9-8). This provides the obvious advantage of using the same code for both previewing and printing.

Report can be previewed before printing

Figure 9-8. Report can be previewed before printing

A Report Example

This example is intended to illustrate the basic elements of printing a multi-page report. It includes a data source that provides an unknown number of records, a title and column header for each page, and detailed rows of data consisting of left-justified text and right-justified numeric data.

Data Source for the Report

The data in a report can come from a variety of sources, although it most often comes from a database. Because database access is not discussed until Chapter 11, “ADO.NET,” let's use a text file containing comma-delimited inventory records as the data source. Each record consists of a product ID, vendor ID, description, and price:

1000761,1050,2PC/DRESSER/MIRROR,185.50 

A StreamReader object is used to load the data and is declared to have class-wide scope so it is available to the PrintPage event handler:

// Using System.IO namespace is required
// StreamReader sr; is set up in class declaration
sr = new StreamReader("c:\inventory.txt");

The PrintPage event handler uses the StreamReader to input each inventory record from the text file as a string. The string is split into separate fields that are stored in the prtLine array. The event handler also contains logic to recognize page breaks and perform any column totaling desired.

Code for the Application

Listing 9-3 contains the code for the PrintDocument event handlers implemented in the application. Because you cannot pass your own arguments to an event handler, the code must rely on variables declared at the class level to maintain state information. These include the following:

StreamReader sr;   // StreamReader to read inventor from file
string[]prtLine;   // Array containing fields for one record
Font rptFont;      // Font for report body
Font hdrFont;      // Font for header
string title= "Acme Furniture: Inventory Report";
float lineHeight;  // Height of a line (100ths inches)

The fonts and StreamReader are initialized in the BeginPrint event handler. The corresponding EndPrint event handler then disposes of the two fonts and closes the StreamReader.

Example 9-3. Creating a Report

// pd.PrintPage  += new PrintPageEventHandler(Inven_Report);
// pd.BeginPrint += new PrintEventHandler(Rpt_BeginPrint);
// pd.EndPrint   += new PrintEventHandler(Rpt_EndPrint);
//
// BeginPrint event handler
private void Rpt_BeginPrint(object sender, PrintEventArgs e)
{
   // Create fonts to be used and get line spacing.
   rptFont = new Font("Arial",10);
   hdrFont = new Font(rptFont,FontStyle.Bold);
   // insert code here to set up Data Source...
}
// EndPrint event Handler
private void Rpt_EndPrint(object sender, PrintEventArgs e)
{
   // Remove Font resources
   rptFont.Dispose();
   hdrFont.Dispose();
   sr.Close();       // Close StreamReader
}
// PrintPage event Handler
private void Inven_Report(object sender, PrintPageEventArgs e)
{
   Graphics g = e.Graphics;
   int xpos= e.MarginBounds.Left;
   int lineCt = 0;
   // Next line returns 15.97 for Arial-10 
   lineHeight = hdrFont.GetHeight(g);
   // Calculate maximum number of lines per page
   int linesPerPage = int((e.MarginBounds.Bottom – 
             e.MarginBounds.Top)/lineHeight –2);
   float yPos = 2* lineHeight+ e.MarginBounds.Top;
   int hdrPos = e.MarginBounds.Top;
   // Call method to print title and column headers
   PrintHdr(g,hdrPos, e.MarginBounds.Left);  
   string prod;
   char[]delim=  {','};
   while(( prod =sr.ReadLine())!=null)
   {
      prtLine= prod.Split(delim,4);
      yPos += lineHeight;   // Get y coordinate of line
      PrintDetail(g,yPos);  // Print inventory record
      if(lineCt > linesPerPage)
      {
         e.HasMorePages= true;
         break;
      }
   }
}
private void PrintHdr( Graphics g, int yPos, int xPos) 
{
   // Draw Report Title
   g.DrawString(title,hdrFont,Brushes.Black,xPos,yPos);
   // Draw Column Header
   float[] ts = {80, 80,200};
   StringFormat strFmt = new StringFormat();
   strFmt.SetTabStops(0,ts);
   g.DrawString("Code	Vendor	Description	Cost", 
         hdrFont,Brushes.Black,xPos,yPos+2*lineHeight,strFmt);
}
private void PrintDetail(Graphics g, float yPos)
{
   int xPos = 100;
   StringFormat strFmt = new StringFormat();
   strFmt.Trimming = StringTrimming.EllipsisCharacter;
   strFmt.FormatFlags = StringFormatFlags.NoWrap;
   RectangleF r = new RectangleF(xPos+160,yPos,
                                 200,lineHeight);
   // Get data fields from array
   string invenid = prtLine[0];
   string vendor  = prtLine[1];
   string desc    = prtLine[2];
   decimal price  = decimal.Parse(prtLine[3]);
   g.DrawString(invenid, rptFont,Brushes.Black,xPos, yPos);
   g.DrawString(vendor, rptFont,Brushes.Black,xPos+80, yPos);
   // Print description within a rectangle
   g.DrawString(desc, rptFont,Brushes.Black,r,strFmt);
   // Print cost right justified 
   strFmt.Alignment = StringAlignment.Far;  // Right justify
   strFmt.Trimming= StringTrimming.None; 
   g.DrawString(price.ToString("#,###.00"), 
        rptFont,Brushes.Black, xPos+400,yPos,strFmt);
}

The PrintPage event handler Inven_Report directs the printing process by calling PrintHdr to print the title and column header on each page and PrintDetail to print each line of inventory data. Its responsibilities include the following:

  • Using the MarginBounds rectangle to set the x and y coordinates of the title at the upper-left corner of the page within the margins.

  • Calculating the maximum number of lines to be printed on a page. This is derived by dividing the distance between the top and bottom margin by the height of a line. It then subtracts 2 from this to take the header into account.

  • Setting the HasMorePages property to indicate whether more pages remain to be printed.

The PrintHdr routine is straightforward. It prints the title at the coordinates passed to it, and then uses tabs to print the column headers. The PrintDetail method is a bit more interesting, as it demonstrates some of the classes discussed earlier in the chapter. It prints the inventory description in a rectangle and uses the StringFormat class to prevent wrapping and specify truncation on a character. StringFormat is also used to right justify the price of an item in the last column.

Figure 9-9 shows an example of output from this application. Measured from the left margin, the first three columns have an x coordinate of 0, 80, and 160, respectively. Note that the fourth column is right justified, which means that its x coordinate of 400 specifies where the right edge of the string is positioned. Vertical spacing is determined by the lineHeight variable that is calculated as

float lineHeight = hdrFont.GetHeight(g);
Output from the report example

Figure 9-9. Output from the report example

This form of the GetHeight method returns a value based on the GraphicsUnit of the Graphics object passed to it. By default, the Graphics object passed to the BeginPrint event handler has a GraphicsUnit of 100 dpi. The margin values and all coordinates in the example are in hundredths of an inch. .NET takes care of automatically scaling these units to match the printer's resolution.

Creating a Custom PrintDocument Class

The generic PrintDocument class is easy to use, but has shortcomings with regard to data encapsulation. In the preceding example, it is necessary to declare variables that have class-wide scope—such as the StreamReader—to make them available to the various methods that handle PrintDocument events. A better solution is to derive a custom PrintDocument class that accepts parameters and uses properties and private fields to encapsulate information about line height, fonts, and the data source. Listing 9-4 shows the code from the preceding example rewritten to support a derived PrintDocument class.

Creating a custom PrintDocument class turns out to be a simple and straightforward procedure. The first step is to create a class that inherits from PrintDocument. Then, private variables are defined that support the fonts and title that are now exposed as properties. Finally, the derived class overrides the OnBeginPrint, OnEndPrint, and OnPrintPage methods of the base PrintDocument class.

The overhead required before printing the report is reduced to creating the new ReportPrintDocument object and assigning property values.

string myTitle = "Acme Furniture: Inventory Report";
ReportPrintDocument rpd = new ReportPrintDocument(myTitle);
rpd.TitleFont = new Font("Arial",10, FontStyle.Bold);
rpd.ReportFont = new Font("Arial",10);
PrintPreviewDialog prevDialog = new PrintPreviewDialog();
prevDialog.Document = rpd;
prevDialog.ShowDialog();   // Preview Report
// Show Print Dialog and print report
PrintDialog pDialog = new PrintDialog();
pDialog.Document = rpd;
if (pDialog.ShowDialog() == DialogResult.OK) 
{
   rpd.Print();
}

The preceding code takes advantage of the new constructor to pass in the title when the object is created. It also sets the two fonts used in the report.

Example 9-4. Creating a Custom PrintDocument Class

// Derived Print Document Class
public class ReportPrintDocument: PrintDocument
{
   private Font hdrFont;
   private Font rptFont;
   private string title;
   private StreamReader sr;
   private float lineHeight;
   // Constructors
   public ReportPrintDocument()
   {}
   public ReportPrintDocument(string myTitle)
   {
      title = myTitle;
   }
   // Property to contain report title
   public string ReportTitle
   {
      get {return title;}
      set {title = value;}
   }
   // Fonts are exposed as properties
   public Font TitleFont
   {
      get {return hdrFont;}
      set {hdrFont = value;}
   }
   public Font ReportFont
   {
      get {return rptFont;}
      set {rptFont = value;}
   }
   // BeginPrint event handler
   protected override void OnBeginPrint(PrintEventArgs e)
   {
      base.OnBeginPrint(e);
      // Assign Default Fonts if none selected
      if (TitleFont == null) 
      {
         TitleFont = 
              new Font("Arial",10,FontStyle.Bold);
         ReportFont = new Font("Arial",10);
      }
      / Code to create StreamReader or other data source
      // goes here ...
      sr = new StreamReader(inputFile);
   }
   protected override void OnEndPrint(PrintEventArgs e)
   {
      base.OnEndPrint(e);
      TitleFont.Dispose();
      ReportFont.Dispose();
      sr.Close();
   }
   // Print Page event handler
   protected override void OnPrintPage(PrintPageEventArgs e)
   {
      base.OnPrintPage(e);
      // Remainder of code for this class is same as in 
      // Listing 9-3 for Inven_Report, PrintDetail, and 
      // PrintHdr
   }
}

This example is easily extended to include page numbering and footers. For frequent reporting needs, it may be worth the effort to create a generic report generator that includes user selectable data source, column headers, and column totaling options.

Summary

This chapter has focused on using the GDI+ library to display and print text. The first section explained how to create and use font families and font classes. The emphasis was on how to construct fonts and understand the elements that comprise a font by looking at font metrics.

After a font has been created, the Graphics.DrawString method is used to draw a text string to a display or printer. Its many overloads permit text to be drawn at specific coordinates or within a rectangular area. By default, text printed in a rectangle is left justified and wrapped to the next line when it hits the bounds of the rectangle. This default formatting can be overridden by passing a StringFormat object to the DrawString method. The StringFormat class is the key to .NET text formatting. It is used to justify text, specify how text is truncated, and set tab stops for creating columnar output.

GDI+ provides several classes designed to support printing to a printer. These include the following:

  • PrintDocument. Sends output to the printer. Its Print method initiates the printing process and triggers the BeginPrint, QueryPageSettingsEvent, PrintPage, and EndPrint events. The event handler for the PrintPage event contains the logic for performing the actual printing.

  • PrintPreviewDialog. Enables output to be previewed before printing.

  • PrinterSettings. Has properties that specify the page range to be printed, the list of available printers, and the name of the target printer. These values correspond to those a user can select on the PrintDialog dialog box.

  • DefaultPageSettings. Has properties that set the bounds of a page, the orientation (landscape or portrait), the margins, and the paper size. These values correspond to those selected on the PageSetupDialog dialog box.

An example for printing a columnar report demonstrated how these classes can be combined to create an application that provides basic report writing. As a final example, we illustrated how the shortcomings of the PrintDocument class can be overcome by creating a custom PrintDocument class that preserves data encapsulation.

Test Your Understanding

1:

Does Arial Bold refer to a font family, typeface, or font?

2:

What is the default unit of measurement for a font? What size is it (in inches)?

3:

Which method and enumeration are used to right justify an output string?

4:

When the following code is executed, what is the x coordinate where the third column begins?

float[] tStops = {50f, 60f, 200f, 40f};
StringFormat sf = new StringFormat();
sf.SetTabStops(0,tStops);
string hdr =  "Col1	Col2	Col3	Col4";
g.DrawString(hdr, myFont, Brushes.Black, 10,10,sf);

5:

Which PrintDocument event is called after PrintDocument.Print is executed?

6:

Which class available to the PrintPage event handler defines the margins for a page?

7:

What three steps must be included to permit a document to be previewed before printing?

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

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