Introduction
Your shiny new application is finished to perfection — and it's a week ahead of schedule. After your demonstration to The Management, the CEO says, "It's great. I love it. But I want to have all that information as a report. And I want to be able to fiddle with the columns in the report, move them around and resize them, any way I want. It has to be sortable, and groupable, just like we can see on the screen there. And of course, we have to be able to print preview it before we print it. And can we have it finished by tomorrow?" You consider briefly whether you should mention the PrintScreen key, but you doubt he would be enthused with that solution.
Management love reports. Programmers aren't so keen. They often appear as an afterthought to the requirement. Yet producing nice looking reports is not a trivial task. You can always buy one of the commercial products, if you have Blizzard's budget and a few spare months to figure out how to make them work. There is at least one reasonable open source reporting solution for .NET, but the ramp up time is still considerable.
For a lazy and vain programmer like myself, what I really want is something that takes no effort to implement, yet produces wonderful results. I want to be able to go back to my CEO the same day, show him the nice looking reports that do exactly what he wanted, and then remind him about my overdue raise.
The ListViewPrinter
is designed to be just such a solution.
The ListViewPrinter Solution
The purpose of the ListViewPrinter
is simple: it turns a ListView
into a pretty report. The resulting report can be print previewed, have it's page setup adjusted, and (of course) be printed.
As normal, one major design goal of ListViewPrinter
was that it should quick and easy to use. So, normally, the programmer creates and configures a ListViewPrinter
instance within the IDE. Then, it is put into action like this:
this.listViewPrinter1.PrintPreview();
And that is it! Within the IDE, you can say which list view it should print, as well as setting lots of formatting options. And with only that one line of code, you will produce something like this:
|
This is not a screen shot of the list view. This is a print preview of a report that handles paging and shrinking to fit — a report that you can print out and hand to your CEO and finally get that raise you deserve.
As you can see from the example, the report has six parts, each of which can be customised:
- the page header
- the list view header
- a group header (only shown if the list view being printed was showing groups)
- the rows from the list
- the page footer
- the watermark
All of these parts (except the watermark) can be:
- told what font and brush to use for the text
- have it's background painted with any brush
- have any or all of its borders painted with any pen
The brushes can be gradient, hatch or texture brushes, all of which combine to allow you to make your report as spectacular as you want -- or to make it completely over the top, as you can see was done in the demo.
|
Controlling the Format
The formatting of the output is largely done from within the IDE. Each instance of ListViewPrinter
exposes the following properties: HeaderFormat
, FooterFormat
, ListHeaderFormat
, GroupHeaderFormat
and CellFormat
. These can be unrolled to allow the various aspects to be modified.
This is good but has its limits. Within the IDE there is no standard editor for a Pen
or Brush
object. The best that can be done for a Brush
is to expose a Color
and convert that to a SolidBrush
. And the best that can be done for a Pen
is a width and a color. If you want to use a fancier Brush
or Pen
(which you really should to get the best looking reports), you will have to write some code.
|
When choosing formatting, you should remember that the report is organised around blocks: the page header and footer, the group and list headers, and the list cells, are blocks. Within a single block, you can specify:
- the font to be used for the text
- the brush used to paint the text
- the brush used to fill in the area between the borders.
On each side of the block, you can specify:
- the padding (how much whitespace should separate this block from its neighbour)
- the pen used to draw the border. This includes the width of the pen and the brush to be used for drawing the border.
- the text inset (how much space should be left between the border and the text)
The diagram shows these settings for the Top and Bottom sides. They can equally be set for the Left and Right sides of the block.
|
Much of the flexibility of the formatting comes from the versatility of the Brush
class. There are several flavours of Brushes
:
- the
SolidBrush
(as shown in the diagram) where the area is painted a single color (which can translucent).
- the
HatchBrush
, where the area is painted with a pattern.
- the
TextureBrush
, where an area is painted with an image.
- the
LinearGradientBrush
, where an area is painted with a gradually changing color (as seen in the header and footer above).
Time spent understanding the various Brush
classes is worth the effort.
Getting Down to Business
Once the configuration is complete, the main programmatic interface to the ListViewPrinter
is simple. The commands correspond directly to the main printing commands:
void PageSetup();
void PrintPreview();
void PrintWithDialog();
The idea is that you hook each of these commands into your corresponding menu event handler, like this:
private void pageSetupToolStripMenuItem_Click(object sender,
EventArgs e) {
this.listViewPrinter1.PageSetup();
}
private void printPreviewToolStripMenuItem_Click(object sender, EventArgs e) {
this.listViewPrinter1.PrintPreview();
}
private void printToolStripMenuItem_Click(object sender, EventArgs e) {
this.listViewPrinter1.PrintWithDialog();
}
To print without any user interaction, you can call:
this.listViewPrinter1.Print();
Interesting Bits of Code
There are always lots of little interesting bits that come up when a project like this is written.
Margins for Error
The margins given in the PrintPageEventArgs.MarginBounds
are not reliable. Well, technically, they are, but they don't mean what they seem to mean. They work fine during print preview, but when you print a real page, the printout always seems strangely off-center. And the off-centered-ness is different from printer to printer.
The problem is the unreachable areas of the page. There are almost always areas of the page that the printer cannot reach, and these areas are different for different printers. .NET 2.0 represents these unprintable areas as the hard margins.
So, to calculate the effective margin for a printout, the code needs to look like this:
if (this.PrintController.IsPreview)
this.pageBounds = (RectangleF)e.MarginBounds;
else
this.pageBounds = new RectangleF(e.MarginBounds.X -
e.PageSettings.HardMarginX,
e.MarginBounds.Y - e.PageSettings.HardMarginY, e.MarginBounds.Width,
e.MarginBounds.Height);
Stateful Execution
One thing to keep in mind when printing is that each page is printed separately. Your code is called to print the next page when the framework requires it, and then it exits again. This means that at the end of each page, the printing code has to store enough state information to be able to resume when the next page is required. If you are not used to programming with finite state machines, this statefulness can take a little getting used to.
Printing the Watermarks
I'm not sure if anyone actually uses watermarks, but it was fun to implement, and so it is there as a feature (not the recommended method of scoping a project). There are two parts to the watermark problem: how to rotate a string, and how to print the text translucently.
Rotating the text requires either a nice DrawRotatedString()
method or messing with matrix transformations. .NET doesn't have a DrawRotatedString()
method so we are stuck with transformations. This is actually a good thing, since transformations are far more powerful. General transformations defines how to map points from one coordinate space into another coordinate space. If that doesn't seem very helpful, then you just have to know that they can be used to produce effects such as rotation, scaling, and skewing. .NET does have a nice matrix class, with utility methods to create various transformation matrices. Once we have the transformation matrix we want, we simply give it to our Graphics
object, and all drawing after that will be transformed.
So to draw our rotated text, we make a rotation matrix and then apply it to the Graphic
object:
Matrix m = new Matrix();
m.RotateAt(watermarkRotation, Utils.CalculateCenter(this.pageBounds));
g.Transform = m;
Printing the text translucently was simply a matter of creating a brush with a translucent color, and then painting the text with that brush:
int alpha = (int)(255.0f * (float)this.WatermarkTransparency /
100.0f);
Brush brush = new SolidBrush(Color.FromArgb(alpha,
this.WatermarkColorOrDefault));
g.DrawString(this.Watermark, this.WatermarkFontOrDefault, brush,
this.pageBounds,
strFormat);
When we have finished drawing the string, we clear the transformation so that drawing returns to normal.
g.ResetTransform();
Limitations
As you may have guessed from the name, the ListViewPrinter
works only with ListViews
. If you don't use a ListView
to present your data, then ListViewPrinter
will not help you. You will have to dig out your checkbook and cancel your social life for a few weeks. But many applications do use ListViews
and this class is designed to help.
A ListView
must be in Details
view for the ListViewPrinter
to know how to print it. If it is in any other view, the report will be blank.
The ListViewPrinter
cannot print a ListView
that is in virtual mode. This is because, in virtual mode, you cannot use the Items
collection. So there is no way to get the n'th ListViewItem
in the ListView
. But see the Shameless Plug below for a way around this limitation.
The ListViewPrinter
is not, and is not designed to be, a general purpose reporting solution. There is no provision for running totals, placing graphics, drawing triangles, or having vertical text. It just prints list views.
Shameless Plug
The ListViewPrinter
works well with normal ListViews
, but it works even better with ObjectListViews
. For example, when used in conjunction with an ObjectListView
, subitems can have images, all custom renderers work, and even virtual list views can be printed. For lazy and vain programmers, the ObjectListView
is a big help. See here for details about ObjectListView.
Still To Do
Allow Pens and Brushes to be edited directly within the IDE.
Conclusion
Hopefully this code will help you to print out your ListView based data, and to finally get the raise you deserve.
History
29 November 2007 - Version 1.2
- List rows now wrap text rather than ellipsing it. You can turn off this behaviour by
CellFormat.CanWrap = false;
Thanks to dgortemaker for the suggestion.
- Handle the case where a
ListViewItem
has less subitems than there are columns in the list view (thanks to Bernd Melchert)
11 November 2007 - Version 1.0