Printing in WPF

Writing code for printing has a bad reputation. WPF’s support for is a bit sketchy, but a basic task like printing out a multipage document actually isn’t that hard, once you know what to do. It turns out the ‘knowing what to do’ bit isn’t that easy, since it took a lot of googling and just plain experimentation to finally get it to work.

In this post I’ll describe a demo application that allows the user to create a number of separate pages, and then to add TextBoxes to each page. Text can be typed into each TextBox, and then the whole set of pages can be sent to the printer.

Although there is a complicated way of producing printout using classes from Microsoft’s XPS (XML Paper Specification) library, you don’t actually need to use this method. The technique I’ll cover here is considerably simpler.

First, I’ll describe the demo application. It consists of a simple interface, with an A4-sized Canvas on the left (for non-European readers, A4 is the standard paper size in the UK, and is 210 mm x 297 mm, or roughly 8.3 x 11.7 inches). On the right are a control that lets you nagivate between pages, and button that inserts a new page at the current location, and a button that opens up the print dialog. You can right-click on the Canvas to insert a TextBox at the mouse point, and then type text into the TextBox. Although there are a few interesting techniques used in the program that deal with various non-printing tasks, I won’t cover them here since I want to concentrate on the printing. (You can download a working project using the link at the bottom of this post.)

In order to print a multipage document, you need to write your own version of the DocumentPaginator class (in the System.Windows.Documents namespace). This is an abstract class that contains several methods and properties that you’ll need to override and fill in to produce printed output. Your overridden DocumentPaginator’s main purpose is to provide the graphics required for each page that is to be printed. The technique I’ll use here involves creating a Canvas onto which you do all your drawing, and then returning a DocumentPage (another class in System.Windows.Documents) that is created from the Canvas. So, without further ado, here’s my version of a DocumentPaginator:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;

namespace PrintingDemo
{
  class Paginator : DocumentPaginator
  {
    Document document;
    Canvas pageCanvas;
    public Paginator()
    {
      MainWindow mainWindow = (MainWindow)Application.Current.MainWindow;
      document = mainWindow.Document;
      pageCanvas = mainWindow.PageCanvas;
    }

    public override DocumentPage GetPage(int pageNumber)
    {
      Canvas printCanvas = new Canvas();
      MainWindow mainWindow = (MainWindow)Application.Current.MainWindow;
      mainWindow.Document.CurrentPageIndex = pageNumber;
      mainWindow.DrawPage(printCanvas);
      printCanvas.Measure(PageSize);
      printCanvas.Arrange(new Rect(new Point(), PageSize));
      printCanvas.UpdateLayout();
      return new DocumentPage(printCanvas);
    }

    public override bool IsPageCountValid
    {
      get { return true; }
    }

    public override int PageCount
    {
      get { return document.PageCount; }
    }

    public override System.Windows.Size PageSize
    {
      get
      {
        return new Size(pageCanvas.Width, pageCanvas.Height);
      }
      set
      {
        throw new NotImplementedException();
      }
    }

    public override IDocumentPaginatorSource Source
    {
      get { return null; }
    }
  }
}

All the methods and properties (except the constructor) are overrides of the corresponding parts of the abstract DocumentPaginator base class. I don’t claim to be an expert here, so I’m not 100% certain what all of them are used for, but some of them should be fairly obvious.

First, I’ve added a couple of local objects to the Paginator class. The Document class is the one that stores the list of pages created by the user (that is, Document is a class I wrote for this demo; it’s not part of the printing code). The Canvas is the usual WPF control, and pageCanvas is the Canvas on which the TextBoxes are drawn by the user on screen. It’s needed here, since we want the dimensions of the printed output to be the same as those of the on-screen Canvas, which is set up to be A4-sized. Since the Canvas is declared in the MainWindow class (it’s actually declared in the XAML, but that’s irrelevant here really), we need to access it. I suppose this isn’t exactly the best class design, but I’m trying to get to printing as quickly as possible, so it’ll do. In any case, the Paginator constructor simply accesses the MainWindow to get these two objects.

Now we can have a look at the overridden methods and properties. We’ll look at the properties first, since the GetPage() method is the meat of the class and is where most of the work is done, so we’ll get the easy stuff out of the way first.

If (as in this demo) you know the number of pages in your document, you can just return this as the PageCount property. In this demo, the Document class contains a property called PageCount which gives the number of pages stored in it, so we can just return that. In more complex cases, such as a long text document, you may have to work out the number of pages by calculating how much text you can put on each page, allowing for headers, footers, margins and so forth. That could be an onerous task, so we won’t look into it here.

The PageSize property is just what it says, and again we just return the size of the Canvas used for on-screen drawing. If you want to specify this size from scratch, remember that graphics in WPF uses the device-independent pixel as its basic unit, and one pixel is exactly 1/96th of an inch. As an example, the A4 canvas comes out to a size of 210 mm = 793.7 pixels wide and 297 mm = 1122.5 pixels high. The ‘set’ part of this property is required in the override, but again it seems never to be called so I haven’t implemented it.

