Click here to Skip to main content
15,437,709 members
Articles / Desktop Programming / Windows Forms
Posted 5 Aug 2009


218 bookmarked

An Enhanced PrintPreviewDialog

Rate me:
Please Sign up or sign in to vote.
4.97/5 (106 votes)
28 May 2014Public Domain8 min read
A PrintPreviewDialog that is faster and better looking than the standard one


This article describes the implementation of an enhanced PrintPreviewDialog class.


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:

// using a PrintPreviewDialog
using (var dlg = new PrintPreviewDialog())
  dlg.Document = this.printDocument1;
// using a CoolPrintPreviewDialog
using (var dlg = new CoolPrintPreview.CoolPrintPreviewDialog())
  dlg.Document = this.printDocument1;

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):

// 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
  // 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:

void _doc_PrintPage(object sender, PrintPageEventArgs e)
  if (_cancel)
    e.Cancel = true;
void _doc_EndPrint(object sender, PrintEventArgs e)
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
    // fire event to indicate we have more pages
    // if the page being previewed changed, refresh to show it
    if (StartPage < 0) StartPage = 0;
    if (i == StartPage || i == StartPage + 1)
    // keep application responsive

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:

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

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:

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

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:

public void Print()
  // select pages to print
  var ps = _doc.PrinterSettings;
  int first = ps.MinimumPage - 1;
  int last = ps.MaximumPage - 1;
  switch (ps.PrintRange)
    case PrintRange.CurrentPage:
      first = last = StartPage;
    case PrintRange.Selection:
      first = last = StartPage;
      if (ZoomMode == ZoomMode.TwoPages)
        last = Math.Min(first + 1, PageCount - 1);
    case PrintRange.SomePages:
      first = ps.FromPage - 1;
      last = ps.ToPage - 1;
    // print using helper class
    var dp = new DocumentPrinter(this, first, last);

The DocumentPrinter class is simple. It inherits from PrintDocument and overrides the OnPrintPage method to print only the pages selected by the user:

internal class DocumentPrinter : PrintDocument
  int _first, _last, _index;
  List<Image> _imgList;
  public DocumentPrinter(CoolPrintPreviewControl preview, int first, int last)
    // save page range and image list
    _first = first;
    _last = last;
    _imgList = preview.PageImages;
    // copy page and printer settings from original document
    DefaultPageSettings = preview.Document.DefaultPageSettings;
    PrinterSettings = preview.Document.PrinterSettings;
  protected override void OnBeginPrint(PrintEventArgs e)
    // start from the first page
    _index = _first;
  protected override void OnPrintPage(PrintPageEventArgs e)
    // render the current page and increment the index
    e.Graphics.PageUnit = GraphicsUnit.Display;
    e.Graphics.DrawImage(_imgList[_index++], e.PageBounds);
    // stop when we reach the last page in the range
    e.HasMorePages = _index <= _last;

This implementation renders the page images assuming all pages have the same size and orientation, which is the case for most documents. If the document contains pages of different sizes, or with different orientation, this simple implementation will not work correctly. To fix this, we would have to check that the current paper size and orientation match the preview image size before printing each page and adjust the printer settings if necessary. That is left as an exercise for the reader.

Previewing Really Long Documents

After I posted the first version of this project, I got some great feedback from other CodeProject users. One of them mentioned a problem I also had a while ago. If the document contains several thousand pages, caching all those images may cause problems. Windows has a limit of 10,000 GDI objects, and each page image represents at least one. If you use too many GDI objects, your application may crash, or cause other apps to crash. Not nice...

One easy way to solve this problem is to convert the page images into streams. You can then store the streams and create images on demand, only when they are needed for previewing or printing.

The code below shows a PageImageList class that does the job. You can use it much like a regular List, except when you get or set an image, it is automatically converted to and from a byte array. This way, the images stored in the list are not GDI objects and don't use up the system resources.

// This version of the PageImageList stores images as byte arrays. It is a little
// more complex and slower than a simple list, but doesn't consume GDI resources.
// This is important when the list contains lots of images (Windows only supports
// 10,000 simultaneous GDI objects!)
class PageImageList
  // ** fields
  var _list = new List<byte[]>();
  // ** object model
  public void Clear()
  public int Count
    get { return _list.Count; }
  public void Add(Image img)
    // stored image data, now dispose of original
  public Image this[int index]
    get { return GetImage(_list[index]); }
    set { _list[index] = GetBytes(value); }
  // implementation
  byte[] GetBytes(Image img)
    Metafile mf = img as Metafile;
    var enhMetafileHandle = mf.GetHenhmetafile().ToInt32();
    var bufferSize = GetEnhMetaFileBits(enhMetafileHandle, 0, null);
    var buffer = new byte[bufferSize];
    GetEnhMetaFileBits(enhMetafileHandle, bufferSize, buffer);
    // return bits
    return buffer;
  Image GetImage(byte[] data)
    MemoryStream ms = new MemoryStream(data);
    return Image.FromStream(ms);
  // use interop to get the metafile bits
  static extern int GetEnhMetaFileBits(int hemf, int cbBuffer, byte[] lpbBuffer);

Note that the Add method disposes of the image after storing it. I normally would not do it this way; the caller owns the image and should be responsible for disposing of it. But in this project, this arrangement allows me to swap the PageImageList implementation with a regular List, which is convenient for testing and benchmarking.

Note also that the GetBytes uses the GetHenhmetafile API. This API makes the metafile invalid, so the original image cannot be used after this method is called. In this case, it is not an issue since the image is destroyed after the conversion anyway. But if you want to use this code in other applications, remember that you cannot use the metafile after this call. If you need the image, re-create it using code similar to the GetImage method above.

