WPF Printing and Print Preview

This article shows some ways on how to print and preview documents using WPF.

Introduction

I was searching on how to print using WPF for my application. The requirement is that there’s already a background image where text will be placed accordingly. The image is based on an actual form, like a purchase order form. The document can also be previewed.

So while searching, I found out that there are several ways to print documents using WPF. Let’s start with the easy ones.

Printing a Visual

A Visual is an object that provides rendering support in WPF. Basically, these are the objects that we usually see in a WPF application, like user interface controls, images, ellipses, etc. Below is the screenshot of a simple application that prints all the contents of the window.


The following code listing shows the event handler for the Print button’s Click event.

private void PrintBtn_Click(object sender, RoutedEventArgs e)

{

PrintDialog printDialog = new PrintDialog();

if (printDialog.ShowDialog() == true)

{

printDialog.PrintVisual(grid, "My First Print Job");

}

}

The PrintDialog control is shown when the Print button is clicked.

The PrintDialog lets the user choose a printer, set printer preferences, and other print options. If the user clicks the Print button then the PrintVisual method is executed. What the PrintVisual method does is create a print job based on a Visual object and add it to the print queue. It takes in 2 parameters: the Visual object to print and the print job description. Here, the Visual object is the grid containing the other Visual objects. Even the Print button gets printed since it is included in the grid. The print job description is just a name to identify the print job in the printer’s user interface.

It is not necessary to show the Print dialog. This, however, removes the capabilities of the user to change some settings before printing. Default settings can be changed using code by changing properties of the PrintDialog, like the print ticket.

I think one use of this approach is by using it in a simple drawing application. Since this approach is very simple to use, it comes with some noticeable limitations. First, it does not support pagination. If the Visual is too long for a single page, then some content won’t get printed. Another one is that there is not much control over the page margins. The default behavior is that a Visual gets printed starting at the top-left corner of the page. Note that not all printers can print near the edges of the paper so it is possible that some content won’t get printed. One workaround for this is setting a margin of the Visual before printing. The following figure shows the output.

Printing a Document

The PrintDialog provides another method for printing, the PrintDocument method. This method sends a DocumentPaginator object to the print queue. The DocumentPaginator class, according to MSDN, provides an abstract base class that supports creation of multiple-page elements from a single document. So unlike the previous approach, this one supports pagination.

Generally, there are two ways to get a DocumentPaginator object.

· A DocumentPaginator can be obtained from a WPF document. A document is divided into two categories: FlowDocument and FixedDocument. A FlowDocument is a document that supports flowing of content. It means that the content can be rearranged to some extent depending on what the user prefers for his viewing pleasure. Meanwhile, a FixedDocument refers to a document with fixed content and commonly known as a What You See Is What You Get (WYSIWYG) document.

· We can create an object derived from DocumentPaginator because the DocumentPaginator can’t be instantiated directly since it is abstract. There are two existing derived classes but one is abstract and one is only used for adding annotations.

To demonstrate how to use the PrintDocument method, let’s create an application that prints a FlowDocument.

Here is the corresponding XAML code for the figure above, although I’ve resized the window in the figure.

<Window

x:Class="PrintDocument.MainWindow"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

Title="Print Document"

Width="500"

Height="500">

<Grid>

<Grid.RowDefinitions>

<RowDefinition Height="Auto"/>

<RowDefinition Height="*"/>

</Grid.RowDefinitions>

<Menu

Height="22">

<MenuItem

Header="File">

<MenuItem

Header="Print"

Click="PrintMenu_Click"/>

</MenuItem>

</Menu>

<FlowDocumentReader

Grid.Row="1">

<FlowDocument

x:Name="flowDocument"

IsOptimalParagraphEnabled="true"

IsHyphenationEnabled="true"

IsColumnWidthFlexible="false"

ColumnWidth="Auto"

PagePadding="Auto" >

<Paragraph>

WPF Printing

</Paragraph>

<!-- REMOVED OTHER PARAGRAPHS FOR READABILITY -->

</FlowDocument>

</FlowDocumentReader>

</Grid>

</Window>

The following code listing shows the event handler for the Print menu item’s Click event.

private void PrintMenu_Click(object sender, RoutedEventArgs e)

{

PrintDialog printDialog = new PrintDialog();

if (printDialog.ShowDialog() == true)

{

printDialog.PrintDocument(((IDocumentPaginatorSource)flowDocument).DocumentPaginator, "Flow Document Print Job");

}

}

