11: Printing

This entry is part 11 of a 12-part series on WPF 3D.

Simple Printing

I've said it before and I'll say it again now:  It's just incredibly cool that WPF's integration of 3D is so thorough that printing is supported.

Hardware accelerated 3D graphics APIs (like OpenGL and Direct3D) are all about screens, not paper.  If you're building a 3D graphics app, these APIs are great.  They not only make things a bit easier, but they offer a big performance boost as well.  But then if you want your app to be able to print, the API disappears and you're left all alone.

In contrast, WPF makes this relatively simple.  Printing a Viewport3D in WPF looks like this:

PrintDialog dlg = new PrintDialog();
if ((bool)dlg.ShowDialog().GetValueOrDefault())
{
    dlg.PrintVisual(myViewport3D, "Sawdust");
}

Five lines.  Were it not for my pathological need to use braces on every if statement, it would be three lines.

XPS

As cool as it is to be able to print so easily, the code above is rather simplistic.  Most applications will want to offer their users more.

My current printing code for Sawdust is focused on XPS, which is printer-friendly document file format, conceptually similar to PDF.  After constructing an XPS file, I can spool it to the printer or save it to disk so the user may view, print or archive it like a PDF file.  More people use PDF, but for a WPF 3D app, XPS is much easier to generate.  Start by creating a FixedDocument.

FixedDocument

In the August 2007 issue of MSDN Magazine, Markus Egger has a nice overview of WPF's FlowDocument class.  FlowDocument is designed for viewing document-oriented content on a screen.  Its sister class is FixedDocument, which is designed for putting document-oriented content on paper.

As you might expect, a FixedDocument is simply a collection of pages:

public static void CreateFixedDocument()
{
    FixedDocument doc = new FixedDocument();
    doc.DocumentPaginator.PageSize = new Size(96 * 8.5, 96 * 11);

    foreach (page that I want)
    {
        PageContent page = new PageContent();
        FixedPage fixedPage = CreateOneFixedPage();
        ((IAddChild)page).AddChild(fixedPage);

        doc.Pages.Add(page);
    }
    return doc;
}

The IAddChild cast is rather funky.  Microsoft's own sample code shows this technique, but their documentation for IAddChild says: "This member supports the Microsoft .NET Framework version 3.0 infrastructure and is not intended to be used directly from your code."  Hmmph.

The real work of creating aFixedDocument is in creating each page.  A FixedPage is a UIElement that acts somewhat like a Canvas.  To layout your page, you simply add a bunch of things to it, each with a specific size and position.

First, create a FixedPage:

FixedPage page = new FixedPage();
page.Background = Brushes.White;
page.Width = 96 * 8.5;
page.Height = 96 * 11;

Now let's add a big title at the top of the page:

TextBlock tbTitle = new TextBlock();
tbTitle.Text = "My Page Title";
tbTitle.FontSize = 24;
tbTitle.FontFamily = new FontFamily("Arial");
FixedPage.SetLeft(tbTitle, 96 * 0.75); // left margin
FixedPage.SetTop(tbTitle, 96 * 0.75); // top margin
page.Children.Add((UIElement)tbTitle);

Now we add our Viewport3D to the page.  We want it to be 2 inches from the top of the sheet and 2 inches from the left side.  We'll assume we have already prepared a Viewport3D which is the proper size to fit.  I think I'll draw a thin black border around it as well.

Border b = new Border();
b.BorderThickness = new Thickness(1);
b.BorderBrush = Brushes.Black;
b.Child = myViewport3D;
FixedPage.SetLeft(b, 96 * 2);
FixedPage.SetTop(b, 96 * 2);
page.Children.Add((UIElement)b);

Once you've constructed all the elements on your page, you need to call Measure(), Arrange() and UpdateLayout() to get it ready for drawing:

Size sz = new Size(96 * 8.5, 96 * 11);
page.Measure(sz);
page.Arrange(new Rect(new Point(), sz));
page.UpdateLayout();

From FixedDocument to XPS

Once we have constructed a FixedDocument object in memory, what do we do with it?  Writing it out to an XPS file is pretty simple:

FixedDocument doc = CreateFixedDocument();
XpsDocument xpsd = new XpsDocument(filename, FileAccess.ReadWrite);
XpsDocumentWriter xw = XpsDocument.CreateXpsDocumentWriter(xpsd);
xw.Write(doc);
xpsd.Close();

Viewing an XPS Document

For viewing XPS files, I recommend you download the XPS Essentials Pack from Microsoft.  It contains a nice viewer application.

For WPF applications, XPS is the way to implement any serious printing capabilities.  I'm no expert with XPS yet, but so far I'm quite pleased with the results I'm getting.