The boldness of asking deep questions may require unforeseen flexibility if we are to accept the answers. | ||
--Brian Greene |
One of the more common and important tasks in a Windows application is the ability to print text or graphics. Printing was somewhat tricky to implement in the days prior to .NET, but now there is a versatile framework to support it within the System.Drawing.Printing
namespace. The majority of the print mechanism is within the PrintDocument
class, which represents a component that sends output to a printer. This class is very modular, so it allows you to implement either simple or complex printing logic and execute it using this class alone. Other classes exist to support printer configuration and page setup properties such as orientation. This chapter is all about using the managed mechanisms within the .NET Class Framework to implement printing support within applications. You should have a basic familiarity with the Graphics
class within the System.Drawing
namespace.
The first thing that we must implement is the actual printing logic, which is done by linking into events on PrintDocument
. There is a method on PrintDocument
called Print()
which, you guessed it, prints the document. When this method is called, a BeginPrint
event is fired, followed by a PrintPage
event for each page, and finally stopping with an EndPrint
event. You do not really have to do much with the begin and end events; the core logic exists in the PrintPage
event. This event is passed a PrintPageEventArgs
parameter that contains a property called HasMorePages
. If this property is set to true, a new page is created and the PrintPage
event is raised again when the event handler returns.
The pseudologic for the PrintPage
event handler is basically: Print the page content using the page setting information provided, using the Graphics
context provided. Determine if more pages are needed to completely print all the content for the document. If yes, set HasMorePages
to true; otherwise set it to false.
The following code shows how to instantiate a PrintDocument
, wire up to the PrintPage
event handler, and start printing.
PrintDocument printDocument = new PrintDocument(); printDocument.PrintPage += new PrintPageEventHandler(printDocument_PrintPage); printDocument.Print();
The following code describes the simplest implementation of the PrintPage
event handler, assuming that no additional pages are needed.
private void printDocument_PrintPage(Object sender, PrintPageEventArgs e) { string outputText = "Game Engine Toolset Development rocks!"; Font printFont = new Font("Verdana", 9.75F, FontStyle.Regular, GraphicsUnit.Point, ((byte)(0))); e.Graphics.DrawString(outputText, printFont, Brushes.Black, 0, 0); }
Complex printing logic that you wish to reuse across multiple places requires that you inherit from PrintDocument
, handling the PrintPage
event by overriding the OnPrintPage
method instead of using the event handler. The following code shows a sample implementation of a PrintDocument
that correctly handles text printing that spans multiple pages with varying font and page settings.
public class SimplePrintDocument : PrintDocument { private StringReader inputStream = null; private string bufferOverflow = null; private Font printFont = null; public SimplePrintDocument(StringReader inputStream, Font printFont) : base() { this.inputStream = inputStream; this.printFont = printFont; } protected override void OnBeginPrint(PrintEventArgs e) { base.OnBeginPrint(e); bufferOverflow = null; } protected override void OnPrintPage(PrintPageEventArgs e) { base.OnPrintPage(e); // Figure out how many lines can fit within the page boundaries float linesPerPage = e.MarginBounds.Height / printFont.GetHeight(e.Graphics); int lineCount = 0; // Deal with any remaining overflow lines from a previous page first while (lineCount < linesPerPage && bufferOverflow != null) { float positionY = e.MarginBounds.Top + (lineCount * printFont.GetHeight(e.Graphics)); lineCount += PrintLine(e, bufferOverflow, positionY); } // Now handle the current line buffer string line = null; while (lineCount < linesPerPage && ((line = inputStream.ReadLine()) != null)) { float positionY = e.MarginBounds.Top + (lineCount * printFont.GetHeight(e.Graphics)); lineCount += PrintLine(e, line, positionY); } // Print a new page if there are more lines to print if (line != null) e.HasMorePages = true; else e.HasMorePages = false; } private int PrintLine(PrintPageEventArgs e, string text, float positionY) { RectangleF rectangle = new RectangleF(e.PageSettings.Margins.Left, positionY, e.MarginBounds.Width, e.MarginBounds.Height); int lines; int characters; StringFormat format = new StringFormat(); e.Graphics.MeasureString(text, printFont, rectangle.Size, format, out characters, out lines); // Total text will not fit on page; bump to overflow buffer for next page if (characters < text.Length) { bufferOverflow = text.Substring(characters); } else { bufferOverflow = null; } e.Graphics.DrawString(text, printFont, Brushes.Black, rectangle, format); // Handle empty lines lines = lines == 0 ? 1 : lines; return lines; } }
Using the new SimplePrintDocument
class is easy; instantiate it as you did with PrintDocument
and call the Print()
method!
We currently have the logic for printing support implemented, so the next logical step is to provide the ability to select a printer using the standard Windows Print dialog. Right now, you are simply calling the print method on the document, but in a real world application, you let the user select the printer she wants to use and also support the ability to cancel printing. Using the PrintDialog
class, we can provide this functionality to users. Attach the print document to the Document
property of the dialog and show the dialog as normal. If the dialog returns successfully, call the print method of the document. The following code shows a sample implementation of printer selection.
private void PrintButton_Click(object sender, EventArgs e) { using (StringReader inputText = new StringReader(PrintTextField.Text)) { SimplePrintDocument printDocument = new SimplePrintDocument(inputText, printFont); PrintDialog printDialog = new PrintDialog(); printDialog.Document = printDocument; if (printDialog.ShowDialog() == DialogResult.OK) printDocument.Print(); } }
Figure 21.1 shows the printer selection dialog in action.
Another common print feature provided by real-world applications is the ability to choose page settings like the orientation of the paper or the margin sizes. This can be done with the PageSetupDialog
class and a stored instance of the PageSettings
class, as shown with the following code.
private void PageSetupButton_Click(object sender, EventArgs e) { PageSetupDialog pageSetupDialog = new PageSetupDialog(); if (cachedSettings == null) cachedSettings = new PageSettings(); pageSetupDialog.PageSettings = cachedSettings; pageSetupDialog.ShowDialog(); }
You can now alter the printing logic to set the page settings to our cached instance, as shown with the following code.
private void PrintButton_Click(object sender, EventArgs e) { using (StringReader inputText = new StringReader(PrintTextField.Text)) { SimplePrintDocument printDocument = new SimplePrintDocument(inputText, printFont); if (cachedSettings != null) printDocument.DefaultPageSettings = cachedSettings; PrintDialog printDialog = new PrintDialog(); printDialog.Document = printDocument; if (printDialog.ShowDialog() == DialogResult.OK) printDocument.Print(); } }
Figure 21.2 shows the Page Setup dialog in action.
The last common print feature is the ability to preview a document before actually printing it. This is done with the PrintPreviewDialog
class. Simply attach your print document to the Document
property of the dialog and show the dialog as usual. The following code shows how to do this.
private void PrintPreviewButton_Click(object sender, EventArgs e) { using (StringReader inputText = new StringReader(PrintTextField.Text)) { SimplePrintDocument printDocument = new SimplePrintDocument(inputText, printFont); if (cachedSettings != null) printDocument.DefaultPageSettings = cachedSettings; PrintPreviewDialog printPreviewDialog = new PrintPreviewDialog(); printPreviewDialog.Document = printDocument; printPreviewDialog.ShowDialog(); } }
Figure 21.3 shows the print preview dialog in action.
This chapter covered the full implementation of a PrintDocument
class that can print arbitrary text with varying fonts and page settings. The user can select which printer to use and can modify page properties before printing. In addition to configuration, the user can also bring up a print preview dialog that shows the document as it would print out before actually committing himself to a print job.
Although this chapter did not cover printing graphics, remember that the PrintPage
event handler is passed a Graphics
context that functions like any other context. You can call methods like FillRectangle()
or DrawEllipse()
on it and achieve the desired effect. It is a little trickier when you start introducing graphics, because you need to implement some form of flow layout to determine the lines per page and how you position your content when printing.
The Companion Web site contains the full source code from this chapter, along with an example utilizing the custom print logic. Figure 21.4 shows the interface of the example, which is simply a front-end to the code discussed throughout this chapter.
3.144.110.155