WPF Printing Part 2 - Pagination

Skill

WPF Printing Part 2 - Pagination

Posted in:

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.

KJ
03/06/2009 - 10:24

Hello Mate,

The code looks amazing, i have a little doubt on implementing the same logic on tabcontrol....

I am trying to print multiple tabpages in single PrintDialog.PrintVisual method....

though, couldn't get all the page, apart from current page getting printed all the time.
could you shed some light on this approach...?

thx
KJ

reply

Asadi
03/08/2009 - 09:51

Great work!
Thanks

reply

rbriggs
12/30/2009 - 16:14

How can we use this technique to print out image resources with our row data? I have tried and it isn't rendering properly. (center of page and very large)

reply

Henry Minute
06/24/2010 - 05:56

Very interesting. I cannot believe how difficult it is to find examples of printing from WPF. Even the MS SampleViewer wimps out by not implementing the functionality for its print button.

One question. Running the code and selecting the XPS Writer as the printer results in a blank second page. Is this intended or a problem? Or possibly me being dumb (quite possible)? :-)

reply

Vins
11/12/2010 - 10:35

I like your approach.. But, I have a slightly different scenario.. I have a grid containing my data. How can I bind my grid direcrly rather than drawing the text like you did in OnRender method? My grid may contain single page data or multiple page. I need to print all the contents of my Grid.

Thanks

reply

Vins
12/15/2010 - 10:43

Just for everyone’s benefit.. Here's the code that I came up with to print my grid.. I could not really bind the entire grid as it is.. But, this code pretty much works like the printing of Excel spread sheet with n number of columns and n number of rows:

This is the enhanced version of Paginator class:

public class DocPaginator : DocumentPaginator
{
    #region Fields

    private int _rows;
    private int _columns;
    private int _rowsPerPage;
    private int _columnsPerPage;
    private int _firstColumnWidth;
    private int _horizontalPageCount;
    private int _verticalPageCount;
    private System.Windows.Size _pageSize;        
    private DataGrid _dataGrid;
    private List<string> _columnsList;
    private bool _isFirstColumnLarger;
    private List<Dictionary<int, Pair<int, int>>> _columnSlotsList;
    private string _strTitle;
    private string _strComment;

    #endregion Fields

    #region Constructor

    public DocPaginator(DataGrid dataGrid, System.Windows.Size pageSize, List<string> columnsList,
                            bool isFirstColumnLarger = false, string strTitle = null, string strComment = null)
    {
        _rows = dataGrid.Items.Count;
        _columns = dataGrid.Columns.Count;            
        _dataGrid = dataGrid;
        _columnsList = columnsList;
        _isFirstColumnLarger = isFirstColumnLarger;
        _strTitle = strTitle;
        _strComment = strComment;

        CalculateFirstColumnWidth();
           
        PageSize = pageSize;

        _horizontalPageCount = HorizontalPageCount;
        _verticalPageCount = VerticalPageCount;
           
        GenerateColumnSlots();
    }

    #endregion Constructor

    #region Public Methods

    public override DocumentPage GetPage(int pageNumber)
    {
        double pgNumber = Math.IEEERemainder(pageNumber, _verticalPageCount) >= 0 ?
                            Math.IEEERemainder(pageNumber, _verticalPageCount) :
                                Math.IEEERemainder(pageNumber, _verticalPageCount) + _verticalPageCount;

        int currentRow = Convert.ToInt32(_rowsPerPage * pgNumber);

        var page = new PageElement(currentRow, Math.Min(_rowsPerPage, _rows - currentRow), _dataGrid,
                                        _columnsList, _firstColumnWidth, _isFirstColumnLarger,
                                            GetColumnSlot(pageNumber), _strTitle, _strComment, GetPageNumber(pageNumber))
        {
            Width = PageSize.Width,
            Height = PageSize.Height,
        };

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

        return new DocumentPage(page);
    }



    #endregion Public Methods

    #region Public Properties

    public override bool IsPageCountValid
    { get { return true; } }
       
    public override int PageCount
    { get { return _horizontalPageCount * _verticalPageCount; } }

