Saturday, 15 November 2014

Printing

Using the Print contract requires Seven main steps:
  • A reference to a PrintManager instance for each view that you want users to be able to print.
  • Implement a PrintTask instance representing the actual printing operation.
  • Create a PrintDocument instance to hold a reference to the content that you want to print and handle the events raised during the printing process.
    • Calculate how many pages you need and distribute the content among them
    • Render a preview
    • Print your pages
  • Finally you have to deference all of the events you made before navigating away from the page.
To get started let's create a simple UI

<Page
    x:Class="pc.print.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:pc.print"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid x:Name="MainLayout" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel Margin="0 100">
        <TextBox x:Name="Data_TextBox" Height="500" TextWrapping="Wrap" 
                 Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce in metus dui, a scelerisque neque. Morbi eget sapien lectus, hendrerit semper orci. Donec elit sem, pharetra in ornare ac, dictum sed massa. Donec rhoncus consequat urna. Sed non enim ut quam aliquet adipiscing. Ut a enim a sem blandit lobortis. Donec non volutpat orci. In at massa nunc, vel lobortis leo. Fusce vel erat sit amet justo sollicitudin varius" />
            <Button Content="Click" Click="Print_BTN_Click" />
        </StackPanel>
    </Grid>   
</Page>

With that done lets take a look at our codebehind, I've gone ahead and added some of the initital steps

using System;
using Windows.Graphics.Printing;
using Windows.UI.Xaml.Controls;

namespace pc.print
{
    public sealed partial class MainPage : Page
    {
        PrintManager _printManager;
        PrintDocument _printDocument; 

        public MainPage() {
            this.InitializeComponent();

            _printDocument = new PrintDocument();
           
            _printManager = PrintManager.GetForCurrentView();
            _printManager.PrintTaskRequested += PrintManager_PrintTaskRequested;
        }

        private void PrintManager_PrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs args) {
            throw new NotImplementedException();
        }



        //Show printUI
        async void Print_BTN_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
        {
            await PrintManager.ShowPrintUIAsync();

        }
    }
}

Above we referenced the print manager and created a PrinterTask, now to test this we run our app and from the charms bar we selected Devices->Print; now this will fire our PrinteTaskRequested event which we subscribted the PrinterManager_PrintTaskRequested event handler, so this block will be the one that fires. Currently it just throws an exception, but let's change that

//print UI called
void PrintManager_PrintTaskRequested(PrintManager sender,
                                    
PrintTaskRequestedEventArgs args)
{
    PrintTask pt = args.Request.CreatePrintTask("Test", async taskArgs =>
    {  
        //create a deferral, because the document to print can only be set on the UI thread
        var deferral = taskArgs.GetDeferral();

        await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
        {
            taskArgs.SetSource(_printDocument.DocumentSource);
            deferral.Complete();
        });
    });

}

We create a printer task and grab a deferral, this is because the document to print can only be set on the UI thread, now at this point we don't accomplish very much at all, but we do get the PrintUI to show up.

No preview but at least we're heading in the right direction, so lets get this thing printing, because lets face it a preview is nice, but printing is a tad more important, don't worry we'll go back and set the preview up but for now lets get something on paper.

what we are going to have to do is create a page level element called PrintDocument and attach a handler to deal with passing pages to the printing API, the one restriction is that whatever we send to print has to inherit form FrameworkElement, which is just about every UI Element.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using Windows.Graphics.Printing;
using Windows.UI;
using Windows.UI.Core;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Printing;

namespace pc.print
{
    public sealed partial class MainPage : Page
    {
        PrintManager _printManager;
        PrintDocument _printDocument;

        public MainPage()
        {
            this.InitializeComponent();

            _printDocument = new PrintDocument();
            _printDocument.AddPages += _printDocument_AddPages;

            _printManager = PrintManager.GetForCurrentView();
            _printManager.PrintTaskRequested += PrintManager_PrintTaskRequested;
        }

        //Show printUI
        async void Print_BTN_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
        {
            await PrintManager.ShowPrintUIAsync();
        }

        //print UI called
        void PrintManager_PrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs args)
        {
            PrintTask pt = args.Request.CreatePrintTask("Test", async taskArgs =>
            {
                //create a deferral, because the document to print can only be set on
                //the UI thread
                var deferral = taskArgs.GetDeferral();

                await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
                {
                    taskArgs.SetSource(_printDocument.DocumentSource);
                    deferral.Complete();
                });
            });
        }

        //print command fired
        void _printDocument_AddPages(object sender, AddPagesEventArgs e)
        {
            //add the pages that will be printed, in this case a TextBlock that contains our Data
            _printDocument.AddPage(new TextBlock
            {
                Foreground = new SolidColorBrush(Colors.Black),
                Text = this.Data_TextBox.Text,
                FontSize = 75,
                TextWrapping = TextWrapping.Wrap
            });
            _printDocument.AddPagesComplete();
        }
    }
}


