This article is Day #20 in a series called 31 Days of Windows 8. Each of the articles in this series will be published for both HTML5/JS and XAML/C#. You can find additional resources, downloads, and source code on our website.
Today, we’re going to talk about printing from your Windows 8 app. In my past as a software developer, the only time I’ve ever been concerned with printing was from a web page. It was a simple call: window.print() and several things happened:
- A print dialog box was opened with options for the user.
- A print preview was created.
- Pagination options were available based on the actual “length” of my page.
Despite all of my experience with XAML over the years, printing is not something I have had to do. As such, I was surprised at how much code is necessary to print a simple page, but I certainly understand why it happens. We will have to create all of those things from code, and for good reason.
Our pages are not laid out for the size or shape of paper.
Therefore, we are going to need to supplement a “printable” version of our page behind the scenes to make this easier for ourselves. For the example today, I’m using a simple page with some RichTextBlock controls, but your page can be as complex as you’d like. Just optimize it for the shape of a sheet of paper.
Additionally, writing this article brought me to the realization that printing from XAML/C# apps in Windows 8 is just hard. Not only will we be handling all of the steps required to generate print previews and the like, we also have to manually handle all of the pagination required to make our document print appropriately. This is definitely a situation where the HTML5/JS crew has an advantage over us, and it made me physically angry about how convoluted this process really is.
Making Printing Available From Your App
If you’ve tried to print already, you’ve probably discovered that it’s not possible from your base Windows 8 application. We have to create a PrintManager object on our page first. This is not something we can do at the application level, so each page that you might want to print from will have to repeat this code. (I’m putting this inside my OnNavigatedTo and OnNavigatingFrom events…you can put this logic behind a click if you’d prefer.)
PrintManager manager = PrintManager.GetForCurrentView();
manager.PrintTaskRequested += manager_PrintTaskRequested;
By adding this code (and the subsequent event method, PrintTastRequested) to our page, it takes our user’s device charm menu from this:
To this:
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
I’m currently writing this article from a location where I don’t have any printers available to me, but the same print options are available to us when “printing” to OneNote or the Microsoft XPS Document Writer. Using those is a perfectly acceptable test, and it’s environmentally friendly. When working on printing for your app, please use one of these options over printing many, many pieces of paper. It’s just wasteful, and the electronic documents will look exactly the same.
Making Printing Happen in 27 Easy Steps
If you added the two lines of code above and attempted to print, you were probably disappointed. First, you couldn’t run the app. We didn’t build the event handler yet. Second, if you did create the event handler, you got an error the moment you tapped a printer to use.
First, we have a couple of global variables we’ll be using:
PrintDocument document = null;
IPrintDocumentSource source = null;
List<UIElement> pages = null;
FrameworkElement page1;
protected event EventHandler pagesCreated;
protected const double left = 0.075;
protected const double top = 0.03;
Let’s explain what each of these are for now, as we’ll see them in much of the code we have left to write.
- PrintDocument – this is the actual document object we’ll be adding our content to.
- IPrintDocumentSource – this is the source of our PrintDocument object.
- List<UIElement> – this will be where we store the individual pages of our printable document after we have determined the pagination.
- FrameworkElement – this will hold our first page of our document.
- protected event – this will be used to determine if we have actually created any pages in this process.
- left, top – these constants will be used to define a margin in our page, so the content doesn’t run right to the edge of our page.
Let’s first look at my OnNavigatedTo event handler, where I get everything set up:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
document = new PrintDocument();
source = document.DocumentSource;
document.Paginate += printDocument_Paginate;
document.GetPreviewPage += printDocument_GetPreviewPage;
document.AddPages += printDocument_AddPages;
PrintManager manager = PrintManager.GetForCurrentView();
manager.PrintTaskRequested += manager_PrintTaskRequested;
pages = new List<UIElement>();
PrepareContent();
}
First, we instantiate our document and source objects, and then create three new event handlers for our document, Paginate, GetPreviewPage, and AddPages. Finally, we create our PrintManager (the snippet from earlier), and as two last steps, we create our pages List<> and call a method PrepareContent(). We will revisit PrepareContent a little later. For now, we have to look at first event handler.
void manager_PrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs args)
{
PrintTask task = null;
task = args.Request.CreatePrintTask("Simple Print Job", sourceRequested =>
{
sourceRequested.SetSource(source);
});
}
When the user selects a printer, the method above will be fired. Inside, we create a PrintTask object, where we assign it a name, and set its source to our source object that we created earlier. The name is the actual value that will appear when you user looks in their print queue, like this:
Once a printer has been selected, they will see a new window that looks like this:
You should notice a couple of things:
- We haven’t even specified what to print yet.
- The Print Preview box seems to be struggling to show something.
Let’s fix the first problem, which is a lack of content. Here’s my PrepareContent() method from earlier:
private void PrepareContent()
{
if (page1 == null)
{
page1 = new PageForPrinting();
StackPanel header = (StackPanel)page1.FindName("header");
header.Visibility = Windows.UI.Xaml.Visibility.Visible;
}
PrintContainer.Children.Add(page1);
PrintContainer.InvalidateMeasure();
PrintContainer.UpdateLayout();
}
There are several important things happening in this method. First, we’re assigning a new object, PageForPrinting to our page1 element. PageForPrinting is actually a completely seperate XAML file in our application that is a formatted page for printing. Second, you’ll see that we’re actually reaching into PageForPrinting, and looking for an element named “header.” (This is entirely optional, but it’s a useful illustration.) This is a StackPanel in our formatted page that is only displayed when the page is printed. This is a great way to add custom headers and footers to pages, especially when you don’t want them shown when the page is displayed on-screen. Finally, you can see that we’re adding our page1 to an element named PrintContainer. We actually want to commit our page to the existing visual tree to force it to go through layout so that all of the containers distribute the content within them.
Here’s what our sample PageForPrinting.xaml file looks like:
<Page
x:Class="Day20_Printing.PageForPrinting"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Day20_Printing"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid x:Name="printableArea">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="3*"/>
<RowDefinition Height="3*"/>
<RowDefinition Height="4*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="6*" />
<ColumnDefinition Width="4*"/>
</Grid.ColumnDefinitions>
<StackPanel x:Name="header" Grid.Row="0" Grid.ColumnSpan="2" Height="60" Visibility="Collapsed">
<StackPanel Orientation="Horizontal" >
<Image Source="ms-appx:///Assets/StoreLogo.png" HorizontalAlignment="Left" Stretch="None"/>
<RichTextBlock Foreground="Black" FontSize="20" TextAlignment="Left" FontFamily="Segoe UI">
<Paragraph>Day #20 - Printing</Paragraph>
</RichTextBlock>
</StackPanel>
</StackPanel>
<RichTextBlock Foreground="Black" x:Name="textContent" FontSize="18" Grid.Row="1" Grid.ColumnSpan="2" OverflowContentTarget="{Binding ElementName=firstLinkedContainer}"
IsTextSelectionEnabled="True" TextAlignment="Left" FontFamily="Segoe UI" VerticalAlignment="Top" HorizontalAlignment="Left">
<Paragraph FontSize="32">Lorem ipsum dolor sit amet, consectetur</Paragraph>
<Paragraph></Paragraph>
<Paragraph >Sed convallis ornare velit et interdum. Donec sapien neque, aliquet consequat convallis at, interdum et enim. Donec iaculis, lectus vel pulvinar cursus, lectus diam interdum ante, a rhoncus tortor quam porta metus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Etiam pulvinar fringilla vestibulum. Pellentesque pharetra nunc in turpis tempus sed faucibus ligula sagittis. Praesent hendrerit est vitae lorem mattis in porttitor urna vestibulum. Phasellus adipiscing aliquam libero ac adipiscing. In a erat sit amet erat sollicitudin bibendum id vitae dui. Vestibulum non consequat nisl. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Mauris elit nisi, blandit et porttitor quis, malesuada nec mi.</Paragraph>
<Paragraph></Paragraph>
<Paragraph >Aliquam erat volutpat. In non urna ut libero ultricies fringilla. Proin tellus neque, aliquam lacinia consequat at, vulputate et arcu. Maecenas odio nunc, lobortis sit amet pulvinar sit amet, accumsan et leo. Suspendisse erat lectus, commodo ac auctor eget, rutrum in mi. Suspendisse potenti. Proin ac elit non lacus rutrum mollis. Vivamus venenatis, tellus vel placerat lacinia, arcu ligula dignissim orci, consectetur consectetur eros massa vel nulla. Quisque malesuada iaculis ornare. Nullam tincidunt accumsan egestas. Mauris sit amet scelerisque arcu. Proin euismod sodales magna faucibus commodo. Nam in fringilla orci. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.</Paragraph>
<Paragraph></Paragraph>
<Paragraph >Sed eget nunc quis tellus interdum aliquet. Suspendisse rhoncus malesuada nisi a imperdiet. Suspendisse ullamcorper mi sed purus tristique interdum. Mauris lobortis, ante ultrices varius consequat, eros ante hendrerit enim, vulputate convallis dui ligula eget velit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec eget lectus fermentum nisi consequat dictum. Sed volutpat justo non purus semper vel pretium enim molestie. Nam consectetur, lectus quis feugiat malesuada, neque nunc faucibus velit, nec vehicula risus est id sapien. Vestibulum ut metus massa, ut placerat lacus. Fusce condimentum vehicula tortor, nec vestibulum ligula iaculis ut. Nulla facilisi. Phasellus tincidunt scelerisque erat, ut fermentum urna pretium eu. Donec ut nibh orci. Curabitur sodales metus dictum mauris varius vitae mollis tellus pulvinar. Quisque facilisis ligula sed risus laoreet non lacinia odio luctus. Nam lobortis rhoncus felis vitae ultrices.</Paragraph>
<Paragraph></Paragraph>
<Paragraph>Aliquam erat volutpat. In non urna ut libero ultricies fringilla. Proin tellus neque, aliquam lacinia consequat at, vulputate et arcu. Maecenas odio nunc, lobortis sit amet pulvinar sit amet, accumsan et leo. Suspendisse erat lectus, commodo ac auctor eget, rutrum in mi. Suspendisse potenti. Proin ac elit non lacus rutrum mollis. Vivamus venenatis, tellus vel placerat lacinia, arcu ligula dignissim orci, consectetur consectetur eros massa vel nulla. Quisque malesuada iaculis ornare. Nullam tincidunt accumsan egestas. Mauris sit amet scelerisque arcu. Proin euismod sodales magna faucibus commodo. Nam in fringilla orci. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.</Paragraph>
<Paragraph></Paragraph>
<Paragraph >Sed eget nunc quis tellus interdum aliquet. Suspendisse rhoncus malesuada nisi a imperdiet. Suspendisse ullamcorper mi sed purus tristique interdum. Mauris lobortis, ante ultrices varius consequat, eros ante hendrerit enim, vulputate convallis dui ligula eget velit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec eget lectus fermentum nisi consequat dictum. Sed volutpat justo non purus semper vel pretium enim molestie. Nam consectetur, lectus quis feugiat malesuada, neque nunc faucibus velit, nec vehicula risus est id sapien. Vestibulum ut metus massa, ut placerat lacus. Fusce condimentum vehicula tortor, nec vestibulum ligula iaculis ut. Nulla facilisi. Phasellus tincidunt scelerisque erat, ut fermentum urna pretium eu. Donec ut nibh orci. Curabitur sodales metus dictum mauris varius vitae mollis tellus pulvinar. Quisque facilisis ligula sed risus laoreet non lacinia odio luctus. Nam lobortis rhoncus felis vitae ultrices.</Paragraph>
</RichTextBlock>
<RichTextBlockOverflow x:Name="firstLinkedContainer" OverflowContentTarget="{Binding ElementName=continuationPageLinkedContainer}" Grid.Row="2" Grid.Column="0"/>
<RichTextBlockOverflow x:Name="continuationPageLinkedContainer" Grid.Row="3" Grid.ColumnSpan="2"/>
<Image Source="ms-appx:///Assets/Logo.png" x:Name="scenarioImage" HorizontalAlignment="Right" Grid.Row="2" Grid.Column="1" Margin="10"/>
<StackPanel x:Name="footer" Grid.Row="4" Grid.Column="0" VerticalAlignment="Top" Visibility="Collapsed">
<Image Source="ms-appx:///Assets/StoreLogo.png" HorizontalAlignment="Left" Stretch="None"/>
<RichTextBlock Foreground="Black" FontSize="16" TextAlignment="Left" FontFamily="Segoe UI">
<Paragraph>Copyright © 31 Days of Windows 8. But please reuse this.</Paragraph>
</RichTextBlock>
</StackPanel>
</Grid>
</Page>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
We will be using the RichTextBlockOverflow elements link page breaks. There is nothing changed in the code behind file for this page, but if you were loading this content dynamically, there certainly would be.
At this point, we have handled the user’s choice of printer, and we’ve pulled in a formatted page for printing. Our next step is to handle the other three event handlers that we’ve created, starting with the Paginate one:
void printDocument_Paginate(object sender, PaginateEventArgs e)
{
pages.Clear();
PrintContainer.Children.Clear();
RichTextBlockOverflow lastRTBOOnPage;
PrintTaskOptions printingOptions = ((PrintTaskOptions)e.PrintTaskOptions);
PrintPageDescription pageDescription = printingOptions.GetPageDescription(0);
lastRTBOOnPage = AddOnePrintPreviewPage(null, pageDescription);
while (lastRTBOOnPage.HasOverflowContent && lastRTBOOnPage.Visibility == Windows.UI.Xaml.Visibility.Visible)
{
lastRTBOOnPage = AddOnePrintPreviewPage(lastRTBOOnPage, pageDescription);
}
if (pagesCreated != null)
{
pagesCreated.Invoke(pages, null);
}
PrintDocument printDoc = (PrintDocument)sender;
printDoc.SetPreviewPageCount(pages.Count, PreviewPageCountType.Intermediate);
}
Each time that this method is called, we want to clear our “pages” list, as well as anything we may have committed to the PrintContainer object that is on our MainPage. (It’s just a canvas with an Opacity set to zero, so that we can add content to it, but the user doesn’t see it.)
We’re also creating a RichTextBlockOverflow object to keep track of which page we’re currently on. Finally, we’re calling a method called AddOnePrintPreviewPage. This method does all of the heavy lifting for us, calculating the size of the page, setting margins, and saving them to the pages List<>. You’ll also see that we activate the footer in our page as well.
private RichTextBlockOverflow AddOnePrintPreviewPage(RichTextBlockOverflow lastRTBOAdded, PrintPageDescription printPageDescription)
{
FrameworkElement page;
RichTextBlockOverflow link;
if (lastRTBOAdded == null)
{
page = page1;
StackPanel footer = (StackPanel)page.FindName("footer");
footer.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
}
else
{
page = new ContinuationPage(lastRTBOAdded);
}
page.Width = printPageDescription.PageSize.Width;
page.Height = printPageDescription.PageSize.Height;
Grid printableArea = (Grid)page.FindName("printableArea");
double marginWidth = Math.Max(printPageDescription.PageSize.Width - printPageDescription.ImageableRect.Width, printPageDescription.PageSize.Width * left * 2);
double marginHeight = Math.Max(printPageDescription.PageSize.Height - printPageDescription.ImageableRect.Height, printPageDescription.PageSize.Height * top * 2);
printableArea.Width = page1.Width - marginWidth;
printableArea.Height = page1.Height - marginHeight;
PrintContainer.Children.Add(page);
PrintContainer.InvalidateMeasure();
PrintContainer.UpdateLayout();
// Find the last text container and see if the content is overflowing
link = (RichTextBlockOverflow)page.FindName("continuationPageLinkedContainer");
// Check if this is the last page
if (!link.HasOverflowContent && link.Visibility == Windows.UI.Xaml.Visibility.Visible)
{
StackPanel footer = (StackPanel)page.FindName("footer");
footer.Visibility = Windows.UI.Xaml.Visibility.Visible;
}
// Add the page to the page preview collection
pages.Add(page);
return link;
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
If another RichTextBlockOverflow element is found, it is returned to our calling method, and we loop through this process until we’ve added all of the pages to our List<>.
Next, we need to add those pages to our original PrintDocument object, and we do this with the AddPages event we registered at the beginning. We simply loop through all of the pages that were created in our Paginate method.
void printDocument_AddPages(object sender, AddPagesEventArgs e)
{
for (int i = 0; i < pages.Count; i++)
{
document.AddPage(pages[i]);
}
PrintDocument printDoc = (PrintDocument)sender;
printDoc.AddPagesComplete();
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
When that Print Preview box loads on the user’s screen, the GetPreviewPage is fired. This allows us to provide the first page of the set to the Print Preview window:
void printDocument_GetPreviewPage(object sender, GetPreviewPageEventArgs e)
{
PrintDocument printDoc = (PrintDocument)sender;
printDoc.SetPreviewPage(e.PageNumber, pages[e.PageNumber - 1]);
}
This is the last step. Once we provide our preview pages, we’re done. Your user should now see a menu like this:
And if they click Print when using the Microsoft XPS Document Writer, they end up with a file in their Documents folder:
Finally, when we open that file, we can see that our formatted content has been spread over two pages in this new document, just as it would have been on a printer.
Summary
In short, this is a really messy, difficult process that seems like it HAS to be simpler than it is. Sadly, I’ve made this one about as simple as it gets for now. I think I am going to revisit this problem in the future, and build out a PrintHelper class that just allows you to build a page, formatted with a few simple rules, and call a method to make all of this magic happen. Very few people will build apps that print if it isn’t made to be easier.
If you’d like to download a working sample of the code shown in this article, click the icon below (I would HIGHLY recommend it for this topic):
Tomorrow, we’re going to touch on a more useful (and far simpler to implement) subject: capturing data from the camera. See you then!
Hi Jeff,
Firstly I would like to say thanks for the 31 Day series. It has been a huge help.
Is it possible to print a dynamic grid over multiple pages without using a RTB overflow method as per the tut ?
Thanks
I probably spent about 25% of my time trying to make printing work on my app (TenCalc). I got it there, but am not at all happy with the effort required to do something that should be far simpler. Among other things, I never did get the RTBO stuff to work consistently/correctly. I’m doing multi-column, multi-page. Ugly. Finally had to revert back to using RichTextBlocks and figuring out my own pagination. Did I say ugly? Anything you can do in the future to help simplify this mess would be much appreciated.
Can you please help me to print RichEditBox content ? Please note RichEditBox content is formatted by user. Thanks.
Hi Jeff,
I want to display Word files in WP7.
I download it’s array of bytes from server.
How can I do it ?
Thanks in Advance.
is it possible to print two bills at a time ???