Previous Section  < Day Day Up >  Next Section

9.3. 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梬hich has special support in Visual Studio.NET梠r 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梐 member of the System.Drawing.Printing namespace梡rovides 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.

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梑ased on the page size梐nd 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.

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

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梡age 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?tt>AllPages, Selection, or SomePages?/span>that 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;


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

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.

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梞entioned earlier梩hat 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梠r as a substitute for printing梚s 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.

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.

Listing 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 ?);

   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\tVendor\tDescription\tCost", 

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


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梥uch as the StreamReader梩o 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.

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

    Previous Section  < Day Day Up >  Next Section