I have to be honest and say that I don’t really know what the IsPageCountValid and Source properties do, but for this simple example, returning ‘true’ for the former and ‘null’ for the latter seems to work well, so we’ll not try to fix something that seems not to be broken.

Finally, we consider the GetPage() method. This method takes a zero-based page number as its argument, and its responsibility is to generate a DocumentPage that contains the graphics that is to be printed for that page number. Obviously the code you put in here depends on what you’re trying to print, but there are a few things you always need to do, no matter what your graphics are.

The approach I’ve taken here is to create a local Canvas on which the current page will be drawn. Since the on-screen interface of the program shows only one page at a time, there is a DrawPage() method in MainWindow that draws the current page using the data stored in the Document object. Document doesn’t store any actual graphical controls; rather it stores a list of data objects that specify what text is to be placed in each TextBox, and where the TextBox should be drawn. The code is this:

    public void DrawPage(Canvas canvas)
    {
      canvas.Children.Clear();
      Page page = document.CurrentPage;
      foreach (TextArea textArea in page.TextBoxList)
      {
        TextBox textBox = new TextBox();
        textBox.Style = (Style)FindResource("TextBoxStyle");
        textBox.DataContext = textArea;
        Canvas.SetLeft(textBox, textArea.X);
        Canvas.SetTop(textBox, textArea.Y);
        canvas.Children.Add(textBox);
      }
    }

The same Canvas is used to draw each page, so its Children collection is first cleared to make room for the new page about to be drawn. We then retrieve the current Page from the Document (again, Page is a class I wrote for this demo). The Page class contains a list of TextArea (another class I wrote, which stores the text and location for each TextBox) objects. For each TextArea, we create a TextBox. I’ve defined a WPF Style in the XAML, and used data binding to connect the Text property of the TextBox to the text stored in the TextArea object. Again, all this stuff is peripheral to the printing, but see the source code if you’re interested in the details.

Each TextBox is then positioned on the Canvas and added as a child of that Canvas. It is this method that is used to produce the graphical representation of the data stored in a Page object, and it is worth noting that it is the same code that is used to produce both the on-screen graphics and the graphics that are sent to the printer. This means that WYSIWYG is pretty well guaranteed.

Back in the GetPage() method in the Paginator code, you’ll notice four lines after the call to DrawPage(). These lines are the essential bit for all GetPage() methods. You’ll need to call Measure() first, and pass it the size of the Canvas. Then you’ll need to call Arrange(), and pass it a Rect whose arguments are an upper left coordinate of (0,0) (which is produced by a Point object by default), and again the Canvas size. Finally, you need to call UpdateLayout(). If you leave out the calls to Measure() and Arrange(), the TextBoxes won’t appear at all; if you omit UpdateLayout(), the TextBoxes will appear, but the text inside them won’t.

Finally, you create a DocumentPage object and pass it the Canvas. This completes the creation of the graphics for the printout.

OK, so how you do the actual printing? If all you want to do is print out all the pages in the document, this is quite easy. If you want to specify a range of pages from within the document, this is a bit trickier and we’ll get to that later.

To print all the pages, we’ll look at the code in the event handler for the Print button:

    private void printButton_Click(object sender, RoutedEventArgs e)
    {
      PrintDialog printDialog = new PrintDialog();
      if (printDialog.ShowDialog() == true)
      {
        DocumentPaginator paginator = new Paginator();
        printDialog.PrintDocument(paginator, "Print demo");
      }
    }

We create and display a PrintDialog. If the user presses ‘Print’ in the dialog, the dialog returns ‘true’, and we then simply create a Paginator object and pass it to the PrintDialog’s PrintDocument() method. And that’s it.

One final note. If you’re worried about wasting reams of paper testing your printing code, you should be able to see the results on-screen without having to actually print anything. If you’ve installed .NET (which you have to do to use Visual Studio anyway), you should notice the Microsoft XPS Document Writer as one of the available printers in the PrintDialog. If you select that, your output will be saved to an XPS file, which you can then look at with the XPS Viewer (again, this should have been installed when you installed Visual Studio) which should open if you double-click on the XPS file. Another possibility, if you have Office 2010 installed, is that OneNote allows you to send printer output to it, so again you can see it without wasting any paper. Some other programs may have readers for printer output, so check what shows up in your printer list in the PrintDialog.

Get the source code here.

Advertisements
Post a comment or leave a trackback: Trackback URL.

Trackbacks

  • By WPF Context Menus « Programming tutorials on June 16, 2012 at 5:36 PM

    […] bar at the top. As an example, we’ll examine the code we used for the printing demo in the last post, and in the process add a feature to it that illustrates some of the properties of context […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: