Introduction
The ability to print in Silverlight 4 opens the door to creating better web-based applications than ever before. Silverlight now provides us with the opportunity to tie into a user's printer to create rich printouts of the very content they may be viewing.
While setting up a PrintDocument
is a rather simple task, and there is much we can do with it, Silverlight leaves much to be desired in terms of how the item appears on the page.
This article aims to provide a very simplified extension method that will allow virtually any control to be printed in a clean and consistent manner.
Background
If you've been developing any kind of application in Silverlight, particularly ones which render large data sets, you've undoubtedly had the need to print the datasets. While there are a number of ways to do this - including pulling the user out of the application and into a separate browser window which renders an HTML table - none of the methods take full advantage of the Silverlight Printing APIs, while also providing a simple and clean document print out.
The difficult part with the printing APIs is that they too don't offer much help in rendering your Silverlight control on the print page - often bleeding off the edges, or otherwise appearing on odd locations on the page.
With the careful use of Transforms, and a simple extension method, a clean and consistent printing function can be achieved.
Using the code
While I've implemented this code by extending FrameworkElement
, this method can be easily modified to be implemented in other locations. I prefer the extension method, as it allows for a very simple execution, such as:
MyControl.Print("My Print Document", HorizontalAlignment.Center,
VerticalAlignment.Top, new Thickness(100), true, true, null);
To print multiple items, you simply call the same function on a list of items, such as:
List<Grid> myListOfGrids = new List<Grid>();
...
myListOfGrids.Print("My Print Document",
HorizontalAlignment.Center, VerticalAlignment.Top,
new Thickness(100), true, true, null);
Grid g;
...
g.Children.Print"My Print Document", HorizontalAlignment.Center,
VerticalAlignment.Top, new Thickness(100), true, true, null);
So let's see how it all works.
First, we create our extension method's static class, which will house our extension method. Once this is done, any class that needs to make use of it will make a reference to the extension method namespace.
public static class Extensions
{ }
Then, we want to provide three methods, two which overload the last. The first method is on a single element, the second being for a UIElementCollection
, while the last is a for generic List
of elements, and will ultimately be the method which does all the work.
public static class Extensions
{
public static void Print(this FrameworkElement element, string Document,
HorizontalAlignment HorizontalAlignment,
VerticalAlignment VerticalAlignment, Thickness PageMargin,
bool PrintLandscape, bool ShrinkToFit, Action OnPrintComplete)
{
Print(new List<FrameworkElement>() { element }, Document,
HorizontalAlignment, VerticalAlignment, PageMargin,
PrintLandscape, ShrinkToFit, OnPrintComplete);
}
public static void Print(this UIElementCollection elements,
string Document, HorizontalAlignment HorizontalAlignment,
VerticalAlignment VerticalAlignment, Thickness PageMargin,
bool PrintLandscape, bool ShrinkToFit, Action OnPrintComplete)
{
Print(elements.ToList(), Document, HorizontalAlignment, VerticalAlignment,
PageMargin, PrintLandscape, ShrinkToFit, OnPrintComplete);
}
public static void Print<T>(this List<T> elements, string Document,
HorizontalAlignment HorizontalAlignment,
VerticalAlignment VerticalAlignment, Thickness PageMargin,
bool PrintLandscape, bool ShrinkToFit, Action OnPrintComplete)
{
...
}
}
If an object does not derive from FrameworkElement
, an exception should be thrown. We also need to make sure that each item that is being printed has actually been rendered on the control. The reason for this is that we require use of the ActualWidth
and ActualHeight
of an element - this means the element must appear and have a parent object.
It's possible to get around this restriction by rendering your controls to a parent control which has its Opacity
set to zero. Then clear the parent control when printing is complete.
if (!typeof(FrameworkElement).IsAssignableFrom(elements[currentItemIndex].GetType()))
{
throw new Exception("Element must be an object inheriting from FrameworkElement");
}
FrameworkElement element = elements[currentItemIndex] as FrameworkElement;
if (element.Parent == null || element.ActualWidth == double.NaN ||
element.ActualHeight == double.NaN)
{
throw new Exception("Element must be rendered, " +
"and must have a parent in order to print.");
}
When printing begins, the PrintPage
event is called. This event is what we tie into to render our pages. When this occurs, we want to apply a TranslateTransform
, and then our RotateTransform
(if we're printing in Landscape mode), and then ScaleTransform
. Lastly, we'll apply the TranslateTransform
to position the element on the page, as specified by the parameters.
It is important to note that we want to do this in this particular order so we know the point around which we're applying all our transforms.
It is in this event method that we iterate over our list of FrameworkElement
s, applying the transforms individually, then printing the page. We set HasMorePages
to true
each time, until we reach the end of our list.
Lastly, if we are printing in Landscape mode, it is important to note that we want to compare our element's width to the page's height, and the element's height to the page's width.
if (!typeof(FrameworkElement).IsAssignableFrom(elements[currentItemIndex].GetType()))
{
throw new Exception("Element must be an object inheriting from FrameworkElement");
}
FrameworkElement element = elements[currentItemIndex] as FrameworkElement;
if (element.Parent == null || element.ActualWidth == double.NaN ||
element.ActualHeight == double.NaN)
{
throw new Exception("Element must be rendered, " +
"and must have a parent in order to print.");
}
TransformGroup transformGroup = new TransformGroup();
transformGroup.Children.Add(new TranslateTransform() {
X = (evt.PrintableArea.Width - element.ActualWidth) / 2,
Y = (evt.PrintableArea.Height - element.ActualHeight) / 2 });
double scale = 1;
if (PrintLandscape)
{
transformGroup.Children.Add(new RotateTransform() { Angle = 90,
CenterX = evt.PrintableArea.Width / 2,
CenterY = evt.PrintableArea.Height / 2 });
if (ShrinkToFit)
{
if ((element.ActualWidth + PageMargin.Left + PageMargin.Right) >
evt.PrintableArea.Height)
{
scale = Math.Round(evt.PrintableArea.Height /
(element.ActualWidth + PageMargin.Left + PageMargin.Right), 2);
}
if ((element.ActualHeight + PageMargin.Top + PageMargin.Bottom) >
evt.PrintableArea.Width)
{
double scale2 = Math.Round(evt.PrintableArea.Width /
(element.ActualHeight + PageMargin.Top + PageMargin.Bottom), 2);
scale = (scale2 < scale) ? scale2 : scale;
}
}
}
else if (ShrinkToFit)
{
if ((element.ActualWidth + PageMargin.Left + PageMargin.Right) >
evt.PrintableArea.Width)
{
scale = Math.Round(evt.PrintableArea.Width /
(element.ActualWidth + PageMargin.Left + PageMargin.Right), 2);
}
if ((element.ActualHeight + PageMargin.Top + PageMargin.Bottom) >
evt.PrintableArea.Height)
{
double scale2 = Math.Round(evt.PrintableArea.Height /
(element.ActualHeight + PageMargin.Top + PageMargin.Bottom), 2);
scale = (scale2 < scale) ? scale2 : scale;
}
}
if (scale != 1)
{
transformGroup.Children.Add(new ScaleTransform() { ScaleX = scale,
ScaleY = scale, CenterX = evt.PrintableArea.Width / 2,
CenterY = evt.PrintableArea.Height / 2 });
}
if (VerticalAlignment == VerticalAlignment.Top)
{
if (PrintLandscape)
{
transformGroup.Children.Add(new TranslateTransform() { X = 0,
Y = PageMargin.Top - (evt.PrintableArea.Height -
(element.ActualWidth * scale)) / 2 });
}
else
{
transformGroup.Children.Add(new TranslateTransform() { X = 0,
Y = PageMargin.Top - (evt.PrintableArea.Height -
(element.ActualHeight * scale)) / 2 });
}
}
else if (VerticalAlignment == VerticalAlignment.Bottom)
{
if (PrintLandscape)
{
transformGroup.Children.Add(new TranslateTransform() { X = 0,
Y = ((evt.PrintableArea.Height -
(element.ActualWidth * scale)) / 2) - PageMargin.Bottom });
}
else
{
transformGroup.Children.Add(new TranslateTransform() { X = 0,
Y = ((evt.PrintableArea.Height -
(element.ActualHeight * scale)) / 2) - PageMargin.Bottom });
}
}
if (HorizontalAlignment == HorizontalAlignment.Left)
{
if (PrintLandscape)
{
transformGroup.Children.Add(new TranslateTransform() {
X = PageMargin.Left - (evt.PrintableArea.Width -
(element.ActualHeight * scale)) / 2, Y = 0 });
}
else
{
transformGroup.Children.Add(new TranslateTransform() {
X = PageMargin.Left - (evt.PrintableArea.Width -
(element.ActualWidth * scale)) / 2, Y = 0 });
}
}
else if (HorizontalAlignment == HorizontalAlignment.Right)
{
if (PrintLandscape)
{
transformGroup.Children.Add(new TranslateTransform() {
X = ((evt.PrintableArea.Width -
(element.ActualHeight * scale)) / 2) - PageMargin.Right, Y = 0 });
}
else
{
transformGroup.Children.Add(new TranslateTransform() {
X = ((evt.PrintableArea.Width -
(element.ActualWidth * scale)) / 2) - PageMargin.Right, Y = 0 });
}
}
evt.PageVisual = element;
evt.PageVisual.RenderTransform = transformGroup;
currentItemIndex++;
evt.HasMorePages = currentItemIndex < elements.Count;
Finally, when we've finished printing, we'll want to undo all the transforms we did. For this, we tie into the EndPrint
method and iterate over our list of elements, resetting all transforms.
We end by calling the OnPrintComplete Action
, if it is set.
foreach (var item in elements)
{
FrameworkElement element = item as FrameworkElement;
TransformGroup transformGroup = new TransformGroup();
transformGroup.Children.Add(new ScaleTransform() { ScaleX = 1, ScaleY = 1 });
transformGroup.Children.Add(new RotateTransform() { Angle = 0 });
transformGroup.Children.Add(new TranslateTransform() { X = 0, Y = 0 });
element.RenderTransform = transformGroup;
}
if (OnPrintComplete != null)
{
OnPrintComplete();
}
Put together, our final product looks like:
public static class Extensions
{
public static void Print(this FrameworkElement element,
string Document, HorizontalAlignment HorizontalAlignment,
VerticalAlignment VerticalAlignment, Thickness PageMargin,
bool PrintLandscape, bool ShrinkToFit, Action OnPrintComplete)
{
Print(new List<FrameworkElement>() { element }, Document,
HorizontalAlignment, VerticalAlignment, PageMargin,
PrintLandscape, ShrinkToFit, OnPrintComplete);
}
public static void Print(this UIElementCollection elements, string Document,
HorizontalAlignment HorizontalAlignment,
VerticalAlignment VerticalAlignment, Thickness PageMargin,
bool PrintLandscape, bool ShrinkToFit, Action OnPrintComplete)
{
Print(elements.ToList(), Document, HorizontalAlignment,
VerticalAlignment, PageMargin, PrintLandscape,
ShrinkToFit, OnPrintComplete);
}
public static void Print<T>(this List<T> elements,
string Document, HorizontalAlignment HorizontalAlignment,
VerticalAlignment VerticalAlignment, Thickness PageMargin,
bool PrintLandscape, bool ShrinkToFit, Action OnPrintComplete)
{
PrintDocument printDocument = new PrintDocument();
PageMargin = PageMargin == null ? new Thickness(10) : PageMargin;
Document = (string.IsNullOrEmpty(Document)) ? "Print Document" : Document;
int currentItemIndex = 0;
printDocument.PrintPage += delegate(object sender, PrintPageEventArgs evt)
{
if (!typeof(FrameworkElement).IsAssignableFrom(
elements[currentItemIndex].GetType()))
{
throw new Exception("Element must be an " +
"object inheriting from FrameworkElement");
}
FrameworkElement element = elements[currentItemIndex] as FrameworkElement;
if (element.Parent == null || element.ActualWidth == double.NaN ||
element.ActualHeight == double.NaN)
{
throw new Exception("Element must be rendered, " +
"and must have a parent in order to print.");
}
TransformGroup transformGroup = new TransformGroup();
transformGroup.Children.Add(new TranslateTransform() {
X = (evt.PrintableArea.Width - element.ActualWidth) / 2,
Y = (evt.PrintableArea.Height - element.ActualHeight) / 2 });
double scale = 1;
if (PrintLandscape)
{
transformGroup.Children.Add(new RotateTransform() { Angle = 90,
CenterX = evt.PrintableArea.Width / 2,
CenterY = evt.PrintableArea.Height / 2 });
if (ShrinkToFit)
{
if ((element.ActualWidth + PageMargin.Left +
PageMargin.Right) > evt.PrintableArea.Height)
{
scale = Math.Round(evt.PrintableArea.Height /
(element.ActualWidth + PageMargin.Left + PageMargin.Right), 2);
}
if ((element.ActualHeight + PageMargin.Top + PageMargin.Bottom) >
evt.PrintableArea.Width)
{
double scale2 = Math.Round(evt.PrintableArea.Width /
(element.ActualHeight + PageMargin.Top + PageMargin.Bottom), 2);
scale = (scale2 < scale) ? scale2 : scale;
}
}
}
else if (ShrinkToFit)
{
if ((element.ActualWidth + PageMargin.Left +
PageMargin.Right) > evt.PrintableArea.Width)
{
scale = Math.Round(evt.PrintableArea.Width /
(element.ActualWidth + PageMargin.Left + PageMargin.Right), 2);
}
if ((element.ActualHeight + PageMargin.Top + PageMargin.Bottom) >
evt.PrintableArea.Height)
{
double scale2 = Math.Round(evt.PrintableArea.Height /
(element.ActualHeight + PageMargin.Top + PageMargin.Bottom), 2);
scale = (scale2 < scale) ? scale2 : scale;
}
}
if (scale != 1)
{
transformGroup.Children.Add(new ScaleTransform() { ScaleX = scale,
ScaleY = scale, CenterX = evt.PrintableArea.Width / 2,
CenterY = evt.PrintableArea.Height / 2 });
}
if (VerticalAlignment == VerticalAlignment.Top)
{
if (PrintLandscape)
{
transformGroup.Children.Add(new TranslateTransform() {
X = 0, Y = PageMargin.Top - (evt.PrintableArea.Height -
(element.ActualWidth * scale)) / 2 });
}
else
{
transformGroup.Children.Add(new TranslateTransform() { X = 0,
Y = PageMargin.Top - (evt.PrintableArea.Height -
(element.ActualHeight * scale)) / 2 });
}
}
else if (VerticalAlignment == VerticalAlignment.Bottom)
{
if (PrintLandscape)
{
transformGroup.Children.Add(new TranslateTransform() { X = 0,
Y = ((evt.PrintableArea.Height -
(element.ActualWidth * scale)) / 2) - PageMargin.Bottom });
}
else
{
transformGroup.Children.Add(new TranslateTransform() { X = 0,
Y = ((evt.PrintableArea.Height -
(element.ActualHeight * scale)) / 2) - PageMargin.Bottom });
}
}
if (HorizontalAlignment == HorizontalAlignment.Left)
{
if (PrintLandscape)
{
transformGroup.Children.Add(new TranslateTransform() {
X = PageMargin.Left - (evt.PrintableArea.Width -
(element.ActualHeight * scale)) / 2, Y = 0 });
}
else
{
transformGroup.Children.Add(new TranslateTransform() {
X = PageMargin.Left - (evt.PrintableArea.Width -
(element.ActualWidth * scale)) / 2, Y = 0 });
}
}
else if (HorizontalAlignment == HorizontalAlignment.Right)
{
if (PrintLandscape)
{
transformGroup.Children.Add(new TranslateTransform() {
X = ((evt.PrintableArea.Width -
(element.ActualHeight * scale)) / 2) - PageMargin.Right, Y = 0 });
}
else
{
transformGroup.Children.Add(new TranslateTransform() {
X = ((evt.PrintableArea.Width -
(element.ActualWidth * scale)) / 2) - PageMargin.Right, Y = 0 });
}
}
evt.PageVisual = element;
evt.PageVisual.RenderTransform = transformGroup;
currentItemIndex++;
evt.HasMorePages = currentItemIndex < elements.Count;
};
printDocument.EndPrint += delegate(object sender, EndPrintEventArgs evt)
{
foreach (var item in elements)
{
FrameworkElement element = item as FrameworkElement;
TransformGroup transformGroup = new TransformGroup();
transformGroup.Children.Add(
new ScaleTransform() { ScaleX = 1, ScaleY = 1 });
transformGroup.Children.Add(new RotateTransform() { Angle = 0 });
transformGroup.Children.Add(
new TranslateTransform() { X = 0, Y = 0 });
element.RenderTransform = transformGroup;
}
if (OnPrintComplete != null)
{
OnPrintComplete();
}
};
printDocument.Print(Document);
}
}