    public override System.Windows.Size PageSize
    {
        get { return _pageSize; }
        set
        {
            _pageSize = value;
            _rowsPerPage = PageElement.RowsPerPage(PageSize.Height);
            _columnsPerPage = PageElement.ColumnsPerPage(PageSize.Width, _firstColumnWidth);

            //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; } }

    public int HorizontalPageCount
    {
        get { return (int)Math.Ceiling((_columns - 1) / (double)_columnsPerPage); }
    }

    public int VerticalPageCount
    {
        get { return (int)Math.Ceiling(_rows / (double)_rowsPerPage); }
    }

    #endregion Public Properties

    #region Private Methods

    private void CalculateFirstColumnWidth()
    {
        int maxDataLen = 0;

        for (int i = 0; i < _dataGrid.Items.Count; i++)
        {
            List<Object> icol = (List<Object>)_dataGrid.Items[i];
            var largestDataItem = (from d in icol
                                    select d != null ? d.ToString().Length : 0).Max();
            maxDataLen = maxDataLen < largestDataItem ? largestDataItem : maxDataLen;
        }

        string strDataLen = string.Join("a", new string[maxDataLen + 1]);

        _firstColumnWidth = PageElement.CalculateBitLength(strDataLen,
                                            new Font("Tahoma", 8, System.Drawing.FontStyle.Regular, GraphicsUnit.Point));
    }

    private void GenerateColumnSlots()
    {
        _columnSlotsList = new List<Dictionary<int, Pair<int, int>>>();

        for (int i = 0; i < _horizontalPageCount; i++)
        {
            Dictionary<int, Pair<int, int>> columnSlot = new Dictionary<int, Pair<int, int>>();
            columnSlot.Add(1, new Pair<int, int>((_columnsPerPage * i) + 1,
                                                    Math.Min(_columnsPerPage * (i + 1), _columns - 1)));

            _columnSlotsList.Add(columnSlot);
        }
    }

    private Dictionary<int, Pair<int, int>> GetColumnSlot(int pageNumber)
    {
        for (int i = 0; i <= _columnSlotsList.Count; i++)
        {
            if (i == Math.Ceiling(Convert.ToDouble(pageNumber / _verticalPageCount)))
                return _columnSlotsList[i];
        }
        return new Dictionary<int, Pair<int, int>>();
    }

    private string GetPageNumber(int intPageNumber)
    {
        string strPageNumber = String.Empty;

        if (_horizontalPageCount == 1)
            strPageNumber = (intPageNumber + 1).ToString();
        else
        { }

        return strPageNumber;
    }

    #endregion Private Methods
}

#region Pair Class

public class Pair<TStart, TEnd>
{
    public Pair(TStart start, TEnd end)
    {
        Start = start;
        End = end;
    }

    public TStart Start { get; set; }
    public TEnd End { get; set; }
}

#endregion

And this is the enhanced version of PageElement:

public class PageElement : UserControl
{
    #region Constants

    private const int PAGE_MARGIN = 40;
    private const int HEADER_HEIGHT = 50;
    private const int LINE_HEIGHT = 20;
    private const int COLUMN_WIDTH = 60;
    private const int HEADER_CHR_WIDTH = 9;
    private const int HEADER_LINE_HEIGHT = 12;
    private const string EXCAPE_CHAR = "\r\n";
    private const string NOT_APPLICAPLE = "N/A";

    #endregion Constants

    #region Fields

    private int _currentRow;
    private int _rows;
    private DataGrid _dataGrid;
    private List<string> _columns;
    private int _firstColumnWidth;        
    private bool _isFirstColumnLarger;
    private static int _columnsPerPage;
    private Dictionary<int, Pair<int, int>> _columnSlot;
    private string _strTitle;
    private string _strComment;
    private string _strPageNumber;        

    #endregion Fields

    #region Constructor

    public PageElement(int currentRow, int rows, DataGrid dataGrid, List<string> columns,
                        int firstColumnWidth, bool isFirstColumnLarger, Dictionary<int,
                            Pair<int, int>> columnSlot, string strTitle, string strComment,
                            string strPageNumber)
    {
        Margin = new Thickness(PAGE_MARGIN);
        _currentRow = currentRow;
        _rows = rows;
        _dataGrid = dataGrid;
        _columns = columns;
        _firstColumnWidth = firstColumnWidth;
        _isFirstColumnLarger = isFirstColumnLarger;
        _columnSlot = columnSlot;
        _strTitle = strTitle;
        _strComment = strComment;
        _strPageNumber = strPageNumber;
    }

    #endregion Constructor

    #region Public Static Functions

    public static int RowsPerPage(double height)
    {
        //5 times Line Height deducted for: 1 for Title and Comments each; 2 for Page Number and 1 for Date
        return (int)Math.Floor((height - (2 * PAGE_MARGIN) - HEADER_HEIGHT - (5 * LINE_HEIGHT)) / LINE_HEIGHT);
    }

    public static int ColumnsPerPage(double width, int firstColumnWidth)
    {
        _columnsPerPage = (int)Math.Floor((width - (2 * PAGE_MARGIN) - firstColumnWidth) / COLUMN_WIDTH);
        return _columnsPerPage;
    }

    public static int CalculateBitLength(string strData, d.Font font)
    {
        using (d.Graphics graphics = d.Graphics.FromImage(new d.Bitmap(1, 1)))
        {
            d.SizeF dtsize = graphics.MeasureString(strData, font);

            return Convert.ToInt32(dtsize.Width);
        }  
    }

    #endregion Public Static Functions

    #region Private Functions

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

    #endregion Private Functions

    #region Protected Functions

    protected override void OnRender(DrawingContext dc)
    {
        Point curPoint = new Point(0, 0);
        int beginCounter = 0;
        double intYAxisTracker = 0;
        double TitleHeight = 0;

        //Print Title.
        if (_strTitle != null)
        {
            int intTitleLength = CalculateBitLength(_strTitle, new d.Font("Tahoma", 9, d.FontStyle.Regular));
            curPoint.X = ((Width - (2 * PAGE_MARGIN)) / 2) - (intTitleLength / 2);
            dc.DrawText(MakeText(_strTitle, 9), curPoint);
            curPoint.Y += LINE_HEIGHT;
            curPoint.X = 0;
        }

        //Print Comment.
        if (_strTitle != null)
        {
            int intCommentLength = CalculateBitLength(_strComment, new d.Font("Tahoma", 9, d.FontStyle.Regular));
            curPoint.X = ((Width - (2 * PAGE_MARGIN)) / 2) - (intCommentLength / 2);
            dc.DrawText(MakeText(_strComment, 9), curPoint);
            curPoint.Y += LINE_HEIGHT;
            curPoint.X = 0;
        }

        //Print current Date.
        int intDatLength = CalculateBitLength(String.Format("{0:MMMM dd, yyyy}", DateTime.Now), new d.Font("Tahoma", 9, d.FontStyle.Regular));
        curPoint.X = ((Width - (2 * PAGE_MARGIN)) / 2) - (intDatLength / 2);
        dc.DrawText(MakeText(String.Format("{0:MMMM dd, yyyy}", DateTime.Now), 9), curPoint);
        curPoint.Y += LINE_HEIGHT;
        curPoint.X = 0;

        TitleHeight = curPoint.Y;

        //Print First column of header row.
        dc.DrawText(MakeText(_columns[0], 9), curPoint);
        curPoint.X += _firstColumnWidth;
        beginCounter = _columnSlot[1].Start;

        //Print other columns of header row
        for (int i = beginCounter; i <= _columnSlot[1].End; i++)
        {
            //Remove unwanted characters
            _columns[i] = _columns[i].Replace(EXCAPE_CHAR, " ");

            if (_columns[i].Length > HEADER_CHR_WIDTH)
            {
                //Loop through to wrap the header text
                for (int k = 0; k < _columns[i].Length; k += HEADER_CHR_WIDTH)
                {
                    int subsLength = k > _columns[i].Length - HEADER_CHR_WIDTH ? _columns[i].Length - k : HEADER_CHR_WIDTH;
                    dc.DrawText(MakeText(_columns[i].Substring(k, subsLength), 9), curPoint);
                    curPoint.Y += HEADER_LINE_HEIGHT;
                }
            }
            else
                dc.DrawText(MakeText(_columns[i], 9), curPoint);

            //YAxisTracker keeps track of maximum lines used to print the headers.
            intYAxisTracker = intYAxisTracker < curPoint.Y ? curPoint.Y : intYAxisTracker;
            curPoint.X += COLUMN_WIDTH;
            curPoint.Y = TitleHeight;
        }

        //Reset X and Y pointers
        curPoint.X = 0;
        curPoint.Y += intYAxisTracker - TitleHeight;
           
        //Draw a solid line
        dc.DrawRectangle(Brushes.Black, null, new Rect(curPoint, new Size(Width, 2)));
        curPoint.Y += HEADER_HEIGHT - (2 * LINE_HEIGHT);

        //Loop through each collection in dataGrid to print the data
        for (int i = _currentRow; i < _currentRow + _rows; i++)
        {
            List<Object> icol = (List<Object>)_dataGrid.Items[i];

            //Print first column data
            dc.DrawText(MakeText(icol[0].ToString(), 10), curPoint);
            curPoint.X += _firstColumnWidth;
            beginCounter = _columnSlot[1].Start;

            //Loop through items in the collection; Loop only the items for currect column slot.
            for (int j = beginCounter; j <= _columnSlot[1].End; j++)
            {
                dc.DrawText(MakeText(icol[j] == null ? NOT_APPLICAPLE : icol[j].ToString(), 10), curPoint);
                curPoint.X += COLUMN_WIDTH;
            }
            curPoint.Y += LINE_HEIGHT;
            curPoint.X = 0;
        }

        //Print Page numbers
        curPoint.Y = Height - (2 * PAGE_MARGIN) - LINE_HEIGHT;
        curPoint.X = Width - (2 * PAGE_MARGIN) - COLUMN_WIDTH;

        dc.DrawText(MakeText(_strPageNumber, 9), curPoint);

    }

    #endregion Protected Functions
}

Hope this helps..

reply

Anonymous
02/23/2011 - 12:09

Hi, I was wondering if there was a way to add a background color to every other line. So, say if I wanted a light gray on every other line when it prints to help seperate the lines.

Also, does this code support column wrapping? So if I set a column width and the text is to big for the column, will it just wrap to the next line in the column? Thanks

reply

Anonymous
03/15/2011 - 18:00

The code that I have posted does support column wrapping. Check out the code under the comment "Loop through to wrap the header text".

Although, now I have a better way of handling my wrapping. When I display my columns on the screen I use "\r\n" for break in line and wrap big column names. I pass my column names along with the special characters and call MakeText which automatically wraps my text wherever it finds these characters and then I draw the text in one single go. So now I don't have to loop through and manually wrap the text and it shows exactly the same as on the screen, which is even great!

Here's a code snippet of that:

//Print other columns of header row
for (int i = beginCounter; i <= _columnSlot[1].End; i++)
{
numOfLines = Regex.Matches(_columns[i].Key.ToString(), EXCAPE_CHAR).Count + 1;
dc.DrawText(MakeText(_columns[i].Key.ToString(), 9), curPoint);
curPoint.X += _otherColumnWidth;
curPoint.Y = TitleHeight;

//Keep track of maximum lines used to print the headers.
intMaxNumOfLines = intMaxNumOfLines < numOfLines ? numOfLines : intMaxNumOfLines;
}

reply

Chetan
03/09/2011 - 07:44

Hi Vins,
I went through the code snippet which you have posted.
I am getting the below error when i try to use the code:

Error:The namespace or directive 'd' couldnot be found.
Is this is a sturcture 'd' ??, can you please let me know how to overcome the above error.

I have WPF:DataGrid,Will i be above to send all the contents of WPF:DataGrid,including the images,hyperlinks which are in my WPF:DataGrid to PrintPreview Window's DocumentViewer???

Please let me know about this ASAP, i have spent lot of time for this functionality .

It would be really helpfull if you put the code snippet for PrintPreview in which we will calls docPaginator class constructor which you have created.
i.e, I need code replacement for below lines of code:

System.Windows.Controls.PrintDialog dialog = new System.Windows.Controls.PrintDialog();
var paginator = new RowPaginator(_Rows, new System.Windows.Size(dialog.PrintableAreaWidth, dialog.PrintableAreaHeight));

string tempFileName = System.IO.Path.GetTempFileName();
File.Delete(tempFileName);
using (XpsDocument xpsDocument = new XpsDocument(tempFileName, FileAccess.ReadWrite))
{
XpsDocumentWriter writer = XpsDocument.CreateXpsDocumentWriter(xpsDocument);
writer.Write(paginator);

PrintPreviewWindow printPreview = new PrintPreviewWindow
{
//owner = this,
Document = xpsDocument.GetFixedDocumentSequence()
};
printPreview.ShowDialog();
}

Thanks in advance..

reply

Vins
03/15/2011 - 18:37

Hello Chetan,

I apologize. I should have included using statements as well. d stands for System.Drawing. Here are all my using statements for PageElement:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Media;
using System.Globalization;
using Microsoft.Windows.Controls;
using d = System.Drawing;
using ThomsonReuters.Baseline.Utils.Helpers;
using System.Text.RegularExpressions;

The PageElement class accepts DataGrid as the parameter and loop through it to pull the data out and print. I see no reason why it would not take hyperlinks or any other kind of text and print on the page. I am not sure about the images though. In what format do you have images saved in your data grid? I am not very optimistic that this class will print images as they appear on the screen. Never tried.. Give it a shot though and see what happens.

Here's my code for instantiating the DocPaginator class:

var paginator = new DocPaginator
(Report.reportGrid,
new Size(dialog.PrintableAreaWidth,
dialog.PrintableAreaHeight), columnHeaders,
(bool)firstColumnCompany.IsChecked,
snapshotLayoutManager.CurrentLayout.Title,
LayoutMgr.CurrentLayout.Comment);

And if you notice in the DocPaginator constructor the last three parameters are optional so you can omit them if you don't need to set. So your call would look something like this..

var paginator = new DocPaginator(_Rows,
new System.Windows.Size(dialog.PrintableAreaWidth,
dialog.PrintableAreaHeight), columnHeaders);

You'll have to create a list of Column Headers just by looping through your data grid before calling the constructor of DocPaginator.

Remaining code for your XPS Document writer should remain the same.. Hope this helps.. Let me know how it works out..

reply

Terry
11/01/2011 - 10:51

Excellent one !!!

I was wondering how to print multiple visuals in multiple pages and your WPF Printing PArt 1 & 2 helped me solve my problem.

Am extremely happy and excited to see the expected results in just one try. Very good explaination. Your explaination helped me figure out how should I create my DocumentPage.

Thanks a lot.

reply

Terry
11/11/2011 - 05:21

I want to show the Preview of my Document.
For that how do I pass IDocumentSource of Paginator class :
public IDocumentPaginatorSource Document
{
get { return null; }
set { ;
}
}

My PrintPreviewWindow XML :

---------- In Preview method of the application :
paginator = new AttendancePaginator(totalRecords,
new Size(pageWidth, pageHeight), mainDs);

PrintPreviewWindow preview = new PrintPreviewWindow();
preview.Owner = this;
preview._viewer = paginator;
//preview.Document = paginator;
preview.ShowDialog();

None of the above works. How do I get paginator.Source() that will return IDocumentPaginatorsource OR What to do to set the DocumentViewer of the PrintPreviewWindow to show the pages in preview.

PLEASE Try to help me at the earliest. I found using XPSDocument but none of this type to work out.

reply

Add Comment

Put code snippets inside language tags:
[language] [/language]

Examples:
[javascript] [/javascript]
[actionscript] [/actionscript]
[csharp] [/csharp]

See here for supported languages.

Javascript must be enabled to submit anonymous comments - or you can login.
CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.