Notice that using the PrintDocument method is almost the same as using the PrintVisual method. To get the DocumentPaginator object for a FlowDocument, we have to cast it to an IDocumentPaginatorSource first. If we are using a FixedDocument, we can get the DocumentPaginator object through the DocumentPaginator property. Now let’s look at the print output.

Only 2 out of 4 pages are presented here. You can see that there are unused spaces. This might not be the desired output because of the wasted space. This happened because the print output of a flow document depends on what container the document is hosted. In this example, a FlowDocumentReader is used. There are 3 controls specifically used for viewing flow documents.

· A FlowDocumentScrollViewer has a scroll bar to allow the user to scroll up and down the flow document continuously, much like viewing a web page.

· A FlowDocumentPageViewer is similar to the FlowDocumentReader in the example. The flow document is shown 1 page at a time.

· A FlowDocumentReader has different viewing modes: Page Mode, Two Page Mode, and Scroll Mode.

The following figure shows the output when a FlowDocumentScrollViewer is used.

If a FlowDocumentPageViewer is used, the output is the same as that of using a FlowDocumentReader in Page Mode, like in the example where there are unused spaces. Meanwhile, the output of the FlowDocumentReader depends on the current viewing mode. If the viewing mode is Scroll Mode, then the output will be the same as when a FlowDocumentScrollViewer is used. If the viewing mode is Page Mode, then the output will be the same as when a FlowDocumentPageViewer is used.

Custom Printing

As said earlier, we can create our own DocumentPaginator-derived class. This gives us more control on how a document is printed. However, it might be difficult to do this depending on the requirements. For example, if you want to display long text, then you should know how to split them over multiple lines and how to split them over pages. To demonstrate how to create a custom DocumentPaginator, let’s create a simple application that prints a list of inventory items and their current quantity. The following code is the custom DocumentPaginator class.

/// <summary>

/// Document paginator for inventory items.

/// </summary>

public class InventoryDocumentPaginator : DocumentPaginator

{

private readonly InventoryItem[] inventoryItems;

private Size pageSize;


private
int pageCount;


private
int maxRowsPerPage;

/// <summary>

/// Constructor.

/// </summary>

/// <param name="inventoryItems">The list of inventory items to display.</param>

/// <param name="pageSize">The size of the page in pixels.</param>

public InventoryDocumentPaginator

(

InventoryItem[] inventoryItems,

Size pageSize

)

{

this.inventoryItems = inventoryItems;

this.pageSize = pageSize;

PaginateInventoryItems();

}


///
<summary>

/// Computes the page count based on the number of inventory items

/// and the page size.

/// </summary>

private void PaginateInventoryItems()

{

FormattedText text = Utilities.FormatText("Any Text");

maxRowsPerPage = (int)(pageSize.Height / text.Height);

pageCount = (int)Math.Ceiling((double)inventoryItems.Count() / maxRowsPerPage);

}

/// <summary>

/// Gets a range of inventory items from an array.

/// </summary>

/// <param name="array">The inventory items array.</param>

/// <param name="start">Start index.</param>

/// <param name="end">End index.</param>

/// <returns></returns>

private static InventoryItem[] GetRange(InventoryItem[] array, int start, int end)

{

List<InventoryItem> inventoryItems = new List<InventoryItem>();

for (int i = start; i < end; i++)

{

if (i >= array.Count())

{

break;

}

inventoryItems.Add(array[i]);

}

return inventoryItems.ToArray();

}


#region
DocumentPaginator Members


///
<summary>

/// When overridden in a derived class, gets the DocumentPage for the

/// specified page number.

/// </summary>

/// <param name="pageNumber">

/// The zero-based page number of the document page that is needed.

/// </param>

/// <returns>

/// The DocumentPage for the specified pageNumber, or DocumentPage.Missing

/// if the page does not exist.

/// </returns>

public override DocumentPage GetPage(int pageNumber)

{

// Compute the range of inventory items to display

int start = pageNumber * maxRowsPerPage;

int end = start + maxRowsPerPage;


InventoryListPage
page = new InventoryListPage(GetRange(inventoryItems, start, end), pageSize);

page.Measure(pageSize);

page.Arrange(new Rect(pageSize));


return
new DocumentPage(page);

}

/// <summary>

/// When overridden in a derived class, gets a value indicating whether

/// PageCount is the total number of pages.

/// </summary>

public override bool IsPageCountValid

{

get { return true; }

}


///
<summary>

/// When overridden in a derived class, gets a count of the number of

/// pages currently formatted.

/// </summary>

public override int PageCount

{

get { return pageCount; }

}

/// <summary>

/// When overridden in a derived class, gets or sets the suggested width

/// and height of each page.

/// </summary>

public override System.Windows.Size PageSize

{

get

{

return pageSize;

}

set

{

if (pageSize.Equals(value) != true)

{

pageSize = value;

PaginateInventoryItems();

}

}

}

/// <summary>

/// When overridden in a derived class, returns the element being paginated.

/// </summary>

public override IDocumentPaginatorSource Source

{

get { return null; }

}


#endregion

}

