Printing under WPF
February 20, 2006
New York City
This morning I got printing to work using the PrintVisual method of PrintDialog. In theory, this method lets you print a single page without much fuss, but in all my previous attempts with this method, I took the definition of the first argument literally. It's defined as an object of type Visual so I figured I could create a Canvas (for example), decorate it with elements and controls, and then print it. But the only result of those experimentations was a blank page ejected from the printer.
This morning, however, I tried passing a DrawingVisual to PrintVisual after having first opened a DrawingContext and drawing on that. That worked!
The TryPrinting.csx (and rename to .cs) program (and TryPrinting.csproj project file) shows simple use of PageSetupDialog and PrintDialog (both of which you'll find in the System.Windows.Controls namespace) to print a single page.
The PageSetupDialog gives you a couple pieces of information. The first is a PrintTicket object, which you can get in and out of the dialog box through the GetPrintTicket and SetPrintTicket methods. (These are methods rather than a read/write property because the methods make a copy of the object.) The PrintTicket has stuff like selected page size, printer resolution, and a PageOrientation property for portrait or landscape.
The PageSetupDialog also lets the user set left, top, right, and bottom margins for the page and provides LeftMargin, TopMargin, RightMargin, and BottomMargin properties to initialize the values and obtain the user's preferences. Although the dialog displays these values in inches, the values of the properties are in units of 1/1000 millimeter. (This was a bit of a shock seeing how almost everything else in WPF is in units of 1/96 inch. The TryPrinting program has two methods at the bottom to convert between these units and normal device-independent units.)
The TryPrinting program initializes a Thickness object with 1-inch margins (in device-independent units) and uses that to initialize and save the margin settings from PageSetupDialog. The program also defines a PrintTicket field but always obtains that from PageSetupDialog.
PrintDialog processing begins with a possible call to SetPrintTicket with the PrintTicket obtained from PageSetupDialog. If the user clicks Print, the method creates a DrawingVisual, obtains a DrawingContext from it, draws on the DrawingContext, and then closes it, leaving behind a DrawingVisual that it passes to PrintVisual.
The PrintableAreaWidth and PrintableAreaHeight properties from PrintDialog actually report the physical size of the page in device-independent units with orientation (portrait or landscape) taken into account. (This information is really more useful than the printable area of the page because without knowing the unprintable widths on all four sides of the page, you can't properly apply margins to the printable area.) My printing method creates a Rect object taking margins into account and calls DrawRectangle with it. A FormattedText object is created displaying the "printable area" (actually physical paper size) properties in inches. My printing method then obtains the size of that text, centers it within the margins of the page, and then draws a rectangle around the text.
I was surprised to see that the PrintableAreaWidth and PrintableAreaHeight properties were influenced by the orientation setting, but actual printing was not. However, it was easy enough to set up a little switch statement that rotated the DrawingVisual based on the PageOrientation property.