Documents, Printing & Text
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
Most desktop line-of-business applications eventually have to produce a document: a report the user reads on screen, a letter the user edits, a sheet that goes to a printer or to a file. WPF treats this as a first-class concern. It has two document models (flow and fixed), a rich text editing control, one printing API built on the XPS print path, and a text engine that exposes OpenType typography and several rendering options. This page covers all of them, using the Bookshelf Desktop scenario: a printable book report, an editable notes field, an A4 label sheet and an XPS export.
The page was written against these versions, current in October 2026:
-
.NET 10 LTS, latest patch 10.0.13, with C# 14. .NET 11 is at release candidate 2 (general availability is expected 10 to 13 November 2026). The Microsoft Learn API reference for
TextOptions,TextFormattingModeandFormattedTextlists the same members for .NET Framework 4.8 and for .NET 10 and 11, so the text and document features described here carry over from .NET Framework unchanged, with the exception of theFormattedTextconstructor covered in the Text Rendering section. -
CommunityToolkit.Mvvm 8.4.2 for the view models in the examples.
-
Visual Studio 2026 18.11.1.
-
Windows 10 and Windows 11. Everything about flow documents, fixed documents, XPS and
System.Printingis WPF only. WinUI 3 (Windows App SDK 2.5.1) has its own, much smaller text and printing model, compared at the end.
What Microsoft Learn Provides, and What It Does Not
The WPF documentation has a documents overview, a flow document overview, a RichTextBox overview, a printing
overview with how-to topics, a typography overview, an OpenType feature page, and pages on text performance and
packaged fonts. The API reference documents every type used below.
Microsoft Learn does not provide an MVVM-friendly way to bind the content of a RichTextBox or to build a
FlowDocument from a data collection. The snippets that generate a report from Book objects, the bound viewer and
the print service wrapper in this page are patterns built on the documented APIs, not official samples. The same
applies to the dependency-injection registration of the print service. Where this page departs from a documented
sample, it says so in the text before the example.
Two Document Models
WPF divides documents into two categories according to what the document is for.
| Flow document | Fixed document | |
|---|---|---|
Purpose |
Reading and editing; content reflows to the available space |
Exact page layout; what you see is what you print |
Root element |
|
|
Layout |
Columns, hyphenation and pagination computed at run time from window size, font size and viewer |
Absolute positions on a page of fixed size, identical on every device |
Viewers |
|
|
Persisted as |
XAML, RTF (through |
XPS ( |
Typical use |
Help, articles, reports, notes, e-mail bodies |
Forms, labels, invoices, anything with a precise template |
Both models share the same text elements and typography, both can be printed through the same DocumentPaginator
abstraction, and both can be written to an XPS file. The choice is not about quality; it is about whether the layout
belongs to the document (fixed) or to the reader (flow).
Flow Documents
A FlowDocument is a tree of blocks that contain inlines. The tree is content, not UI: block and inline elements
derive from TextElement, which is lighter than a UIElement, and a FlowDocument is not displayed until a viewer
or a RichTextBox hosts it.
Elements
| Element | Kind | Purpose |
|---|---|---|
|
Block |
A run of inlines laid out as one paragraph; the workhorse of flow content |
|
Block |
Groups other blocks; applies no formatting of its own, but property values set on it apply to its children |
|
Block |
Bulleted or numbered lists |
|
Block |
Rows and cells of blocks, in |
|
Block |
Hosts one |
|
Inline |
A span of plain text; the cheapest way to place text in a |
|
Inline |
Formatting containers for other inlines |
|
Inline |
A link, navigated with |
|
Inline |
A forced break |
|
Inline |
Hosts one |
|
Inline |
A positioned piece of content that can be anchored relative to the page, column or content |
|
Inline |
Content that floats beside the text; sized at one column by default |
The FlowDocument Properties That Matter
| Property | Effect |
|---|---|
|
Preferred column width; the document creates as many columns as fit the viewer |
|
Distributes white space as evenly as possible; disabled by default, and costs performance |
|
Automatic hyphenation; disabled by default, and starts COM interoperability when enabled |
|
The padding between the page edge and the content |
|
Page size used when the document is paginated for print or for a page viewer |
|
Inherited by every block and inline in the document |
Because formatting properties are inherited, a document sets its font once on the FlowDocument and overrides it only
where needed. This is the same property value inheritance described on the dependency properties page of this section.
The Viewers
Three controls display a flow document. They differ in how much they let the user change.
| Control | Behavior | Cost |
|---|---|---|
|
The user switches between a one-page view, a two-pages-at-a-time (book) view and continuous scrolling; has search, zoom and page navigation |
The most resource intensive |
|
Fixed in page-at-a-time mode |
Lighter |
|
Fixed in continuous scrolling mode; a vertical scroll bar is always shown, a horizontal one only when needed; no toolbar unless |
Lightest |
The reader’s view modes are the FlowDocumentReaderViewingMode enumeration. A control that exposes the choice to the
user costs more than one that fixes it, so use FlowDocumentScrollViewer for a read-only help page and
FlowDocumentReader only where switching modes is a feature.
Source: Flow Document Overview
Example: A Book Report Built From Data
The Bookshelf Desktop report lists the books with a title, an author and a running count. The builder below turns an
IReadOnlyList<Book> into a FlowDocument. It uses Run for text, as the performance guidance recommends, and sets
fonts once at the document level.
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
using Bookshelf.Core;
namespace Bookshelf.Desktop.Reporting;
public static class BookReportBuilder
{
public static FlowDocument Build(IReadOnlyList<Book> books, DateOnly generatedOn)
{
var document = new FlowDocument
{
FontFamily = new FontFamily("Palatino Linotype"),
FontSize = 14,
ColumnWidth = 400,
PagePadding = new Thickness(48),
IsOptimalParagraphEnabled = false,
IsHyphenationEnabled = false,
};
document.Blocks.Add(new Paragraph(new Run("Bookshelf report"))
{
FontSize = 28,
FontWeight = FontWeights.Bold,
Margin = new Thickness(0, 0, 0, 4),
});
document.Blocks.Add(new Paragraph(new Run($"{books.Count} books, generated {generatedOn:D}"))
{
Foreground = Brushes.DimGray,
});
var table = new Table { CellSpacing = 0, BorderBrush = Brushes.Gray, BorderThickness = new Thickness(0, 0, 0, 1) };
table.Columns.Add(new TableColumn { Width = new GridLength(40) });
table.Columns.Add(new TableColumn { Width = new GridLength(3, GridUnitType.Star) });
table.Columns.Add(new TableColumn { Width = new GridLength(2, GridUnitType.Star) });
var rows = new TableRowGroup();
rows.Rows.Add(HeaderRow("#", "Title", "Author"));
var index = 1;
foreach (var book in books.OrderBy(b => b.Author).ThenBy(b => b.Title))
{
rows.Rows.Add(DataRow(index++, book));
}
table.RowGroups.Add(rows);
document.Blocks.Add(table);
return document;
}
private static TableRow HeaderRow(params string[] headings)
{
var row = new TableRow { FontWeight = FontWeights.Bold, Background = Brushes.LightGray };
foreach (var heading in headings)
{
row.Cells.Add(new TableCell(new Paragraph(new Run(heading))));
}
return row;
}
private static TableRow DataRow(int index, Book book)
{
var row = new TableRow();
row.Cells.Add(new TableCell(new Paragraph(new Run(index.ToString()))));
row.Cells.Add(new TableCell(new Paragraph(new Run(book.Title))));
row.Cells.Add(new TableCell(new Paragraph(new Run(book.Author))));
return row;
}
}
Source: Flow Document Overview
Notes on the example. FlowDocument is a DispatcherObject, so build and use it on the UI thread. PagePadding and
the GridLength column widths are ordinary dependency properties. The builder is a plain static function of its input
and does not know about the view, which keeps it testable and reusable for both display and printing.
Showing the Document in a Viewer
FlowDocumentReader.Document and FlowDocumentScrollViewer.Document are dependency properties, so a view model can
expose the built document and the view binds to it. A view model that exposes a FlowDocument does reference a WPF
type, which breaks strict separation; many teams accept that for report previews, others expose the data and let a
view-side converter or behavior build the document. The example takes the first route for brevity.
using System.Windows.Documents;
using Bookshelf.Core;
using Bookshelf.Desktop.Reporting;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Bookshelf.Desktop;
public partial class BookReportViewModel(IBookRepository repository, IPrintService printService) : ObservableObject
{
[ObservableProperty]
public partial FlowDocument? Report { get; set; }
[RelayCommand]
private async Task LoadAsync(CancellationToken cancellationToken)
{
var books = await repository.GetAllAsync(cancellationToken);
Report = BookReportBuilder.Build(books, DateOnly.FromDateTime(DateTime.Today));
}
[RelayCommand(CanExecute = nameof(CanPrint))]
private void Print()
{
if (Report is not null)
{
printService.PrintFlowDocument(Report, "Bookshelf report");
}
}
private bool CanPrint() => Report is not null;
partial void OnReportChanged(FlowDocument? value) => PrintCommand.NotifyCanExecuteChanged();
}
Source: RelayCommand attribute
<Window x:Class="Bookshelf.Desktop.BookReportWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Bookshelf report" Width="900" Height="640"
Loaded="OnLoaded">
<DockPanel>
<ToolBar DockPanel.Dock="Top">
<Button Content="Print..." Command="{Binding PrintCommand}" />
</ToolBar>
<FlowDocumentReader Document="{Binding Report}"
ViewingMode="Scroll"
IsFindEnabled="True" />
</DockPanel>
</Window>
Source: Flow Document Overview
The window has no DataContext element, because BookReportViewModel takes constructor parameters; the application
resolves it from the Generic Host, as the project structure page of this section shows, and the window constructor
assigns DataContext. The OnLoaded handler in the code-behind then calls await viewModel.LoadCommand.ExecuteAsync(null).
RichTextBox: Editing Flow Content
RichTextBox hosts a FlowDocument and lets the user edit it. Its content property is Document. It supports
real-time spell checking, a default context menu and the formatting commands of the EditingCommands class, such as
ToggleBold (Ctrl+B). A TextBox, by comparison, supports spell checking and the context menu but not formatting
commands or flow content, and it needs fewer resources, so it remains the right choice for plain form fields.
| Feature | TextBox | RichTextBox |
|---|---|---|
Real-time spell checking |
Yes |
Yes |
Context menu |
Yes |
Yes |
Formatting commands such as |
No |
Yes |
Flow content (images, paragraphs, tables) |
No |
Yes |
Flow content inside a RichTextBox does not behave exactly like flow content in a viewer. There are no columns and so
no automatic resizing, and the viewer features (search, view modes, page navigation, zoom) are not available.
Example: A Notes Editor With a Formatting Toolbar
The notes field of the Bookshelf edit window uses a toolbar wired to EditingCommands. The buttons set
CommandTarget to the editor, because a toolbar sits in a different focus scope.
<DockPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DockPanel.Resources>
<Style x:Key="FormatButton" TargetType="Button">
<Setter Property="Width" Value="30" />
<Setter Property="CommandTarget" Value="{Binding ElementName=NotesEditor}" />
</Style>
</DockPanel.Resources>
<ToolBar DockPanel.Dock="Top">
<Button Style="{StaticResource FormatButton}" Command="ApplicationCommands.Undo" Content="Undo" Width="48" />
<Button Style="{StaticResource FormatButton}" Command="ApplicationCommands.Redo" Content="Redo" Width="48" />
<Separator />
<Button Style="{StaticResource FormatButton}" Command="EditingCommands.ToggleBold" ToolTip="Bold">
<TextBlock FontWeight="Bold">B</TextBlock>
</Button>
<Button Style="{StaticResource FormatButton}" Command="EditingCommands.ToggleItalic" ToolTip="Italic">
<TextBlock FontStyle="Italic" FontWeight="Bold">I</TextBlock>
</Button>
<Button Style="{StaticResource FormatButton}" Command="EditingCommands.ToggleUnderline" ToolTip="Underline">
<TextBlock TextDecorations="Underline" FontWeight="Bold">U</TextBlock>
</Button>
<Separator />
<Button Style="{StaticResource FormatButton}" Command="EditingCommands.ToggleBullets" Content="List" Width="48" />
<Button Style="{StaticResource FormatButton}" Command="EditingCommands.AlignLeft" Content="Left" Width="48" />
<Button Style="{StaticResource FormatButton}" Command="EditingCommands.AlignCenter" Content="Center" Width="56" />
</ToolBar>
<RichTextBox x:Name="NotesEditor" AcceptsTab="True" SpellCheck.IsEnabled="True">
<FlowDocument>
<Paragraph>Notes about <Bold>this book</Bold> go here.</Paragraph>
</FlowDocument>
</RichTextBox>
</DockPanel>
Source: RichTextBox Overview
AcceptsTab="True" lets the editor receive tab characters; by default Tab moves the focus to the next control. The
commands, CommandTarget pattern and AcceptsTab come from the official sample. Routed commands and why a toolbar
needs CommandTarget are explained on the commands page of this section.
Saving and Loading Rich Content
The editor never exposes its text as a bindable string. To persist the notes, wrap the document in a TextRange and
save it to a stream in one of the DataFormats the range supports. The official sample saves and loads the XAML
package format (DataFormats.XamlPackage), which keeps images. The service below does the same through a small
abstraction, and uses DataFormats.Rtf for the interchange case.
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
namespace Bookshelf.Desktop.Reporting;
public static class RichTextPersistence
{
public static async Task SaveAsync(RichTextBox editor, string path, string dataFormat)
{
var range = new TextRange(editor.Document.ContentStart, editor.Document.ContentEnd);
await using var stream = new FileStream(path, FileMode.Create, FileAccess.Write);
range.Save(stream, dataFormat);
await stream.FlushAsync();
}
public static async Task LoadAsync(RichTextBox editor, string path, string dataFormat)
{
if (!File.Exists(path))
{
return;
}
var range = new TextRange(editor.Document.ContentStart, editor.Document.ContentEnd);
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
range.Load(stream, dataFormat);
}
}
// Usage in the notes window:
// await RichTextPersistence.SaveAsync(NotesEditor, path, DataFormats.XamlPackage);
// await RichTextPersistence.LoadAsync(NotesEditor, path, DataFormats.Rtf);
Source: RichTextBox Overview
TextRange.Save and TextRange.Load are synchronous APIs; the await above only covers the stream flush. The
example is a pattern based on the official one: the official sample uses FileStream without using and
DataFormats.XamlPackage only. That DataFormats.Rtf is accepted by TextRange.Save is documented on the API page
of TextRange, not on the overview page cited here.
Detecting Changes
Use TextChanged to know that the content changed, not KeyDown, because content changes without key presses (paste,
undo, a toolbar command, a drag and drop). Unlike a TextBox, RichTextBox.Document is not a dependency property, so
it cannot be the target of a binding; load and save it in code-behind or in a behavior, as shown above.
Fixed Documents and XPS
A FixedDocument is an ordered sequence of PageContent elements. PageContent is the only allowed child, and each
one refers to the content of one page, in page order. FixedDocument is marked as having Pages as its content
property, so PageContent elements nest directly in markup. Each page’s content is a FixedPage, a canvas-like
surface of fixed size on which elements are positioned absolutely with the FixedPage.Left and FixedPage.Top
attached properties. A FixedDocument is read-only for the user: selection, keyboard navigation and search work,
editing does not.
| Type | Role |
|---|---|
|
One document: a sequence of pages with fixed layout; exposes |
|
One entry in the page sequence; holds a |
|
The page itself, with a fixed |
|
Several fixed documents treated as one (this is what an XPS package gives you) |
|
The control that displays fixed content, with printing, copy, zoom and text search built in |
Example: An A4 Label Sheet
The label sheet prints three spine labels per row. A4 is 210 by 297 millimetres, which at WPF’s 96 units per inch is about 793.7 by 1122.5 units, so the page is sized in device-independent units.
<DocumentViewer xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<FixedDocument>
<PageContent>
<FixedPage Width="793.7" Height="1122.5" Background="White">
<TextBlock FixedPage.Left="48" FixedPage.Top="40"
FontFamily="Palatino Linotype" FontSize="22" FontWeight="Bold"
Text="Bookshelf spine labels" />
<Border FixedPage.Left="48" FixedPage.Top="100" Width="220" Height="120"
BorderBrush="Black" BorderThickness="1" Padding="10">
<StackPanel>
<TextBlock FontFamily="Palatino Linotype" FontSize="16" FontWeight="Bold"
TextWrapping="Wrap" Text="Dune" />
<TextBlock FontFamily="Palatino Linotype" FontSize="12" Text="Frank Herbert" />
<TextBlock FontFamily="Palatino Linotype" FontSize="12"
Typography.NumeralAlignment="Tabular" Text="Shelf 03 / Row 02" />
</StackPanel>
</Border>
<Border FixedPage.Left="288" FixedPage.Top="100" Width="220" Height="120"
BorderBrush="Black" BorderThickness="1" Padding="10">
<StackPanel>
<TextBlock FontFamily="Palatino Linotype" FontSize="16" FontWeight="Bold"
TextWrapping="Wrap" Text="Neuromancer" />
<TextBlock FontFamily="Palatino Linotype" FontSize="12" Text="William Gibson" />
<TextBlock FontFamily="Palatino Linotype" FontSize="12"
Typography.NumeralAlignment="Tabular" Text="Shelf 03 / Row 03" />
</StackPanel>
</Border>
</FixedPage>
</PageContent>
</FixedDocument>
</DocumentViewer>
Source: FixedDocument class
The official reference page describes FixedDocument, PageContent and the content property, but it contains no XAML
sample, so the markup above, including the FixedPage attached properties, is written from the documented structure
and not copied from a sample. A real label sheet is generated in code from the selected books; the code-behind version
creates a FixedDocument, adds PageContent objects whose Child is a FixedPage, and adds the label elements to
FixedPage.Children.
XPS: The Package Behind Fixed Documents
XPS, the XML Paper Specification, is simultaneously an electronic document format, a print spool format and a page
description language. An XpsDocument is a ZIP package (built on the System.IO.Packaging APIs and the Open
Packaging Conventions) that holds one or more fixed documents together with all the resources needed to render them:
XML pages plus binary parts such as images and fonts. Relationships between parts say which content depends on which
resource. An XPS document can be opened in a viewer, displayed in a DocumentViewer, sent to the print spool or sent
directly to an XPS-capable printer. The Microsoft XPS Document Writer printer in the print dialog creates XPS
(and OpenXPS) files.
Packages can carry digital signatures (PackageDigitalSignature) to identify the originator of a part and to detect
modification; a signature does not prevent a part from being changed, only makes validation fail if it is.
Example: Export the Report to an XPS File
The export writes a flow document through its paginator into an XPS package, then reopens the package in a
DocumentViewer.
using System.IO;
using System.IO.Packaging;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Xps;
using System.Windows.Xps.Packaging;
namespace Bookshelf.Desktop.Reporting;
public static class XpsExport
{
public static void Save(FlowDocument document, string xpsPath, Size pageSize)
{
// Paginate for a fixed page size. Use a clone in a real app (see Pitfalls).
var paginator = ((IDocumentPaginatorSource)document).DocumentPaginator;
paginator.PageSize = pageSize;
using var package = Package.Open(xpsPath, FileMode.Create, FileAccess.ReadWrite);
using var xps = new XpsDocument(package, CompressionOption.Maximum);
XpsDocument.CreateXpsDocumentWriter(xps).Write(paginator);
}
public static void ShowInViewer(DocumentViewer viewer, string xpsPath)
{
var xps = new XpsDocument(xpsPath, FileAccess.Read);
viewer.Document = xps.GetFixedDocumentSequence();
}
}
Source: Printing documents overview
The official pages show new XpsDocument(path, FileAccess.Read) and GetFixedDocumentSequence() (reused above in
ShowInViewer) and describe XpsDocumentWriter.Write overloads. They do not show the Package.Open plus
CompressionOption constructor or XpsDocument.CreateXpsDocumentWriter(XpsDocument) used in Save; those signatures
are from the API reference and from knowledge, and should be checked when you compile. The XPS Viewer app is an
optional Windows feature on current Windows versions, so do not assume that users can double-click an .xps file;
embed a DocumentViewer instead.
Printing
Printing in WPF is built on the XPS print path. Instead of translating a document into a printer language such as PCL
or PostScript, or into a GDI spool format such as EMF, the application hands XPS to the print system and, when the
printer has an XPSDrv driver, the XPS goes to the driver unchanged. If the target queue does not have an XPSDrv driver,
the Write and WriteAsync methods of XpsDocumentWriter convert the content from XPS to GDI automatically, so the
same code prints to every printer.
| Benefit of the XPS print path | Explanation |
|---|---|
WYSIWYG |
The document the user sees is the document that is spooled |
Color and transparency |
Native support for advanced color profiles, CMYK, named colors, transparency and gradients |
Smaller spool files |
Font subsetting, shared resources, ZIP compression and native graphics primitives instead of rasterization |
Performance |
XPS features are only available to applications that target the XPS print path |
The API Levels
| Level | Types and use |
|---|---|
Basic |
|
Advanced |
|
The WPF PrintDialog should not be confused with System.Windows.Forms.PrintDialog; they are different types for
different frameworks.
PrintDialog
PrintDialog shows the standard Windows print dialog. ShowDialog() returns a nullable Boolean that is true when the
user presses Print. The dialog lets the user pick a printer (including the XPS document writer) and, when
UserPageRangeEnabled is true, a range of pages; the choice is available through PageRangeSelection and PageRange.
| Member | Use |
|---|---|
|
Shows the dialog; true means the user chose to print |
|
Prints a single visual (a control or a |
|
Prints a paginated document |
|
Lets the user enter a page range |
|
Whether the user selected all pages or a user-defined range |
|
The |
Printing without showing the dialog is possible, but the official guidance recommends the PrintQueue.AddJob method or
the XpsDocumentWriter.Write and WriteAsync methods for performance.
Example: A Print Service
The service wraps PrintDialog so view models depend on an interface, not on WPF dialogs. It prints a flow document
by giving the paginator the printable size reported by the dialog, and prints a visual with PrintVisual.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
namespace Bookshelf.Desktop;
public interface IPrintService
{
/// <summary>Shows the print dialog and prints the document. Returns false when the user cancels.</summary>
bool PrintFlowDocument(FlowDocument document, string description);
/// <summary>Shows the print dialog and prints a single visual. Returns false when the user cancels.</summary>
bool PrintVisual(Visual visual, string description);
}
public sealed class WpfPrintService : IPrintService
{
public bool PrintFlowDocument(FlowDocument document, string description)
{
var dialog = new PrintDialog { UserPageRangeEnabled = true };
if (dialog.ShowDialog() != true)
{
return false;
}
var paginator = ((IDocumentPaginatorSource)document).DocumentPaginator;
paginator.PageSize = new Size(dialog.PrintableAreaWidth, dialog.PrintableAreaHeight);
dialog.PrintDocument(paginator, description);
return true;
}
public bool PrintVisual(Visual visual, string description)
{
var dialog = new PrintDialog();
if (dialog.ShowDialog() != true)
{
return false;
}
dialog.PrintVisual(visual, description);
return true;
}
}
Source: How to display a print dialog
The official how-to shows ShowDialog, PrintDocument, UserPageRangeEnabled, PageRangeSelection and
PageRange. PrintableAreaWidth and PrintableAreaHeight and the use of a FlowDocument paginator with PageSize
come from the API reference and from common practice, not from a Learn sample. Register the service as a singleton in
the Generic Host (services.AddSingleton<IPrintService, WpfPrintService>()), and it can be replaced by a fake in
view-model tests.
Printing a Page Range
The paginator returned by a FlowDocument or an XPS document prints all pages. To honor a user page range, the
official how-to extends the abstract DocumentPaginator class with a wrapper that maps page numbers to the chosen
range (PageCount, GetPage, PageSize, Source, IsPageCountValid), and passes it to PrintDocument when
PageRangeSelection is PageRangeSelection.UserPages. The wrapper in the sample also contains a reflection workaround
for the "FixedPage cannot contain another FixedPage" error that occurs when printing fixed pages to XPS. Reflection on
private members is fragile; test the workaround against the .NET version you ship.
Source: How to display a print dialog
Advanced Printing: PrintQueue, PrintTicket and XpsDocumentWriter
For batch printing, silent printing or control of device features, use the System.Printing classes directly. A
PrintTicket tells the printer how to process a job; PrintCapabilities tells you what the printer supports, so a
ticket can use only supported features. The queue’s DefaultPrintTicket is the starting point.
using System.Printing;
namespace Bookshelf.Desktop;
public static class PrintTickets
{
/// <summary>Creates a ticket that prefers collated, duplex output when the printer supports it.</summary>
public static PrintTicket ForBookReport(PrintQueue printQueue)
{
PrintCapabilities capabilities = printQueue.GetPrintCapabilities();
PrintTicket ticket = printQueue.DefaultPrintTicket;
if (capabilities.CollationCapability.Contains(Collation.Collated))
{
ticket.Collation = Collation.Collated;
}
if (capabilities.DuplexingCapability.Contains(Duplexing.TwoSidedLongEdge))
{
ticket.Duplexing = Duplexing.TwoSidedLongEdge;
}
return ticket;
}
public static PrintQueueCollection LocalQueues()
{
var server = new LocalPrintServer();
return server.GetPrintQueues();
}
}
Source: Printing documents overview
Adding an XPS document to a queue together with a ticket is done with XpsDocumentWriter. The writer is created for a
queue with PrintQueue.CreateXpsDocumentWriter, and Write or WriteAsync accepts a FixedDocumentSequence and a
PrintTicket.
using System.IO;
using System.Printing;
using System.Windows.Documents;
using System.Windows.Xps;
using System.Windows.Xps.Packaging;
namespace Bookshelf.Desktop;
public static class XpsPrinting
{
public static void Print(string xpsFilePath, PrintQueue printQueue, PrintTicket printTicket)
{
XpsDocumentWriter writer = PrintQueue.CreateXpsDocumentWriter(printQueue);
using var xpsDocument = new XpsDocument(xpsFilePath, FileAccess.Read);
FixedDocumentSequence sequence = xpsDocument.GetFixedDocumentSequence();
// Synchronous. Use writer.WriteAsync(sequence, printTicket) to avoid blocking the UI thread.
writer.Write(sequence, printTicket);
}
}
Source: Printing documents overview
The official sample does not dispose the XpsDocument; the example above adds using, which is sound for the
synchronous Write. With WriteAsync, keep the document open until the write completes.
Printing a Visual
PrintVisual is the simplest way to print something that is already on screen, or something drawn in code. The
Bookshelf spine label below is a DrawingVisual that is rendered with FormattedText; the same visual can be
printed without being added to the visual tree.
using System.Globalization;
using System.Windows;
using System.Windows.Media;
namespace Bookshelf.Desktop.Reporting;
public static class SpineLabel
{
public static DrawingVisual Create(string title, string author, double pixelsPerDip)
{
var typeface = new Typeface("Palatino Linotype");
var visual = new DrawingVisual();
using (DrawingContext context = visual.RenderOpen())
{
var heading = new FormattedText(
title,
CultureInfo.CurrentUICulture,
FlowDirection.LeftToRight,
typeface,
emSize: 20,
foreground: Brushes.Black,
pixelsPerDip: pixelsPerDip)
{
MaxTextWidth = 220,
MaxTextHeight = 60,
};
var byline = new FormattedText(
author,
CultureInfo.CurrentUICulture,
FlowDirection.LeftToRight,
typeface,
emSize: 13,
foreground: Brushes.DimGray,
pixelsPerDip: pixelsPerDip);
context.DrawRectangle(null, new Pen(Brushes.Black, 1), new Rect(0, 0, 240, 120));
context.DrawText(heading, new Point(10, 8));
context.DrawText(byline, new Point(10, 80));
}
return visual;
}
}
// Usage (from a window):
// var dpi = VisualTreeHelper.GetDpi(this).PixelsPerDip;
// printService.PrintVisual(SpineLabel.Create("Dune", "Frank Herbert", dpi), "Spine label");
Source: FormattedText constructors
The FormattedText constructors without a pixelsPerDip argument are marked obsolete in the API reference (message:
use the PixelsPerDip override), so new code uses the overload shown. MaxTextWidth and MaxTextHeight make the text
end with an ellipsis when it overflows, as the performance page describes.
Typography and OpenType
Text in WPF is rendered with ClearType and can be hardware accelerated. On top of that, the Typography object
exposes OpenType features through attached properties that work on any element that has text properties: Run,
Paragraph, TextBlock, and the controls. Most OpenType fonts expose only some of the features, and a property has no
visible effect if the font does not implement it.
| Property | Values (default) | Effect |
|---|---|---|
|
|
Capital forms; with |
|
|
More space between letters of all-capital text |
|
|
fi, fl, ff joined into one glyph; set |
|
|
Ornamental ligatures |
|
|
Pair kerning |
|
|
Fraction forms |
|
|
Lining or old-style figures |
|
|
Tabular figures have equal widths and align in columns |
|
|
A zero with a slash, to tell it from the letter O |
|
|
Superscripts and subscripts from the font |
|
|
Alternate glyph sets, selected by number |
|
|
Font-defined stylistic sets |
|
|
Decorative swash glyphs |
|
|
Context-dependent alternates |
The same properties are exposed on the object’s Typography property in code, for example
run.Typography.Capitals = FontCapitals.SmallCaps;.
Example: A Price Column and a Title Style
Bookshelf Desktop’s report shows list prices. Tabular numerals make the digits line up; small capitals give the title a book-like look; a superscript marks an edition.
<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
FontFamily="Palatino Linotype" FontSize="16">
<Paragraph FontSize="28">
<Run Typography.Capitals="SmallCaps">Bookshelf price list</Run>
</Paragraph>
<Paragraph>
Dune, 2<Run Typography.Variants="Superscript">nd</Run> edition
</Paragraph>
<Table>
<Table.Columns>
<TableColumn Width="200" />
<TableColumn Width="100" />
</Table.Columns>
<TableRowGroup Typography.NumeralAlignment="Tabular">
<TableRow>
<TableCell><Paragraph>Dune</Paragraph></TableCell>
<TableCell><Paragraph TextAlignment="Right">12.50</Paragraph></TableCell>
</TableRow>
<TableRow>
<TableCell><Paragraph>Neuromancer</Paragraph></TableCell>
<TableCell><Paragraph TextAlignment="Right">111.00</Paragraph></TableCell>
</TableRow>
</TableRowGroup>
</Table>
<Paragraph Typography.Fraction="Slashed">Half price: 1/2, three quarters: 3/4</Paragraph>
<Paragraph Typography.StandardLigatures="False">Ligatures off: fi ff fl</Paragraph>
</FlowDocument>
Source: OpenType Font Features
Each property used here appears in the official page’s XAML samples (small capitals, superscripts, fractions, tabular numerals and disabling standard ligatures), with Palatino Linotype used in several of them. The XAML above combines them into one document and was not run against the font on a target machine.
Beyond Controls: Glyphs, FormattedText and TextFormatter
Text rendering has three layers in WPF. The highest uses controls such as TextBlock and FlowDocument. The middle
layer is FormattedText, which draws multi-line text with per-character formatting through a DrawingContext, with
ellipsis support, and can be converted to Geometry for outlined or filled text effects. The lowest layer is
Glyphs and GlyphRun, designed for fixed-format document presentation and print scenarios, and the most efficient
way to render text. For a custom text editor, the System.Windows.Media.TextFormatting namespace (TextFormatter,
TextSource) lets you implement your own layout. Almost all applications stay on the top layer.
Source: Typography in WPF
Text Rendering
The TextOptions class defines three attached properties that change how text is displayed in an element.
| Attached property | Values | Meaning |
|---|---|---|
|
|
|
|
|
Anti-aliasing algorithm used to draw the text |
|
|
|
Display formatting and Fixed hinting produce the sharpest static text at small sizes on a standard-DPI display, at
the cost of the resolution independence that Ideal gives. For text that moves (a marquee, a scale animation), set
TextHintingMode="Animated" on the animated element. The Auto defaults are right for most applications.
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Crisp small text in a data-dense list on a standard-DPI display. -->
<ListView TextOptions.TextFormattingMode="Display"
TextOptions.TextHintingMode="Fixed" />
<!-- Moving text: favor smooth animation over static quality. -->
<TextBlock Text="Now reading..." TextOptions.TextHintingMode="Animated">
<TextBlock.RenderTransform>
<TranslateTransform x:Name="Slide" />
</TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
Source: TextOptions class
The enum values come from the API reference. The recommendation to use Display with Fixed for small static text is
common practice, not a statement on Microsoft Learn.
Performance Guidance
| Guidance | Reason |
|---|---|
Use |
|
Inside a |
|
Inside a |
Learn’s measurement for 1000 elements: creation 146 ms with a |
Bind frequently updated strings to |
Learn’s measurement: update time 835 ms for |
Group several |
Fewer objects |
Avoid |
Hyphenation uses COM interoperability; optimal paragraph costs layout time |
Declare |
A |
Source: Optimizing Performance: Text
The timings are those published on the page for its test scenario (1000 elements); treat them as relative, not as numbers for your hardware.
Fonts
System Fonts and Fallback
WPF text is Unicode, independent of the system locale. A FontFamily is described by separate weight, stretch and
style, not by Boolean bold and italic flags. When a character is missing from a font, WPF can use composite fonts
(a portable XML description of font linking and fallback), and an incorrect font reference makes the application fall
back to an available font instead of throwing.
Packaging a Font With the Application
Bookshelf Desktop can ship its own typeface for labels. Fonts can be added as Resource items, embedded in the
assembly, or as Content items copied next to the executable. The build action for embedded fonts must be Resource;
EmbeddedResource is not supported.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<!-- Embedded in the assembly. Use Resource, not EmbeddedResource. -->
<Resource Include="Fonts\BookshelfSerif-Regular.ttf" />
<Resource Include="Fonts\BookshelfSerif-Bold.ttf" />
</ItemGroup>
</Project>
<TextBlock xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
FontFamily="./Fonts/#Bookshelf Serif"
Text="Aegean Sea" />
using System.Windows.Controls;
using System.Windows.Media;
namespace Bookshelf.Desktop;
public static class LabelFonts
{
// Two-part reference: a base URI plus a relative location with the family name after '#'.
public static void Apply(TextBlock textBlock)
{
textBlock.FontFamily = new FontFamily(new Uri("pack://application:,,,/"), "./Fonts/#Bookshelf Serif");
}
public static IEnumerable<FontFamily> Packaged()
=> Fonts.GetFontFamilies(new Uri("pack://application:,,,/"), "./Fonts/");
}
The font file names and the family name "Bookshelf Serif" are placeholders for a font you have the right to
distribute. The official page lists these limits: WPF does not check or enforce the font embedding permission bits,
does not accept http or ftp font references, does not accept an absolute pack: URI as the string in a
FontFamily object created in code, has no design-time support for finding fonts and embedding them automatically,
and does not support font subsets for non-fixed documents. A resource-only font library is referenced with
/LibraryName;Component/#Family.
WPF, WinUI 3 and WinForms
| WPF | WinUI 3 (Windows App SDK) | WinForms | |
|---|---|---|---|
Rich text editing |
|
|
|
Read-only rich text |
|
|
|
Flow and fixed documents |
|
Not available |
Not available |
XPS |
Native: |
Not part of the documented UI API |
Not part of the framework |
Printing API |
|
|
|
Print UI |
Standard Windows print dialog ( |
System print UI from |
Standard print dialog |
Typography |
|
Different text model; the WPF |
|
Two consequences follow. Code that builds a FlowDocument for display and printing is WPF only; moving the same
report to WinUI 3 means drawing pages as UIElement trees in the Paginate handler. And a report library that must
serve all three stacks should keep the report as data and put the rendering in each stack’s own project.
Printing in WinUI 3
The WinUI printing flow differs from the WPF dialog-driven one. The app registers for printing on the window (using
the window handle, so after the window exists), creates a PrintDocument, and handles three events: Paginate
(build the pages and report the count), GetPreviewPage (supply the page for the preview) and AddPages (supply the
final pages and call AddPagesComplete).
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Printing;
using Windows.Graphics.Printing;
namespace Bookshelf.WinUI;
public sealed partial class ReportPage : Microsoft.UI.Xaml.Controls.Page
{
private PrintDocument? printDocument;
private IPrintDocumentSource? printDocumentSource;
private readonly List<UIElement> printPages = [];
private void RegisterForPrinting()
{
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
PrintManager printManager = PrintManagerInterop.GetForWindow(hWnd);
printManager.PrintTaskRequested += OnPrintTaskRequested;
printDocument = new PrintDocument();
printDocumentSource = printDocument.DocumentSource;
printDocument.Paginate += OnPaginate;
printDocument.GetPreviewPage += OnGetPreviewPage;
printDocument.AddPages += OnAddPages;
}
private async void OnPrintClicked(object sender, RoutedEventArgs e)
{
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
await PrintManagerInterop.ShowPrintUIForWindowAsync(hWnd);
}
private void OnPrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs args)
{
args.Request.CreatePrintTask("Bookshelf report", sourceArgs => sourceArgs.SetSource(printDocumentSource));
}
private void OnPaginate(object sender, PaginateEventArgs e)
{
printPages.Clear();
var options = (PrintTaskOptions)e.PrintTaskOptions;
PrintPageDescription page = options.GetPageDescription(0);
// One page: a panel sized to the printer page. Real reports flow content over several pages here.
var layout = new Microsoft.UI.Xaml.Controls.StackPanel { Width = page.PageSize.Width, Height = page.PageSize.Height };
printPages.Add(layout);
((PrintDocument)sender).SetPreviewPageCount(printPages.Count, PreviewPageCountType.Intermediate);
}
private void OnGetPreviewPage(object sender, GetPreviewPageEventArgs e)
=> ((PrintDocument)sender).SetPreviewPage(e.PageNumber, printPages[e.PageNumber - 1]);
private void OnAddPages(object sender, AddPagesEventArgs e)
{
var document = (PrintDocument)sender;
foreach (UIElement page in printPages)
{
document.AddPage(page);
}
document.AddPagesComplete();
}
}
Source: Print from your app
The structure is that of the official page, shortened: the official sample also unregisters the handlers in
OnNavigatedFrom (otherwise an exception is thrown when the user leaves the page and returns), guards with
PrintManager.IsSupported() and reports PrintTask.Completed. Register from the Loaded event, not from
OnNavigatedTo, because the window handle can still be null there. Re-check the settings before reflowing in
Paginate, because it is called again whenever the user changes a print setting.
Printing in WinForms
WinForms prints through the PrintDocument component. Print() raises PrintPage, which draws one page with the
Graphics from PrintPageEventArgs; set HasMorePages to true while pages remain.
using System.Drawing;
using System.Drawing.Printing;
namespace Bookshelf.WinForms;
public partial class ReportForm : Form
{
private string stringToPrint = "";
private void OnPrintClicked(object? sender, EventArgs e)
{
printDocument1.DocumentName = "Bookshelf report";
stringToPrint = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "report.txt"));
printDocument1.Print();
}
private void OnPrintPage(object? sender, PrintPageEventArgs e)
{
int charactersOnPage = 0;
int linesPerPage = 0;
// How much of the remaining text fits within the page margins.
e.Graphics!.MeasureString(stringToPrint, Font, e.MarginBounds.Size, StringFormat.GenericTypographic,
out charactersOnPage, out linesPerPage);
e.Graphics.DrawString(stringToPrint, Font, Brushes.Black, e.MarginBounds, StringFormat.GenericTypographic);
stringToPrint = stringToPrint[charactersOnPage..];
e.HasMorePages = stringToPrint.Length > 0;
}
}
The method in the official page does not support printing non-plain text such as a Word document or a PDF. It uses
Graphics.DrawString for printing because the DrawText methods of TextRenderer are not supported for printing.
The null-forgiving ! and the range operator [charactersOnPage..] are the only changes from the official sample;
they follow current C# with nullable enabled.
Rich Text in WinUI 3
RichEditBox is for editing documents that live outside the app, typically .rtf files. Its Document property is an
ITextDocument, unlike RichTextBlock, which uses Block objects, and the document is loaded and saved to a stream.
Spell checking is on by default and is turned off with IsSpellCheckEnabled.
using Microsoft.UI.Text;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Bookshelf.WinUI;
public static class NotesStorage
{
public static async Task LoadAsync(Microsoft.UI.Xaml.Controls.RichEditBox editor, StorageFile file)
{
using IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.Read);
editor.Document.LoadFromStream(TextSetOptions.FormatRtf, stream);
}
public static async Task SaveAsync(Microsoft.UI.Xaml.Controls.RichEditBox editor, StorageFile file)
{
using IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.ReadWrite);
editor.Document.SaveToStream(TextGetOptions.FormatRtf, stream);
}
}
Source: RichEditBox
The official sample uses a file picker and CachedFileManager around the same two calls; they are omitted here.
In a packaged desktop app, file pickers also need the window handle, which the picker pages of the Windows App SDK
documentation explain.
Pitfalls
-
Printing the document that the viewer is showing. Setting
PageSizeon the paginator of the displayedFlowDocumentchanges its layout in the reader. Print a copy of the document (build a second one from the same data with the builder, or duplicate the content), not the instance on screen. This is common guidance; the Learn pages cited here do not discuss it. -
RichTextBox.Documentis not bindable. It is not a dependency property. Load and save it from code-behind or a behavior, and expose a command to the view model, not the document. -
Choosing
FlowDocumentReaderby default. It is the most resource intensive viewer. UseFlowDocumentScrollViewerorFlowDocumentPageViewerwhen the view mode is fixed. -
TextBlockinside aFlowDocument. It works but costs more thanRun; keepTextBlockfor UI text. -
Enabling hyphenation or optimal paragraphs everywhere. Both are off by default for a reason; turn them on only for long, justified reading text.
-
Using
Labelfor changing text. BindingLabel.Contentto a frequently changing string is several times slower thanTextBlock.Textin Learn’s measurement. -
Forgetting
pixelsPerDip. TheFormattedTextconstructors without it are obsolete, and text drawn with a wrong value is blurry on high-DPI screens. Get the value withVisualTreeHelper.GetDpi(visual).PixelsPerDip. -
Fixed sizes in device-independent units. An A4 page is about 793.7 by 1122.5 units at 96 units per inch. Using pixels from a 150 DPI measurement makes the page too large.
-
Printer-specific tickets. A
PrintTicketmust be built from the capabilities of the queue it will be sent to; setting duplex or stapling on a printer that cannot do it is a mistake. CheckPrintCapabilitiesfirst, as the official sample does. -
Blocking the UI thread. The synchronous
PrintDocumentandXpsDocumentWriter.Writecalls block while pages are produced. UseWriteAsyncfor large reports. -
The reflection workaround in the page-range sample. It relies on private members of WPF types; test it with every .NET release you ship on.
-
Font licensing. WPF does not check font embedding permission bits. Embedding a font you are not licensed to redistribute is your responsibility, and the fonts supplied with Windows generally may not be redistributed.
-
EmbeddedResourcefor fonts. UseResource; the other build action is not supported. -
pack:URIs in aFontFamilystring. An absolutepack:URI as the family string is invalid; use the two-part form with a baseUriand a relative reference. -
Assuming OpenType features exist. A
Typographyproperty that the font does not implement has no effect. Check with the font and keep a readable fallback. -
XPS Viewer availability. Do not tell users to open the exported
.xpsfile in a viewer you do not control; display it in aDocumentViewerin your own app, or export a format your users already have. -
Trimming and AOT. WPF and WinForms do not support Native AOT or trimming (see the Windows App Platforms page of this section), so the reflection in the page-range workaround is not a restriction today.