May 2011

Volume 26 Number 05

UI Frontiers - Silverlight Printing Basics

By Charles Petzold | May 2011

Charles PetzoldSilverlight 4 added printing to the Silverlight feature list, and I want to plunge right in by showing you a tiny program that put a big smile on my face.

The program is called PrintEllipse and that’s all it does. The XAML file for MainPage contains a Button, and Figure 1 shows the MainPage codebehind file in its entirety.

Figure 1 The MainPage Code for PrintEllipse

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Printing;
using System.Windows.Shapes;

namespace PrintEllipse
{
  public partial class MainPage : UserControl
  {
    public MainPage()
    {
      InitializeComponent();
    }
    void OnButtonClick(object sender, RoutedEventArgs args)
    {
      PrintDocument printDoc = new PrintDocument();
      printDoc.PrintPage += OnPrintPage;
      printDoc.Print("Print Ellipse");
    }
    void OnPrintPage(object sender, PrintPageEventArgs args)
    {
      Ellipse ellipse = new Ellipse
      {
        Fill = new SolidColorBrush(Color.FromArgb(255, 255, 192, 192)),
        Stroke = new SolidColorBrush(Color.FromArgb(255, 192, 192, 255)),
        StrokeThickness = 24    // 1/4 inch
      };
      args.PageVisual = ellipse;
    }
  }
}

Notice the using directive for System.Windows.Printing. When you click the button, the program creates an object of type PrintDocument and assigns a handler for the PrintPage event. When the program calls the Print method, the standard print dialog box appears. The user can take this opportunity to set which printer to use and set various properties for printing, such as portrait or landscape mode.

When the user clicks Print on the print dialog, the program receives a call to the PrintPage event handler. This particular program responds by creating an Ellipse element and setting that to the PageVisual property of the event arguments. (I deliberately chose light pastel colors so the program won’t use too much of your ink.) Soon a page will emerge from your printer filled with a giant ellipse.

You can run this program from my Web site at bit.ly/dU9B7k and check it out yourself. All the source code from this article is also downloadable, of course.

If your printer is like most printers, the internal hardware prohibits it from printing to the very edge of the paper. Printers usually have an intrinsic built-in margin in which nothing is printed; printing is instead restricted to a “printable area” that’s less than the full size of the page.

What you’ll notice with this program is that the ellipse appears in its entirety within the printable area of the page, and obviously this happens with minimum effort on the part of the program. The printable area of the page behaves much like a container element on the screen: It only clips a child when an element has a size that exceeds the area. Some far more sophisticated graphics environments—such as Windows Presentation Foundation (WPF)—don’t behave nearly as well (but, of course, WPF offers much more printing control and flexibility than Silverlight).

PrintDocument and Events

Besides the PrintPage event, PrintDocument also defines BeginPrint and EndPrint events, but these aren’t nearly as important as PrintPage. The BeginPrint event signals the beginning of a print job. It’s fired when the user exits the standard print dialog by pressing the Print button and gives the program the opportunity to perform initialization. The call to the BeginPrint handler is then followed by the first call to the PrintPage handler.

A program that wishes to print more than one page in a particular print job can do so. In every call to the PrintPage handler, the HasMorePages property of PrintPageEventArgs is initially set to false. When the handler is finished with a page, it can simply set the property to true to signal that at least one more page must be printed. PrintPage is then called again. The PrintDocument object maintains a PrintedPageCount property that’s incremented following every call to the PrintPage handler.

When the PrintPage handler exits with HasMorePages set to its default value of false, the print job is over and the EndPrint event is fired, giving the program the opportunity to perform cleanup chores. The EndPrint event is also fired when an error occurs during the printing process; the Error property of EndPrintEventArgs is of type Exception.

Printer Coordinates

The code shown in Figure 1 sets the StrokeThickness of the Ellipse to 24, and if you measure the printed result, you’ll discover that it’s one-quarter inch wide. As you know, a Silverlight program normally sizes graphical objects and controls entirely in units of pixels. However, when the printer is involved, coordinates and sizes are in device-independent units of 1/96th inch. Regardless of the actual resolution of the printer, from a Silverlight program the printer always appears to be a 96 DPI device.

As you might know, this coordinate system of 96 units to the inch is used throughout WPF, where the units are sometimes referred to as “device-independent pixels.” This value of 96 DPI wasn’t chosen arbitrarily: By default, Windows assumes that your video display has 96 dots to the inch, so in many cases a WPF program is actually drawing in units of pixels. The CSS specification assumes that video displays have a 96 DPI resolution, and that value is used for converting between pixels, inches and millimeters. The value of 96 is also a convenient number for converting font sizes, which are commonly specified in points, or 1/72nd inch. A point is three-quarters of a device-independent pixel.