If you are concerned about performance, the extra conversion causes a performance hit of about 10% while generating the document. I think this is a reasonable price for the benefit if your documents have hundreds or thousands of pages.

If you are concerned about memory usage, consider compressing the byte arrays when you store them. I did that in my original implementation; metafiles tend to compress really well. Of course, there would be a small additional performance penalty involved.

Zooming with the Mouse Wheel

Pressing the control key and moving the scroll wheel has become a de-facto standard command for zooming in and out of documents, supported in most modern apps including browsers and MS Office.

Adding this support to the CoolPrintPreview control was just a matter of overriding the OnMouseWheel method and changing the value of the Zoom property as follows:

// zoom in/out using control+wheel
protected override void OnMouseWheel(MouseEventArgs e)
    if ((Control.ModifierKeys & Keys.Control) != 0)
        var zoom = Zoom *= e.Delta > 0 ? 1.1 : 0.9;
        Zoom = Math.Min(5, Math.Max(.1, zoom));
This change is included in the version of the code.


This is the third version of the CoolPrintPreviewDialog article. This version has a shorter and more efficient implementation of the PageImageList class, and includes a Visual Basic implementaion as well.

After I published the first version, someone pointed out another article that deals with the PrintPreviewDialog but somehow escaped me when I decided to write this one. It has a slightly different focus and describes localization very nicely. Hopefully, you can get some good ideas from both. Check it out here.

Thanks for the feedback so far. If you have other requests, or suggestions for further improvements, please let me know. Happy previewing!


This article, along with any associated source code and files, is licensed under A Public Domain dedication

Written By
Software Developer
Brazil Brazil
Software Architect/Developer with several years experience creating and delivering software.

Full-stack Web development (including React, Firebase, TypeScript, HTML, CSS), Entity Framework, C#, MS SQL Server.

Passionate about new technologies and always keen to learn new things as well as improve on existing skills.

Comments and Discussions

PraiseCool this PrintPreviewDialog Pin
kornisch8-Feb-21 3:16
Memberkornisch8-Feb-21 3:16 
Questionnewbie question: how to use this? Pin
Tinker-Bel10-Sep-19 5:41
MemberTinker-Bel10-Sep-19 5:41 
SuggestionUser can change printer/layout from Print dialog Pin
kbroutley13-Aug-18 8:33
Memberkbroutley13-Aug-18 8:33 
QuestionReleasing the GDI objects Pin
DanielDaniel8-Apr-17 21:19
MemberDanielDaniel8-Apr-17 21:19 
AnswerRe: Releasing the GDI objects Pin
Member 1376591812-Apr-18 21:55
MemberMember 1376591812-Apr-18 21:55 
GeneralRe: Releasing the GDI objects Pin
Member 1376591816-Apr-18 21:05
MemberMember 1376591816-Apr-18 21:05 
GeneralRe: Releasing the GDI objects Pin
kbroutley13-Aug-18 9:23
Memberkbroutley13-Aug-18 9:23 
GeneralRe: Releasing the GDI objects Pin
kbroutley15-Aug-18 5:35
Memberkbroutley15-Aug-18 5:35 
QuestionWrap Text Pin
thenndral30-Nov-16 18:32
Memberthenndral30-Nov-16 18:32 
QuestionGDI Error render many pages Pin
P. Dutta2-Nov-16 20:49
MemberP. Dutta2-Nov-16 20:49 
AnswerRe: GDI Error render many pages Pin
Bernardo Castilho2-Nov-16 23:32
professionalBernardo Castilho2-Nov-16 23:32 
QuestionPrinted header line not straight Pin
Member 126391983-Oct-16 20:07
MemberMember 126391983-Oct-16 20:07 
QuestionPrint / Cancel in Print Dialog Pin
Member 1263919830-Sep-16 21:08
MemberMember 1263919830-Sep-16 21:08 
QuestionPrint selected pages from listbox Pin
Member 1263919819-Sep-16 3:38
MemberMember 1263919819-Sep-16 3:38 
AnswerRe: Print selected pages from listbox Pin
Bernardo Castilho20-Sep-16 11:08
professionalBernardo Castilho20-Sep-16 11:08 
QuestionPrint listbox items pagewise Pin
Member 1263919818-Sep-16 19:41
MemberMember 1263919818-Sep-16 19:41 
QuestionHow to display render images based on orientation. Pin
Ashok_kavi21-Jul-16 18:47
MemberAshok_kavi21-Jul-16 18:47 
QuestionMay I include your code in my article? Pin
PhilMcGahan19-May-16 8:53
MemberPhilMcGahan19-May-16 8:53 
AnswerRe: May I include your code in my article? Pin
Bernardo Castilho19-May-16 9:08
professionalBernardo Castilho19-May-16 9:08 
QuestionFillRectangle or colored DrawString doesn't work Pin
mordehai4-May-16 13:51
Membermordehai4-May-16 13:51 
AnswerRe: FillRectangle or colored DrawString doesn't work Pin
WildeBeest1224-Jan-18 1:18
MemberWildeBeest1224-Jan-18 1:18 
GeneralMy vote of 5 Pin
Member 1039595820-Mar-16 10:25
MemberMember 1039595820-Mar-16 10:25 
GeneralRe: My vote of 5 Pin
Bernardo Castilho28-Mar-16 8:04
professionalBernardo Castilho28-Mar-16 8:04 
QuestionProblem with Adobe PDF printer Pin
DanMacy9-Feb-16 12:40
MemberDanMacy9-Feb-16 12:40 
AnswerRe: Problem with Adobe PDF printer Pin
Bernardo Castilho10-Feb-16 0:51
professionalBernardo Castilho10-Feb-16 0:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.