The code in the DocumentPaginator region contains the overridden properties and a method.

· The GetPage method returns a DocumentPage object for the specified page number. This method is called by the printer driver to get the pages to print. To create a DocumentPage, a Visual object must be supplied to its constructor. In the example, an InventoryListPage UserControl is the Visual object. The list of inventory items to display and the page size are passed to the InventoryListPage constructor. It then handles the drawing of the inventory items by overriding the OnRender method. We’ll take a look at this later.

An alternative to creating a UserControl is to create a DrawingVisual object and call the RenderOpen method to get the DrawingContext, which is used to render into the DrawingVisual. In the example, the Measure and Arrange methods are called on the Visual object to ensure that the Visual’s layout is correct. Otherwise, the pages will be empty. This is not necessary if the Visual is displayed in the application’s user interface since these methods are called already.

· The IsPageCountValid property checks if the total number of pages is valid. In the example, it always returns true since the PageCount is easy to compute. However, there will be times that it won’t be easy to determine the PageCount, especially in applications with complex algorithms to create the layout of every page, so the value should be set to false. Every time the GetPage method is called by the printer driver, the IsPageCountValid is also called. You should provide a condition that determines if the PageCount is already valid, like when all data have been printed. Otherwise, the printer driver will think that pagination is still in process and will continue calling the GetPage method.

· The PageCount property returns the total number of pages. If this can’t be determined immediately, the PageCount may return the number of pages currently formatted.

· The PageSize property is the size of the page in pixels, where a pixel is 1/96th of an inch. So if we want to print to an 8.5 x 11 in. paper, we just have to multiple the dimensions by 96 and we’ll get 816 x 1056 pixels. Notice also that unlike the previous properties, this property has a set accessor. If the value is changed, then the document should be repaginated.

· The Source property returns the element being paginated, which is of type IDocumentPaginatorSource. Personally, I can’t find much use of this property but fortunately, we can always return a null value.

The PaginateInventoryItems computes the page count and the maximum number of rows of inventory items that can fit within a page. To compute the number of rows, we must also know the height of the text. This is obtained from the Height property of a FormattedText object. The following code listing shows the FormatText utility method, which returns a FormattedText object.

public static class Utilities

{

/// <summary>

/// Gets a FormattedText equivalent of the specified text string.

/// </summary>

/// <param name="text">The text string to format.</param>

/// <returns>FormattedText object.</returns>

public static FormattedText FormatText(string text)

{

return new FormattedText

(

text,

CultureInfo.CurrentCulture,

FlowDirection.LeftToRight,

new Typeface("Arial"),

12,

Brushes.Black

);

}

}

Instead of using constant values for arguments like the font size, you can add properties in the custom DocumentPaginator to change these values. If this is done in the example, the PaginateInventoryItems method should be called every time the font size is changed.

Let’s take a look at the InventoryListPage UserControl, the Visual where the actual drawing of content is done. The XAML code, which is default XAML generated for a UserControl, is not shown here.

public partial class InventoryListPage : UserControl

{

private readonly InventoryItem[] inventoryItems;


private
readonly Size pageSize;


public
InventoryListPage

(

InventoryItem[] inventoryItems,

Size pageSize

)

{

InitializeComponent();

this.inventoryItems = inventoryItems;

this.pageSize = pageSize;

}


protected
override void OnRender(DrawingContext drawingContext)

{

base.OnRender(drawingContext);


Point
point = new Point(0, 0);

foreach (InventoryItem item in inventoryItems)

{

point.X = 0;
FormattedText
text = Utilities.FormatText(item.Name);

drawingContext.DrawText(text, point);


point.X = pageSize.Width / 2;

text = Utilities.FormatText(item.Quantity.ToString());

drawingContext.DrawText(text, point);

point.Y += text.Height;

}

}

}


