Silverlight 4 Rough Notes: Printing

Note – these posts are put together after a short time with Silverlight 4 as a way of providing pointers to some of the new features that Silverlight 4 has to offer. I’m posting these from the PDC as Silverlight 4 is announced for the first time so please bear that in mind when working through these posts.

Printing in Silverlight must be one of the most asked for features. People mention it all the time and for business applications it’s a must.

Today, it’s possible to print the browser window containing the Silverlight content (and any HTML surrounding it) and that works pretty well but there’s no way to actually hook into any printing process with your own code – Silverlight 4 changes that.

There’s a new namespace in Silverlight 4 – System.Windows.Printing where you’ll find a few classes – the most important of which is the new PrintDocument which provides an event driven approach to printing. You construct one of these things, give it a name and tell it to print and then it calls you back to find out what it is you’d like to print.

The callbacks are;

  • Before printing starts where ( right now ) you’re not provided with a tonne of information in the StartPrintEventArgs but perhaps that’ll grow over time as previews progress.
  • As printing progresses where you are given a PrintPageEventArgs containing some slots;
    • PageVisual – the place to put what you want to have printed
    • PrintableArea – a Size telling you how back the print area on the page is
    • HasMorePages – a boolean slot where you can return whether you have more pages or not
  • When printing ends you’re provided with a EndPrintEventArgs which provides you with an Exception called Error from the printing process.

So, let’s imagine that I want to build a simple Silverlight application that lets you drag-drop a few photos into the browser and then offers you the facility to print them out. With a simple UI;

<UserControl
    x:Class="SilverlightApplication31.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:ctl="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">
    <UserControl.Resources>
    </UserControl.Resources>
    <Grid
        x:Name="LayoutRoot"
        Background="White">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition
                Height="Auto" />
        </Grid.RowDefinitions>
        <ListBox
            AllowDrop="True"
            Drop="OnListBoxDrop"
            Margin="5"
            ItemsSource="{Binding Images}"
            ScrollViewer.HorizontalScrollBarVisibility="Disabled">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border
                        Margin="5"
                        CornerRadius="3"
                        BorderBrush="Gray">
                        <Border.Effect>
                            <DropShadowEffect />
                        </Border.Effect>
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition />
                                <RowDefinition />
                                <RowDefinition />
                            </Grid.RowDefinitions>
                            <Image
                                Width="192"
                                Height="192"
                                Stretch="Fill"
                                Source="{Binding ImageSource}" />
                            <TextBlock
                                Grid.Row="2"
                                HorizontalAlignment="Center"
                                TextAlignment="Center"
                                Text="{Binding ImageName}" />
                        </Grid>
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Grid
            Grid.Row="1"
            Margin="5">
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition />
                <RowDefinition />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <TextBlock
                Text="Image Width" />
            <TextBlock
                Text="Image Height"
                Grid.Row="1" />
            <TextBox
                Grid.Column="1"
                Text="{Binding ImageWidth,Mode=TwoWay}" />
            <TextBox
                Grid.Column="1"
                Grid.Row="1"
                Text="{Binding ImageHeight,Mode=TwoWay}" />
            <Button
                Grid.Row="2"
                Grid.Column="1"
                Content="Print"
                Click="OnPrint" />
        </Grid>
    </Grid>
</UserControl>

and some code behind it;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Printing;
using System.Collections.ObjectModel;
using System.IO;
using System.Windows.Media.Imaging;

namespace SilverlightApplication31
{

  public partial class MainPage : UserControl
  {
    public ObservableCollection<PictureInfo> Images { get; set; }
    public int ImageWidth { get; set; }
    public int ImageHeight { get; set; }

    public MainPage()
    {
      InitializeComponent();

      this.Loaded += (s, e) =>
        {
          Images = new ObservableCollection<PictureInfo>();
          ImageWidth = 192;
          ImageHeight = 192;
          this.DataContext = this;
        };
    }
    void OnListBoxDrop(object sender, DragEventArgs e)
    {
      if (e.Data.GetDataPresent(DataFormats.FileDrop))
      {
        FileInfo[] files = e.Data.GetData(DataFormats.FileDrop) as FileInfo[];

        if (files != null)
        {
          foreach (FileInfo file in files)
          {
            BitmapImage imageSource = new BitmapImage()
            {
              CreateOptions = BitmapCreateOptions.None
            };
            using (Stream stream = file.OpenRead())
            {
              imageSource.SetSource(stream);
              stream.Close();
            }
            Images.Add(new PictureInfo()
            {
              ImageName = file.Name,
              ImageSource = imageSource
            });
          }
        }
      }
    }
    void OnPrint(object sender, RoutedEventArgs e)
    {
    }
  }
}

which gives me a UI that I can drag and drop image files into;

image

Now, what if I wanted to implement that OnPrint functionality and that I want my print view to be different from the screen view in that want to take the values for “Image Width” and “Image Height” from the screen and I want to then print the images at that size with the label stuck underneath them ( just as an example ).