PrintPageEventArgs has two handy get-only properties that also report sizes in units of 1/96th inch: PrintableArea of type Size provides the dimensions of the area of the printable area of the page, and PageMargins of type Thickness is the width of the left, top, right and bottom of the unprintable edges. Add these two together (in the right way) and you get the full size of the paper.

My printer—when loaded with standard 8.5 x 11 inch paper and set for portrait mode—reports a PrintableArea of 791 x 993. The four values of the PageMargins property are 12 (left), 6 (top), 12 (right) and 56 (bottom). If you sum the horizontal values of 791, 12 and 12, you’ll get 815. The vertical values are 994, 6 and 56, which sum to 1,055. I’m not sure why there’s a one-unit difference between these values and the values of 816 and 1,056 obtained by multiplying the page size in inches by 96.

When a printer is set for landscape mode, then the horizontal and vertical dimensions reported by PrintableArea and PageMargins are swapped. Indeed, examining the PrintableArea property is the only way a Silverlight program can determine whether the printer is in portrait or landscape mode. Anything printed by the program is automatically aligned and rotated depending on this mode.

Often when you print something in real life, you’ll define margins that are somewhat larger than the unprintable margins. How do you do this in Silverlight? At first, I thought it would be as easy as setting the Margin property on the element you’re printing. This Margin would be calculated by starting with a desired total margin (in units of 1/96th inch) and subtracting the values of the PageMargins property available from the PrintPageEventArgs. That approach didn’t work well, but the correct solution was almost as easy. The PrintEllipseWithMargins program (which you can run at bit.ly/fCBs3X) is the same as the first program except that a Margin property is set on the Ellipse, and then the Ellipse is set as the child of a Border, which fills the printable area. Alternatively, you can set the Padding property on the Border. Figure 2 shows the new OnPrintPage method.

Figure 2 The OnPrintPage Method to Calculate Margins

void OnPrintPage(object sender, PrintPageEventArgs args)
{
  Thickness margin = new Thickness
  {
    Left = Math.Max(0, 96 - args.PageMargins.Left),
    Top = Math.Max(0, 96 - args.PageMargins.Top),
    Right = Math.Max(0, 96 - args.PageMargins.Right),
    Bottom = Math.Max(0, 96 - args.PageMargins.Bottom)
  };
  Ellipse ellipse = new Ellipse
  {
    Fill = new SolidColorBrush(Color.FromArgb(255, 255, 192, 192)),
    Stroke = new SolidColorBrush(Color.FromArgb(255, 192, 192, 255)),
    StrokeThickness = 24,   // 1/4 inch
    Margin = margin
  };
  Border border = new Border();
  border.Child = ellipse;
  args.PageVisual = border;
}

The PageVisual Object

There are no special graphics methods or graphics classes associated with the printer. You “draw” something on the printer page the same way you “draw” something on the video display, which is by assembling a visual tree of objects that derive from FrameworkElement. This tree can include Panel elements, including Canvas. To print that visual tree, set the topmost element to the PageVisual property of the PrintPageEventArgs. (PageVisual is defined as a UIElement, which is the parent class to FrameworkElement, but in a practical sense, everything you’ll be setting to PageVisual will derive from FrameworkElement.)

Almost every class that derives from FrameworkElement has non-trivial implementations of the MeasureOverride and ArrangeOverride methods for layout purposes. In its MeasureOverride method, an element determines its desired size, sometimes by determining the desired sizes of its children by calling its children’s Measure methods. In the ArrangeOverride method, an element arranges its children relative to itself by calling the children’s Arrange methods.

When you set an element to the PageVisual property of PrintPageEventArgs, the Silverlight printing system calls Measure on that topmost element with the PrintableArea size. This is how (for example) the Ellipse or Border is automatically sized to the printable area of the page.

However, you can also set that PageVisual property to an element that’s already part of a visual tree being displayed in the program’s window. In this case, the printing system doesn’t call Measure on that element, but instead uses the measurements and layout already determined for the video display. This allows you to print something from your program’s window with reasonable fidelity, but it also means that what you print might be cropped to the size of the page.

You can, of course, set explicit Width and Height properties on the elements you print, and you can use the PrintableArea size to help out.

Scaling and Rotating

The next program I took on turned out to be more of a challenge than I anticipated. The goal was a program that would let the user print any image file supported by Silverlight—namely PNG and JPEG files—stored on the user’s local machine. This program uses the OpenFileDialog class to load these files. For security purposes, OpenFileDialog only returns a FileInfo object that lets the program open the file. No filename or directory is provided.

I wanted this program to print the bitmap as large as possible on the page (excluding a preset margin) without altering the bitmap’s aspect ratio. Normally this is a snap: The Image element’s default Stretch mode is Uniform, which means the bitmap is stretched as large as possible without distortion.

