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:

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

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

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:

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:

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.

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:

History

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!

Thank you for making this Cool Print Preview control available as open source. You are super awesome !

By the way, I need to add selecting-text capability to this form. Similar to Acrobat reader or XPS viewer. Is it possible ? Which route do you suggest ? I know PrintDocument object is already a rendered image. Still trying to find other ways..

Yes, adding selection to the control is technically possible. The page images are really enhanced metafiles, so you can call an API to enumerate the GDI calls within the metafile. Each element in the metafile has a bounding rectangle, so you could use that to figure out which GDI calls contain the current mouse coordinates, and those would correspond to the text you want to select.

But notice I said 'possible', not easy. In addition to enumerating the GDI calls, you will have to do some coordinate translations to convert the metafile coordinates into mouse/control coordinates.

You choose the font and all the document content when you create the PrintDocument.

The steps are:

1 - instantiate a PrintDocument2 - add an event handler to the PrintPage event3 - call PrintDocument.Print4 - handle the PrintPage event to add content to each page of the document5 - print or preview the document

The PrintPreview control and dialog are involved only in step 5. They know nothing about the content of the document (whether it contains text, font names, sizes, etc).

The content (including fonts and text) is generated in step 4. A PrintDocument usually contains text rendered in several different fonts (regular paragraphs, headers, footers, titles, etc).

The sample attached to the article uses the following code to generate the document content:

.... kind of ridicules having the options there if they don't actually do anything ... I mean how would we make use of the duplex setting if it's not built in?? ... seems like someone @ M$ did a 1/2 assed job!

It took me hours digging into the code to figure out the problem "why ReportPrinting"was not working with other PrintPreview dialogs except the standard PrintPreviewDialog (especially those that use BeginPrint, PrintPage, and EndPrint methods" !!!

Notice that in CoolPrintPreview and CoolPrintPreviewControl the methods BeginPrint, PrintPage, and EndPrint are not called by ReportDocument . These methods are essential for these components to work properly (especially PrintPage in CoolPrintPreviewControl that is responsible for capturing document images).

To make CoolPrintPreview and CoolPrintPreviewControl work with ReportDocument, append the methods OnBeginPrint(), OnPrintPage(), and OnEndPrint() in ReportDocument.cs with base.OnBeginPrint(e), base.OnPrintPage(e), base.OnEndPrint(e), respectively.

Good job with this great print preview.I have been using it for printing reports and some of the charts I'm printing are on landscape, so before print them I would need be able to rotate the page to check them. Well, the point is that after trying several things like playing with the "graphics translate transform", I was able to do it changing Image to Bitmap and using RotateFlip but... it looks terrible and sometimes I get an overflowmemory error if the Bitmap is not shrinked.

From what you said, it sounds like the best solution would be to change the orientation document, or of some pages, rather than rotate the page image. This can be done easily by handling the QueryPageSettings event.

The only tricky part is you have to figure out which pages should be rendered in landscape when you finish rendering the previous page. This may be easy or hard depending on how you are creating the document.

For example, the code below renders a document mostly in portrait, but with the second page in landscape:

I hope this will solve your issue. If not, then my suggestion would be to create the rotated image as a Metafile rather than a bitmap. It will be a little harder, but the results will be a lot better (scalable, less memory usage).

Do you have any idea on How I can add 'Fit to page' in this?In my case: The print appears in multiple pages (originally) and I would like to have an option that it fits to one page. How can i achieve it?

I understood what you meant. But, Lets say if i have only one page of actual content appearing on screen, and the width of the content exceeds that(width) of A4 or A3 page. so in this case how can i fit to one page (eg. A4). Provided, the content width also exceeds the page width when landscape mode selected too.

do you have any idea how i can implement scaling with this? so that i could scale the document proportionately in accordance to the page dimensions before printing.

Coming up with a generic way to do this would not be simple. Here's what I would try:

1) Write a RenderPage routine that takes the page size as a parameter and returns the actual size used.

2) Call this RenderPage routine from the PrintDocument's RenderPage event handler, with the actual page size. So nothing changed yet.

