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:
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:
static void printing_PrintPage(object sender, PrintPageEventArgs e)
{
IEnumerable<Product> items = sender as IEnumerable<Product>;
if (items != null)
{
}
}
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:
using System;
using System.Collections.Generic;
using System.Drawing.Printing;
using System.Linq;
namespace SMBarcodeCheatSheet
{
internal class PrintProductDocument<T> : PrintDocument
{
#region Fields
private int numberProducts = 0;
#endregion
#region Properties
public IEnumerable<T> Products { get; set; }
public int NextProduct { get; set; }
public int PageNumber { get; set; }
public int NumberPages { get; set; }
#endregion
#region Constructors
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:
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:
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
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?