However, I decided that I didn’t want to require the user to specifically set portrait or landscape mode on the printer commensurate with the particular image. If the printer was set to portrait mode, and the image was wider than its height, I wanted the image to be printed sideways on the portrait page. This little feature immediately made the program much more complex.

If I were writing a WPF program to do this, the program itself could have switched the printer into portrait or landscape mode. But that isn’t possible in Silverlight. The printer interface is defined so that only the user can change settings like that.

Again, if I were writing a WPF program, alternatively I could have set a LayoutTransform on the Image element to rotate it 90 degrees. The rotated Image element would then be resized to fit on the page, and the bitmap itself would have been adjusted to fit the Image element.

But Silverlight doesn’t support LayoutTransform. Silverlight only supports RenderTransform, so if the Image element must be rotated to accommodate a landscape image printed in portrait mode, the Image element must also be manually sized to the dimensions of the landscape page.

You can try out my first attempt at bit.ly/eMHOsB. The OnPrintPage method creates an Image element and sets the Stretch property to None, which means the Image element displays the bitmap in its pixel size, which on the printer means that each pixel is assumed to be 1/96th inch. The program then rotates, sizes and translates that Image element by calculating a transform that it applies to the RenderTransform property of the Image element.

The hard part of such code is, of course, the math, so it was pleasant to see the program work with portrait and landscape images with the printer set to portrait and landscape modes.

However, it was particularly unpleasant to see the program fail for large images. You can try it yourself with images that have dimensions somewhat greater (when divided by 96) than the size of the page in inches. The image is displayed at the correct size, but not in its entirety.

What’s going on here? Well, it’s something I’ve seen before on video displays. Keep in mind that the RenderTransform affects only how the element is displayed and not how it appears to the layout system. To the layout system, I’m displaying a bitmap in an Image element with Stretch set to None, meaning that the Image element is as large as the bitmap itself. If the bitmap is larger than the printer page, then some of that Image element need not be rendered, and it will, in fact, be clipped, regardless of a RenderTransform that’s shrinking the Image element appropriately.

My second attempt, which you can try out at bit.ly/g4HJ1C, takes a somewhat different strategy. The OnPrintPage method is shown in Figure 3. The Image element is given explicit Width and Height settings that make it exactly the size of the calculated display area. Because it’s all within the printable area of the page, nothing will be clipped. The Stretch mode is set to Fill, which means that the bitmap fills the Image element regardless of the aspect ratio. If the Image element won’t be rotated, one dimension is correctly sized, and the other dimension must have a scaling factor applied that reduces the size. If the Image element must also be rotated, then the scaling factors must accommodate the different aspect ratio of the rotated Image element.

Figure 3 Printing an Image in PrintImage

void OnPrintPage(object sender, PrintPageEventArgs args)
{
  // Find the full size of the page
  Size pageSize = 
    new Size(args.PrintableArea.Width 
    + args.PageMargins.Left + args.PageMargins.Right,
    args.PrintableArea.Height 
    + args.PageMargins.Top + args.PageMargins.Bottom);

  // Get additional margins to bring the total to MARGIN (= 96)
  Thickness additionalMargin = new Thickness
  {
    Left = Math.Max(0, MARGIN - args.PageMargins.Left),
    Top = Math.Max(0, MARGIN - args.PageMargins.Top),
    Right = Math.Max(0, MARGIN - args.PageMargins.Right),
    Bottom = Math.Max(0, MARGIN - args.PageMargins.Bottom)
  };

  // Find the area for display purposes
  Size displayArea = 
    new Size(args.PrintableArea.Width 
    - additionalMargin.Left - additionalMargin.Right,
    args.PrintableArea.Height 
    - additionalMargin.Top - additionalMargin.Bottom);

  bool pageIsLandscape = displayArea.Width > displayArea.Height;
  bool imageIsLandscape = bitmap.PixelWidth > bitmap.PixelHeight;

  double displayAspectRatio = displayArea.Width / displayArea.Height;
  double imageAspectRatio = (double)bitmap.PixelWidth / bitmap.PixelHeight;

  double scaleX = Math.Min(1, imageAspectRatio / displayAspectRatio);
  double scaleY = Math.Min(1, displayAspectRatio / imageAspectRatio);

  // Calculate the transform matrix
  MatrixTransform transform = new MatrixTransform();

  if (pageIsLandscape == imageIsLandscape)
  {
    // Pure scaling
    transform.Matrix = new Matrix(scaleX, 0, 0, scaleY, 0, 0);
  }
  else
  {
    // Scaling with rotation
    scaleX *= pageIsLandscape ? displayAspectRatio : 1 / 
      displayAspectRatio;
    scaleY *= pageIsLandscape ? displayAspectRatio : 1 / 
      displayAspectRatio;
    transform.Matrix = new Matrix(0, scaleX, -scaleY, 0, 0, 0);
  }

  Image image = new Image
  {
    Source = bitmap,
    Stretch = Stretch.Fill,
    Width = displayArea.Width,
    Height = displayArea.Height,
    RenderTransform = transform,
    RenderTransformOrigin = new Point(0.5, 0.5),
    HorizontalAlignment = HorizontalAlignment.Center,
    VerticalAlignment = VerticalAlignment.Center,
    Margin = additionalMargin,
  };

  Border border = new Border
  {
    Child = image,
  };

  args.PageVisual = border;
}

