Skip to main content

C# WPF Printing Part 2 - Pagination [Intermediate]


About two weeks ago, we had a tutorial here at SOTC on the basics of printing in WPF. It covered the standard stuff, like popping the print dialog, and what you needed to do to print visuals (both created in XAML and on the fly). But really, that's barely scratching the surface - any decent printing system in pretty much any application needs to be able to do a lot more than that. So today, we are going to take one more baby step forward into the world of printing - we are going to take a look at pagination.

The main class that we will need to do pagination is the DocumentPaginator. I mentioned this class very briefly in the previous tutorial, but only in the context of the printing methods on PrintDialog, PrintVisual (which we focused on last time) and PrintDocument (which we will be focusing on today). This PrintDocument function takes a DocumentPaginator to print - and this is why we need to create one. Unfortunately, making a DocumentPaginator is not as easy as just creating an instance. It is an abstract class, and so you actually need to create your own class that derives from it, filling in all the abstract methods with the functionality that you need.

Today, we are going to be writing our own DocumentPaginator that spits out random tabular data - I called it the RandomTabularPaginator. Every page printed will have a header, and then as many rows of data as can be fit onto the page. Below you can see the top of an example printed page:

Random Printed Data Example

So let's take a look at what we are getting into by trying to write our own DocumentPaginator. The following is what Visual Studio stubs out for you when you create a class that derives from DocumentPaginator:
public class RandomTabularPaginator : DocumentPaginator
{
  /// <summary>
  /// When overridden in a derived class, 
  /// gets the DocumentPage for the specified page number
  /// </summary>
  /// <param name="pageNumber">
  /// The zero-based page number of the document 
  /// page that is needed.
  /// </param>
  /// <returns>
  /// The DocumentPage for the specified pageNumber, 
  /// or DocumentPage.Missing if the page does not exist.
  /// </returns>
  public override DocumentPage GetPage(int pageNumber)
  {
    throw new NotImplementedException();
  }

  /// <summary>
  /// When overridden in a derived class, gets a value 
  /// indicating whether PageCount is the total number of pages. 
  /// </summary>
  public override bool IsPageCountValid
  {
    get { throw new NotImplementedException(); }
  }

  /// <summary>
  /// When overridden in a derived class, gets a count 
  /// of the number of pages currently formatted.
  /// </summary>
  public override int PageCount
  {
    get { throw new NotImplementedException(); }
  }

  /// <summary>
  /// When overridden in a derived class, gets or 
  /// sets the suggested width and height of each page.
  /// </summary>
  public override Size PageSize
  {
    get
    {
      throw new NotImplementedException();
    }
    set
    {
      throw new NotImplementedException();
    }
  }

  /// <summary>
  /// When overridden in a derived class, 
  /// returns the element being paginated.
  /// </summary>
  public override IDocumentPaginatorSource Source
  {
    get { throw new NotImplementedException(); }
  }
}
 
Most of those method comments are actually pretty self-explanatory. As you probably figured out, the really important method here is GetPage. This method will get called once for every page in your document (the number of pages in your document being defined by the PageCount property). Each time, you have to return a DocumentPage instance. This might sound onerous, but it actually isn't that bad (as we will see in a moment) - a DocumentPage is just a shell around a visual that you want to print.

Ok, time to get started filling out these methods. Below is the code for everything except the GetPage (because that method is a bit more complicated):
public class RandomTabularPaginator : DocumentPaginator
{
  private int _RowsPerPage;
  private Size _PageSize;
  private int _Rows;

  public RandomTabularPaginator(int rows, Size pageSize)
  {
    _Rows = rows;
    PageSize = pageSize;
  }

  public override DocumentPage GetPage(int pageNumber)
  {
    throw new NotImplementedException();
  }

  public override bool IsPageCountValid
  { get { return true; } }

  public override int PageCount
  { get { return (int)Math.Ceiling(_Rows / (double)_RowsPerPage); } }

  public override Size PageSize
  {
    get { return _PageSize; }
    set
    {
      _PageSize = value;

      _RowsPerPage = PageElement.RowsPerPage(PageSize.Height);

      //Can't print anything if you can't fit a row on a page
      Debug.Assert(_RowsPerPage > 0);
    }
  }

  public override IDocumentPaginatorSource Source
  { get { return null; } }
}
 
