Introduction
This article describes the implementation of an enhanced PrintPreviewDialog class.
Background
The PrintPreviewDialog is convenient and easy to use. All you need to do is create an
instance of the dialog class, assign your PrintDocument object to the Document property,
and call the ShowDialog method.
However, PrintPreviewDialog has some shortcomings, including the following:
The entire document must be rendered before the preview appears. This is
annoying for long documents.
There are no options for choosing the printer, adjusting the page layout, or
selecting specific pages to print.
The dialog looks outdated. It hasn't changed since .NET 1.0, and it wasn't exactly
cutting-edge even back then.
The dialog allows little or no customization.
There is no option to export the document to other formats such as PDF.
Page images are cached in the control, which limits the size of the documents that
can be previewed.
The CoolPrintPreviewDialog class presented here addresses these shortcomings. It is
just as easy to use as the standard PrintPreviewDialog, but has the following
enhancements:
Pages can be previewed as soon as they are rendered. The first page is shown
almost instantly and subsequent pages become available while the user browses
the first ones.
The "Print" button shows a dialog that allows users to select the printer and page
ranges to print. A "Page Layout" button is also available so users can change page
size, orientation, and margins.
The dialog uses a ToolStrip control instead of the old toolbar.
You have the source and can customize everything from appearance to behavior.
The control creates a list of images which can be exported to other formats
including PDF (although the version presented here doesn't actually do that).
Using the Code
Using the CoolPrintPreviewDialog is as easy as using the traditional
PrintPreviewDialog. You instantiate the control, set the Document property to the
PrintDocument you want to preview, then call the dialog's Show method.
If you have code that uses the PrintPreviewDialog class, switching to the
CoolPrintPreviewDialog only requires changing one line of code. For example:
Collapse
// using a PrintPreviewDialog
using (var dlg = new PrintPreviewDialog())
{
dlg.Document = this.printDocument1;
dlg.ShowDialog(this);
}
// using a CoolPrintPreviewDialog
using (var dlg = new CoolPrintPreview.CoolPrintPreviewDialog())
{
dlg.Document = this.printDocument1;
dlg.ShowDialog(this);
}
Generating the Preview Images
The core of the CoolPrintPreviewDialog class is a CoolPreviewControl that
generates and shows the page previews.
The PrintDocument object has a PrintController property that specifies an object
responsible for creating the Graphics objects where the document is rendered. The
default print controller creates Graphics objects for the default printer and is not
interesting in this case. But .NET also defines a PreviewPrintController class that
creates metafiles instead. These remain available to the caller to be shown in the preview
area.
The CoolPreviewControl works by temporarily replacing the document's original print
controller with a PreviewPrintController, calling the document's Print method, and
getting the page images while the document is rendered. The images represent pages in
the document, and are scaled and displayed in the control just like any regular Image
object.
The code that creates the page previews looks like this (this code is simplified for clarity,
refer to the source for a better version):
Collapse
// list of page images
List<Image> _imgList = new List<Image>();
// generate page images
public void GeneratePreview(PrintDocument doc)
{
// save original print controller
PrintController savePC = doc.PrintController;
// replace it with a preview print controller
doc.PrintController = new PreviewPrintController();
// hook up event handlers
doc.PrintPage += _doc_PrintPage;
doc.EndPrint += _doc_EndPrint;
// render the document
_imgList.Clear();
doc.Print();
// disconnect event handlers
doc.PrintPage -= _doc_PrintPage;
doc.EndPrint -= _doc_EndPrint;
// restore original print controller
doc.PrintController = savePC;
}
The code installs the controller and hooks up the event handlers, then calls the Print
method to generate the pages, and cleans up when it's done.
When the Print method is invoked, the document starts firing events. The PrintPage
and EndPrint event handlers capture the pages as soon as they are rendered and add
them to an internal image list.
The event handlers also call the Application.DoEvents method to keep the dialog
responsive to user actions while the document renders. This allows users to switch pages,
adjust the zoom factor, or cancel the document generation process. Without this call, the
dialog would stop operating until the whole document finishes rendering.
This is the code that does all this:
Collapse
void _doc_PrintPage(object sender, PrintPageEventArgs e)
{
SyncPageImages();
if (_cancel)
{
e.Cancel = true;
}
}
void _doc_EndPrint(object sender, PrintEventArgs e)
{
SyncPageImages();
}
void SyncPageImages()
{
// get page previews from print controller
var pv = (PreviewPrintController)_doc.PrintController;
var pageInfo = pv.GetPreviewPageInfo();
// add whatever images are missing from our internal list
for (int i = _img.Count; i < pageInfo.Length; i++)
{
// add to internal list
_img.Add(pageInfo[i].Image);
// fire event to indicate we have more pages
OnPageCountChanged(EventArgs.Empty);
// if the page being previewed changed, refresh to show it
if (StartPage < 0) StartPage = 0;
if (i == StartPage || i == StartPage + 1)
{
Refresh();
}
// keep application responsive
Application.DoEvents();
}
}
This is the core of the preview code. The rest is concerned with housekeeping tasks such
as scaling the preview images, updating the scrollbars, handling navigation buttons,
mouse, keyboard, and so on. Please refer to the source code for the implementation
details.
Updating the Page Layout
The preview dialog allows users to update the print layout. This is very easy to
implement, thanks to the .NET PageSetupDialog class. Here is the code that gets called
when users click the "Page Layout" button:
Collapse
void _btnPageSetup_Click(object sender, EventArgs e)
{
using (var dlg = new PageSetupDialog())
{
dlg.Document = Document;
if (dlg.ShowDialog(this) == DialogResult.OK)
{
// user changed the page layout, refresh preview images
_preview.RefreshPreview();
}
}
}
The code shows a PageSetupDialog that allows the user to change the paper size,
orientation, and margins. Changes made by the user are reflected in the document's
DefaultPageSettings property.
If the user clicks OK, then we assume that the page layout has been modified, and call the
RefreshPreview method on the preview control. This method regenerates all preview
images using the new settings, so the user can see the changes applied to margins, page
orientation, and so on.
Printing the Document
When the user clicks the "Print" button, the dialog shows a PrintDialog so the user can
select the printer, page range, or change his mind and cancel the printing.
Unfortunately, page range selections are not honored if you simply call the Print method
directly on the document. To remedy this, the dialog calls the Print method on the
enhanced preview control instead. That implementation uses the page images already
stored in the control, and honors page ranges defined in the document's
PrinterSettings properties.
This is the code that gets called when the user clicks the "Print" button:
Collapse
void _btnPrint_Click(object sender, EventArgs e)
{
using (var dlg = new PrintDialog())
{
// configure dialog
dlg.AllowSomePages = true;
dlg.AllowSelection = true;
dlg.Document = Document;
// show allowed page range
var ps = dlg.PrinterSettings;
ps.MinimumPage = ps.FromPage = 1;
ps.MaximumPage = ps.ToPage = _preview.PageCount;
// show dialog
if (dlg.ShowDialog(this) == DialogResult.OK)
{
// print selected page range
_preview.Print();
}
}
}
The Print method in the preview control starts by determining the range of pages that
should be rendered. This may be the full document, a specific range, or the current
selection (page being previewed). Once the page range has been determined, the code
creates a DocumentPrinter helper class to perform the actual printing:
Collapse
public void Print()
{
// select pages to print
var ps = _doc.PrinterSettings;