Title: Print the contents of a ListView control in C#
The ListView control, like most controls, includes no support for printing. If you want to display a ListView control's contents on a print out, you need to do all of the printing yourself.
Unfortunately printing in C# is extremely flexible so it's hard to anticipate what you might want to do with it. For example, suppose a ListView control contains too many columns to fit on one page. Should the program split the print out across multiple pages? Should it print in landscape mode? Should it reduce the print size? Should it ignore some columns that hold less interesting data?
This example displays a print preview of the contents of a ListView control with its View property set to Details. It makes the printed columns in the print out as wide as they currently are in the control.
Depending on your computer, that may not fit well on a printed page. For example, my laptop screen is wider than an 8.5" x 11" page so it can easily display a ListView that won't fit on one page.
This example also assumes that all of the data will fit on one page vertically. If you must print hundreds of entries, you'll need to modify the code to span multiple pages. (I may make an example like that at some point.)
At this point, you can probably see that any printing program will require a lot of assumptions.
At design time I added a PrintPreviewDialog named ppdListView and a PrintDocument named pdocListView to the program's form. I also set the dialog's Document property to pdocListView. When you click the Preview button, the following event handler starts the printing process.
// Print the ListView's contents.
private void btnPreview_Click(object sender, EventArgs e)
{
// Start maximized.
Form frm = ppdListView as Form;
frm.WindowState = FormWindowState.Maximized;
// Start at 100% scale.
ppdListView.PrintPreviewControl.Zoom = 1.0;
// Display.
ppdListView.ShowDialog();
}
This event handler first casts the PrintDialog into a Form (a PrintDialog is a kind of form so thaht's allowed) and maximizes it. It then finds the PrintPreviewControl on the dialog and sets its Zoom level to 100%.
The code then calls the dialog's ShowDialog method to display the dialog modally. Behind the scenes the dialog invokes the PrintDocument to figure out what it should print. The PrintDocument then raises its PrintPage event to let the program tell it what to print.
The following code shows the PrintDocument object's PrintPage event handler.
// Print the ListView's data.
private void pdocListView_PrintPage(object sender, PrintPageEventArgs e)
{
// Print the ListView.
lvwBooks.PrintData(e.MarginBounds.Location,
e.Graphics, Brushes.Blue,
Brushes.Black, Pens.Blue);
}
This code simply invokes the ListView control's PrintData extension method shown in the following code. This is where all the interesting work occurs.
// Print the ListView's data at the indicated location
// assuming everything will fit within the column widths.
public static void PrintData(this ListView lvw, Point location,
Graphics gr, Brush header_brush, Brush data_brush, Pen grid_pen)
{
const int x_margin = 5;
const int y_margin = 3;
float x = location.X;
float y = location.Y;
// See how tall rows should be.
SizeF row_size = gr.MeasureString(lvw.Columns[0].Text, lvw.Font);
int row_height = (int)row_size.Height + 2 * y_margin;
// Get the screen's horizontal resolution.
float screen_res_x;
using (Graphics screen_gr = lvw.CreateGraphics())
{
screen_res_x = screen_gr.DpiX;
}
// Scale factor to convert from screen pixels
// to printer units (100ths of inches).
float screen_to_printer = 100 / screen_res_x;
// Get the column widths in printer dots.
float[] col_wids = new float[lvw.Columns.Count];
for (int i = 0; i < lvw.Columns.Count; i++)
col_wids[i] = (lvw.Columns[i].Width + 4 * x_margin) *
screen_to_printer;
int num_columns = lvw.Columns.Count;
using (StringFormat string_format = new StringFormat())
{
// Draw the column headers.
string_format.Alignment = StringAlignment.Center;
string_format.LineAlignment = StringAlignment.Center;
for (int i = 0; i < num_columns; i++)
{
RectangleF rect = new RectangleF(
x + x_margin,
y + y_margin,
col_wids[i] - x_margin,
row_height - y_margin);
gr.DrawString(lvw.Columns[i].Text,
lvw.Font, header_brush, rect, string_format);
rect = new RectangleF(x, y, col_wids[i], row_height);
gr.DrawRectangle(grid_pen, rect);
x += col_wids[i];
}
y += row_height;
// Draw the data.
foreach (ListViewItem item in lvw.Items)
{
x = location.X;
for (int i = 0; i < num_columns; i++)
{
RectangleF rect = new RectangleF(
x + x_margin, y,
col_wids[i] - x_margin, row_height);
switch (lvw.Columns[i].TextAlign)
{
case HorizontalAlignment.Left:
string_format.Alignment = StringAlignment.Near;
break;
case HorizontalAlignment.Center:
string_format.Alignment = StringAlignment.Center;
break;
case HorizontalAlignment.Right:
string_format.Alignment = StringAlignment.Far;
break;
}
gr.DrawString(item.SubItems[i].Text,
lvw.Font, header_brush, rect, string_format);
rect = new RectangleF(x, y, col_wids[i], row_height);
gr.DrawRectangle(grid_pen, rect);
x += col_wids[i];
}
y += row_height;
}
}
}
The static PrintData extension method is contained in the static ListViewExtensions class. The ListView parameter named lvw is the control whose PrintData method is being called. The method takes as parameters the location where the control should be drawn and the Graphics object on which to draw. Normally that's the Graphics object used by the PrintPage event handler to represent the printed page.
The method also takes parameters giving the brushes that it should use to draw the data's headers and the data itself, and a pen used to draw a grid around the data. You could add other parameters to specify such things as background colors and fonts, but that would make this method even more complex and it's already complicated enough for an example.
The code defines constants x_margin and y_margin, which it uses to allow a bit of space between the data items and the grid lines around them. The code initializes variables x and y to be the upper left corner of the area where the control should be printed.
Next the code uses the Graphics object's MeasureString method to see how tall the header row should be and adds 2 * y_margin to allow a little extra room.
The next step is one of the more confusing. The control on the screen measures in pixels but the printer, and the Graphics object that represents it, measures in 100ths of an inch. To make the grid and other printed elements line up properly, the code needs to translate from screen coordinates to printer coordinates.
To do that, the code creates a new Graphics object associated with the ListView control. That Graphics object is created as if it were going to be used on the screen. The code reads that object's DpiX property to get the screen's horizontal resolution in pixels per inch.
The code then divides 100 by the screen resolution to get a scale factor to convert from pixels to 100ths of an inch.
Next the code loops through the ListView control's Columns collection to make an array named col_wids holding the widths of each column. (The code could look up the widths as needed, but the col_wids array makes the code a little easier to read.)
Finally the code is ready to print. It creates a StringFormat object and prepares it to center text vertically and horizontally.
It then loops through the ListView control's columns printing each column's header. To do that, it makes a RectangleF object indicating where the header should be drawn. That RectangleF is located at the current x and y position. It is as wide as the column and as tall as the previously calculated row height.
After printing the header text, the code uses the Graphics object's DrawRectangle method to draw a rectangle around the text. It then adds the column's width to the current x value.
After it has drawn all of the headers, the code adds the row height to y to draw the next line of data moved down one row.
Now the program draws the data. For each row, the code resets x to the left edge of the location where the method should print. Next the code loops through the pieces of data in the row, drawing each piece of text and then drawing a rectangle around it.
That's basically all there is to it, but I did pull a fast one here. The code uses the Graphics object's DrawRectangle method but, strangely, there is no version of the DrawRectangle method that takes a RectangleF as a parameter. There are versions that take a Rectangle or four int or float values, but none that takes a RectangleF.
To make drawing rectangles easier, I added the following overloaded extension method to the Graphics class.
// Draw a RectangleF.
public static void DrawRectangle(this Graphics gr, Pen pen,
RectangleF rectf)
{
gr.DrawRectangle(pen,
rectf.Left, rectf.Top, rectf.Width, rectf.Height);
}
You could make lots of modifications to this example, but this version should get you started.
Download the example to experiment with it and to see additional details.
|