3) Create a FitToPage method that calls the RenderPage routine with a very large page size (sure not to be exceeded), and use the return value to calculate the zoom factor needed to make the page fit (zoom = min(realHeight / neededHeight, realWidth / neededWidth)

4) Apply the zoom factor using a transform and render the document into a single page.

When PrintRange is set to PrintAll when the CoolPrintPreviewDialog is instantiated, a two-page document (as viewed in the print preview window) comes out of the printer as a single blank Page 3 (code shown below). When I set PrinterSettings.FromPage = 1 and PrinterSettings.ToPage = 2 and then set to PrintSome, pages 1, 2 and 5 print. I suspect I'm just not setting some initial value properly before showing the dialog. Any ideas?

Thanks for responding, Bernardo. Your project deserves six out of five stars! The answer to your question is a long one, I'll do my best to be concise. I'm a professional software developer by day and an VS Express player by night. Believe me, this would have been a lot easier with MS Visual Reports or Crystal Reports. But learning this is profitable.

The printing part of my project is based on http://www.devarticles.com/c/a/C-Sharp/Printing-Using-C-sharp, which uses a "print engine" class that is descended from PrintDocument. It in turn uses several more classes to define how to draw several primitives. I've added a primitive called "cell." Several cells are strung together on the same yPos to form columns whose values can be right- or left-justified, etc. All of this comes together beautifully in your extended CoolPrintPreviewDialog. It looks exactly the way I want it to come out on the printer.

My problem lies in what doesn't happen when I click the printer tool in the dialog, as described previously. My OnPrintPage handles pagination correctly, drawing the various elements where they belong on each page, using Draw routines. But I really am confused about how to use OnBeginPrint for multiple-page documents. Both of these methods are declared inside the "print engine" class, which is itself the PrintDocument. _pageSetupDialog is declared as a private field within the class because its value needs to be available to several overloaded methods within the class. Since this is inside the class, making settings boils down to instantiating _pageSetupDialog and then assigning printer settings to it as shown in the code snippet I provided. It also explains the statement dialog.Document = this.

A report is invoked from the main form's menu bar by instantiating a "print engine" and passing several parameters in. Then it's displayed by calling the print engine's ShowReport() method. This reduces the code volume in the main form to only two lines inside the on-click stub for the menu item. Report characteristics (name, number of columns, headings, column justification, etc.) are stored in SQL tables forming a poor man's "report template" that is easy for the programmer to use for creating new reports.

I suspect my problem is (1) complete inexperience with drawing and printing in Windows, (2) confusion over how to use OnBeginPrint, and (3) the difference between .Draw() and .Print(). (At present, my code has no .Print() calls.) In general, how does the document inside the PrintPreview frame get redrawn on the printer? What logic has to occur for this transfer to happen? My hope was that once the CoolPrintPreviewDialog had the report, that clicking the print tool would just send what it had to the printer, but this was obviously a naïve expectation on my part.

Thanks for your patience with this long submission. I don't expect you to fix my code, I just need some suggestions on how to move forward with better understanding.

And I understand your confusion, printing in .NET is pretty powerful but not simple. I will try to summarize the pieces as I see them, hope the description will shed some light and hopefully help you solve the problem.

Piece 1: PrintDocumentThe PrintDocument should probably be called the "DocumentRenderer". It is not a static document, but rather an object that has a Print method and event handlers (PrintPage, etc) that create the document on demand, given parameters that define the page and printer settings.

Piece 2: PrintControllerThe PrintController is responsible for creating the pages (which may be paper pages, metafile images, PostScript files, or whatever). If you don't specify a controller, you get a PrintController by default.

Piece 3: PrintDialog, PageSetupDialogThese dialogs allow users to configure a document's PrinterSettings and DefaultPageSettings. You don't have to use them at all. If you simply want to print a document using the default page and printer settings, all you need to do is call PrintDocument.Print.

OK, so how do these pieces work together in the CoolPrintPreview?

