Click here to Skip to main content
15,886,873 members
Articles / Desktop Programming / WPF

WPF Print Engine : Part I

Rate me:
Please Sign up or sign in to vote.
4.87/5 (99 votes)
12 Aug 2011CPOL10 min read 283.7K   13.8K   176  
WPF Print Engine makes it easier for .net developers working with WPF applications to leverage printing facility.
This is an old version of the currently published article.
Download WPFPrintEngine_Alpha_1.zip - 938.96 KB

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 WPF world and to my surprise it wasn’t as easy as 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 part in all these years of my programming experience where i sometimes ran out clue and started figuring out solutions with guess work sometimes. 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. However it be, i landed up to a stage where i could address it quite fine to my requirements.

What is WPF Print Engine ?

WPF Print Engine makes is easier for .net developers working with WPF applications to leverage printing facility.

It is a standalone component that takes in few required and optional parameters to give encapsulate developers from all heavy lifting in order to deal with printing. 

In an attempt to explain things i split it into 5 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

WPF print engine comes with the following features at the moment :

  • Smart print preview to see what it would look like on after printing on the printer and 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 

 demoapp1.jpg
 

The above snapshot shows the demo application.  As you can see it has as this stage, support for generating print preview for WPF Visual or a DataTable as input. Below is a snapshot of what we get then select “Print This Visual”. You can see that the visual is split in 2 separate pages because the current selected paper (A4) is smaller than the visual’s size.

visualPreview_thumb.png

You can change the printer, the printer preferences, 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.

printingoption_thumb.png 

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 you 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.

printSize_thumb.png 

Notice below the updated snapshot after it has been shrunken just enough to fit into 1 pages instead of 2.

visualPreview1page_thumb.png 

Similarly the demo application show an example for using DataTable as input for the print preview. Notice that the pagination is done such that no columns or row get cut and yet the maximum possible row or column is fit in the selected paper size.

datatable_thumb.png

Usage

Printing a WPF Visual: 

First you need to create an instance of the printcontrol which you can using the PrintControlFactory.Create method. It has 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. 

C#
var visualSize = new Size(visual.ActualWidth, visual.ActualHeight);
var printControl = PrintControlFactory.Create(visualSize, visual);
printControl.ShowPrintPreview();  

Printing a DataTable as source :

In order to give a datatable as a 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 datable and columnsWidths as arguments.

C#
var columnWidths = new List<double>() {30, 40, 300, 300, 150};
var printControl = PrintControlFactory.Create(dataTable, columnWidths, headerTemplate);
printControl.ShowPrintPreview(); 
Printing a DataTable as source 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 it place holder, simple place the verbatim string “@PageNumber” in its placeholder.

 

XML
<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> 
C#
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<code> 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 infact the most important bit) are , wait for it..., Paginators

The following diagram displays a flowchart on how the whole process works. I will take on one step at a time and give their implementation details. 

flowchart.png

Step 1 : Initialize Print Engine

The PrintControlFactory.Create() method is responsible for creating an instrance 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 headerTemplate as string. We will look into the working of the engine with DataTable much later. Lets first understand the complete flow when using a WPF visual.

C#
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 an new instance of UnityContainer since i am using prism and unity for MVVM architechture. Here i register all the Views and ViewModels to the container.

Step 2 : Build Graph Visual

C#
 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 create 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 printter 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  whole 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

Another interesting area of the engine and infact was a little challenge for me too at the beginning. This is the part where i connect to the installed printers to fetch their properties such as the PaperSizes, DefaultPaper, Printer hardware margin etc. Also i have made sure that i do these once during its life time because this operation is expensive and sometimes take 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 operation are done only once.

C#
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 and when the control is first loaded. Infact from this step to the last step, everything takes place in the ReloadPreview method.  

C#
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;    
}

Normal 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 according ranging from 0 to 1 where 1 is 100% size and 0% orginal 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 seperate individual pages pages. 

There are several Paginators 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 as so is a base class to the other paginators. Second it is responsible for performing page calculation and clipping of a given visual into seperate 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 features is still not complete because i am trying to add this feature to work even when the DataGrid is in Virtualizaion mode.  
C#
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 detials w.r.t the VisualPaginator. The constructor as you can see is pretty straight forward. 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 with get cut. so when calculating the number of pages required, this hardware margin have 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 straight forward in case of VisualPaginator. Other paginators, specially the ItemsPaginator does the most complex calculation. Infact calculating the page count is what seperates the different paginators. If you want to add support for your own control type, lets 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 login for the horizontal and vertical page count. Ok lets gets back to our VisualPaginator. Here the number of horzontal page count is the total visual width divide by the printable width of each page. Similarly vertical page count is total height of the visual divide by height of each individual page. 

Step 6 : Create Pages

Here I do a iteration to walk through the whole DrawingVisual and save each block as a seperate page. This is done by first looping from 0 to the number of horizontal pages ( calculated in the previous step) and then move to next row and repeat untill i covered total horizontal page count. All these seperate 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 custom implementation of the .net frameworks DocumentPaginator class it should be fairly startforward to simple give the paginator to the DocumentViewer. But theres a catch. When the number of pages increase, specially in the area of several hundreds, the stardard 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 have created simple WPF Visual object from each of these DrawingVisuals and display them in a StackPanel. The code is rather self explanatory.  

C#
 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 steps is when the user select a different PaperSize, PageOrientation etc. Steps 4 to 7 is performed again to calculate new pages and display them. 

Conclusion        

This is the 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 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 bring in better improvements and features.

Also i have plan to give Visual Studio design time support for creating printing templates. 


 


License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United Kingdom United Kingdom
Currently living and working in London. He is an enthusiastic software developer passionate about microsoft technologies, specially C#, WPF, Silverlight WCF and windows Azure. Contributes to several open source project and msdn forums.

My Blog
twitter : @sarafuddin

Comments and Discussions

Discussions on this specific version of this article. Add your comments on how to improve this article here. These comments will not be visible on the final published version of this article.