The code is certainly messy—and I suspect there might be simplifications not immediately obvious to me—but it works for bitmaps of all sizes.

Another approach is to rotate the bitmap itself rather than the Image element. Create a WriteableBitmap from the loaded BitmapImage object, and a second WritableBitmap with swapped horizontal and vertical dimensions. Then copy all the pixels from the first WriteableBitmap into the second with rows and columns swapped.

Multiple Calendar Pages

Deriving from UserControl is an extremely popular technique in Silverlight programming to create a reusable control without a lot of hassle. Much of a UserControl is a visual tree defined in XAML.

You can also derive from UserControl to define a visual tree for printing! This technique is illustrated in the PrintCalendar program, which you can try out at bit.ly/dIwSsn. You enter a start month and an end month, and the program prints all the months in that range, one month to a page. You can tape the pages to your walls and mark them up, just like a real wall calendar.

After my experience with the PrintImage program, I didn’t want to bother with margins or orientation; instead, I included a Button that places the responsibility on the user, as shown in Figure 4.

The PrintCalendar Button

Figure 4 The PrintCalendar Button

The UserControl that defines the calendar page is called CalendarPage, and the XAML file is shown in Figure 5. A TextBlock near the top displays the month and year. This is followed by a second Grid with seven columns for the days of the week and six rows for up to six weeks or partial weeks in a month.

Figure 5 The CalendarPage Layout

<UserControl x:Class="PrintCalendar.CalendarPage"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  FontSize="36">  
  <Grid x:Name="LayoutRoot" Background="White">
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <TextBlock Name="monthYearText" 
      Grid.Row="0"
       FontSize="48"
       HorizontalAlignment="Center" />
    <Grid Name="dayGrid" 
      Grid.Row="1">
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
      </Grid.RowDefinitions>
    </Grid>
  </Grid>
</UserControl>

Unlike most UserControl derivatives, CalendarPage defines a constructor with a parameter, as shown in Figure 6.

Figure 6 The CalendarPage Codebehind Constructor

public CalendarPage(DateTime date)
{
  InitializeComponent();
  monthYearText.Text = date.ToString("MMMM yyyy");
  int row = 0;
  int col = (int)new DateTime(date.Year, date.Month, 1).DayOfWeek;
  for (int day = 0; day < DateTime.DaysInMonth(date.Year, date.Month); day++)
  {
    TextBlock txtblk = new TextBlock
    {
      Text = (day + 1).ToString(),
      HorizontalAlignment = HorizontalAlignment.Left,
      VerticalAlignment = VerticalAlignment.Top
    };
    Border border = new Border
    {
      BorderBrush = blackBrush,
      BorderThickness = new Thickness(2),
      Child = txtblk
    };
    Grid.SetRow(border, row);
    Grid.SetColumn(border, col);
    dayGrid.Children.Add(border);
    if (++col == 7)
    {
      col = 0;
      row++;
    }
  }
  if (col == 0)
    row--;
  if (row < 5)
    dayGrid.RowDefinitions.RemoveAt(0);
  if (row < 4)
    dayGrid.RowDefinitions.RemoveAt(0);
}

The parameter is a DateTime, and the constructor uses the Month and Year properties to create a Border containing a TextBlock for each day of the month. These are each assigned Grid.Row and Grid.Column attached properties, and then added to the Grid. As you know, often months only span five weeks, and occasionally February only has four weeks, so RowDefinition objects are actually removed from the Grid if they’re not needed.

UserControl derivatives normally don’t have constructors with parameters because they usually form parts of larger visual trees. But CalendarPage isn’t used like that. Instead, the PrintPage handler simply assigns a new instance of CalendarPage to the PageVisual property of PrintPageEventArgs. Here’s the complete body of the handler, clearly illustrating how much work is being performed by CalendarPage:

args.PageVisual = new CalendarPage(dateTime);
args.HasMorePages = dateTime < dateTimeEnd;
dateTime = dateTime.AddMonths(1);

Adding a printing option to a program is so often viewed as a grueling job involving lots of code. To be able to define most of a printed page in a XAML file makes the whole thing much less frightful.        


Charles Petzold is a longtime contributing editor to MSDN Magazine. His new book, “Programming Windows Phone 7” (Microsoft Press, 2010), is available as a free download at bit.ly/cpebookpdf.

Thanks to the following technical experts for reviewing this article: Saied Khanahmadi and Robert Lyon