Take Control of Print Preview
Learn how to implement a print preview dialog that uses the PrintPreviewControl to add features that aren't in the default PrintPreviewDialog component.
Technology Toolbox: VB.NET, .NET Framework
The .NET Framework brings many new features that make complex or cumbersome programming tasks a breeze. Take printing for example. While Visual Basic 6.0 had easy-to-use features for creating printer output, you had to build the print preview features yourself if you wanted your user to preview the output. The Visual Studio .NET Windows Forms system includes not only one, but two methods to present a preview of the printer output to your user: the PrintPreviewDialog component and the PrintPreviewControl component. They both have the advantage of being able to use a single class (PrintDocument) to handle printing to both the preview device and the actual printer.
The PrintPreviewDialog control packages a ready-to-use form that displays your printer output. But why should you use that when you can leverage the power of the PrintPreviewControl control instead? In this article, I'll cover PrintPreviewDialog's features, then show you how to implement a print preview dialog that uses PrintPreviewControl to add a few features that aren't in the default PrintPreviewDialog component.
PrintPreviewDialog includes all of the most basic features you generally require when previewing printed output. It displays each page graphically, allows you to zoom in or out, enables you to jump to a specific page by number, and permits you to view multiple pages at once. Yet it also has some liabilities. As a "black box," there's little you can do to enhance its features. Sure, you could derive a new class from it, but because its constituent controls are inside "the box," there is only so much you can (or should) do in the derived instance. (The PrintPreviewControl component is also a black box, but the fact that it is a control opens up a much larger range of possibilities.)
There is another issue with PrintPreviewDialog. The PrintDocument class that you use to draw your per-page output doesn't know whether it's sending its output to the screen or to a print device. This is by design, and generally is a great thing. However, there might be times when you want to generate different output for print preview and final print. I needed to print some mailing labels recently. During print preview, I opted to draw the boundaries of each label using a light color to give the user a sense of where the text would appear in each physical label. On my print form, I had separate Print and Print Preview selections, and I set a previewMode Boolean flag as needed. It worked until the user clicked on the Print button in Print Preview. Now, PrintDocument was going to the actual printer, but my previewMode flag was still set to True. PrintPreviewDialog does not expose an event for the Print button. Oh despair. There are tricks you can use to hook into the event stream and catch the message for the Print button, but they might not work in future releases of the .NET Framework.
So, in general, the PrintPreviewDialog component is useful, but it goes against one of the main themes of software development: Give the programmer the maximum features possible so that he or she can provide the right features to the end user. You, as a programmer, require sufficient features from your software language and tools so that you can provide the best, the most precise, and the most controlled experience for users.
Put the Code on the Table
Another feature the basic PrintPreviewDialog doesn't include is a table of contents: a quick reference index that allows the user to jump quickly to any section within a large document. The sample program I've provided with this article implements a print preview form using PrintPreviewControl, which displays such a table of contents.
Four classes make up the sample application. The first class, TableOfContentsEntry, is a simple informational class used for each item that appears in the table of contents list (implemented as a ListBox control, but you could enhance it to use a TreeView or any other control) on the preview form. The ToString function within the class allows the ListBox control to display each item correctly:
Public Class TableOfContentsEntry
Public EntryText As String
Public Indent As Byte
Public PageNumber As Integer
Public Overrides Function _
ToString() As String
' ----- Return the display text.
Return (New String(" "c, 5 * _
Indent), 10) - 1))) & _
The second class, DocumentWithTOC, inherits from and extends the PrintDocument class from the System.Drawing.Printing namespace. The enhanced class adds support for the table of contents. It includes a new public member, an ArrayList of table-of-contents entries:
Public TableOfContents As _
Also, this class overrides the base versions of OnBeginPrint and Dispose so that you can create and destroy the TableOfContents member appropriately. When generating the preview document through an instance of this class, you call the new AddTOCEntry routine to add new entries to the table of contents:
Public Sub AddTOCEntry( _
ByVal entryName As String, _
ByVal indentLevel As Byte, _
ByVal whichPage As Integer)
' ----- Add a new TOC entry.
Dim tocEntry As TableOfContentsEntry
tocEntry = New PrintingTest. _
tocEntry.EntryText = entryName
tocEntry.Indent = indentLevel
tocEntry.PageNumber = whichPage
The third class is the actual print preview form implementation. Inheriting from Windows.Forms.Form, it is meant to be used as a ShowDialog, returning DialogResult.OK if the user actually wants to print the document, or DialogResult.Cancel if the user chooses to forget the whole thing. Take a look at the constituent controls as they are arranged on the design surface (see Figure 1). A Panel control (I gave it the name PanelToolbar) plays the role of a basic toolbar, with subordinate controls for Zoom, Multipage, and Page Selection features. Along the left is the table of contents ListBox (TOCList). Most of the form hosts the PrintPreviewControl component (PreviewArea), and a Splitter control (TOCSplitter) appears between the table of contents and the preview area. All of these controls are docked to the edges of the form. Make sure you dock them in the right order. Otherwise, the preview control will be partially obscured by the other controls docked on top of it. Dock the controls in the following order: (1) PrintToolbar on Top, (2) TOCList on Left, (3) TOCSplitter on Left, and (4) PreviewArea as Fill.
The PrintPreviewControl component exposes convenient features that allow you to zoom the displayed preview page, indicate how many pages are visible at once, and indicate that a specific page should be displayed. The form's toolbar buttons provide user access to these features. The right edge of the toolbar includes a NumericUpDown control (PageRange) that lets the user indicate the page to view. When the value in this control is changed at run time, you tell the PrintPreviewControl component to change to the new page through its StartPage property:
Private Sub PageRange_ValueChanged(...)
' ----- StartPage is base-0.
PreviewArea.StartPage = _
CInt(PageRange.Value) - 1
Zoom in on Your View
The zoom functions, available through the magnifying-glass toolbar button and an associated context menu on the form, are just as easy to implement. The PrintPreviewControl's Zoom property lets you quickly set the zoom factor as a decimal percentage (where 1.0 equals 100 percent). For instance, here's the sample code to zoom to 200 percent (triggered by a click on the Menu200 menu item):
Private Sub Menu200_Click(...)
' ----- Zoom to 200%.
Private Sub ZoomDisplay( _
ByVal zoomFactor As Double)
' ----- Set the auto-zoom as needed.
PreviewArea.AutoZoom = _
CBool(zoomFactor = 0.0)
If (zoomFactor > 0.0) Then _
PreviewArea.Zoom = zoomFactor
The PrintPreviewControl component can display multiple pages at once in a grid format. You control the number of rows and columns through the Rows and Columns properties. As an example, the six-page toolbar button (ActSixPages) sets the preview area to two rows and three columns:
Private Sub ActSixPages_Click(...)
' ----- Display 3x2 pages.
PreviewArea.Columns = 3
PreviewArea.Rows = 2
That's it for the basic features available already in the PrintPreviewDialog variation. You can now add the table of contents code. Basically, you simply respond to clicks on the ListBox and set the StartPage value as you did with the PageRange (NumericUpDown) control. Let's wait a bit on the population of the list. It's enough for now to know that each list element is an instance of your TableOfContentsEntry class:
Private Sub _
' ----- Jump to a TOC entry.
Dim tocEntry As PrintingTest. _
If (TOCList.SelectedIndex = -1) _
Then Exit Sub
tocEntry = CType(TOCList. _
SelectedItem, PrintingTest. _
If (tocEntry.PageNumber > 0) And _
(tocEntry.PageNumber <= _
PageRange.Maximum) Then _
PageRange.Value = _
Notice in this code how the page number was checked against the PageRange.Maximum value. When you implement the table of contents, it is useful to know how many pages are in the printed document. Unfortunately, PrintPreviewControl doesn't expose a page count, which might seem strange. It seems that, because you control printing through the PrintDocument class, PrintPreviewControl assumes that you know that information already. However, you can still find out how many pages there are by counting them as they pass by. To do this, you need to piggyback on the PrintDocument class events (actually, your special DocumentWithTOC class). Your preview form implementation includes two private members for page number tracking:
Private WithEvents InternalDoc As _
Private TotalPages As Integer
The WithEvents keyword is required to become part of the event stream for the PrintDocument instance. You could set InternalDoc to your DocumentWithTOC class as well, but PrintDocument is more generic. InternalDoc is set by the creator of your form's instance by calling your custom Document property, which also sets the document of the PrintPreviewControl instance:
Public Property Document() As _
' ----- Set internal version of doc.
Set(ByVal Value As _
InternalDoc = Value
PreviewArea.Document = Value
Monitor the Print Process
You can monitor the print process by latching on to the BeginPrint, PrintPage, and EndPrint events of PrintDocument (see Listing 1). You don't even have to worry that the code that creates this print preview instance is also using these same events. The .NET Framework makes sure that both event handlers get called for each event. Note that the EndPrint event contains the code that populates the table of contents ListBox.
The last class in the sample project is a simple form that tests your preview form. It has a few private members:
Private StateArray() As String
Private PageSoFar As Integer
Private WithEvents BarcodeDoc As _
BarcodeDoc is the actual instance of the PrintDocument (through DocumentWithTOC)the one that is piggybacked in the preview form. In this sample, StateArray is an array of America's 50 state names (see Figure 2). To start the printing process, the user clicks on the form's TestByControl button (see Listing 2).
You're probably saying to yourself, "Tim, you didn't add that many features to print previewing." But the point is that you can add any features you wish. PrintPreviewDialog is basic, and it remains basic. With PrintPreviewControl, the sky's the limit. It's true that the PrintPreviewControl component is also a black box of sorts, but you can integrate it more easily into your custom environment. You can provide a richer printing environment within your application by placing it alongside other useful controls on a form of your own design. You now have better control of exactly what the user experiences, which is a primary goal of software development.
Tim Patrick has spent more than thirty years as a software architect and developer. His two most recent books on .NET development -- Start-to-Finish Visual C# 2015, and Start-to-Finish Visual Basic 2015 -- are available from http://owanipress.com. He blogs regularly at http://wellreadman.com.