now Lets take a look at our last event handler, the AddPages one, so the print manager does the printing and the print document is what's printed. We have to add pages to our pint document, this event is raised once we hit the print button on the printUI (seen earlier). now if you hit the print button, you'll see a toast notification with your file saved (if your using xps)

if you click on the toast, you'll see you'r file printed (saved in xps viewer)

It looks awful, and it's cut off at the bottom and we have no preview, but at least we're printing.

so lets' try getting our preview to work; it's actually fairly easy first off in our page initialization we have to give the PrintDoucment's event GetPreviewPage a handler

public MainPage()
{
    this.InitializeComponent();

    _printDocument = new PrintDocument();
    //Added Preview Event Handler
    _printDocument.GetPreviewPage += _printDocument_GetPreviewPage;
    _printDocument.AddPages += _printDocument_AddPages;

    _printManager = PrintManager.GetForCurrentView();
    _printManager.PrintTaskRequested += PrintManager_PrintTaskRequested;

}

with that done we create the handler

//generate preview for print
void _printDocument_GetPreviewPage(object sender, GetPreviewPageEventArgs e)
{
    //set the preview element
    _printDocument.SetPreviewPage(e.PageNumber, new TextBlock
    {
        Foreground = new SolidColorBrush(Colors.Black),
        Text = this.Data_TextBox.Text,
        FontSize = 75,
        TextWrapping = TextWrapping.Wrap
    });

}
now when we select our device from the print UI, we get an actual preview


however as you see above we're duplicating code, which is never a good thing, we're basically creating the same TextBlock to preview as we do to print, now we could very well create a page level text block and build it in the preview and then reuse it for the add pages method, which we will, but not till we handle this paging issue first.

before we get stated know up front that this will not be a demonstration in an optimal sizing strategy, this is just to demonstrate creating multiple pages for printing. so firstly let's create a page level list of TextBlocks that we can use for our preview and for our print methods, once that's done lets' add the pagination event to our PrintDocument object.

PrintManager _printManager;
PrintDocument _printDocument;
List<TextBlock> _tb;

public MainPage()
{
    this.InitializeComponent();

    _printDocument = new PrintDocument();
    _printDocument.Paginate += _printDocument_Paginate;
    _printDocument.GetPreviewPage += _printDocument_GetPreviewPage;
    _printDocument.AddPages += _printDocument_AddPages;

    _printManager = PrintManager.GetForCurrentView();
    _printManager.PrintTaskRequested += PrintManager_PrintTaskRequested;

}

with that done let's take a look at our event handler.

//Printer selected
void _printDocument_Paginate(object sender, PaginateEventArgs e)
{
    //get printer details, page widht/height, etc
    PrintTaskOptions printingOptions = e.PrintTaskOptions;
    PrintPageDescription pageDescription = printingOptions.GetPageDescription(0);

    //load all text data into our virtual textblock to print
    var page = new TextBlock
    {
        Foreground = new SolidColorBrush(Colors.Black),
        Text = this.Data_TextBox.Text,
        FontSize = 95,
        TextWrapping = TextWrapping.Wrap
    };

    //Create page boundries
    var size = new Size(pageDescription.PageSize.Width, pageDescription.PageSize.Height);

    //recursive function to spread text data over multiple TextBlocks
    _tb = ParsePages(page, new List<TextBlock>(), size);

    //set how many pages in the preview
    _printDocument.SetPreviewPageCount(_tb.Count, PreviewPageCountType.Intermediate);

}

lets dig into our parse pages function

List<TextBlock> ParsePages(TextBlock current, List<TextBlock> result, Size pageSize)
{
    //allow us to get our currnet textblocks actualHeight with text filled in
    current.Measure(pageSize);

    //check if there's an overflow
    if (current.ActualHeight - pageSize.Height <= 0)
    {
        result.Add(current);
        return result;
    }

    //push the overflow to another textblock
    var overflow = new TextBlock
    {
        Foreground = new SolidColorBrush(Colors.Black),
        FontSize = 95,
        TextWrapping = TextWrapping.Wrap,
        Text = current.Text.Substring(100)
    };

    //restrict the current page to the first 100 chars
    current.Text = current.Text.Substring(0, 100);

    //add the current page to the result set
    result.Add(current);

    //call the function again
    return ParsePages(overflow, result, pageSize);

}

with that done lets take a look at our renderPreview Method once again

//generate preview for print
void _printDocument_GetPreviewPage(object sender, GetPreviewPageEventArgs e)
{
    //set the preview element
    var count = e.PageNumber;
    foreach (var p in _tb)
        _printDocument.SetPreviewPage(count++, p);

}

with that complete let's take a look at our actual print method

//print command fired
void _printDocument_AddPages(object sender, AddPagesEventArgs e)
{
    //add the pages that will be printed
    foreach (var p in _tb)
    {
        var txt = p.Text;
        _printDocument.AddPage(p);
    }
    _printDocument.AddPagesComplete();

}

and that's it, you're not printing a multi page document.


One final thought, if you'r application nagivates away from this page make sure to de-reference all of the event handlers you referenced in the constructor for the PrintDocument and PrintManager.