As you can see, we first added a constructor and a couple of fields. The two things this RandomTabularPaginator needs to know are the number of rows of random data to generate, and the page size that the data will be printed on. We pull those in through the constructor and store them. When setting the page size, we calculate the number of rows of data that can fit on a page, and store that as well. If your wondering what the class PageElement is, don't worry - we will be getting to that real soon. For now, you will just have to believe that PageElement.RowsPerPage actually does return the number of rows that can fit on a page for a given page height.

In the PageCount property, we just return the ceiling of the total number of rows divided by the number of rows that can fit on a page. And since we always know the exact number of pages in our document, we can just flat out return true from IsPageCountValid. In more complex printing, you might be unable to calculate the total number of pages right off the bat - which is why this property is there.

Finally, we return null from the Source because we don't have an IDocumentPaginatorSource. Generally, you don't have to worry about this property and can always return null. It is used mostly by FlowDocument and the like.

Now for the GetPage method:
public override DocumentPage GetPage(int pageNumber)
{
  int currentRow = _RowsPerPage * pageNumber;

  var page = new PageElement(currentRow, 
    Math.Min(_RowsPerPage, _Rows - currentRow))
  {
    Width = PageSize.Width,
    Height = PageSize.Height,
  };

  page.Measure(PageSize);
  page.Arrange(new Rect(new Point(0,0), PageSize));     

  return new DocumentPage(page);
}
 
First off, we calculate the current row, based off of the page number passed in and our stored value for the number of rows that fit on a page. Then we create a new PageElement for this page. This is a custom control built just for rendering a page of RandomTabularPaginator content - I generally find it easier to work with printing pages this way, because otherwise the GetPage method can get quite long with all the rendering logic. So before we move on to the rest of this method, let's take a look at the PageElement class:
public class PageElement : UserControl
{
  private const int PageMargin = 75;
  private const int HeaderHeight = 25;
  private const int LineHeight = 20;
  private const int ColumnWidth = 140;

  private int _CurrentRow;
  private int _Rows;

  public PageElement(int currentRow, int rows)
  {
    Margin = new Thickness(PageMargin);
    _CurrentRow = currentRow;
    _Rows = rows;
  }

  public static int RowsPerPage(double height)
  {
    return (int)Math.Floor((height - (2 * PageMargin)
      - HeaderHeight) / LineHeight);
  }

  private static FormattedText MakeText(string text)
  {
    return new FormattedText(text, CultureInfo.CurrentCulture,
      FlowDirection.LeftToRight, new Typeface("Tahoma"), 14, Brushes.Black);
  }

  protected override void OnRender(DrawingContext dc)
  {
    Point curPoint = new Point(0, 0);

    dc.DrawText(MakeText("Row Number"), curPoint);
    curPoint.X += ColumnWidth;
    for (int i = 1; i < 4; i++)
    {
      dc.DrawText(MakeText("Column " + i), curPoint);
      curPoint.X += ColumnWidth;
    }

    curPoint.X = 0;
    curPoint.Y += LineHeight;

    dc.DrawRectangle(Brushes.Black, null, 
      new Rect(curPoint, new Size(Width, 2)));
    curPoint.Y += HeaderHeight - LineHeight;

    Random numberGen = new Random();
    for (int i = _CurrentRow; i < _CurrentRow + _Rows; i++)
    {
      dc.DrawText(MakeText(i.ToString()), curPoint);
      curPoint.X += ColumnWidth;
      for (int j = 1; j < 4; j++)
      {
        dc.DrawText(MakeText(numberGen.Next().ToString()), curPoint);
        curPoint.X += ColumnWidth;
      }
      curPoint.Y += LineHeight;
      curPoint.X = 0;
    }
  }
}
 
This class takes in the row number to start at and the number of rows to generate, and stores them in fields. It also has a bunch of constants about various heights and widths. The real work in this class is in the OnRender method, which is where everything gets drawn. I choose to do everything in OnRender here, but you could have just as easily built up a visual tree with a grid and a whole bunch of TextBlocks. In this case, using OnRender is probably faster, but there are cases where building a full visual tree is better to do.

The actual contents of OnRender for this class are kind of boring - just a whole bunch of loops drawing FormattedText objects - first for the headers, and then for each of the rows. One thing you don't want to forget (and is easy to miss) is that you still need to deal with setting your own page margins (since printers can't print to the edge of pages). Here, we deal with it by setting the margin on the PageElement in its constructor, but you can also deal with it by just making sure you position elements appropriately when drawing or laying things out.

