Introduction
It was summer time. I was enjoying my time. Little did I know that my tough days were coming. Yes, that’s right, I had been assigned the work
on printing. I wouldn’t say it was very difficult. Its just that I had no idea how printing works in the WPF world and to my surprise, it wasn’t as
easy as a few Google searches. So after struggling a lot and spending some extra time apart from normal hours, I ended up with some pretty good
experience that I want to share with you all.
Background
Printing is one of the bizarre areas that I have encountered in all these years of my programming experience where sometimes I ran out of clues and started
figuring out solutions with guess work. It could just be that I was not good enough or could be that the thousands of different types of printers to handle
were just not easy to tame. Whatever it be, I have reached a stage where I could address it quite fine to my requirements.
What is the WPF Print Engine?
The WPF Print Engine makes it easier for .NET developers working with WPF applications to leverage printing facility.
It is a standalone component that takes in a few required and optional parameters to shield developers from all the heavy lifting when dealing with printing.
In an attempt to explain things, I split it into five sections as follows:
- Demonstration: Overview of how it works
- Usage: Code samples
- DocumentPaginator: How the pagination is achieved
- PrinterUtility: Retrieval of available printers and their properties / preferences
Demonstration
The WPF Print Engine comes with the following features at the moment:
- Smart print preview to see what it would look like after printing on the printer and the paper you selected
- Support for changing printer preferences directly
- Scale page content in the print preview to fit in less pages
- Turn on/off page numbers
- Asynchronous printing directly to printer
- Any WPF Visual support
- Any WPF FrameworkElement support
- DataTable support
The above snapshot shows the demo application. As you can see, it has support for generating print preview for WPF Visual or a DataTable as input.
Below is a snapshot of what we get when we select “Print This Visual”. You can see that the visual is split in two separate
pages because the current selected paper (A4) is smaller than the visual’s size.
You can change the printer, the printer preferences, and the number of copies to print from the printing options tool. It also allows you to
print the page numbers on each page or hide them.
There is one neat feature, “Print Size”, that allows you to shrink the generated print preview content’s size to your will so that you can fit
it in less number of pages. This is done with the help of a slider, giving you complete control over the resize ratio so that you can decide
when your data is not being too small. This is unique in the sense that many applications allow you to shrink but not all allow you to control how much.
Notice below the updated snapshot after it has been shrunken just enough to fit into 1 page instead of 2.
Similarly, the demo application shows an example for using DataTable as input for the print preview. Notice that the pagination is done such
that no columns or rows get cut and yet the maximum possible rows or columns are fit in the selected paper size.
Usage
Printing a WPF Visual
First, you need to create an instance of the PrintControl
using the PrintControlFactory.Create
method. It has a few overloads
that I will be adding more to in future. One of those is the one that takes a size and a visual as input. Visual is the WPF visual (could be
any control, panel, grid, window) anything that you want to print. When you specify a visual, all its children, as rendered on the screen, are taken into consideration.
var visualSize = new Size(visual.ActualWidth, visual.ActualHeight);
var printControl = PrintControlFactory.Create(visualSize, visual);
printControl.ShowPrintPreview();
Printing a DataTable
In order to give a DataTable
as the source, you will need to also supply the width of each column as a List<double>
.
Then instantiate a PrintControl
using the PrintControlFactory.Create
method that takes a DataTable
and columns widths as arguments.
var columnWidths = new List<double>() {30, 40, 300, 300, 150};
var printControl = PrintControlFactory.Create(dataTable, columnWidths, headerTemplate);
printControl.ShowPrintPreview();
Printing a DataTable with Header Template
There is also an overload to give a header template. The header template to supply is a string of the XAML file you will create. This can be in the form
of a user control. To denote the page number and its place holder, simple place the verbatim string “@PageNumber” in the placeholder.
<UserControl x:Class="DEMOApplication.HeaderTemplate"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignWidth="919">
<DockPanel LastChildFill="False" Margin="10">
<Image Source="Images/headerDemo.gif"
DockPanel.Dock="Left" Stretch="None" />
<TextBlock DockPanel.Dock="Right"
VerticalAlignment="Bottom" FontWeight="Bold"
TextWrapping="Wrap" Text="Page Number : @PageNumber"/>
</DockPanel>
</UserControl>
var columnWidths = new List<double>() {30, 40, 300, 300, 150};
var ht = new HeaderTemplate();
var headerTemplate = XamlWriter.Save(ht);
var printControl = PrintControlFactory.Create(dataTable, columnWidths, headerTemplate);
printControl.ShowPrintPreview();
Demo
Autodiagrammer by Sacha Barber
now uses this print engine for its print preview facility.
How the control is initiated
At the heart of the print engine are two parts. One is the PrintControlFactory
that creates a DrawingVisual
object to be used later by the control.
I will explain this shortly. The second important part (I should probably mention it first, because this is in fact the most important bit) is, wait for it..., the Paginator
.
The following diagram displays a flowchart of how the whole process works. I will explain one step at a time and give the implementation details.
Step 1: Initialize the Print Engine
The PrintControlFactory.Create()
method is responsible for creating an instance of the PrintControl
. This method has several overloads for working with
a WPF Visual
item. Also, there is an overload that takes a DataTable
, widths of the columns as List<double>
, and
a header template as string
. We will look into the working of the engine with DataTable
much later. Let's first understand the complete
flow when using a WPF Visual
.
var unityContainer = new UnityContainer();
PrintEngineModule.Initialize(unityContainer);
var printControlPresenter =
(PrintControlViewModel)unityContainer.Resolve<IPrintControlViewModel>();
You will notice the above part of code in the create method. This is where the print engine is assigned a new instance of UnityContainer
since
I am using Prism and Unity for MVVM architecture. Here I register all the Views and ViewModels to the container.
Step 2: Build Graph Visual
public static DrawingVisual BuildGraphVisual(PageMediaSize pageSize, Visual visual)
{
var drawingVisual = new DrawingVisual();
using (var drawingContext = drawingVisual.RenderOpen())
{
var visualContent = visual;
var rect = new Rect
{
X = 0,
Y = 0,
Width = pageSize.Width.Value,
Height = pageSize.Height.Value
};
var stretch = Stretch.None;
var visualBrush = new VisualBrush(visualContent) { Stretch = stretch };
drawingContext.DrawRectangle(visualBrush, null, rect);
drawingContext.PushOpacityMask(Brushes.White);
}
return drawingVisual;
}
This method takes a WPF Visual
and creates a DrawingVisual
object. Why we need this is very crucial. If you have already run the sample
application in the source code and played with it, you will notice that based on the selected printer and paper type, the visual is paginated into multiple pages.
This is possible due to the fact that we can clip a certain area of the original DrawingVisual
starting from any X,Y co-ordinate. I will show this further below.
In order to create this DrawingVisual
, I have created a VisualBrush
with the given input and painted with it.
Step 3: Initialize Printer Properties
This is another interesting area of the engine and in fact was a little challenge for me at the beginning. This is the part where I connect to the installed printers
to fetch their properties such as the PaperSize
s, DefaultPaper
, printer hardware margin, etc. Also I have made sure that I do these once during
the life time because this operation is expensive and sometimes takes long depending on the type of printers you have. The class PrintUtlity
deals with
all these operations. One such method that gets the list of installed printers is shown below. I have used Enterprise Library for caching these values so
that these expensive operations are done only once.
public PrintQueueCollection GetPrinters()
{
if (!_cacheHelper.Contains("Printers"))
{
var printServer = new PrintServer();
_cacheHelper.Add("Printers", printServer.GetPrintQueues(
new[] { EnumeratedPrintQueueTypes.Connections,
EnumeratedPrintQueueTypes.Local }));
}
var printers = (PrintQueueCollection)_cacheHelper.GetData("Printers");
return printers;
}
Step 4: Create Scaled Visual
The method ReloadPreview()
executes when the control is first loaded. In fact from this step to the last step, everything takes place
in the ReloadPreview
method.
private DrawingVisual GetScaledVisual(double scale)
{
if (scale == 1)
return DrawingVisual;
var visual = new DrawingVisual();
using (var dc = visual.RenderOpen())
{
dc.PushTransform(new ScaleTransform(scale, scale));
dc.DrawDrawing(DrawingVisual.Drawing);
}
return visual;
}
This step would make no sense at first, but when you use the option to scale the visual so that it takes up less space, the scale
value changes
ranging from 0 to 1 where 1 is 100% size and 0% the original size. This is done by performing a ScaleTransform
on the DrawingVisual
.
Step 5: Setup Paginator
Ahh... the most interesting and intelligent class of the whole system. The Paginator
! Rhymes pretty well will Arnold
Schwarzenegger. This class does all the heavy lifting of cutting the entire visual into separate individual pages.
There are several Paginator
s in the project. Each able to work with one kind of pagination.
VisualPaginator
: This has two roles. First, it contains all the common logic for all the paginators and also is a base class to the other paginators.
Second, it is responsible for performing page calculation and clipping of a given visual into separate individual pages.DataTablePaginator
: This does the pagination when a DataTable
is used as the source for the print preview.DataGridPaginator
: This does the pagination when a WPF DataGrid
control is given as the input. This feature is still not complete because
I am trying to add this feature to work even when the DataGrid
is in Virtualization mode.
public VisualPaginator(DrawingVisual source, Size printSize,
Thickness pageMargins, Thickness originalMargin)
{
DrawingVisual = source;
_printSize = printSize;
PageMargins = pageMargins;
_originalMargin = originalMargin;
}
I will describe the details of all the paginators in part II of the series as well as complete the DataGridPaginator
and its usage sample.
In this part, we will only look into the details with respect to the VisualPaginator
. The constructor as you can see is pretty straightforward.
Next comes the Initialize()
method. It performs two important pieces of work:
1: Calculates the Printable Page Width and Height
This calculation is necessary because different printers have different hardware margins. So if we draw anything outside the bounds of this margin,
that part will get cut. So when calculating the number of pages required, this hardware margin has to be kept under consideration.
var totalHorizontalMargin = PageMargins.Left + PageMargins.Right;
var toltalVerticalMargin = PageMargins.Top + PageMargins.Bottom;
PrintablePageWidth = PageSize.Width - totalHorizontalMargin;
PrintablePageHeight = PageSize.Height - toltalVerticalMargin
2: Calculates the Horizontal and Vertical Page Count
This is fairly straightforward in the case of VisualPaginator
. Other paginators, specially the ItemsPaginator
does the most complex calculation.
In fact, calculating the page count is what separates the different paginators. If you want to add support for your own control type, let's say a third part datagrid
like xCeed grid control, then all you do is create a new paginator inheriting from VisualPaginator
and write your own logic for the horizontal and vertical page count.
OK, let's gets back to our VisualPaginator
. Here the horizontal page count is the total visual width divided by the printable width of each page.
Similarly, the vertical page count is the total height of the visual divided by the height of each individual page.
Step 6: Create Pages
Here I do an iteration to walk through the whole DrawingVisual
and save each block as a separate page. This is done by first looping from
0 to the number of horizontal pages (calculated in the previous step) and then moving to the next row and repeating until I cover the total horizontal page count.
All these sepsrate pages are saved in a collection, DrawingVisuals
, to be later used during printing or showing the preview.
private void CreateAllPageVisuals()
{
DrawingVisuals = new List<DrawingVisual>();
for (var verticalPageNumber = 0;
verticalPageNumber < _verticalPageCount;
verticalPageNumber++)
{
for (var horizontalPageNumber = 0;
horizontalPageNumber < HorizontalPageCount;
horizontalPageNumber++)
{
const float horizontalOffset = 0;
var verticalOffset = (float)(verticalPageNumber * PrintablePageHeight);
var pageBounds = GetPageBounds(horizontalPageNumber,
verticalPageNumber, horizontalOffset, verticalOffset);
var visual = new DrawingVisual();
using (var dc = visual.RenderOpen())
{
CreatePageVisual(pageBounds, DrawingVisual,
IsFooterPage(horizontalPageNumber), dc);
}
DrawingVisuals.Add(visual);
}
}
}
Step 7: Show Preview
After the pages are calculated, we are ready to show them to the user as preview. Since I am using a standard mechanism for the pagination and using a custom implementation
of the .NET Framework's DocumentPaginator
class, it should be
fairly straightforward to simply give the paginator to the DocumentViewer
.
But there is a catch. When the number of pages increase, specially in the area of several hundreds, the standard DocumentViewer
starts behaving awkward and even fails
to display all the pages sometimes. So instead I went for a rather custom, but simple solution.
Using the collection of drawing visuals created by the paginator, I create a simple WPF Visual
object from each of these DrawingVisual
s
and display them in a StackPanel
. The code is rather self explanatory.
private Border GetPageUiElement(int i, DocumentPaginator paginator, double scale)
{
var source = paginator.GetPage(i);
var border = new Border() { Background = Brushes.White };
border.Margin = new Thickness(10 * scale);
border.BorderBrush = Brushes.DarkGray;
border.BorderThickness = new Thickness(1);
var margin = new Thickness();
var rectangle = new Rectangle();
rectangle.Width =
((source.Size.Width * 0.96 - (margin.Left + margin.Right)) * scale);
rectangle.Height =
((source.Size.Height * 0.96 - (margin.Top + margin.Bottom)) * scale);
rectangle.Margin = new Thickness(margin.Left * scale,
margin.Top * scale, margin.Right * scale, margin.Bottom * scale);
rectangle.Fill = Brushes.White;
var vb = new VisualBrush(source.Visual);
vb.Opacity = 1;
vb.Stretch = Stretch.Uniform;
rectangle.Fill = vb;
border.Child = rectangle;
return border;
}
Step 8: Change Paper / Printer Options
This is the step where the user selects a different paper size, page orientation, etc. Steps 4 to 7 are performed again to calculate new pages and display them.
Conclusion
This was just the introduction to both the project and its documentation. At this moment, I believe there are many areas that can be improved, including facility
for the WPF DataGrid
to be given as input, support for footer template, refactoring the codebase more, better samples, to name a few.
I would love to welcome anyone interested in contributing to the source code and bringing in better improvements and features.
I also plan to add Visual Studio design time support for creating printing templates.