The important part here is the OnRender method. The DrawingContext object provides various methods for drawing into the Visual. Now, the custom DocumentPaginator can be used. The following code shows the Main method of a console application using the DocumentPaginator.

class Program

{

[STAThread]

static void Main(string[] args)

{

List<InventoryItem> inventoryItems = new List<InventoryItem>();


// Create a list of inventory items where the quantity is random.

Random random = new Random();

for (int i = 1; i <= 100; i++)

{

inventoryItems.Add(new InventoryItem("Inventory Item " + i, random.Next(100)));

}


// 8.5 x 11 paper

Size pageSize = new Size(816, 1056);

InventoryDocumentPaginator paginator = new InventoryDocumentPaginator

(

inventoryItems.ToArray(),

pageSize

);

PrintDialog printDialog = new PrintDialog();


if
(printDialog.ShowDialog() == true)

{

printDialog.PrintDocument(paginator, "Custom Paginator Print Job");

}

}

}

I think most of the code in the previous listing is self-explanatory. Now let’s take a look at the result. It’s a bit ugly since there are no margins and too much wasted space.



XPS Document Printing


In the previous example, there was no print preview functionality. This could be done by using XML Paper Specification (XPS) documents, which will be the topic in this section. An XPS document is a package that contains one or more fixed documents. One advantage of using an XPS document is that it prints better because of device and resolution independence since it is a vector-based format. It also uses a new print path, GDI being the older one.

So to print an XPS document, there should be a printer driver that is XPS-enabled. If a printer driver only supports the (Graphics Device Interface) GDI model, then WPF converts the XPS content to its GDI equivalent.


To demonstrate how to create and print preview an XPS document, let’s create a simple application according to the original requirement: there is an existing form to use, which is an image, and lay out text accordingly. The following screenshot show the application’s main window.



The user can enter purchase order data. After clicking on the Print button, a print preview window will be shown containing the purchase order document filled with data previously entered. Let’s take a look at the XAML code of the main window.

<Window
x
:Class="XPSDocumentPrinting.MainWindow"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

Title="MainWindow"

SizeToContent="WidthAndHeight">

<Grid>

<Grid.RowDefinitions>

<RowDefinition Height="Auto"/>

<RowDefinition Height="Auto"/>

<RowDefinition Height="Auto"/>

</Grid.RowDefinitions>

<Grid.ColumnDefinitions>

<ColumnDefinition Width="Auto"/>

<ColumnDefinition Width="Auto"/>

</Grid.ColumnDefinitions>

<Label

Content="PO #:"/>

<TextBox

Height="23"

Width="150"

Grid.Column="1"

Margin="4"

Text="{Binding Number}"/>

<Label

Grid.Row="1"

Content="Supplier:"/>

<TextBox

Height="23"

Width="150"

Grid.Column="1"

Grid.Row="1"

Text="{Binding Supplier}"/>

<Button

Height="23"

Width="75"

Grid.Column="1"

Grid.Row="2"

Margin="4"

HorizontalAlignment="Right"

Content="Print"

Click="PrintBtn_Click"/>

</Grid>

</Window>

We used data binding to assign the values from the text boxes to the properties of a PurchaseOrder object, which is set as the window’s DataContext as shown in the code-behind file below.

public partial class MainWindow : Window

{

public MainWindow()

{

InitializeComponent();


DataContext = new PurchaseOrder();

}


private
void PrintBtn_Click(object sender, RoutedEventArgs e)

{

PrintHelper.PrintPreview(this, (FormData)DataContext);

}

}

The PurchaseOrder class is shown below. It inherits from FormData, an empty abstract class. The FormData is a parameter to the PrintPreview method which is shown later. This is useful if we have different forms. We don’t need to create a specific PrintPreview method for each form.

public class PurchaseOrder : FormData

{

public int Number { get; set; }


public
string Supplier { get; set; }

}

When the user clicks on the Print button, the static PrintPreview method of the PrintHelper class is called. Let’s take a look at the PrintHelper class.

public static class PrintHelper