Ok, back to the rest of that GetPage method. Once we have a PageElement instance, we set its width and height, and then run it through a Measure/Arrange pass. You may remember from the previous tutorial that you always have to Measure/Arrange brand new visuals before passing them to the PrintVisual method on PrintDialog. The same concept applies here - if you don't Measure/Arrange, you will end up with blank sheets of paper. Finally, we take the PageElement, package it in a DocumentPage, and return it. And that's it for implementing the RandomTabularPaginator!

But now you are probably wondering how you use a DocumentPaginator that you have written. Well, it is really easy - the following is the XAML and C# code from the simple app that tested the RandomTabularPaginator:
<Window x:Class="PaginatorExample.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="100" Width="100">
  <StackPanel>
    <StackPanel Orientation="Horizontal">
      <TextBlock VerticalAlignment="Center">Rows: </TextBlock>
      <TextBox x:Name="NumRows">50</TextBox>
    </StackPanel>
    <Button Click="PrintClick">Print</Button>
  </StackPanel>
</Window>

using System.Windows;
using System.Windows.Controls;

namespace PaginatorExample
{
  public partial class Window1 : Window
  {
    public Window1()
    { InitializeComponent(); }

    private void PrintClick(object sender, RoutedEventArgs e)
    {
      int rows = 0;
      if(!int.TryParse(NumRows.Text, out rows) || rows < 0)
      {
        MessageBox.Show("Not a valid number of rows.");
        return;        
      }

      var printDialog = new PrintDialog();
      if (printDialog.ShowDialog() == true)
      {
        var paginator = new RandomTabularPaginator(rows,
          new Size(printDialog.PrintableAreaWidth, 
            printDialog.PrintableAreaHeight));

        printDialog.PrintDocument(paginator, "My Random Data Table");
      }
    }
  }
}
 
This code pretty much comes down to three steps (after the work of getting the button click and grabbing the number of rows to generate). First, pop the print dialog. If the return value is true, you move on to step two - create a RandomTabularPaginator. We have the number of rows parsed already, so all we have to do is grab the currently selected page size off of the PrintDialog through the PrintableAreaWidth and PrintableAreaHeight properties. Finally, we pass the new RandomTabularPaginator to the PrintDocument, and we are done!

Well, that is it for this installment of printing in WPF. There is plenty more to come, though, because we still haven't event touched the PrintQueue or the XpsDocumentWriter. If you would like the code behind the example in this tutorial, you can grab a zip file containing the Visual Studio solution from the download link in the source files section below. As always, if you have any questions please leave a comment and I'll do my best to answer it.

Source Files:

Comments

Popular posts from this blog

C# Snippet - Shuffling a Dictionary [Beginner]

Randomizing something can be a daunting task, especially with all the algorithms out there. However, sometimes you just need to shuffle things up, in a simple, yet effective manner. Today we are going to take a quick look at an easy and simple way to randomize a dictionary, which is most likely something that you may be using in a complex application. The tricky thing about ordering dictionaries is that...well they are not ordered to begin with. Typically they are a chaotic collection of key/value pairs. There is no first element or last element, just elements. This is why it is a little tricky to randomize them. Before we get started, we need to build a quick dictionary. For this tutorial, we will be doing an extremely simple string/int dictionary, but rest assured the steps we take can be used for any kind of dictionary you can come up with, no matter what object types you use. Dictionary < String , int > origin = new Dictionary < string , int >(); ...

C# WPF Tutorial - Implementing IScrollInfo [Advanced]

The ScrollViewer in WPF is pretty handy (and quite flexible) - especially when compared to what you had to work with in WinForms ( ScrollableControl ). 98% of the time, I can make the ScrollViewer do what I need it to for the given situation. Those other 2 percent, though, can get kind of hairy. Fortunately, WPF provides the IScrollInfo interface - which is what we will be talking about today. So what is IScrollInfo ? Well, it is a way to take over the logic behind scrolling, while still maintaining the look and feel of the standard ScrollViewer . Now, first off, why in the world would we want to do that? To answer that question, I'm going to take a an example from a tutorial that is over a year old now - Creating a Custom Panel Control . In that tutorial, we created our own custom WPF panel (that animated!). One of the issues with that panel though (and the WPF WrapPanel in general) is that you have to disable the horizontal scrollbar if you put the panel in a ScrollV...