Step 1: Get images (metafiles) for each page- start with a PrintDocument- set the document's PrintController to a PreviewPrintController- call PrintDocument.Print to render the whole document into the PreviewPrintController- now the controller has metafiles that represent each page- restore the original controller - use the metafiles to render the document on the screen (preview) or to print

Step 2: Preview the Document- get images for each page (described above)- show the images on the screen, with panning/zooming, etc- note that you can get and show the images as they are generated

Step 3: Print the Document- create a new PrintDialog- set the dialog's Document property to the PrintDocument- show the dialog so the user can select printer, orientation, pages, etc- if dialog returns OK, print using the PrintDocument.PrinterSettings parameters

This last step is a little tricky. Calling PrintDocument.Print always prints the entire document, regardless of page ranges selected in the PrintDocument.PrinterSettings. This seems like a pretty silly limitation of the .NET printing framework, but that's the way things are as far as I can tell.

So in this case the CoolPrintPreview creates a new document and uses the metafiles generated in the first step to populate this new document with the pages selected by the user (see the DocumentPrinter class in the project).

I suspect the problem you describe has to do with different versions of the page settings being used in different parts of the code and confusing the print controller. But that's just a guess.

In general, you shouldn't have to worry about the page or printer settings at all. The PrintDocument events have parameters that describe the page size, and that's all you should need.

Very informative, thanks. Searching through my existing code for these components reveals that there is overlap between the original code (link in the previous reply) and CoolPrintPreviewDialog, which I'll have to sort out. My guess is that that's the origin of the confusion of the print controller you suggested.

I understand the general logic of the steps you described. Since my output appears beautifully in the CoolPrintPreviewDialog window and the controls (zoom, paging, etc.) are fully functional, I think it's safe to assume the metafiles have been created properly. If I understand your description correctly, CoolPrintPreviewDialog, which now has metafiles created from my original "print engine" print document, creates a fresh PrintDocument internally, and this is what gets sent to the printer.

It seems to me that my original code snippet sends the original PrintDocument (my "print engine") to CoolPrintPreviewDialog by declaring a CoolPrintPreviewDialog "dialog" and assigning "this" to its Document. PrinterSettings and PageSetup parameters are made against "dialog".

I'll see if I can trace the document's progress through these stages, paying attention to which PrinterSettings parameters are being used. Thanks again for your guidance. I'll keep you posted.

Back again. After tracing code here's what I found. You'll recall that I am trying to retain earlier work that enables the design of printing primitives that use the Graphics.Draw routines. The PrintDocument object, referred to as the "print engine," has its own OnPrintBegin() and OnPrintPage() routines with extensive code to render page headers, page footers and page bodies with columnar data, including pagination for body content that exceeds page size. This code is the origin of what appears correctly in the CoolPrintPreviewDialog's window before clicking the print button.

If I understood your steps explained above correctly, the output is now in the form of metafiles, and clicking the printer button should cause a new PrintDocument to be created internally by CoolPrintPreviewControl, which has its own OnBeginPrint() and onPrintPage() methods. When execution completes and exits CoolPrintPreviewDialog's _doc_BeginPrint() method (in Region Job Control at the bottom of the CoolPrintPreviewDialog.cs file), the intent is to call CoolPrintPreviewControl's OnBeginPrint() and OnPrintPage() methods with the metafile page images, but instead it is calling my original OnBeginPrint() and OnPrintPage() routines. Apparently CoolPrintPreviewDialog is confused by the existence of OnBeginPrint() and OnPrintPage() methods in both code-behind files, even though they exist in separate namespaces and classes.

Do you have any suggestions on how to keep CoolPrintPreviewDialog from hooking the wrong OnBeginPrint() and OnPrintPage() routines?

Thanks for the offer to look over my code, Bernardo -- you have a big heart. I just feel that asking you to do that goes too far beyond a "discussion thread" in spite of your willingness. You've been very generous with your time already. I'm going to let this thread go dormant for awhile, and take a fresh run at my code from scratch still using your CoolPrintPreviewDialog. That will take some time (this is, after all, an evening/weekend spare time project). Once I get back to the point of seeing my report in the dialog window, if I still have problems getting it to print, I'll come back with more concise questions and a better level of understanding. Thanks again for your help so far.

