WPF provides powerful printing support, allowing you to use the majority of its graphical features in print as well as on-screen. Dynamic features such as animation and event handling don’t translate to static paper output, of course, but you can use any stationary graphics. You can also use framework services such as data binding and layout to construct output for printing.
For many years, Windows has used a print spool system based on the Win32 Enhanced Meta File (EMF) format. This does not support the full WPF rendering feature set, so a new format has been introduced: XPS, the XML Paper Specification. This enables WPF-based graphics to be sent for printing without any loss of fidelity.
XPS is a fixed-layout page description format, and not only is it the basis for WPF print spooling, but also you can use it as a standalone file format. For example, you might build an XPS file so that you can email it to someone as a preview of what the printed output will look like.
Whether your WPF application is sending output directly to the printer or creating an XPS file containing the output, you will use the XPS APIs. So, in order to look at printing in WPF, we must begin by looking at XPS.
XPS is an open specification[104] for a file format designed to hold printable output from a WPF application. As the name suggests, an XPS file describes exactly how the document should look on paper.
The XPS format has been designed to be easy to create and consume. It builds on two very widely supported standards: the ZIP file format and XML. There are a few binary elements of an XPS file, such as embedded fonts and bitmaps, but these also use common standards—OpenType and TrueType for fonts, and TIFF, PNG, JPEG, and WMP for bitmaps.
The way XPS uses ZIP and XML is very similar to the XML-based file formats introduced in Office 2007. This is no coincidence. All of these files share a common system, described by the Open Packaging Conventions.
The Open Packaging Conventions (OPC) specification is part of the Office Open XML suite of standards. These standards are owned by ECMA.[105] OPC describes a consistent scheme for packaging multiple streams into a single ZIP file, and a standard mechanism by which one stream can refer to the contents of another stream. For example, in XPS, each page is represented by a stream of XML. If the page uses a bitmap, the bitmap would be stored in a separate stream, and the page XML would point to that stream with a relative URL. As well as defining the means by which one stream refers to another, the packaging conventions also define mechanisms for incorporating common file properties (title, creator, etc.), thumbnail images, and digital signatures.
OPC makes it very easy to inspect the contents of a file manually. If you change the extension of an XPS file to .zip, you can then extract its contents as you would any normal ZIP file. Although this offers a very useful way to learn about an OPC-based format by inspecting example files, it’s important not to presume too much about the exact structure of the file. Even though you might observe common patterns, you often cannot count on these. For example, XPS files often contain a FixedDocSeq.fdseq stream in the root of the package, which typically defines the overall structure of the document. Similarly, Word files typically contain a document.xml stream in the word subdirectory in which you will find the document contents. However, you should not rely on these parts always having these names, as the names are of no significance. Parts are always located by relationships or references embedded within other parts. Only one well-known part will always be present: a stream called _rels/.rel. Applications start by opening this, and work out where to go next from there. Example 15-1 shows the contents of this stream from an XPS file. (A couple of lines have been split because they are too long to fit across the page.)
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"> <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties"
Target="docProps/core.xml"
/> <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail"
Target="docProps/thumbnail.jpeg"
/> <Relationship Id="rId1" Type="http://schemas.microsoft.com/xps/2005/06/fixedrepresentation
"Target="FixedDocSeq.fdseq"
/> </Relationships>
This shows where to locate three parts of the document. Each is
identified with a fixed URI. To find the thumbnail image for this
file, for example, an application would open this well-known _rels/.relstream, locate the Relationship
element with the relevant
Type
URI, and then open the stream
to which it refers with its Target
property.
So, although you can learn a lot about the structure of an OPC-based file by looking inside it, you should never rely on the parts having particular names or locations. You should always use relationships to locate the parts. Moreover, if you’re using XPS, this issue will typically be dealt with for you, because WPF will use relationships and locate parts on your behalf if you use the XPS-specific classes it provides.
The classes for working with XPS documents are spread across several namespaces, because there are several different levels at which you may wish to work. Table 15-1 shows the various namespaces that contain XPS functionality, and the scenarios for which they are intended.
Namespace | Purpose of classes |
System.Windows.Documents | Contains classes that represent the logical internal structure of XPS documents, and which also add extra runtime functionality for constructing documents |
System.Windows.Xps | Abstract API for creating XPS documents, for either printing or writing to disk |
System.Windows.Xps.Serialization | Provides fine-grained control of how the XPS file’s contents are generated |
System.Windows.Xps.Packaging | Provides access to package-level aspects of XPS, such as loading and saving XPS documents, or adding thumbnail images |
System.IO.Packaging | Allows reading and writing of any OPC file (not limited to XPS) |
XPS file structure is most directly represented by the classes in
the System.Windows.Xps.Packaging
namespace. If you want to work directly with the streams of data that
make up an XPS file, these classes will give you the most control.
However, they are not all that convenient to use because of their
low-level nature. There is an even lower level beneath the XPS packaging
classes: the System.IO.Packaging
namespace implements OPC. This is the foundation on which the XPS
packaging classes are built. Although you could use the lowest-level
classes to open an XPS file or any other OPC file, they don’t give you
any more control—they merely take more work. In practice, the
XPS-specific packaging classes are as low-level as you need. We mention
System.IO.Packaging
here because the
System.Windows.Xps.Packaging
classes
use it—some types from the lower-level packaging API crop up in the XPS
packaging API.
The classes in System.Windows.Documents
also reflect the
physical structure of the file, but add higher-level WPF services such
as data binding and layout. So, we will start by looking at these
higher-level classes.
The basic structure of an XPS file is very simple: a single XPS
file contains one or more documents, and each document consists of one
or more pages. (In printing terms, you could think of an XPS file as
analogous to a print job containing one or more documents.) The System.Windows.Documents
namespace represents
this structure with three classes. FixedDocumentSequence
represents the set of
documents in the file. This contains a collection of FixedDocument
objects, one for each document.
These in turn contain a collection of FixedPage
objects, one for each page.
All of the XPS document structure classes begin with Fixed
to make it clear that the formatting
of any XPS document is frozen. This distinguishes these classes from
the flow document classes, which are also in the System.Windows.Documents
namespace.
If you unzip an XPS file, its physical structure will typically
embody this logical structure. For example, Figure 15-1 shows the parts in an XPS file
that correspond to the classes just discussed. (The file we dissected
here happens to be the XPS 1.0 specification itself—it is distributed as
an XPS file.) In the root is a file corresponding to the FixedDocumentSequence
: FixedDocSeq.fdseq, in this case. (Remember,
this root file may not always have this name. The reliable way to find
it is to follow the link in the _rels/.rel file.) The root also contains a
Documents folder, with a
subdirectory for each document in the sequence.
This particular file contains just one document, so there is a
single subdirectory, 1. This
contains a FixedDoc.fdoc file,
which would be represented by a corresponding FixedDocument
object if you loaded the XPS
file into memory. The directory also has a Pages subfolder. This contains a series of
.fpage files, each corresponding to
a FixedPage
object. Each page file
contains graphical elements describing the exact appearance of the
page.
All three levels—fixed document sequence, fixed document, and fixed page—are stored as XAML. We will now look at the classes WPF supplies for working with each of these structural levels, and the corresponding XAML in the XPS file.
Logically, a FixedDocumentSequence
is a collection of
FixedDocument
objects. Very often,
it will contain just one FixedDocument
, but the structure supports
any number. Although this logical structure is simple, the details are
slightly more involved. Example 15-2 shows the steps
required to create a FixedDocumentSequence
and add a single
FixedDocument
to it.
FixedDocumentSequence fds = new FixedDocumentSequence( ); FixedDocument doc = new FixedDocument( ); DocumentReference docReference = new DocumentReference( ); docReference.SetDocument(doc); fds.References.Add(docReference);
The slightly surprising part of this example is that we are
required to wrap the FixedDocument
in a DocumentReference
object.
Surely it would be simpler if documents could be added directly to a
collection in some property of the document sequence. However, the
XAML in Example 15-3 offers a clue to
why this would not be a good idea. This is the FixedDocSeq.fdseq
stream from an XPS
file—the serialized version of a FixedDocumentSequence
.
<FixedDocumentSequence xmlns="http://schemas.microsoft.com/xps/2005/06"> <DocumentReference Source="/Documents/1/FixedDoc.fdoc"/> </FixedDocumentSequence>
If the documents were held directly inside a property of the
fixed document sequence, they would need to appear inline in this
XAML. If this approach were used consistently throughout the file, the
fixed pages would appear inline, too—the entire structure of the
document would be stored in one huge XAML stream. This would be
unwieldy for large documents, particularly for a viewer that wants to
start displaying a document before the file is completely downloaded.
To avoid having one huge XAML stream for the whole document, each
FixedPage
and each FixedDocument
gets its own stream.
(Separation of documents is typically less critical than separation of
pages because it’s more common to have a high page count than a large
number of documents in a single file, but it’s more consistent to have
distinct streams at all levels.)
The purpose of the DocumentReference
is to provide an extra
level of indirection that enables this separation. The exact location
of the relevant XAML stream in the file is determined by a relative
URL—the Source
property of the
DocumentReference
, in this case.
The physical structure is not required to mirror the logical
structure, even though it often does in practice.
Example 15-2
showed how to create from scratch a FixedDocumentSequence
containing a FixedDocument
. You can also use these
classes to inspect an existing XPS file—Example 15-4 shows how to load
a file from disk into these objects.
FixedDocumentSequence fds; using (XpsDocument xpsDocumentFile = new XpsDocument("MyXpsDoc.xps", FileAccess.Read)) { fds = xpsDocumentFile.GetFixedDocumentSequence( ); } foreach (DocumentReference docRef in fds.References) { FixedDocument document = docRef.GetDocument(false); ...use document... }
The XpsDocument
class reads
the file and is able to return a FixedDocumentSequence
object representing
the top level of the file structure. We can then iterate through the
DocumentReference
objects in the
References
property. We call
GetDocument
, passing false
to tell WPF that we don’t require it
to reload the document from disk—WPF caches parts of the file in
memory, and if you expect the file to have changed, you can pass
true
here instead. We don’t expect
the file to have changed in between constructing the XpsDocument
and calling GetDocument
—you would normally pass true
here only if you kept the document open
for long enough that changes might have occurred. GetDocument
returns the FixedDocument
object representing the
document to which the reference points.
The FixedDocument
class
represents a single document. Logically, it consists of a sequence of
pages, but as with the FixedDocumentSequence
, an extra level of
indirection enables its children to be defined in separate streams in
the package.
Adding pages to a document is very similar to adding documents
to a document sequence. Just as each document must be wrapped in a
DocumentReference
, each page must
be wrapped in a PageContent
object,
although as Example 15-5
shows, the way you provide a PageContent
with its FixedPage
is slightly different. You need to
call the IAddChild.AddChild
method.
FixedDocument doc = new FixedDocument( ); FixedPage page1 = new FixedPage( ); PageContent page1Content = new PageContent( ); ((IAddChild)page1Content).AddChild(page1); doc.Pages.Add(page1Content); FixedPage page2 = new FixedPage( ); PageContent page2Content = new PageContent( ); ((IAddChild) page2Content).AddChild(page2); doc.Pages.Add(page2Content);
Example 15-6 shows how the FixedDocument
created in Example 15-5 looks in
XAML.
<FixedDocument xmlns="http://schemas.microsoft.com/xps/2005/06"> <PageContent Source="Pages/1.fpage" /> <PageContent Source="Pages/2.fpage" /> </FixedDocument>
Although we now have enough code to create a complete document,
it’s not very interesting because all the pages are empty. For a
document to be worth printing or viewing, the pages will need some
content. This means working with the FixedPage
class.
The XPS specification places very strict limitations on what is
allowed inside a FixedPage
. Page
content must be built from just three element types: Canvas
, Glyphs
, and Path
. Of course, Canvas
has no intrinsic appearance—it is a
layout panel. In XPS, it is used simply to group items, enabling a
single transform to be applied to a collection of objects. So, we are
left with just two elements for describing the page content: Glyphs
, the lowest-level mechanism in WPF
for presenting text, and Path
, the
general-purpose shape element.
At first glance, the FixedPage
class seems not to enforce these
restrictions. Example 15-7
populates a FixedPage
with a
Canvas
containing a TextBlock
. This appears to contravene the
XPS specification, which requires us to represent text with Glyphs
, not TextBlock
.
FixedPage page1 = new FixedPage( ); Canvas content1 = new Canvas( ); page1.Children.Add(content1); TextBlock text1 = new TextBlock( ); text1.Text = "Hello, world!"; text1.FontFamily = new FontFamily("Palatino Linotype"); text1.FontSize = 50; Canvas.SetLeft(text1, 100); Canvas.SetTop(text1, 200); content1.Children.Add(text1);
In fact, the FixedPage
class
fully supports the XPS specification. But instead of imposing
restrictions on the FixedPage
object’s contents, WPF meets the XPS requirements by converting page
content into the lower-level types when necessary. This conversion
occurs when you print, or when you write a FixedDocument
into an XPS file. Example 15-8 shows the results
of the conversion.
<FixedPage xmlns="http://schemas.microsoft.com/xps/2005/06" xmlns:x="http://schemas.microsoft.com/xps/2005/06/resourcedictionary-key" xml:lang="en-us" Width="816" Height="1056"> <Glyphs OriginX="100" OriginY="252.49" FontRenderingEmSize="50" FontUri="/Resources/53698881-83d0-479f-a8a4-5e5a58691f15.ODTTF" UnicodeString="Hello, world!" Fill="#FF000000" /> </FixedPage>
The TextBlock
has been
replaced by a Glyphs
element. And,
because WPF has determined that the Canvas
we added was serving no purpose
beyond positioning the text, it has omitted that entirely.
This illustrates that the conversion to XPS is a one-way trip.
WPF preserves just the information required to create the right
appearance and discards everything else. You cannot reconstruct the
original UI from the XPS file—there is no way to tell from Example 15-8 that it started
life as a TextBlock
. The output
would have looked exactly the same if we had used a Label
or a Glyphs
element in Example 15-7 to create the text. So,
if you load a generated XPS file back in, its structure will usually
not be the same as the original—only the appearance will be
preserved.
Note that although the OriginX
property matches the position we
specified with a call to Canvas.SetLeft
, the OriginY
property is slightly different from
the position we set. This is because the attached Canvas.Top
property indicates where the
top of an element’s bounding box should be,
whereas the OriginY
property
indicates the position of the text baseline for the Glyphs
.
The FixedPage
itself has a
Width
and Height
of 816 × 1056. As always in WPF,
these are in units of device-independent pixels, which are 1/96th of
an inch in size. So these dimensions correspond to 8.5 × 11 inches.
This is the default size for a FixedPage
, even if you live in a country
where, say, A4 is more popular. You are free to set any size you like,
although if you are generating XPS for the purposes of printing, you
would normally set the size to match the target media, which is the
topic of the next section.
FixedPage
allows you to
specify three sizes for your page. The Width
and Height
properties are obvious enough—these
specify the size of the paper for which the output is intended. The
other two sizes are necessary to deal with the technical limitations
of printing.
Most printers cannot print right to the edge of the paper.
Control over the paper position is somewhat approximate, so printers
always leave a blank area near the edge of the paper to ensure that
they don’t attempt to deposit ink or toner onto the rollers that
feed the paper. A FixedPage
can
designate the area of the page in which it is intending to print by
setting the ContentBox
property.
As Figure 15-2 shows,
the ContentBox
typically
identifies an area smaller than the physical page.
By default, this is just information—setting ContentBox
does not imply any particular
behavior. However, a helpful XPS viewer application could use this
to check that a document’s content fits into the printable area of
the target printer, and scale the output to fit if necessary or at
least alert users of a potential problem.
The final page size measure relates to the standard workaround
for printing to the edge of the page. If you really need ink all the
way to the edge of a page, you simply print onto a larger sheet of
paper and then trim it down. As long as the printable area of the
printer is bigger than the size to which you will be trimming, you
will be able to print across the entire area of your final output.
However, it’s not quite as simple as printing something the exact
size of the final, trimmed page—if you did that, even the slightest
paper misalignment during the trimming process would result in an
annoying white gap at the edge of the page. The standard solution to
this is to print a slightly larger image than is required. This is
referred to as bleed—the
content bleeds out of the page area and into the area destined to be
trimmed. Often, the bleed area also contains marks used in the print
production process, such as crop lines and registration marks. The
BleedBox
indicates how large this
area is, as Figure 15-3 shows.
If the bleed area is being used only to hold production features such as printing marks, the content box may still be smaller than the physical page size, as shown in Figure 15-3. However, if the bleed box is being used in order to allow ink all the way to the edge of the page, this implies that the page is completely full, so the content box will fill the whole page.
Again, the BleedBox
is
usually just for information. But it could be important information
for a print bureau producing the finished output—it could use this
to verify that everything is set up correctly before committing to a
print run.
It is convenient to be able to add any WPF content to a
FixedPage
and have it
automatically converted to the element types supported in XPS.
However, this conversion process has its limitations. WPF offers a
couple of features that cannot be represented directly with the
limited repertoire available in an XPS file’s FixedPage
: 3D and bitmap
effects.
You can use these features in a FixedPage
, but if you print the page or
save it to an XPS file, then all 3D content, and all elements that
use the BitmapEffect
property,
will be converted into bitmaps. They will be represented in the
FixedPage
as a Path
object with a rectangular shape and a
bitmap fill. This does not guarantee perfect fidelity—the image
quality for such features is only as good as the resolution of the
generated bitmap.
If you want to print content that uses effects such as drop shadows, you might be better off trying to approximate the effects you require using opacity masks and gradient brushes. Although these are not as convenient as the bitmap effects, they can be represented in an XPS file without any loss of fidelity.
The Glyphs
element in Example 15-8 indicates the
font through its FontUri
property. This does not name the font—instead it refers to a font
file embedded in the XPS file. Fonts, bitmaps, thumbnails, color
profiles, and any other necessary resources are embedded as separate
streams in the package. These are then referred to by path. For
example, the FontUri
in Example 15-8 is:
/Resources/53698881-83d0-479f-a8a4-5e5a58691f15.ODTTF
This indicates that the XPS package contains a Resources
folder, and that this contains an embedded font file with the
specified name. In general, you do not need to worry about creating
these resources, unless you choose to work with the lower-level
System.Windows.Xps.Packaging
API.
If you print or save a fixed document using the higher-level API,
WPF adds the necessary resources for you.
It is important to be aware that WPF can automatically embed font files, or subsets of a font file. It does this to guarantee that a document can be displayed or printed correctly. However, it is your responsibility to ensure that you do not generate XPS files that contravene font licensing terms.
We have now seen all the high-level classes that represent the
structure of an XPS document. We saw in Example 15-4 how to read an
existing XPS file off the disk and into a tree of objects
representing its structure. But what about the creation process,
where we build an XPS document structure, and want to either print
it or save it to disk? For this, we must use the XpsDocumentWriter
class.
Whether you are printing, or writing your output to an XPS file,
you can use the same API: the XpsDocumentWriter
class. If you wish to print,
you can obtain one of these from the printing API, as Example 15-9 shows.
PrintDocumentImageableArea imageArea = null;
XpsDocumentWriter xpdw = PrintQueue.CreateXpsDocumentWriter(ref imageArea);
if (xpdw != null) { ...provide XpsDocumentWriter with output here... }
This will cause the standard print dialog to be shown, allowing
the user to select a printer. If the user cancels the dialog, CreateXpsDocumentWriter
returns null, but
otherwise it returns an XpsDocumentWriter
, along with an object that
describes the size of the target’s paper and the margins of the
printable area. If you wish to exercise more control over the print
dialog and printer selection, there are several variations on this
theme, described later in this chapter.
If you wish to send your output to an XPS file instead of a printer, you can obtain the document writer using the code shown in Example 15-10.
using (XpsDocument xpsFile = new XpsDocument(xpsOutputPath, FileAccess.Write)) {
XpsDocumentWriter xpdw = XpsDocument.CreateXpsDocumentWriter(xpsFile);
...provide XpsDocumentWriter with output here...}
Once you have obtained an XPS document writer, you can use the same code whether you are writing to an XPS file or to a printer. For clarity, we will use the term printing to refer to both kinds of output; unless otherwise specified, any discussion of printing in the following sections also applies to generating XPS files.
You can provide an XpsDocumentWriter
with content in many forms.
For most of the supported content types, you call a suitable overload of
the Write
method. This is a one-shot
process: once you’ve called Write
, it
will print the document straight away, and any further calls to Write
will throw an exception. The following
sections describe the various types of content accepted by a document
writer.
XpsDocumentWriter
offers
overloads of its Write
method that
accept a FixedPage
, a FixedDocument
, or a FixedDocumentSequence
. These allow you to
submit a single page, a whole document, or a job containing several
documents for printing, respectively. If you want to print more than
one fixed page you must pass either a FixedDocument
or a FixedDocumentSequence
, because you get to
call the Write
method only once.
Example 15-11 creates and
prints a FixedDocument
.
FixedDocument doc = new FixedDocument( );
// Add first page to document
FixedPage page1 = new FixedPage( );
PageContent page1Content = new PageContent( );
((IAddChild)page1Content).AddChild(page1);
doc.Pages.Add(page1Content);
// Add content to first page
Canvas content1 = new Canvas( );
page1.Children.Add(content1);
TextBlock text1 = new TextBlock( );
text1.Text = "Hello";
text1.FontSize = 50;
Canvas.SetLeft(text1, 100);
Canvas.SetTop(text1, 200);
content1.Children.Add(text1);
// Add second page to document
FixedPage page2 = new FixedPage( );
PageContent page2Content = new PageContent( );
((IAddChild) page2Content).AddChild(page2);
doc.Pages.Add(page2Content);
// Add content to second page
Canvas content2 = new Canvas( );
page2.Children.Add(content2);
TextBlock text2 = new TextBlock( );
text2.Text = "World";
text2.FontSize = 50;
Canvas.SetLeft(text2, 100);
Canvas.SetTop(text2, 200);
content2.Children.Add(text2);
// Print document
PrintDocumentImageableArea imageArea = null;
XpsDocumentWriter xpsdw = PrintQueue.CreateXpsDocumentWriter(ref imageArea);
if (xpsdw != null) {
xpsdw.Write(doc);
}
This builds a FixedDocument
with two FixedPage
s. The first
contains the text “Hello,” and the second contains the text “World”.
The user will be shown the print dialog at the point at which the code
asks PrintQueue
for the document
writer. The print job will be submitted to the print queue when it
calls Write
—when that method
returns, print spooling will already be complete, and the job will be
in the Windows print queue for the target printer.
If your FixedPage
objects use
any WPF element types other than Canvas
, Glyphs
, or Path
, the conversion to these simple element
types occurs when you call the XpsDocumentWriter.Write
method.
You do not need to create FixedDocument
or FixedPage
objects explicitly. WPF is able to
generate a fixed page automatically from any object that derives from
Visual
. Because Visual
is the base class of all WPF user
interface elements, this means you can print any element. It will even
accept elements that are already part of a visual tree, enabling you
to print a screenshot of a user interface.
Unlike the ordinary bitmap-based screenshots you get by pressing PrtScn or Alt-PrtScn in Windows, these screenshots will be resolution-independent. Also, they aren’t screenshots in the sense of being a direct copy of what’s visible on-screen—they will contain your application’s visuals and nothing else, even if your application is currently obscured by other programs.
To print a Visual
, just pass
it to the relevant overload of the Write
method. Example 15-12 shows how to print a TextBlock
element.
PrintDocumentImageableArea area = null; XpsDocumentWriter xpsdw = PrintQueue.CreateXpsDocumentWriter(ref area); if (xpsdw != null) { TextBlock myVisual = new TextBlock( ); double leftMargin = area.OriginWidth; double topMargin = area.OriginHeight; double rightMargin = area.MediaSizeWidth - area.ExtentWidth - leftMargin; double bottomMargin = area.MediaSizeHeight - area.ExtentHeight - topMargin; myVisual.Margin = new Thickness(leftMargin, topMargin, rightMargin, bottomMargin); myVisual.Text = "Hello, world";Size outputSize = new Size(area.MediaSizeWidth, area.MediaSizeHeight);
myVisual.Measure(outputSize);
myVisual.Arrange(new Rect(outputSize));
myVisual.UpdateLayout( );
xpsdw.Write(myVisual); }
This example performs layout on the TextBlock
explicitly. WPF does this
automatically for elements in a user interface, but because this
TextBlock
is not hosted in a
window, it is our responsibility to perform the layout. Otherwise, it
would not have a valid width and height, so it would be invisible when
we try to print it. If we were trying to print an element that was
already in the visual tree of a window, this layout step would not be
necessary.
Note that the margin and size of the TextBlock
have been based on the page size
information in the PrintDocumentImageableArea
returned by
PrintQueue
, ensuring that the
TextBlock
doesn’t fall into the
unprintable area around the edge of the paper. PrintDocumentImageableArea
reports the total
page size with its MediaSizeWidth
and MediaSizeHeight
properties. The
top-left position of the printable area within the page is indicated
by the OriginWidth
and OriginHeight
properties. The ExtentWidth
and ExtentHeight
properties describe the size of
the printable area.
Although basing the print margins on the physical capabilities of the printer guarantees to make full use of the available space, it means that output will vary from one printer to another. In practice, it’s common to choose fixed margins with sufficiently conservative values that the content is likely to fit with any printer’s printable region. Example 15-13 shows modifications to Example 15-12 that hardcode the margins. For some applications it may be appropriate to provide the user with a way to modify the margins; WPF doesn’t have a built-in page setup dialog,[106] so it’s up to you how to present such settings.
... TextBlock myVisual = new TextBlock( ); double leftMargin = 96; // 1 inch double topMargin = 144; // 1.5 inches double rightMargin = 96; // 1 inch double bottomMargin = 144; // 1.5 inches // Making sure that we're inside the physical // limitations of the printer Debug.Assert(leftMargin >= area.OriginWidth); Debug.Assert(topMargin >= area.OriginHeight); Debug.Assert(rightMargin <= area.OriginWidth + area.ExtentWidth); Debug.Assert(topMargin <= area.OriginHeight + area.ExtentHeight); myVisual.Margin = new Thickness(leftMargin, topMargin, rightMargin, bottomMargin); ...
The use of Debug.Assert
in
Example 15-13 is for illustration
only. In practice, there is no one correct way to respond if the
configured margins fail to place the content entirely within the
printable area. An application might show the user a warning dialog
the first time the problem occurs for a particular document.
Alternatively, it might choose to adjust the content to fit.
Because the overload of the Write
method used in Example 15-12 accepts any object that has
Visual
as a base class, we are free
to pass in whole trees of objects. For example, Grid
derives from Visual
, so you can pass any Grid
, and it will print the grid and all its
contents.
However, if you need to print multiple pages, you need to take a
slightly different approach. As with the previous examples, you are
allowed to call this overload of the Write
method only once, meaning you can
print only a single page. If you want to print multiple pages as
visuals, you need to use a different method of the XpsDocumentWriter
class: CreateVisualsCollator
.
CreateVisualsCollator
returns
a SerializerWriterCollator
object.
This offers a Write
method that
accepts a visual, just like XpsDocumentWrite.Write
. The difference is
that you can call it several times in a row, once for each page. Example 15-14 uses this technique to print 10
pages, with a TextBlock
showing
each page’s number.
PrintDocumentImageableArea area = null; XpsDocumentWriter xpsdw = PrintQueue.CreateXpsDocumentWriter(ref area); if (xpsdw != null) { SerializerWriterCollator c = xpsdw.CreateVisualsCollator( ); c.BeginBatchWrite( ); for (int i = 1; i <= 10; ++i) { TextBlock tb = new TextBlock( ); tb.Text = i.ToString( ); tb.TextAlignment = TextAlignment.Center; tb.VerticalAlignment = VerticalAlignment.Center; tb.FontSize = 500; tb.FontFamily = new FontFamily("Verdana"); tb.Margin = new Thickness(area.OriginWidth, area.OriginHeight, 0, 0); Size outputSize = new Size(area.ExtentWidth, area.ExtentHeight); tb.Measure(outputSize); tb.Arrange(new Rect(outputSize)); tb.UpdateLayout( ); c.Write(tb); } c.EndBatchWrite( ); }
We must indicate the start and end of the range of pages by
calling BeginBatchWrite
and
EndBatchWrite
so that WPF knows
when to start and complete the print spooling process. Once you have
called EndBatchWrite
, you cannot
perform any more output with either the collator or the document
writer.
Some WPF classes have an intrinsic ability to split their
content into pages. The FlowDocument
, FixedDocument
, and FixedDocumentSequence
classes all advertise
this capability by implementing the IDocumentPaginatorSource
interface. XpsDocumentWriter
can work with
self-paginating objects directly, which can make multipage printing
much simpler than the previous example. Example 15-15 shows a function that you
can use to print an instance of any type that implements IDocumentPaginatorSource
.
public static void PrintPaginatedDocument(IDocumentPaginatorSource dps) {
PrintDocumentImageableArea area = null;
XpsDocumentWriter xpsdw = PrintQueue.CreateXpsDocumentWriter(ref area);
if (xpsdw != null) {
xpsdw.Write(dps.DocumentPaginator);
}
}
If the document paginator source is a fixed document or a fixed
document sequence, Example 15-15
will generate an output page for each fixed page in the source. This
is not usefully different from just passing the fixed document or
fixed document sequence directly to the Write
overload that accepts those types, so
in practice, you will not usually print fixed documents this way.
However, if the source document is a FlowDocument
(which was described in Chapter 14), it will not have a fixed idea
of how many pages it contains. In this case, the paginator is more
useful, because you can use it to split the document into pages that
fit the target media.
You might expect the FlowDocument
pagination to be based
automatically on the target page size. In fact, this is not the case:
the paginator it returns defaults to a page size of 816 × 1056 pixels
(i.e., 8.5 × 11 inches regardless of the real paper size). You can
inspect or change the paginator’s page size using its PageSize
property. If you are writing to an
XPS file instead of printing, the generated XPS file will have pages
of this size. If you are printing, the flow document will be split
into pages of this size regardless of whether they fit onto the target
paper. If you want to be sure that the flow document’s output will fit
the available space offered by the target printer, you should use the
information returned in the PrintDocumentImageableArea
to adjust the
PageSize
property, as shown in
Example 15-16.
PrintDocumentImageableArea area = null; XpsDocumentWriter xpsdw = PrintQueue.CreateXpsDocumentWriter(ref area); if (xpsdw != null) {DocumentPaginator paginator = dps.DocumentPaginator;
paginator.PageSize = new Size(area.ExtentWidth, area.ExtentHeight);
xpsdw.Write(paginator); }
This will ensure that the flow document is broken into pages that fit into the available space, whatever the paper size may be.
There is a limitation with printing a FlowDocument
in this way: you cannot add
other features to the page, such as page numbers, without doing a
little more work. You have two options: either you can implement your
own DocumentPaginator
as a wrapper
around the one provided by the FlowDocument
, or you can call into the
provided paginator directly to retrieve the pages, and incorporate
this into the content you require to represent the whole page.
Example 15-17 shows
some code that uses the second technique to print a FlowDocument
with page numbers.
public static void PrintFlowDocWithPageNumbers(FlowDocument myFlowDoc) { PrintDocumentImageableArea area = null; XpsDocumentWriter xpsdw = PrintQueue.CreateXpsDocumentWriter(ref area); if (xpsdw != null) { IDocumentPaginatorSource dps = myFlowDoc; DocumentPaginator sourceFlowDocPaginator = dps.DocumentPaginator;const int HeaderFooterHeight = 30;
sourceFlowDocPaginator.PageSize = new Size(area.ExtentWidth,
area.ExtentHeight - 2 * HeaderFooterHeight);
if (!sourceFlowDocPaginator.IsPageCountValid) {
sourceFlowDocPaginator.ComputePageCount( );
}
FixedDocument outputFixedDoc = BuildFixedDocument(myFlowDoc, area,
sourceFlowDocPaginator, HeaderFooterHeight);
xpsdw.Write(outputFixedDoc); } }
This obtains an XpsDocumentWriter
in the usual way. It then
retrieves the document paginator for the flow document we wish to
print. Next, it sets the PageSize
on the paginator, specifying the full width of the printable page
area, but reducing the height in order to allow space for a header and
footer to be added. This ensures that the content, header, and footer
fit within the printable area of the page.
Next, we make sure the paginator has an up-to-date page count—we
changed the page size, which is likely to have changed the number of
pages required for the document. If IsPageCountValid
indicates that the page
count is no longer up-to-date, we call ComputePageCount
.
Finally, Example 15-17 builds a FixedDocument
to hold the paginated output,
combined with the header and footer, and prints it by calling the
Write
method of the XpsDocumentWriter
. The document is created
by the BuildFixedDocument
helper
method, which is shown in Example 15-18.
static FixedDocument BuildFixedDocument(FlowDocument myFlowDoc,
PrintDocumentImageableArea area, DocumentPaginator sourceFlowDocPaginator,
int headerFooterHeight) {
FixedDocument outputFixedDoc = new FixedDocument( ); for (int pageNo = 0; pageNo < sourceFlowDocPaginator.PageCount; ++pageNo) { Canvas pageCanvas = new Canvas( ); pageCanvas.Margin = new Thickness(192);AddHeaderAndFooter(myFlowDoc, area, sourceFlowDocPaginator,
headerFooterHeight, pageNo, pageCanvas);
AddPageBody(sourceFlowDocPaginator,
headerFooterHeight, pageNo, pageCanvas);
AddPageToDocument(area, outputFixedDoc, pageCanvas);
}
return outputFixedDoc;
}
This iterates through the pages provided by the paginator,
creating a Canvas
for each page.
The Canvas
will hold the page
content, and the header and footer. We set the Margin
on this canvas to ensure that it
appears within the printable area of the page. The fixed margin of 2
inches (192 device-independent pixels) all around should be enough for
any normal printer.
Next, we add the header and footer to the canvas, making sure
they appear horizontally centered, and at the appropriate vertical
position on the page. We add these with the AddHeaderAndFooter
helper function, which is
shown in Example 15-19.
static void AddHeaderAndFooter(FlowDocument myFlowDoc,
PrintDocumentImageableArea area,
DocumentPaginator sourceFlowDocPaginator,
int headerFooterHeight, int pageNo, Canvas pageCanvas) {
TextBlock header = new TextBlock( ); header.Text = "My Document"; header.FontSize = 20; header.FontWeight = FontWeights.Bold; header.TextAlignment = TextAlignment.Center; header.Width = sourceFlowDocPaginator.PageSize.Width; pageCanvas.Children.Add(header); TextBlock footer = new TextBlock( ); footer.Text += "Page " + (pageNo + 1); footer.TextAlignment = TextAlignment.Center; footer.FontFamily = myFlowDoc.FontFamily; footer.Width = sourceFlowDocPaginator.PageSize.Width; Canvas.SetTop(footer, area.ExtentHeight - headerFooterHeight); pageCanvas.Children.Add(footer);}
Finally, we need to incorporate the page content from the flow
document. WPF offers a special element type, DocumentPageView
, for hosting content from a
particular page of a paginated document, which is used by the AddPageBody
helper function shown in Example 15-20.
static void AddPageBody(DocumentPaginator sourceFlowDocPaginator,
int headerFooterHeight, int pageNo, Canvas pageCanvas) {
DocumentPageView dpv = new DocumentPageView( ); dpv.DocumentPaginator = sourceFlowDocPaginator; dpv.PageNumber = pageNo; Canvas.SetTop(dpv, headerFooterHeight); pageCanvas.Children.Add(dpv);}
The Canvas
defining the
appearance of our page is now complete. The last thing the loop in
Example 15-18 does is to
create a FixedPage
to host the
Canvas
, wrap it in a PageContent
, and add that to the FixedDocument
that will be returned to Example 15-17. This is
accomplished by the AddPageToDocument
helper shown in Example 15-21.
static void AddPageToDocument(PrintDocumentImageableArea area,
FixedDocument outputFixedDoc, Canvas pageCanvas) {
FixedPage fp = new FixedPage( ); fp.Width = area.MediaSizeWidth; fp.Height = area.MediaSizeHeight; fp.Children.Add(pageCanvas); PageContent pc = new PageContent( ); ((IAddChild) pc).AddChild(fp); outputFixedDoc.Pages.Add(pc);}
You can use the code in Example 15-17, along with its various helper functions, to print any flow document. It will add a header to each page with the text “My Document,” and a footer showing the page number.
Printing a large document can be a slow process. The Write
methods shown in the previous sections
are synchronous (i.e., they do not return until the document has been
completely spooled out to the print queue). This can be bad news for
the user—if you print from the main thread, the application will
become unresponsive until printing finishes. Fortunately, WPF offers
asynchronous versions of all of these Write
methods. Each overload of Write
has a corresponding WriteAsync
method.
The WriteAsync
methods return
immediately. Printing will then progress during idle time. However,
because WPF will be using the objects you passed in, you should make
sure you don’t attempt to change them until printing is complete. The
simplest way to ensure that is to create objects especially for
printing. For example, build a FixedDocument
that will be passed to an XPS
document writer’s WriteAsync
method, and which will never be used for anything else. (This rule
also applies to each FixedPage
object and all the objects that make up the page content.)
The XpsDocumentWriter
will
periodically raise the WritingProgressChanged
event to tell you how
far along it is. It will raise its WritingCompleted
event once printing has
completed spooling. The document may still be in the print queue at
this point, so this event doesn’t necessarily mean the document is
sitting on the printer awaiting collection; it just means that the
application has finished generating the printer output—the application
could exit without losing the output. Example 15-22 shows a version of Example 15-17 modified to use
asynchronous printing.
void PrintPaginatedDocument(IDocumentPaginatorSource dps) { PrintDocumentImageableArea area = null; XpsDocumentWriter xpsdw = PrintQueue.CreateXpsDocumentWriter(ref area); xpsdw.WritingCompleted += OnPrinted; if (xpsdw != null) { xpsdw.WriteAsync(dps.DocumentPaginator); } } void OnPrinted(object sender, WritingCompletedEventArgs e) { MessageBox.Show("Printing completed!"); }
Sometimes users need to cancel print jobs. They can, of course,
do this via the printing user interface in Windows. Alternatively,
your application could call the CancelAsync
method on the XpsDocumentWriter
anytime before it signals
completion. If the document is cancelled, the WritingCancelled
event will be raised
instead of the WritingCompleted
event.
Everything in the preceding sections applies equally to generating XPS files and sending output to a printer. However, some tasks are specific to XPS file generation, and some apply only in printing scenarios. The next section describes parts of the .NET 3.0 API that are concerned solely with XPS file generation.
As we’ve seen, you can use the same code path to generate both
printed output and XPS files by using the XpsDocumentWriter
class. However, you can
build some extra features into an XPS file that would not be useful for
print output, but which can enhance its usefulness as a standalone file.
Indeed, unless you plan to exploit some of these features, there’s
probably not much point in adding a “save as XPS” feature to your
application—Windows automatically offers basic XPS generation to any
application that can print.
Any machine with either .NET 3.0 or the Microsoft XPS Essentials Pack[107] installed will have a “Microsoft XPS Document Writer” printer. Windows Vista has .NET 3.0 built in. You can install .NET 3.0 on Windows XP or Windows Server 2003. You can install the XPS Essentials Pack on these systems, and on Windows 2000 machines.
When an application “prints” to this printer, an XPS file is created. At the start of the print process, a dialog opens asking the user where he’d like to save the XPS file, and once printing is complete, the XPS viewer application will run, displaying the results.
This is very useful because it means that any print-enabled application can generate an XPS file. However, it is possible for applications to build a richer, more useful XPS file than this pseudoprinter can. An application knows things about its documents that the print system cannot, and can use this to enhance the XPS file. For example, XPS files can contain hyperlinks to allow easy navigation within documents. An application may also wish to control aspects of the file creation process, such as whether the compression strategy favors speed of decoding, or minimizing the file size.
The XpsDocumentWriter
class
is convenient in that it lets us use functionality not directly
supported by the XPS file format itself. As we’ve seen, if we use
non-XPS elements in a FixedPage
, or
we choose to work with Visual
objects directly, the document writer will convert these objects to
glyphs and paths for us. However, there is a price to pay: XpsDocumentWriter
does not give you complete
control over all XPS file features.
WPF provides a lower-level API that lets you work directly with
the streams of an XPS file, giving you full control over all of the
features, while still providing XPS-specific features that you don’t
get from the low-level System.IO.Packaging
classes, such as an
intrinsic understanding of the parts and relationships required in an
XPS file. Of course, the downside is that you have to do more work to
build up a complete document from scratch. Fortunately, in a lot of
cases, you can use both techniques: you can build the basic document
using the higher-level API, and then use the lower-level API on the
same file to add the XPS features you require.
The package-level API is defined in the System.Windows.Xps.Packaging
namespace.
Table 15-2 shows the types it
provides for dealing with the core XPS structural elements, and the
nearest equivalents in the high-level API.
Document structures | Type | System.Windows.Documents equivalent |
XPS file | XpsDocument | None |
Document structures |
| FixedDocumentSequence |
Document |
| FixedDocument |
Page |
| FixedPage |
Working with the package-level API requires a procedural style. Instead of building up a data structure in memory representing the documents and their pages, you issue a series of instructions in strict order, describing which things to write out. Example 15-23 shows how to create a document from scratch at this level.
using (XpsDocument xpsDoc = new XpsDocument("Out.xps", FileAccess.Write)) { IXpsFixedDocumentSequenceWriter sequenceWriter = xpsDoc.AddFixedDocumentSequence( ); IXpsFixedDocumentWriter docWriter = sequenceWriter.AddFixedDocument( ); IXpsFixedPageWriter pageWriter = docWriter.AddFixedPage( ); XmlWriter pageXml = pageWriter.XmlWriter; pageXml.WriteStartElement("FixedPage"); pageXml.WriteAttributeString("xmlns", "http://schemas.microsoft.com/xps/2005/06"); pageXml.WriteAttributeString("Width", "793.7"); pageXml.WriteAttributeString("Height", "1122.5"); pageXml.WriteAttributeString("xml:lang", "en-GB"); pageXml.WriteStartElement("Path"); pageXml.WriteAttributeString("Data", "M 10,550 L 396,164 782,550 396,936 z"); pageXml.WriteAttributeString("Fill", "#ffff0000"); pageXml.WriteEndElement( ); pageXml.WriteEndElement( ); pageWriter.Commit( ); docWriter.Commit( ); sequenceWriter.Commit( ); }
Figure 15-4 shows how this generated page looks in the XPS viewer.
As you can see, you need to generate the raw XML for the fixed
page content by hand if you are working at this level, and it is your
responsibility to ensure that it conforms to the requirements laid out
in the XPS specification. You cannot use the FixedPage
class or any WPF elements with
these APIs. Life gets even more complex if you wish to use text, as
you need to generate embedded font resources. This is why it’s often
preferable to use a hybrid approach. Example 15-24 shows how to generate the basic
output with XpsDocumentWriter
, and
then use the package-level API to set a file property.
using (Package xpsPackage = Package.Open("Out.xps", FileMode.Create,
FileAccess.ReadWrite))
using (XpsDocument doc = new XpsDocument(xpsPackage)) {
FixedPage page = new FixedPage( );
Path p = new Path( );
p.Data = StreamGeometry.Parse("M 10,550 L 396,164 782,550 396,936 z");
p.Fill = Brushes.Red;
page.Children.Add(p);1">
TextBlock text = new TextBlock( );
text.Text = "XPS Output";
text.FontSize = 150;
page.Children.Add(text);
XpsDocumentWriter dw = XpsDocument.CreateXpsDocumentWriter(doc);
dw.Write(page);
doc.CoreDocumentProperties.Description = "Some text and a red square";
}
There are some limitations to this technique, because you are
leaving certain parts of the XPS file generation to the XpsDocumentWriter
. For example, if you want
fine control over the generated XML, you will have to stick to the
lower-level technique. However, most of the techniques described in
the following sections can use this hybrid approach, where the bulk of
the XPS file is generated with XpsDocumentWriter
, and then a few details
are edited with the System.Windows.Xps.Packaging
API.
OPC defines a representation for a set of document properties. These enable documents to supply common properties such as title, description, and keywords in a standard way. These properties are embedded in an XML stream in the package, with one element per property.
Several of the supported properties are defined by the Dublin Core Metadata Initiative,[108] a standard that defines how to represent certain common types of metadata in markup. There are also a number of properties not found in the Dublin Core, but which are common to all OPC-based file formats, including XPS and all of the Office Open XML file formats. Table 15-3 shows all the properties, indicating which are shared with the Dublin Core.
Property | Usage | In Dublin Core |
Category | Categorization of document | No |
ContentStatus | Status of document (e.g., “Draft”) | No |
ContentType | Type of content, such as “Whitepaper” (note: this is not a MIME content type) | No |
Created | Creation date | Yes |
Creator | Entity primarily responsible for creating document | Yes |
Description | Short description of the content | Yes |
Identifier | An unambiguous reference to the document within a given context | Yes |
Keywords | Set of keywords to facilitate searching | No |
Language | Language in which document is written | Yes |
LastModifiedBy | User who last modified the document | No |
LastPrinted | Date on which the document was last printed | No |
Modified | Date of last change | Yes |
Revision | Revision number | No |
Subject | Topic of document | Yes |
Title | Name of document | Yes |
Version | Version number | No |
You’re not obliged to set any of these properties—set just those
that make sense for your application. You do so using the XpsDocument
class’s CoreDocumentProperties
property. This
contains an instance of the PackageProperties
type that defines CLR
properties corresponding to each available core document property.
Example 15-25 shows how to use
this.
PackageProperties props = xpsOutput.CoreDocumentProperties; props.Creator = "Ian Griffiths"; props.Title = "XPS Output"; props.Subject = "Example XPS output document"; props.Description = "XPS document generated by example from " + "the book 'Programming WPF' published by O'Reilly"; props.Category = "Demo output"; props.ContentStatus = "Final"; props.Keywords = "XPS; Demo; WPF";
Once you have set these properties, Windows Explorer is able to extract them, because it knows how to retrieve core properties from XPS files. Figure 15-5 shows how this looks. The labels Windows uses do not always correspond to the underlying property names. This is because Windows has some long-standing conventions for property names that predate the adoption of the Dublin Core properties by the OPC.
Windows Explorer allows the user to edit metadata on certain file formats. (Applications can register handlers to enable this editing. There is a built-in handler for XPS files.) You can see this in Figure 15-5: look at the “Content type” entry. Our code did not set this property, so the shell has shown an “Add text” prompt. If the user moves the mouse over this or any of the other editable properties, the value will take on a text-box-like appearance to let the user know she can edit the value. If she edits or adds values, a Save button appears, allowing her to commit the changes to disk. So be aware that the properties you write into an XPS file at creation time are not necessarily the properties that it will have for its lifetime.
OPC defines a way to embed thumbnail bitmap images. A thumbnail is a small bitmap containing a
preview of the document. Windows Vista can use this as the document’s
icon in Explorer. Thumbnails enable a rough image of a document to be
presented quickly, because they require very little processing—loading
and rendering a small bitmap is likely to be faster than loading and
rendering a FixedPage
for all but
the most trivial of documents.
The most common style of thumbnail is a small view of the first page of the document (or if the XPS file contains multiple fixed documents, the first page of the first document). XPS allows thumbnails to be provided for each page, but the default viewer does not use these, and neither the XPS Print Driver nor Microsoft Office’s XPS export feature generates them. In practice, a single thumbnail for the file should suffice.
To set the file’s thumbnail, you just call the XpsDocument
object’s AddThumbnail
method, passing an XpsImageType
value to specify the image
type. Although the XpsImageType
enumeration lists four bitmap types, the specification requires
thumbnails to be in either JPEG or PNG format, so you should not use
the other types here—the enumeration includes the TIFF and WDP types
because those are supported in other parts of an XPS document.
AddThumbnail
returns an object of
type XpsThumbnail
, which provides a
method called GetStream
. We then
write the bitmap into that stream. It is our job to provide the raw
JPEG or PNG byte stream. Example 15-26 shows how to
generate a suitable bitmap and add it as a thumbnail.
void AddThumbnailToXpsDocument(XpsDocument xpsOutput) {
Size thumbnailSize = new Size(256, 256);
Visual thumbnailVisual = CreateThumbnailVisual(thumbnailSize);
XpsThumbnail docThumbnail = xpsOutput.AddThumbnail(XpsImageType.JpegImageType);
WriteVisualAsThumbnail(thumbnailSize, thumbnailVisual, docThumbnail);
}
Visual CreateThumbnailVisual(Size thumbnailSize) {
Grid myThumbnail = new Grid( );
myThumbnail.Background = new LinearGradientBrush(
Colors.LightBlue, Colors.White, 90);
TextBlock thumbText = new TextBlock( );
thumbText.FontSize = 50;
thumbText.Text = "My XPS Document";
thumbText.TextAlignment = TextAlignment.Center;
thumbText.VerticalAlignment = VerticalAlignment.Center;
thumbText.TextWrapping = TextWrapping.Wrap;
myThumbnail.Children.Add(thumbText);
myThumbnail.Measure(thumbnailSize);
myThumbnail.Arrange(new Rect(new Point( ), thumbnailSize));
return myThumbnail;
}
void WriteVisualAsThumbnail(Size thumbnailSize, Visual thumbnailVisual,
XpsThumbnail thumbnail) {
RenderTargetBitmap bmp = new RenderTargetBitmap(
(int) thumbnailSize.Width, (int) thumbnailSize.Height,
96, 96, PixelFormats.Default);
bmp.Render(thumbnailVisual);
JpegBitmapEncoder encoder = new JpegBitmapEncoder( );
encoder.Frames.Add(BitmapFrame.Create(bmp));
encoder.Save(thumbnail.GetStream( )
);
}
Most of the code in this example is there to generate a JPEG
bitmap. Only two lines of code are XPS-specific: the call to XpsDocument.AddThumbnail
near the top, and
the call to GetStream
near the
bottom.
This example generates a 256 × 256-pixel bitmap. The XPS specification doesn’t mandate any particular size, beyond saying that it should be “small.” However, Explorer in Windows Vista expects high-resolution icons to be 256 × 256, and because it is able to extract the thumbnail from an XPS and show it, 256 × 256 seems like a sensible default for an XPS thumbnail. Figure 15-6 shows how Windows Explorer displays the thumbnail created by Example 15-26 in its Large Icons view.
XPS documents can contain hyperlinks. These may be either
internal links within the document or links to external sites. If you
are building an XPS document using XpsDocumentWriter
, the simplest way to
generate hyperlinks is to use the WPF Hyperlink
element.
In practice, navigation with XPS is slightly more complicated
than what we saw in Chapter 11 because the XPS file
format itself does not support the Hyperlink
element directly. Instead, it
requires you to set the FixedPage.NavigateUri
attached property on
the Canvas
, Glyph
, or Path
elements that you would like to act as
links. However, the XpsDocumentWriter
can convert a Hyperlink
into suitably annotated elements.
It generates three elements for each hyperlink: the link text, a
Path
representing the underline,
and a rectangular Path
element with
a transparent fill. This covers the whole area of the Hyperlink
to ensure that it functions
correctly as a click target even if the Hyperlink
itself has a transparent
background. Without this, the link would be much harder to
click—clicking in, say, the space in the middle of a letter
o or anywhere outside of the letter shapes would
fail to activate the link.[109]
Adding a hyperlink to an external resource, such as a web site,
is simple: just set the link’s NavigateUri
to an absolute URL. But if you
want a link on one page to be able to refer to another page, you must
use a fragment URI (i.e., one beginning with a # character). The text
following the # refers to a named element somewhere in the
document.
For an element to be a valid target for a link, you must do two
things. First, you must set the element’s Name
property to match the fragment URI in
the link. (For example, if the link’s NavigateUri
is “#Heading_1” its target is an
element named “Heading_1”.) Second, you must advertise the existence
of the target with a LinkTarget
object associated with the containing page’s PageContent
object.
This second requirement exists to make sure XPS document viewers
don’t need to parse every single page in order to find the link
target. As we saw earlier, the FixedDocument
part of an XPS file contains a
sequence of PageContent
elements
describing the pages that make up the document, such as the one shown
in Example 15-6. If you are using
hyperlinks, this part must declare which pages contain which link
targets. Example 15-27 shows such
a document.
<FixedDocument xmlns="http://schemas.microsoft.com/xps/2005/06"> <PageContent Source="Pages/1.fpage" /> <PageContent Source="Pages/2.fpage"><PageContent.LinkTargets>
<LinkTarget Name="FirstItem" />
</PageContent.LinkTargets>
</PageContent> <PageContent Source="Pages/3.fpage"><PageContent.LinkTargets>
<LinkTarget Name="SecondTarget" />
<LinkTarget Name="Third" />
</PageContent.LinkTargets>
</PageContent> </FixedDocument>
By declaring link targets in the FixedDocument
part, it becomes possible for
an XPS reader to follow internal links quickly. When the user clicks
on a link, an XPS document viewer needs to scan only the FixedDocument
to find the named link, and
then open the containing page. Without this structure, it would need
to scan every single page looking for the named element. This is not
an optional performance enhancement—you are required to advertise link
targets. Failure to do this will cause the links not to work.
This linking mechanism requires you to ensure that your fragment names are unique across the scope of the whole document. Indeed, the XPS specification recommends (but does not absolutely require) that if a single XPS file contains multiple documents, the fragment names should be unique across the scope of the whole file.
The code in Example 15-28 through Example 15-30 creates a document with a table of contents as its first page, containing a set of links to 10 more pages that make up the remainder of the document. Figure 15-7 shows how this first page will look. Clicking on any of these links in an XPS document viewer will jump directly to the relevant page.
Example 15-28 builds
this FixedDocument
. It begins by
adding a page to contain the table of contents. This first page
contains a TextBlock
, which is
initially empty. The for
loop
builds 10 more pages and creates hyperlinks to these pages, adding
those links to the TextBlock
on the
first page, and building up the list of links as shown in Figure 15-7.
public static FixedDocument MakeDocWithToc( ) { FixedDocument doc = new FixedDocument( ); FixedPage tocPage = new FixedPage( ); TextBlock tableOfContents = new TextBlock( ); tocPage.Children.Add(tableOfContents); PageContent pc = new PageContent( ); ((IAddChild) pc).AddChild(tocPage); doc.Pages.Add(pc); for (int pageNumber = 1; pageNumber <= 10; ++pageNumber) { pc = MakePage(pageNumber); doc.Pages.Add(pc); Hyperlink link = MakeLink(pageNumber); tableOfContents.Inlines.Add(link); tableOfContents.Inlines.Add(new LineBreak( )); } return doc; }
This code relies on a helper function, MakePage
, to generate each page. Although a
single page can contain any number of targets, Example 15-29 keeps things
simple by creating a page for each target. It adds a TextBlock
to act as the target itself. The
fragment IDs[110] take the form Page1, Page2, and so on.
static PageContent MakePage(int pageNumber) { PageContent pc; string fragmentId = "Page" + pageNumber; TextBlock tb = new TextBlock( ); tb.Text = "This is page " + pageNumber; tb.Name = fragmentId; FixedPage page = new FixedPage( ); page.Children.Add(tb); pc = new PageContent( ); ((IAddChild) pc).AddChild(page); LinkTarget lt = new LinkTarget( ); lt.Name = tb.Name; pc.LinkTargets.Add(lt); return pc; }
We must also advertise the existence of the targets in the
PageContent
so that an XPS reader
can find the targets without opening all of the FixedPage
parts. This is why Example 15-29 creates a
LinkTarget
for each page.
After calling MakePage
, Example 15-28 creates a Hyperlink
for each page, using the MakeLink
helper function shown in Example 15-30. This sets the
NavigateUri
property to a fragment
URI matching the Name
of the
target.
static Hyperlink MakeLink(int pageNumber) { string fragmentId = "Page" + pageNumber; Run linkText = new Run("Go to page " + pageNumber); Uri linkTarget = new Uri("#" + fragmentId, UriKind.Relative); Hyperlink link = new Hyperlink(linkText); link.NavigateUri = linkTarget; return link; }
If the FixedDocument
created
by Example 15-28 through
Example 15-30 is written
to an XPS file using an XpsDocumentWriter
, the table of contents
pages will contain the hyperlink, converted into Glyphs
and Path
elements as the XPS specification
requires. If the user opens the file in the XPS viewer provided with
.NET 3.0 and clicks on a link, it will take him straight to the
target. Example 15-31 shows an
excerpt from the first fixed page in the XPS file that the preceding
example generates.
<FixedPage xmlns="http://schemas.microsoft.com/xps/2005/06" xmlns:x="http://schemas.microsoft.com/xps/2005/06/resourcedictionary-key" xml:lang="en-us" Width="816" Height="1056"> <Glyphs OriginX="0" OriginY="12.95" FontRenderingEmSize="12" FontUri="/Resources/19775d40-e839-41d3-a9fe-ca4670f35840.ODTTF" UnicodeString="Go to page 1" Fill="#FF0000FF" /> <Path Fill="#FF0000FF" Data="M0,13.65L69.19,13.65 69.19,14.34 0,14.34Z" /> ... <Path FixedPage.NavigateUri="../FixedDocument.fdoc#Page1" Fill="#00000000" Data="F1M0,0L69.19,0 69.19,15.96 0,15.96Z" /> ... </FixedPage>
This shows only the elements for the first link. The Glyphs
element presents the text of the
link: “Go to page 1.” The first Path
renders the underline—hyperlinks are
displayed in the usual “blue with underline” style by default. The
second Path
defines a rectangle
that covers the area occupied by the hyperlink, ensuring that the
whole area is clickable. (It has a transparent fill, ensuring that it
is invisible, but still presents a click target.) As you can see, this
final Path
element has the FixedPage.NavigateUri
attached property. The
XpsDocumentWriter
has replaced our
fragment URI with a relative URI pointing to the LinkTarget
as declared in the fixed document
part—this is the URI form that XPS readers will expect to see in the
final output to represent a hyperlink. The result is that when the
user clicks on a link in the contents page, he is taken to the
corresponding target page.
When you create an XpsDocument
, you have the option to specify
the type of compression you would like to use. If you do not pass a
CompressionOption
, it will default
to maximum compression. However, if you would like to favor speed over
file size, you can pass in either Fast
(as in Example 15-32) or SuperFast
.
XpsDocument doc = new XpsDocument("out.xps", FileAccess.Write, CompressionOption.Fast);
In this section, we saw that many XPS features are of interest only when the output is destined for a file. The .NET 3.0 Framework also provides a number of features that are concerned solely with printing.
The System.Printing
namespace
defines types that provide printing services including managing settings
of print jobs, discovering and selecting print queues, and configuring
printers and print servers. We will explore the most commonly used types
from this namespace.
PrintQueue
represents an
output destination for printing—anytime you print something, you are
sending it to a PrintQueue
. The
standard print dialog presents a list of available printers, and each
printer in this list corresponds to a PrintQueue
.
You have already seen several examples using PrintQueue
—it offers a static CreateXpsDocumentWriter
method that can
return an XpsDocumentWriter
.
Although this method does not explicitly return a PrintQueue
object, the writer it returns is
implicitly bound to the PrintQueue
of the printer the user selected in the print dialog. The examples
shown earlier in this chapter call an overload of CreateXpsDocumentWriter
that does not
specify a target PrintQueue
,
causing a print dialog to be shown automatically. However, if you have
a PrintQueue
object, you can call
one of the overloads that accepts a PrintQueue
, in which case no dialog will be
shown, and the document writer will target the specified queue. Example 15-33 shows one way to
do this.
LocalPrintServer local = new LocalPrintServer( ); PrintQueue pq = local.DefaultPrintQueue; XpsDocumentWriter xpsdw = PrintQueue.CreateXpsDocumentWriter(pq);
This example uses the default queue on the local machine. You
can also obtain a PrintQueue
from
the PrintServer
or the PrintDialog
class. PrintQueue
provides numerous properties
(more than 60). Some of these provide static information about the
queue, such as its name (FullName
)
and print speed (AveragePagesPerMinute
). Others provide
dynamic status information, such as IsPaperJammed
and IsTonerLow
.
PrintServer
represents a
machine offering one or more print queues. It provides a GetPrintQueues
method that can return a
collection of all the PrintQueue
objects associated with the machine.
You can create a PrintServer
by passing the machine name to the constructor. An alternative is a
special derived class we saw in Example 15-33, called LocalPrintServer
, which provides access to
the local machine. This derived class adds a DefaultPrintQueue
property, which returns
the PrintQueue
currently configured
as the default print target.
GetPrintQueues
offers
overloads that enable you to filter the list of print queues. You can
pass in an array of EnumeratedPrintQueueTypes
values, specifying
the kinds of printers you would like to see (e.g., print queues
representing faxes, or print queues published in directory
services).
If you already know exactly which print queue you want, you can
call GetPrintQueue
, passing in the
queue name.
PrintServer
even enables
suitably privileged users to add and remove print queues. InstallPrintQueue
allows you to specify the
queue name, driver name, port names, processor name, and optionally
the share name, comment, and location description. DeletePrintQueue
removes a print
queue.
The PrintQueue
class defines
a GetPrintJobInfoCollection
method.
This returns a collection of PrintSystemJobInfo
objects. This collection
provides roughly the same information you can see by opening the
window showing active print jobs for the printer in Windows.
Some of the information in PrintSystemJobInfo
is static for the job
lifetime, such as JobName
and
NumberOfPages
. Some properties
provide status information; for example, IsPrinting
, TimeSinceStartedPrinting
, and IsPaperOut
. PrintSystemJobInfo
also offers a Cancel
method to cancel the print job, and
Pause
and Resume
methods to allow a job to be
suspended and then resumed.
When you print from a Windows application, the print dialog
provides access to various printing options. These include generic
features such as the number of copies to be printed and collation
settings. There may also be features common to many printers but not
universally supported, such as double-sided printing. In some cases,
you may wish to configure features specific to your particular printer
type. A PrintTicket
object
encapsulates all such print settings.
The PrintCapabilities
class
is related to PrintTicket
. Given a
PrintQueue
, you can call its
GetPrintCapabilities
method to
discover the supported set of features you can add to a PrintTicket
for that queue (see Example 15-34). If you call the overload
of GetPrintCapabilities
that takes
no parameters, it will return a PrintCapabilities
object with properties
indicating which common features are available.
PrintCapabilities caps = pq.GetPrintCapabilities( ); foreach (Duplexing duplexType in caps.DuplexingCapability) { Console.WriteLine(duplexType); }
The PrintCapabilities
class
is convenient. However, it does not provide a complete list of the
features the printer has to offer. It defines only the properties for
common features. To get the complete set, you need to call the
GetPrintCapabilitiesAsXml
method.
This returns an XML document containing all of the features, including
those specific to the model of printer attached to the queue. This
document will contain XML elements named Feature
for each available feature. Example 15-35 shows one such
element.
<psf:Feature name="ns0000:JobEnhancedPCL5Enable"> <psf:Property name="psf:SelectionType"> <psf:Value xsi:type="xsd:QName">psk:PickOne</psf:Value> </psf:Property> <psf:Property name="psk:DisplayName"> <psf:Value xsi:type="xsd:string">EnhancedPCL5Enable</psf:Value> </psf:Property> <psf:Option name="ns0000:False" constrained="psk:None"> <psf:Property name="psk:DisplayName"> <psf:Value xsi:type="xsd:string">False</psf:Value> </psf:Property> </psf:Option> <psf:Option name="ns0000:True" constrained="psk:None"> <psf:Property name="psk:DisplayName"> <psf:Value xsi:type="xsd:string">True</psf:Value> </psf:Property> </psf:Option> </psf:Feature>
Example 15-36 uses
XPath to extract all of the Feature
elements, and to print their DisplayName
properties in order to show the
names of the features. It then retrieves the Option
elements inside each Feature
to display the available
settings.
Stream capsStream = pq.GetPrintCapabilitiesAsXml( ); XmlDocument capsDoc = new XmlDocument( ); capsDoc.Load(capsStream); XmlNamespaceManager nsm = new XmlNamespaceManager(capsDoc.NameTable); nsm.AddNamespace("psf", "http://schemas.microsoft.com/windows/2003/08/printing/printschemaframework"); XmlNodeList features = capsDoc.SelectNodes("//psf:Feature", nsm); foreach (XmlElement feature in features) { XmlNode featureName = feature.SelectSingleNode( "psf:Property[@name='psk:DisplayName']/psf:Value/text( )", nsm); Console.WriteLine("Feature: " + featureName.Value); XmlNodeList options = feature.SelectNodes("psf:Option", nsm); foreach (XmlElement option in options) { XmlNode optionName = option.SelectSingleNode( "psf:Property[@name='psk:DisplayName']/psf:Value/text( )", nsm); if (optionName != null) { Console.WriteLine(" option: " + optionName.Value); } } }
Running this against a real printer generates a lot of output—about 40 features, each with a few options. Because that wouldn’t make very interesting reading, Example 15-37 shows just a short excerpt that corresponds to the XML in Example 15-35. It shows a printer-specific feature from one of the authors’ printers, along with the possible settings for that feature.
If you are interested in only certain features (e.g., you wish
to know whether the printer at the end of the PrintQueue
supports color and duplex
printing), you can build a PrintTicket
containing the features you are
interested in and pass that to either of the methods for retrieving
capabilities. This filters the results, listing only features that are
both supported by the target printer and that were in your PrintTicket
.
It is sometimes appropriate to specify print settings on a more
fine-grained level than an entire print job. For example, if you are
printing to a color printer, you might require color only on certain
pages, and could indicate to the printer that black-and-white printing
should be used on the rest. To exploit this, you would associate a
PrintTicket
for each FixedPage
and FixedDocument
, as well as providing one for
the whole job. You would set the ticket’s OutputColor
property to OutputColor.Color
for the color pages, and
OutputColor.Grayscale
for the other
tickets. (For the job-level ticket, either you can let the user
provide the settings via a print dialog, or you can pass one in to
PrintQueue.CreateXpsDocumentWriter
.)
Although print tickets are used to control print settings, it
may be appropriate to provide them when writing an XPS file. For
example, your application may generate XPS file output that is
ultimately destined for a print bureau. In this case, either you can
set the ticket on the FixedPage
,
FixedDocument
, and FixedDocumentSequence
classes (just as you
would for printing) or you can use the package-level API. The writer
interfaces for fixed pages, documents, and document sequences offer a
settable PrintTicket
property.
As we’ve already seen, if you ask the PrintQueue
class to create an XpsDocumentWriter
without passing in a
specific PrintQueue
, it
automatically shows the standard print dialog. However, sometimes you
will need to exercise more control. For example, you may wish to
enable the page range selection functionality, allowing the user to
print a specific range of pages from the document. You may also wish
to select an initial print queue and print ticket—rather than
accepting the current defaults, you might wish to show the same
settings the user chose the last time a particular document was
printed, for example. Alternatively, although you may be happy with
the default print dialog behavior, you might want to examine the
PrintQueue
or PrintTicket
chosen by the user. In these
cases, you will need to write code to display the PrintDialog
.
Example 15-38 shows how to enable user
page range selection in the PrintDialog
. This also sets an initial
selected range of pages.
PrintDialog pd = new PrintDialog( ); pd.MinPage = 1; pd.MaxPage = 20; pd.UserPageRangeEnabled = true; pd.PageRangeSelection = PageRangeSelection.UserPages; pd.PageRange = new PageRange(4, 8); if (pd.ShowDialog( ) == true) { PrintQueue pq = pd.PrintQueue; PrintTicket pt = pd.PrintTicket; ... }
If the user clicks the Print button, ShowDialog
will return true
. (The return type is a nullable bool,
hence explicit comparison with true
in the if
statement.) The code goes
on to retrieve the print queue and ticket reflecting the user’s chosen
print settings. These could then be passed to PrintQueue.CreateXpsDocumentWriter
, in order
to start printing to the chosen queue with the configured
settings.
Several classes are available in the System.Printing
namespace for working with
the size and resolution of the output media. These are described in
Table 15-4. The reason for having
several different classes to represent “paper size” is that different
scenarios require different amounts of information.
Class | Usage |
PageMediaSize | Describes the paper size |
PageImageableArea | Describes the area of the paper in which printing is possible |
PrintDocumentImageableArea | Combines a description of the paper size and the printable area |
PageResolution | Describes the horizontal and vertical resolution of the target, and contains a “qualitative” resolution such as Draft or High |
The easiest way to display an XPS file is to use the XPS viewer
application supplied with the .NET 3.0 Framework. This viewer runs when
you double-click on an XPS file. As mentioned earlier, Microsoft also
supplies a free viewer for displaying documents on machines without the
framework installed. However, it is sometimes useful to be able to
display an XPS file within an application. WPF supplies the DocumentViewer
control for this. Simply set
its Document
property to refer to
either a FixedDocument
or a FixedDocumentSequence
, and it will render the
document, providing navigation and zoom controls. Example 15-39 shows a simple window containing
a DocumentViewer
.
<Window x:Class="ShowFixedDocument.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Show FixedDocument" Height="300" Width="300"> <DocumentViewer x:Name="viewer" /> </Window>
Given this XAML, the code-behind file can load an XPS document as shown in Example 15-40.
string docPath = Environment.GetCommandLineArgs( )[1]; XpsDocument doc = new XpsDocument(docPath, FileAccess.Read); viewer.Document = doc.GetFixedDocumentSequence( );
This reads the XPS file specified by the first command-line
parameter. You could also use a FixedDocument
or FixedDocumentSequence
built from scratch.
Figure 15-8 shows the XPS specification
itself loaded into this UI.
The XML Paper Specification is at the heart of printing in WPF. It
also acts as a file format for accurately capturing an application’s
printable output. WPF lets us work with XPS files at various levels.
There are the high-level FixedDocumentSequence
, FixedDocument
, and FixedPage
classes, which mirror the basic
structure of an XPS file but allow us to use WPF elements such as layout
primitives that are not directly supported in XPS. The XpsDocumentWriter
maps between this framework
element world and the lower-level world of the XPS file.
We can either build an XPS file on disk or send it directly to the
print system. If we provide a “save as XPS” feature, we may choose to
add extra structure to enhance the resulting XPS files, typically
working directly at the XPS package level with the System.Windows.Xps.Packaging
namespace. Or, if
we wish, we can go lower still, using the System.IO.Packaging
namespace to work directly
at the OPC level.
The System.Printing
namespace
provides us with various types to control the printing process, such as
choosing output servers and queues, and configuring the print
settings.
[104] * You can download the specification from http://www.microsoft.com/whdc/xps/default.mspx (http://tinysells.com/72
[105] * You can find the specifications at http://www.ecma-international.org/memento/tc45.htm (http://tinysells.com/110).
[106] * You could always use the
PageSetupDialog
class provided
by Windows Forms. However, be aware that you will pay the usual
working set overhead for using Windows Forms features on top of
the costs of using WPF. If you are already using Windows Forms in
your application (e.g., you are hosting Windows Forms controls via
interop, as described in Appendix B),
incremental cost of using this dialog will be low. But if it’s the
only Windows Forms feature you use, you might want to consider
whether it’s worth the impact.
[109] * If you choose to generate an XPS document with the package-level APIs, you will need to do something similar when creating hyperlinks in order to make them usable.
[110] * Fragment IDs are the part of the URL following a # symbol. These are a standard URL feature. WPF uses these for much the same purpose as web pages: to identify a particular target location within a page.