Performance Consulting: A Practical Guide for HR and Learning Professionals

 
Chapter 19 - Graphics with GDI+
bySimon Robinsonet al.
Wrox Press 2002

  

In this chapter we've focused so far entirely on drawing to the screen. However, at some point you will probably also want to be able to produce a hard copy of the data too. That's the topic of this section. We're going to extend the CapsEditor sample so that it is able to print preview and print the document that is being edited.

Unfortunately, we don't have enough space to go into too much detail about printing here, so the printing functionality we will implement will be very basic. Usually, if you are implementing the ability for an application to print data, you will add three items to the application's main File menu:

In our case, to keep things simple, we won't implement a Page Setup menu option. Printing will only be possible using default settings. We will note, however, that, if you do want to implement Page Setup , then Microsoft has already written a page setup dialog class for you to use. It is the class System.Windows.Forms.PrintDialog . You will normally want to write an event handler that displays this form, and saves the settings chosen by the user.

In many ways printing is just the same as displaying to a screen. You will be supplied with a device context ( Graphics instance) and call all the usual display commands against that instance. Microsoft has written a number of classes to assist you in doing this; the two main ones that we need to use are System.Drawing.Printing.PrintDocument and System.Drawing.Printing.PrintPreviewDialog . These two classes handle the process of making sure that drawing instructions passed to a device context get appropriately handled for printing, leaving you to think about the logic of what to print where.

There are some important differences between printing/print previewing on the one hand, and displaying to the screen on the other hand. Printers cannot scroll instead they have pages. So you'll need to make sure you find a sensible way of dividing your document into pages, and draw each page as requested . Among other things that means calculating how much of your document will fit onto a single page, and therefore how many pages you'll need, and which page each part of the document needs to be written to.

Despite the above complications, the process of printing is quite simple. Programmatically, the steps you need to go through look roughly like this:

Implementing Print and Print Preview

Now we've gone over the broad steps to be taken, let's see how this works in code terms. The code is downloadable as the PrintingCapsEdit project, and consists of the CapsEditor project, with the changes highlighted below made.

We start off by using the VS .NET design view to add two new items to the File menu: Print and Print Preview . We also use the properties window to name these items menuFilePrint and menuFilePrintPreview , and to set them to be disabled when the application starts up (we can't print anything until a document has been opened!). We arrange for these menu items to be enabled by adding the following code to the main form's LoadFile() method, which we recall is responsible for loading a file into the CapsEditor application:

private void LoadFile(string FileName) { StreamReader sr = new StreamReader(FileName); string nextLine; documentLines.Clear(); nLines = 0; TextLineInformation nextLineInfo; while ((nextLine = sr.ReadLine()) != null) { nextLineInfo = new TextLineInformation(); nextLineInfo.Text = nextLine; documentLines.Add(nextLineInfo); ++nLines; } sr.Close(); if (nLines > 0) { documentHasData = true; menuFilePrint.Enabled = true; menuFilePrintPreview.Enabled = true; } else { documentHasData = false; menuFilePrint.Enabled = false; menuFilePrintPreview.Enabled = false; } CalculateLineWidths(); CalculateDocumentSize(); this.Text = standardTitle + " - " + FileName; this.Invalidate(); }

The highlighted code above is the new code we have added to this method. Next we add a member field to the Form1 class:

public class Form1 : System.Windows.Forms.Form { private int pagesPrinted = 0;

This field will be used to indicate which page we are currently printing. We are making it a member field, since we will need to remember this information between calls to the PrintPage event handler.

Next, the event handlers for when the user selects the Print or Print Preview menu options:

private void menuFilePrintPreview_Click(object sender, System.EventArgs e) { this.pagesPrinted = 0; PrintPreviewDialog ppd = new PrintPreviewDialog(); PrintDocument pd = new PrintDocument(); pd.PrintPage += new PrintPageEventHandler (this.pd_PrintPage); ppd.Document = pd; ppd.ShowDialog(); } private void menuFilePrint_Click(object sender, System.EventArgs e) { this.pagesPrinted = 0; PrintDocument pd = new PrintDocument(); pd.PrintPage += new PrintPageEventHandler (this.pd_PrintPage); pd.Print(); }

We've already explained the broad procedure involved in printing, and we can see that these event handlers are simply implementing that procedure. In both cases we are instantiating a PrintDocument object and attaching an event handler to its PrintPage event. For the case of printing, we call PrintDocument.Print() , while for print previewing, we attach the PrintDocument object to a PrintPreviewDialog , and call the preview dialog object's ShowDialog() method. The real work is going to be done in that event handler to the PrintPage event and this is what that handler looks like:

private void pd_PrintPage(object sender, PrintPageEventArgs e) { float yPos = 0; float leftMargin = e.MarginBounds.Left; float topMargin = e.MarginBounds.Top; string line = null; // Calculate the number of lines per page. int linesPerPage = (int)(e.MarginBounds.Height / mainFont.GetHeight(e.Graphics)); int lineNo = this.pagesPrinted * linesPerPage; // Print each line of the file. int count = 0; while(count < linesPerPage && lineNo < this.nLines) { line = ((TextLineInformation)this.documentLines[lineNo]).Text; yPos = topMargin + (count * mainFont.GetHeight(e.Graphics)); e.Graphics.DrawString(line, mainFont, Brushes.Blue, leftMargin, yPos, new StringFormat()); lineNo++; count++; } // If more lines exist, print another page. if(this.nLines > lineNo) e.HasMorePages = true; else e.HasMorePages = false; pagesPrinted++; }

After declaring a couple of local variables , the first thing we do is work out how many lines of text can be displayed on one page which will be the height of a page divided by the height of a line and rounded down. The height of the page can be obtained from the PrintPageEventArgs.MarginBounds property. This property is a RectangleF struct that has been initialized to give the bounds of the page. The height of a line is obtained from the Form1.mainFont field, which we recall from the CapsEditor sample is the font used for displaying the text. There is no reason here for not using the same font for printing too. Note that for the PrintingCapsEditor sample, the number of lines per page is always the same, so we arguably could have cached the value the first time we calculated it. However, the calculation isn't too hard, and in a more sophisticated application the value might change, so it's not bad practice to recalculate it every time we print a page.

We also initialize a variable called lineNo . This gives the zero-based index of the line of the document that will be the first line of this page. This information is important because in principle, the pd_PrintPage() method could have been called to print any page, not just the first page. lineNo is computed as the number of lines per page times the number of pages that have so far been printed.

Next we run through a loop, printing each line. This loop will terminate either when we find that we have printed all the lines of text in the document, or when we find that we have printed all the lines that will fit on this page whichever condition occurs first. Finally, we check whether there is any more of the document to be printed, and set the HasMorePages property of our PrintPageEventArgs accordingly , and also increment the pagesPrinted field, so that we know to print the correct page the next time the PrintPage event handler is invoked.

One point to note about this event handler is that we do not worry about where the drawing commands are being sent. We simply use the Graphics object that was supplied with the PrintPageEventArgs . The PrintDocument class that Microsoft has written will internally take care of making sure that, if we are printing, the Graphics object will have been hooked up to the printer, while if we are print previewing then the Graphics object will have been hooked up to the print preview form on the screen.

Finally, we need to ensure the System.Drawing.Printing namespace is searched for type definitions:

using System; using System.Drawing; using System.Drawing.Printing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.Data; using System.IO;

All that remains is to compile the project and check that the code works. We can't really show screenshots of a printed document(!) but this is what happens if you run CapsEdit , load a text document (as before, we've picked the C# source file for the project), and select Print Preview :

In the screenshot, we have scrolled through to page 5 of the document, and set the preview to display normal size. The PrintPreviewDialog has supplied quite a lot of features for us, as can be seen from the toolbar at the top of the form. The options available include actually printing the document, zooming in or out, and displaying two, three, four or six pages together. These options are all fully functional, without our needing to do any work. For example, if we change the zoom to auto and click to display four pages (third toolbar button from the right), we get this.

  

Категории