Click here to Skip to main content
15,889,116 members
Articles / Programming Languages / C#
Tip/Trick

Passing Parameters to a PrintDocument.PrintPage Event

Rate me:
Please Sign up or sign in to vote.
4.20/5 (2 votes)
3 Sep 2017CPOL2 min read 22.8K   4   1
How to pass parameters to a PrintDocument.PrintPage event

Introduction

PrintDocument is very useful, and it's easy enough to use. But ... the MSDN example doesn't do it in a "nice" way. Specifically, if you want to print a subset of all possible values, the way it shows is to store the collection in a class level variable. So if your code is threaded and tries to print two different collections, things could get confused.

It would be a lot more sensible to pass a parameter - the collection itself - to the PrintPage event handler. And you can do that in a couple of ways.

Option 1: A Simple Lambda Expression

Just replace the print event handler name with a lambda:

C#
/// <summary>
/// Print a collection of Products
/// </summary>
/// <param name="items">Products  to print</param>
public static void Print(IEnumerable<Product> items)
{
    PrintDocument printing = new PrintDocument();
    printing.PrintPage += (sender, args) => printing_PrintPage(items, args);
    printing.Print();
}

This replaces the PrintDocument instance in the sender parameter with the collection of items to print.

In the handler method, cast it to the collection:

C#
/// <summary>
/// Printing a set of items.
/// </summary>
/// <remarks>
/// To pass a list of printable items, do this:
///             printing.PrintPage += (sender, args) => printing_PrintPage(items, args);
/// You can then cast the sender parameter to the collection.
/// </remarks>
/// <param name="sender"></param>
/// <param name="e"></param>
static void printing_PrintPage(object sender, PrintPageEventArgs e)
{
    IEnumerable<Product> items = sender as IEnumerable<Product>;
    if (items != null)
    {
        // Start printing your items.
    }
}

In your handler, you have access directly to the collection with minimal added code.

