Print a ListView with large contents in C#


[ListView]

The example Print the contents of a ListView control in C# explains how to print the data in a ListView control when its View property is set to Details. One difficulty when printing this kind of data is that a screen may be able to hold much more data than a printed page so it’s hard to fit everything in. The previous example handles that problems by assuming all of the data can fit in the available area. In particular it assumes that every data item can fit within a single row inside its column’s current width.

This example uses a different approach. It makes each piece of data fit within its column’s width but allows data items to span multiple lines if necessary.

The basic approach for each row is to draw a row’s items in their allowed column widths and measure the amount of vertical space each needs. After drawing all of the items, the program draws boxes around them that are as tall as the item that needed the most vertical space. The code then starts drawing the next row of data below that tallest item.

The PrintMultiLineData extension method shown in the following code controls the printing process.

// Print the ListView's data at the indicated location
// allowing data to span multiple lines.
public static void PrintMultiLineData(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;

    // 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 units.
    float[] col_wids = new float[lvw.Columns.Count];
    for (int i = 0; i < lvw.Columns.Count; i++)
        col_wids[i] = (lvw.Columns[i].Width + 2 * 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;
        var header_query =
            from ColumnHeader column in lvw.Columns
            select column.Text;
        DrawMultiLineItems(header_query.ToArray(),
            gr, lvw.Font, header_brush, grid_pen,
            x_margin, y_margin,
            x, ref y, col_wids, num_columns, string_format);

        // Draw the data.
        string_format.Alignment = StringAlignment.Near;
        foreach (ListViewItem item in lvw.Items)
        {
            var subitems_query =
                from ListViewItem.ListViewSubItem subitem
                in item.SubItems
                select subitem.Text;
            DrawMultiLineItems(subitems_query.ToArray(),
                gr, lvw.Font, data_brush, grid_pen,
                x_margin, y_margin,
                x, ref y, col_wids, num_columns, string_format);
        }
    }
}

PrintMultiLineData is an extension method that extends the ListView class. It takes as parameters a Point giving the upper left corner where drawing should begin, the Graphics object on which to draw, and brushes and pens to use when drawing.

The method first gets the screen’s horizontal resolution and uses it to calculate a scale factor for converting from the pixels used to measure column widths to printer units. (For more information, see my previous post.

Next the code copies the ListView control’s column widths into an array, adding some extra room for margins and scaling the result to printer units.

The method makes a StringFormat object to control how text is aligned while printing and then starts printing.

The method uses a LINQ query to select the text displayed by the ListView control’s column headers. It passes an array of those text values, plus some other parameters described shortly, into the DrawMultiLineItems method for printing.

Next, for each row of data, the code uses a LINQ query to select the text values displayed by the sub-items in that row. (Note that the first sub-item contains the main item’s text so you don’t need to treat the item itself as a special case.) The code then calls the DrawMultiLineItems method, passing it the array of sub-item text values plus other parameters.

The following code shows the DrawMultiLineItems method.

// Draw the items in a row.
private static void DrawMultiLineItems(string[] items,
    Graphics gr, Font lvw_font, Brush header_brush, Pen grid_pen,
    float x_margin, float y_margin, float x0, ref float y0,
    float[] col_wids, int num_columns, StringFormat string_format)
{
    float row_height = 0;
    float x = x0;
    for (int i = 0; i < num_columns; i++)
    {
        // Measure the size needed by the text.
        float text_width = col_wids[i] - 2 * x_margin;
        SizeF layout_area = new SizeF(col_wids[i], 1000);
        SizeF row_size =
            gr.MeasureString(items[i], lvw_font, layout_area);
        if (row_height < row_size.Height) row_height = row_size.Height;

        // Draw the text.
        RectangleF rect = new RectangleF(
            x + x_margin, y0 + y_margin,
            text_width, row_size.Height);
        gr.DrawString(items[i], lvw_font,
            header_brush, rect, string_format);

        // Draw the next column.
        x += col_wids[i];
    }

    // Add extra room for the vertical margin.
    row_height += 2 * y_margin;

    // Draw boxes around the column headers.
    x = x0;
    for (int i = 0; i < num_columns; i++)
    {
        // Draw the box.
        RectangleF rect = new RectangleF(
            x, y0, col_wids[i], row_height);
        gr.DrawRectangle(grid_pen, rect);

        // Draw the next column.
        x += col_wids[i];
    }

    // Get ready for the next row.
    y0 += row_height;
}

The DrawMultiLineItems method draws a single row of data items. The variable row_height keeps track of the largest height needed by any item in the row.

For each column in the row, the code uses the Graphics object's MeasureString method to see how much room that column's text needs. If the text needs more vertical space than row_height, the code updates row_height. The code then draws the item's text in the space it requires.

After it has drawn the text for each data item, the code draws boxes around the items. It simply loops through the items drawing the boxes, using the maximum height row_height for each box's height.

After it has drawn all of the boxes, the code adds the row's height to the starting y coordinate y0 so the next row will begin below this one.

This is an improvement on the previous example, but it still has some problems. Most notably, this method assumes that all of the data can fit on one page so it won't break the print out into multiple pages vertically or horizontally. That sort of printing will depend on your specific needs so I won't cover it here. It you need that sort of printing, you can modify this example or email me if you want me to consult for you.

(Note that you may be able to reuse the DrawMultiLineItems method to draw other row of data where items may have different heights.)


Download Example   Follow me on Twitter   RSS feed   Donate




This entry was posted in controls, drawing, graphics, printing and tagged , , , , , , , , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *