Useful Printing Techniques


Our tour of the printing landscape has so far negotiated the spectrum of fundamental concepts, types, and techniques you typically need when you start building a printing solution. Now, we build on these to create useful techniques for solving several printing problems you may encounter, including word wrapping and pagination, configuration of page settings on a per-page basis, and dynamic page counting.

Word Wrapping and Pagination

As you've seen, printing is a graphical process built on a PrintDocument and its PrintPage event. We've dealt only with highly specific, simple scenarios where we knew that the output would fit within a page. But the norm is more complex, even if you are dealing with a simple text file. Printing algorithms must ensure that all file data is displayedthat is, no data disappears beyond any of the margins.

You've seen how to determine the printable area of a page, but you haven't seen how to handle situations when file data doesn't fit within the area, most commonly when a line of text you want printed with a specific font is wider than the printable area at hand. The common solution to this is word wrapping, in which any pieces of text that don't fit are printed onto, or wrapped to, the following line. As you saw in Chapter 6, Graphics.MeasureString and Graphics.DrawString provide native word wrapping.

DrawString automatically wraps text if it doesn't fit into an area defined by a SizeF object that you've specified. But that's the easy part. The hard part is to determine where the next line of text needs to be printed; its start location depends on how high the previous line of text ended up being, and whether it fitted on a single line or needed to be wrapped over multiple lines.

One technique for handling this is to maintain the size and location of the printable area, adjusting it after every line of text printed so that its adjusted size represents the location and size of the remaining area of print in which we will attempt to render the next line of text. Figure 8.13 illustrates this idea.

Figure 8.13. Maintaining Printable Area Information


As each line is printed, the printable area needs to increase its Top property by the height of the previous printed line of text, and reduce its height by the same value. The height of the text is nicely calculated by MeasureString:

// MainForm.cs partial class MainForm : Form {   ...   int rowCount;   Font textFont;   bool preview;   List<String> fileRows = new List<string>(); // Text file   public MainForm() {     InitializeComponent();     // "Load" text file     ...     // Preview text file     this.printPreviewControl.InvalidatePreview();   }   void printDocument_BeginPrint(object sender, PrintEventArgs e) {     // Don't print if nothing to print     if( fileRows == null ) e.Cancel = true;     // Preprinting configuration     this.textFont = new Font("Arial", 25);     this.preview = (e.PrintAction == PrintAction.PrintToPreview);   }     void printDocument_PrintPage(       object sender, Printing.PrintPageEventArgs e) {       Graphics g = e.Graphics;       ...       // Print page text       Rectangle printableArea = GetRealMarginBounds(         e, this.printDocument.PrinterSettings, preview);       while( this.rowCount < fileRows.Count ) {         string line = fileRows[rowCount];         // Get size for word wrap         SizeF printSize =            g.MeasureString(line, this.textFont, printableArea.Width);         // Print line         g.DrawString(line, this.textFont, Brushes.Black, printableArea);         // Calculate and reduce remaining printable area         printableArea.Y += (int)printSize.Height;         printableArea.Height -= (int)printSize.Height;         ++this.rowCount;   }          // Keep printing while more rows      e.HasMorePages = (this.rowCount < fileRows.Count);    }    void printDocument_EndPrint(object sender, PrintEventArgs e) {      // Postprinting cleanup      this.textFont.Dispose();    }    ...  }


Now, we ensure that each line of text is printed on a new line in the document, but we don't handle what happens when there are more lines than will fit on a page. The ability during printing (or rendering to screen like a word processor) to determine when a new page needs to start and, if required, creating a new page and continuing printing, is known as pagination.

A new page basically starts when the remaining printable area is less than the height of the next line to output. Our word-wrapping code actually contains the two pieces of information that we need if we are to determine this for ourselves: the height of the remaining line and the height of the remaining printable area. If the former is greater than the latter, we simply exit the while loop. If there's more file data left, we set HasMorePages to whether or not we have reached the end of the stream, ensuring that we print a new page. The updates are shown here:

public partial class MainForm : Form {   ...   int pageCount;   ...   Font headerFont;   ...   void printDocument_BeginPrint(object sender, PrintEventArgs e) {     // Preprinting configuration     this.headerFont = new Font("Arial", 50);     ...   }   ...   void printDocument_PrintPage(object sender, PrintPageEventArgs e) {     Graphics g = e.Graphics;     // Print page header     string headerText = "Page " + this.pageCount;     Rectangle marginBounds = GetRealMarginBounds(e, preview);     RectangleF headerArea =         new RectangleF(           marginBounds.Left, 0, marginBounds.Width, marginBounds.Top);     using( StringFormat format = new StringFormat() ) {       format.LineAlignment = StringAlignment.Center;       g.DrawString(         headerText, headerFont, Brushes.Black, headerArea, format);     }     ...     while( this.rowCount < fileRows.Count ) {       string line = fileRows[rowCount];       // Get size for word wrap       SizeF printSize =         g.MeasureString(line, this.textFont, printableArea.Width);       if( printableArea.Height > printSize.Height ) {         // Print line         g.DrawString(line, this.textFont, Brushes.Black, printableArea);         // Calculate and reduce remaining printable area         printableArea.Y += (int)printSize.Height;         printableArea.Height -= (int)printSize.Height;         ++this.rowCount;       }       else break;     }     // Increment page count     ++this.pageCount;     // Keep printing while more rows     e.HasMorePages = (this.rowCount < fileRows.Count);   }   void printDocument_EndPrint(object sender, PrintEventArgs e) {     // Postprinting cleanup     this.headerFont.Dispose();     ...   } }


One trick you may have noticed in this code is the buffering of the last read-in line, which happens in the event that the last line read doesn't fit in the remaining area. Because we can't reset the buffer to the position it was in before reading a line that doesn't fit, we need to store it somewhere else and use it during the next read.

Figure 8.14 shows the results of our machinations, with the source text file rendered to a print preview using the PrintPreviewControl.

Figure 8.14. Print Preview of Word-Wrapping and Paginating Print Algorithm


The pagination algorithm is simple, unlike the algorithms in applications like Microsoft Word, which support much more comprehensive editing, previewing, and printing scenarios. That discussion is beyond the scope of this book.

After your print algorithm accommodates pagination, you can easily support the application of page settings to individual pages rather than the entire document.

Per-Page Page Setting Configuration

It is always possible that a document printed to multiple pages might need different settings from one page to the next. For example, for report-style documents, users may prefer to show text using portrait orientation and show graphs and images using landscape orientation. To support this, you need to solve three problems. First, you need to identify individual pages before your print them, a situation we have already enabled through pagination. Second, you need to allow users to assign specific PageSettings to each page. Third, you need to use PageSettings objects while printing.

Using the PrintPreviewControl from the previous example, we easily determine the page that users are on. Then, we use the Page Setup dialog to allow users to specify a specific set of page settings, returned from PageSetup via its PageSettings property. Finally, we internally use a hashtable to store PageSettings, using the page number as the hashtable key value:

Hashtable pageSettings = new Hashtable();  ...  void editPageSettingsButton_Click(object sender, EventArgs e) {    // Set Page Setup dialog with page settings for current page    PageSettings pageCountSettings = (PageSettings)      pageSettings[(int)this.previewPageNumericUpDown.Value];    if( pageCountSettings != null ) {      this.pageSetupDialog.PageSettings = pageCountSettings;    }    else this.pageSetupDialog.PageSettings = new PageSettings();    // Edit page settings    if( this.pageSetupDialog.ShowDialog() == DialogResult.OK ) {      // Store new page settings and apply      pageSettings[(int)this.previewPageNumericUpDown.Value] =        (PageSettings)this.pageSetupDialog.PageSettings.Clone();    } }


If the PageSettings change, your UI should reflect those changes, whether the user is previewing or editing a document. If you use PrintPreviewControl, you make a call to its InvalidatePreview method:

void editPageSettingsButton_Click(object sender, EventArgs e) {   ...   // Edit page settings   if( this.pageSetupDialog.ShowDialog() == DialogResult.OK ) {     ...     this.printPreviewControl.InvalidatePreview();   } }


InvalidatePreview causes PrintPreviewControl to repaint itself. During this process, and when printing, we need to pass the appropriate updated PageSettings object to the PrintController for the currently printing page. As you saw earlier, this is what PrintDocument's QueryPageSettings event is for, and here's how you use it:

void printDocument_QueryPageSettings(   object sender, QueryPageSettingsEventArgs e) {   // Get page settings for the page that's currently printing   PageSettings pageCountSettings =     (PageSettings)pageSettings[pageCount];   if( pageCountSettings != null ) {     e.PageSettings = pageCountSettings;   }   else e.PageSettings = new PageSettings(); }


Figure 8.15 shows a previewed page whose orientation was switched from portrait to landscape.

Figure 8.15. Custom PageSettings Applied


You may have noticed that the QueryPageSettings event handler returns a default PageSettings object if a custom one doesn't exist for the currently printing page. If you don't do this, PrintController uses the last passed PageSettings object, which may not contain the appropriate state.

Dynamic Page Counting

Many of the operations we've discussed, especially applying custom page settings to specific pages, rely on determining the page number and the total number of pages in a document when it is previewed or printed. The most common example of this is to preset the PrintDialog with the correct page range, as shown in Figure 8.16.

Figure 8.16. Setting the Page Range


You'd think you could just count pages with a numeric variable that's incremented every time you handle a PrintDocument's PagePrint event, but there's an issue you should consider: The page count is accurate only for the most recently printed document. However, the page range value you display in PrintDialog needs to accurately reflect the document you're about to print. Between the time you last printed a document and the next time it's printed, the number of pages may have changed as a result of editing.

As it turns out, the most reliable technique for determining the page count at any one time is to actually print the document, counting each page generated from a PrintDocument's PagePrint event handler. The problem is that you don't want to have to run a print preview or a print just to count the number of pages. Either of these tasks is performed by a PrintController, of which you've already seen SimpleDocumentPrintController, PreviewPrintController, and PrintControllerWithStatusDialog. Because they all derive from PrintController, we can do the same thing to create a custom PageCountPrintController class, allowing us to abstract away page-counting code into a single, reusable class that relies on actual generated print output to determine the page count.[1]

[1] Even though PageCountPrintController provides the most accurate page count without generating printed output, it does require your print algorithm to execute. This can raise performance issues you should consider for your own applications.

To count the number of pages, we initialize a page count variable when printing commences, and then we increment it when each subsequent page is printed. PrintController provides two methods we can override for these purposes: OnStartPrint and OnStart Page, respectively. With this knowledge, it is simple to create a custom PageCountPrint Controller:

class PageCountPrintController : PreviewPrintController {    int pageCount = 0;    public override void OnStartPrint(      PrintDocument document, PrintEventArgs e) {      base.OnStartPrint(document, e);      this.pageCount = 0;    }    public override System.Drawing.Graphics OnStartPage(      PrintDocument document, PrintPageEventArgs e) {      // Increment page count      ++this.pageCount;      return base.OnStartPage(document, e);    }    public int PageCount {      get { return this.pageCount; }    }    // Helper method to simplify client code    public static int GetPageCount(PrintDocument document) {      // Must have a print document to generate page count      if( document == null )        throw new ArgumentNullException("PrintDocument must be set.");      // Substitute this PrintController to cause a Print to initiate the      // count, which means that OnStartPrint and OnStartPage are called      // as the PrintDocument prints      PrintController existingController = document.PrintController;      PageCountPrintController controller =        new PageCountPrintController();      document.PrintController = controller;      document.Print();      document.PrintController = existingController;      return controller.PageCount;    } }


The PageCount property simply makes the result available to client code. On the client, you substitute the PageCountPrintController for the PrintDocument's current Print Controller before calling PrintDocument's Print method. The code should be familiar:

void getPageCountButton_Click(object sender, EventArgs e) {    int pageCount =      PageCountPrintController.GetPageCount(this.printDocument);    MessageBox.Show(pageCount.ToString()); }


PrintController is a great base class from which you can derive your own custom print controllers to tackle all manner of printing chores. Even better, you can take it a step further and convert them into full-blown design-time components and thereby enjoy the productivity benefits of declarative configuration. In fact, you'll find the implementation in the sample code for this chapter, and you'll also find an in-depth discussion of the fundamentals of design-time component development in Chapter 9: Components.




Windows Forms 2.0 Programming
Windows Forms 2.0 Programming (Microsoft .NET Development Series)
ISBN: 0321267966
EAN: 2147483647
Year: 2006
Pages: 216

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net