This is a good solution if your print is simple, and you need no access to the PrintDocument object (and mostly, you don't).

Option 2: Deriving from the PrintDocument Class

The lambda works fine, but a better approach would be to derive from PrintDocument and include the collection, page number, and "next item" index in the derived class. That way, the PrintDocument instance remains available to the PrintPage event handler.

That's also easy to do, but it requires a bit more effort in that you need to add a class to your application. This example is from a simple "barcode cheat sheet" printing app:

C#
using System;
using System.Collections.Generic;
using System.Drawing.Printing;
using System.Linq;

namespace SMBarcodeCheatSheet
    {
    /// <summary>
    /// Extends PrintDocument to hold extra info for Product collection printing
    /// </summary>
    internal class PrintProductDocument<T> : PrintDocument
        {
        #region Fields
        /// <summary>
        /// Just a short cut...
        /// </summary>
        private int numberProducts = 0;
        #endregion

        #region Properties
        /// <summary>
        /// Collection of products to print
        /// </summary>
        public IEnumerable<T> Products { get; set; }
        /// <summary>
        /// Next product to print
        /// </summary>
        public int NextProduct { get; set; }
        /// <summary>
        /// Current page number
        /// </summary>
        public int PageNumber { get; set; }
        /// <summary>
        /// Number of pages in total.
        /// </summary>
        public int NumberPages { get; set; }
        #endregion

        #region Constructors
        /// <summary>
        /// Collection constructor
        /// </summary>
        /// <param name="products">
        /// Collection of products to print</param>
        /// <param name="itemsPerPage">
        /// Number of items per page. Defaults to one</param>
        /// <param name="startWith">
        /// Item index to start printing at. Defaults to zero</param>
        /// <param name="pageNumber">
        /// Page number to start with. Defaults to one</param>
        internal PrintProductDocument(IEnumerable<T> products, 
             int itemsPerPage = 1, int startWith = 0, int pageNumber = 1)
            {
            Products = products;
            numberProducts = products.Count();
            NextProduct = Math.Min(Math.Abs(startWith), numberProducts - 1);
            NumberPages = Math.Max(Math.Abs
                          ((numberProducts - NextProduct) / itemsPerPage) + 1, 1);
            PageNumber = Math.Min(Math.Abs(pageNumber), NumberPages);
            }
        #endregion
        }
    }

All I am doing here is deriving a class from PrintDocument and adding properties to it in the usual way. I also added a constructor to setup the values for the print run. Note that this uses a generic IEnumerable collection - so it can be re-used to print any collection of any objects.

Now when we want to print, we construct an example of the derived class instead of the "raw" PrintDocument, and let it know what kind of items we expect it to print. In this case, it's going to be barcode images, eight rows of two columns per page so they are easy to scan:

C#
/// <summary>
/// Print a collection of Products
/// </summary>
/// <param name="items">Products  to print</param>
public static void Print(IEnumerable<Product> items)
    {
    PrintProductDocument<Product>
       printing = new PrintProductDocument<Product>(items, 8 * 2);
    printing.PrintPage += printing_PrintPage;
    printing.Print();
    }

All we need to do now, is actually do the printing:

C#
/// <summary>
/// Printing a set of items.
/// </summary>
/// <remarks>
/// To pass a list of printable items, do this:
///     printing.PrintPage += (sender, args) => printing_PrintPage(items, args);
/// You can then cast the sender parameter to the collection.
/// </remarks>
/// <param name="sender"></param>
/// <param name="e"></param>
static void printing_PrintPage(object sender, PrintPageEventArgs e)
    {
    PrintProductDocument<Product> ppd = sender as PrintProductDocument<Product>;
    if (ppd != null)
        {
        IEnumerable<Product> items = ppd.Products;
        int itemCount = items.Count();
        int top = e.MarginBounds.Top;
        int left = e.MarginBounds.Left;
        Graphics g = e.Graphics;
        int hAdvance = e.MarginBounds.Width / 2;
        int vAdvance = e.MarginBounds.Height / 8;
        for (int itemOnPage = 0; (itemOnPage < 8 * 2) &&
                                  ppd.NextProduct < itemCount; itemOnPage++)
            {
            Product p = items.ElementAt(ppd.NextProduct++);
            Rectangle printArea =
                new Rectangle(left, top, hAdvance - margin, vAdvance - margin);
            Rectangle barcodePrintArea =
                new Rectangle(printArea.X, printArea.Y, printArea.Width,
                (printArea.Height * 2) / 3);
            Rectangle textPrintArea =
                new Rectangle(printArea.X, printArea.Y +
                barcodePrintArea.Height + margin,
                printArea.Width, printArea.Height / 3);
            g.DrawImageUnscaledAndClipped(p.GetBarcodeImage(), barcodePrintArea);
            g.DrawString(p.Description, font, Brushes.Black, textPrintArea);
            if ((itemOnPage & 1) == 0)
                {
                left += hAdvance;
                }
            else
                {
                top += vAdvance;
                left = e.MarginBounds.Left;
                }
            }
        e.HasMorePages = ppd.NextProduct < itemCount;
        }
    }

In this case, I don't want to print the barcode Article Number, just the bars themselves, with a text description below (this is so she herself can scan the codes into our shopping list program without having the actual product handy - she doesn't like new things, so the numbers would just annoy her). So the actual print uses a clipped image, and divides each print area into two halves so they can't overlap.

Then the print is simple: loop to print a page full of information (unless we are on the last page when it could be shorter) and set HasMorePages to indicate if another page is required.

History

  • 2017-09-04: First release version
  • 2017-09-04: Minor typos fixed

License

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


Written By
CEO
Wales Wales
Born at an early age, he grew older. At the same time, his hair grew longer, and was tied up behind his head.
Has problems spelling the word "the".
Invented the portable cat-flap.
Currently, has not died yet. Or has he?

Comments and Discussions

 
SuggestionSource code in downloadable solution Pin
oth1259-Sep-17 12:29
oth1259-Sep-17 12:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.