I wrote a little helper class;

  public class PicturePrintJob
  {
    public PicturePrintJob(string name, IEnumerable<PictureInfo> images, 
      int pictureWidth, int pictureHeight)
    {
      this.name = name;
      this.pictureQueue = new Queue<PictureInfo>(images);
      this.pictureWidth = pictureWidth;
      this.pictureHeight = pictureHeight;
    }
    public void Begin(Action<Exception> completedCallback)
    {
      this.completedCallback = completedCallback;

      PrintDocument document = new PrintDocument()
      {
        DocumentName = name
      };
      document.EndPrint += OnEndPrint;
      document.PrintPage += OnPrintPage;
      document.Print();
    }
    void OnPrintPage(object sender, PrintPageEventArgs e)
    {
      if (pictureQueue.Count > 0)
      {
        // Create a canvas matching the area that we've got to print to.
        Canvas pageVisual = new Canvas()
        {
          Width = e.PrintableArea.Width,
          Height = e.PrintableArea.Height ,
          Background = new SolidColorBrush(Colors.Black)
        };

        double startX = 0.0;
        double startY = 0.0;

        while (pictureQueue.Count > 0)
        {
          // See what the next picture looks like.
          PictureInfo nextPictureToPrint = pictureQueue.Peek();

          // Create the UI we want to display for it.
          StackPanel pictureParentPanel = CreatePrintUIForImage(nextPictureToPrint);

          // How big is that?
          pictureParentPanel.Measure(e.PrintableArea);

          // Does it go off the end of the current "line" of pictures
          if ((startX + pictureParentPanel.DesiredSize.Width) > e.PrintableArea.Width)
          {
            // Assumption here that they all work out at the same height.
            startX = 0.0d;
            startY += pictureParentPanel.DesiredSize.Height;
          }
          // Does it go off the current page?
          if ((startY + pictureParentPanel.DesiredSize.Height) <= (e.PrintableArea.Height))
          {
            pictureParentPanel.SetValue(Canvas.LeftProperty, startX);
            pictureParentPanel.SetValue(Canvas.TopProperty, startY);
            pageVisual.Children.Add(pictureParentPanel);
            pictureQueue.Dequeue();

            startX += pictureParentPanel.DesiredSize.Width;
          }
          else
          {
            // Won't fit - need another page.
            break;
          }
        }
        e.PageVisual = pageVisual;
      }
      e.HasMorePages = (pictureQueue.Count > 0);
    }
    private StackPanel CreatePrintUIForImage(PictureInfo nextImage)
    {
      // Hard-coded UI that displays an image in a different way from
      // how it's displayed on the screen.
      StackPanel imagePanel = new StackPanel()
      {
        Margin = new Thickness(24)
      };

      // Note - using a rectangle here and not an Image because I *THINK*
      // this gets me around a problem I'd have in using an Image which
      // (even though it's sharing a BitmapSource with the screen) I have
      // a sneaky feeling might not load itself in time for the printing
      // stuff to print it - suffice to say I saw a lot of blank Images
      // before I switched to using this rectangle approach.
      Rectangle rect = new Rectangle()
      {
        Width = this.pictureWidth,
        Height = this.pictureHeight,
        Fill = new ImageBrush()
        {
          ImageSource = nextImage.ImageSource
        }
      };
      imagePanel.Children.Add(rect);

      TextBlock imageText = new TextBlock()
      {
        Text = nextImage.ImageName,
        HorizontalAlignment = HorizontalAlignment.Center,
        TextAlignment = TextAlignment.Center
      };
      imagePanel.Children.Add(imageText);

      return imagePanel;
    }
    void OnEndPrint(object sender, EndPrintEventArgs e)
    {
      if (completedCallback != null)
      {
        completedCallback(e.Error);
      }
    }
    Action<Exception> completedCallback;
    Queue<PictureInfo> pictureQueue;
    string name;
    int pictureWidth;
    int pictureHeight;
  }

Ok – so quite a bit of code there to paste but all it’s really doing is encapsulating the details of the print job in terms of its name, the images that need to be printed and the width and height that they are to be printed at.

The code then goes ahead and makes a PrintDocument and asks it to print – what work it does is being done in the OnPrintPage callback function which essentially;

  • loops until it either runs out of images to print or space to print them on
  • adds a Canvas to be the PageVisual that is returned to the caller as part of the PrintPageEventArgs
  • speculatively peeks the next image to be printed from the Queue and
    • Creates some UI for it ( this is hard-coded into the CreatePrintUIForImage function but could perhaps be moved into a user control or something more declarative )
    • Asks that UI how big it would like to be
    • Checks to see if that size of UI will fit on the current “row” of printed images and moves the UI down to the next “row” if necessary
    • Checks to see if that size of UI will fit on the current “column” of printed images and it it does then it adds it in the right place to the Canvas and removes it from the queue of images to be printed. If it doesn’t, it breaks the loop as we’ll need a new page to finish the print job.

And so with that in place I can then go and implement the OnPrint function in my main UI’s code behind as;

void OnPrint(object sender, RoutedEventArgs e)
    {
      PicturePrintJob job = new PicturePrintJob(
        "My Print Job", this.Images, this.ImageHeight, this.ImageWidth);

      job.Begin((ex) =>
        {
          if (ex != null)
          {
            MessageBox.Show(ex.Message, "Print Error", MessageBoxButton.OK);
          }
        });
    }

and then I can spring up my app and print my images and the print view really bears little relation to what’s on the screen;

image image image

I’m using the XPS printer as it saves me a bunch of ink/paper etc. and this gave me a document that looked like;

image

whereas if I set the width and height settings on my UI to something like “400×300” and re-print then my layout “algorithm” ( ahem! ) gives me a 2-page output;

image

and so I’ve managed to customise that printing process without too much effort – needs a bit of refinement but hopefully you get the idea.