{

private static PageMediaSize A4PaperSize = new PageMediaSize(816, 1248);


public
static void PrintPreview(Window owner, FormData data)

{

using (MemoryStream xpsStream = new MemoryStream())

{

using (Package package = Package.Open(xpsStream, FileMode.Create, FileAccess.ReadWrite))

{

string packageUriString = "memorystream://data.xps";

Uri packageUri = new Uri(packageUriString);


PackageStore
.AddPackage(packageUri, package);


XpsDocument
xpsDocument = new XpsDocument(package, CompressionOption.Maximum, packageUriString);

XpsDocumentWriter writer = XpsDocument.CreateXpsDocumentWriter(xpsDocument);

Form visual = new Form(data);


PrintTicket
printTicket = new PrintTicket();

printTicket.PageMediaSize = A4PaperSize;

writer.Write(visual, printTicket);

FixedDocumentSequence document = xpsDocument.GetFixedDocumentSequence();

xpsDocument.Close();


PrintPreviewWindow
printPreviewWnd = new PrintPreviewWindow(document);

printPreviewWnd.Owner = owner;

printPreviewWnd.ShowDialog();

PackageStore.RemovePackage(packageUri);

}

}

}

}

The basic idea is that we should create an XPSDocument object and write our Visual onto it using an XPSDocumentWriter object. We can also write a DocumentPaginator object instead of a Visual. This solves the problem on how to print preview the DocumentPaginator object in the previous section.

Creating an XPSDocument requires a Package object. A Package is used to organize objects into a single entity. Just think of a Package as a ZIP file. We can create a Package from a file or memory stream. It is important to add the Package object to the PackageStore and create the XPSDocument using the constructor where the URI of the Package can be specified. Otherwise, we’ll get an exception when calling the GetFixedDocumentSequence method. The Package should be removed after showing the document’s print preview.

In the example, a Form UserControl is written to the XPS document. The Form object determines what specific UserControl to use as its content based on the type of data. This is achieved by using DataTemplate resources. The following code listing shows how this is done.

<UserControl
x
:Class="XPSDocumentPrinting.Form"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:local="clr-namespace:XPSDocumentPrinting">

<UserControl.Resources>

<DataTemplate DataType="{x:Type local:PurchaseOrder}">

<local:PurchaseOrderForm/>

</DataTemplate>

</UserControl.Resources>

<ContentControl Content="{Binding}"/>

</UserControl>

The Form contains a ContentControl where the Content property is set to the Form’s DataContext property. If the DataContext is a PurchaseOrder, then the Content is set to an instance of a PurchaseOrderForm UserControl. The following code listing shows how the DataContext property is set.

public partial class Form : UserControl

{

public Form(FormData data)

{

InitializeComponent();


DataContext = data;

}

}

The following code listing shows the PurchaseOrderForm’s XAML code.

<UserControl

x:Class="XPSDocumentPrinting.PurchaseOrderForm"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<Grid>

<Image

Source="PurchaseOrder.png"/>

<TextBlock

Height="21"

Margin="100,19,391,0"

VerticalAlignment="Top"

Text="{Binding Number}"/>

<TextBlock

Height="21"

Margin="100,46,391,0"

VerticalAlignment="Top"

Text="{Binding Supplier}"/>

</Grid>

</UserControl>

The DataContext property of the PurchaseOrderForm is automatically set to the PurchaseOrder object so the bindings will work. After the Visual is written on the XPS document, we can now call GetFixedDocumentSequence method that will return a FixedDocumentSequence object. A FixedDocumentSequence object implements IDocumentPaginatorSource, which is the parameter needed in the PrintPreviewWindow, as shown below.



The PrintPreviewWindow just contains a DocumentViewer control, which is shown in the following listing.


<
Window

x:Class="XPSDocumentPrinting.PrintPreviewWindow"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

Title="Print Preview"

Height="Auto"

Width="Auto"

WindowStartupLocation="CenterOwner">

<DocumentViewer

Document="{Binding}"/>

</Window>

The Document property of the DocumentViewer is set using the DataContext which is set to the IDocumentPaginatorSource object we got from the GetFixedDocumentSequence method.

public partial class PrintPreviewWindow : Window

{

public PrintPreviewWindow(IDocumentPaginatorSource document)

{

InitializeComponent();


DataContext = document;

}

}

WPF printing is a subject too large to be covered fully in this article. I hope this article helps WPF developers get started with printing.

By Michael Detras   Popularity  (38531 Views)
Biography - Michael Detras
.NET developer. Interested in WPF, Silverlight, and XNA.
My blog