It's important to remember that in all cases the document renders perfectly in the CoolPrintPreviewDialog window itself. Zoom, pagination controls, etc. all work perfectly. This tells me that CoolPrintPreviewDialog is receiving my PrintDocument correctly. The problem only happens when the document is sent to the printer from the CoolPrintPreviewDialog. I put a breakpoint at the top of every method in CoolPrintPreviewControl.cs and stepped through them, and here's what I found.

You stated previously that CoolPrintPreview doesn't use OnBeginPrint() and OnPrintPage() if I understood you correctly. However, in CoolPrintPreviewControl.cs near the end of the code both of these are present as protected override methods.

When the print tool is clicked on the CoolPrintPreviewDialog toolbar, the Printer Control dialog box appears so the user can select printer and page range. If the PrintDocument handed to CoolPrintPreviewDialog is a single page, the page range is set to AllPages and From/To page is disabled. If the PrintDocument is multiple pages, the page selection range is defaulted correctly to From/To pages (from 1 to the document's maxPages), and the AllPages radio button is enabled but not selected. In either case, whenever AllPages is used, CoolPrintPreviewControl.cs calls the OnPrintPage() routine in my original code that was used to generate the document in the first place. This causes the generation of a new PrintDocument with header, footer, a page number one higher than the ending page in the original document, and for which no data lines are available. What renders to the printer is a headed and footed empty page beyond the last page of the original PrintDocument. On the other hand, if PrintRange is set to From/To Page, CoolPrintPreviewControl calls its own OnPrintPage() method and the document renders correctly to the printer.

Ideally CoolPrintPreviewDialog needs to stop calling my OnPrintPage() routine and stick to its own. This suggests to me that there is something in the event handler that triggers OnPrintPage() that fails to distinguish which OnPrintPage() method to use when PageRange is set to AllPages. Perhaps a clarifying class name or namespace name either in the handler or in the CoolPrintPreviewDialog.cs code is needed.

As a simpler workaround, the Printer Control dialog could be changed to use From/To in all cases (even for single page documents). I'll pursue this approach and let you know if it works. Just thought you'd appreciate my findings. While this gives me a working solution, it seems a shame to settle for this workaround given the power and elegance of CoolPrintPreviewDialog. Hope this is helpful, and thanks again for your interaction.

In attempting to force the PrintDialog to always use SomePages, I discovered that Microsoft's control internally defaults the radio button to the All setting whenever the document is one page. (It took me about 2 hours to verify that in various forums.) So I went back to the original problem. The PrintDialog is not defined in CoolPrintPreviewDialog code, but rather in my original code. I just went back to tinkering with settings there by trial and error. What worked for me in the end was to not only set FromPage and ToPage to 1 and the last page number respectively, but also set MinimumPage and MaximumPage to the same values. From that point on the printer output matched what was in the CoolPrintPreviewDialog window. Apparently leaving these undefined can lead to some VERY strange results. Lesson learned.

This has to be done outside the PrintPreview. I have done something similar myself, the approach is simple (but not necessarily easy):

1) You have to calculate the size of the entire document (width and height)2) Calculate the zoom factor needed to make the content fit the page height or width3) Generate the document applying the zoom factor you obtained in step 2.

This really has nothing to do with the preview part. All output commands, including units and scaling are handled by the standard .NET PrintDocument class.

By default, the units are pixels. The most flexible way to use other units is to convert the coordinates yourself, using the e.Graphics.DpiX and e.Graphics.DpiY.

The easiest way to use other units is to set the e.Graphics.PageUnit property to the unit you want to use. For example, the code below draws a string in a red box 2cm wide, 4cm tall, positioned 8cm from the left of the page, 2cm from the top:

Excellent point. I added CTRL+wheel zooming recently, but as you zoomed the point under the mouse scrolled away from the view. I have modified this to keep the document in the same position as you zoom with the mouse.