Title: Print values in a grid in C#
This example prints a set of values in a grid. The following DrawValueGrid method does all of the printing. It's long but the pieces are relatively simple.
// Draw a grid containing the indicated values.
private void DrawValueGrid(Graphics gr, RectangleF bounds,
float header_hgt, float margin,
string[] headers, string[,] values,
StringAlignment[] header_alignments,
StringAlignment[] header_line_alignments,
StringAlignment[] column_alignments,
StringAlignment[] column_line_alignments,
Font header_font, Font[] value_fonts,
Brush header_brush, Brush value_brush,
Pen row_line_pen, Pen column_line_pen, Pen box_pen)
{
// See how many rows and columns of data there are.
int num_rows = values.GetUpperBound(0) + 1;
int num_cols = values.GetUpperBound(1) + 1;
// Calculate the row height and column width.
float row_hgt = (bounds.Height - header_hgt) /
(float)num_rows;
float col_wid = bounds.Width / num_cols;
// Make a StringFormat to align text.
using (StringFormat sf = new StringFormat())
{
float y = bounds.Y;
float x = bounds.X;
// Draw the headers.
if (header_hgt > 0)
{
for (int col = 0; col < num_cols; col++)
{
// Get the string's layout rectangle.
sf.Alignment = header_alignments[col];
sf.LineAlignment = header_line_alignments[col];
RectangleF rect = new RectangleF(
x + margin, y + margin,
col_wid - 2 * margin,
header_hgt - 2 * margin);
#if DRAW_BG
// For debugging purposes.
gr.FillRectangle(Brushes.Silver, rect);
#endif
// Draw the string.
gr.DrawString(headers[col], header_font,
header_brush, rect, sf);
x += col_wid;
}
y += header_hgt;
}
// Draw the values.
for (int row = 0; row < num_rows; row++)
{
x = bounds.X;
for (int col = 0; col < num_cols; col++)
{
// Get the string's layout rectangle.
sf.Alignment = column_alignments[col];
sf.LineAlignment = column_line_alignments[col];
RectangleF rect = new RectangleF(
x + margin, y + margin,
col_wid - 2 * margin,
row_hgt - 2 * margin);
#if DRAW_BG
// For debugging purposes.
gr.FillRectangle(Brushes.Silver, rect);
#endif
// Draw the string.
gr.DrawString(values[row, col], value_fonts[col],
value_brush, rect, sf);
x += col_wid;
}
y += row_hgt;
}
// Draw row lines.
if (row_line_pen.Color.A > 0)
{
y = bounds.Y;
if (header_hgt > 0)
{
y += header_hgt;
gr.DrawLine(row_line_pen, bounds.Left, y,
bounds.Right, y);
}
for (int r = 1; r < num_rows; r++)
{
y += row_hgt;
gr.DrawLine(row_line_pen, bounds.Left, y,
bounds.Right, y);
}
}
// Draw column lines.
if (column_line_pen.Color.A > 0)
{
x = bounds.X;
for (int c = 1; c < num_cols; c++)
{
x += col_wid;
gr.DrawLine(column_line_pen, x, bounds.Top,
x, bounds.Bottom);
}
}
// Draw a box around it all if desired.
if (box_pen.Color.A > 0)
gr.DrawRectangle(box_pen, Rectangle.Round(bounds));
}
}
The method takes the following parameters.
- gr - The Graphics object on which to draw. Normally that will be the Graphics object provided by a PrintPage event handler, although I suppose you could use the method to draw a grid on something else such as a Bitmap.
- bounds - The area on the Graphics object where the grid should be drawn.
- header_hgt - The amount of vertical area that should be reserved for headers. Set this to 0 if you don't want headers.
- margin - A margin inside each of the grid's cells. If this is 0, then the text inside the cells may come very close to the cells' edges. (This example uses 0.05 inches or 5 print units for the margin. That seems to work fairly well.)
- headers - A array of header values. Include one header per column. (Although you can set a header to "" if you don't want a particular column to have a header.)
- values - A two-dimensional array of values to display in the grid.
- header_alignments - An array giving the horizontal alignment for the headers. For example, you can center some headers and right-justify others such as those over numeric columns.
- header_line_alignments - An array giving the vertical alignment for the headers.
- column_alignments - An array giving the horizontal alignment for the data values. For example, you can center text values and right-justify numeric values.
- column_line_alignments - An array giving the vertical alignment for the data values.
- header_font - The font to use when drawing the column headers.
- value_font - The font to use when drawing the data values.
- header_brush - The brush to use when drawing the column headers.
- value_brush - The brush to use when drawing the data values.
- row_line_pen - The pen to use when drawing lines between rows. Set this to Pens.Transparent if you don't want these lines.
- column_line_pen - The pen to use when drawing lines between columns. Set this to Pens.Transparent if you don't want these lines.
- box_pen - The pen to use when drawing a box around the entire grid. Set this to Pens.Transparent if you don't want this box.
The method starts by examining the bounds in the values array to see how many rows and columns it needs to print.
To determine how tall rows should be, the code subtracts the header height from the available height and divides by the number of rows.
To determine how wide columns should be, the method divides the available width by the number of columns.
Next, the method creates a StringFormat object to use when aligning text. It then sets variables x and y to the upper left corner of the available area.
If the header_hgt parameter is greater than 0, the method prints the column headers. For each column, it sets the StringFormat object's Alignment and LineAlignment properties to properly align the column's header. It then prints the header in the area that it should occupy.
Next, the method loops over the rows in the values array and performs the same steps it used to print the headers. Conditional code lets you fill the background behind the values for debugging purposes (or you may just like that effect). This lets you see how the margins affect the values' placement.
After it has printed all of the values, the method draws lines between the rows, if row_line_pen is not transparent. Then, if column_line_pen is not transparent, the method draws lines between the columns. Finally, if box_pen is not transparent, the method draws a rectangle around the whole grid.
This program's form contains a PrintPreviewDialog component named ppdInvoice. It also contains a PrintDocument object named pdocItems. I used the Properties window to set the PrintPreviewDialog object's Document property equal to the PrintDocument object so the PrintPreviewDialog component knows what document to use to generate its contents. At run time, the program uses the following code to display the PrintPreviewDialog.
// Display the print preview dialog.
private void btnPreview_Click(object sender, EventArgs e)
{
ppdInvoice.ShowDialog();
}
When the dialog is asked to display itself, it tells its PrintDocument object to generate the output that it should display. That raises the PrintDocument object's PrintPage event, and that's where the real magic happens.
The following code shows how the program handles this event and calls the DrawValueGrid method. The event handler is long but it's shorter and even more straightforward than the DrawValueGrid method shown earlier.
// Generate the print out.
private void pdocItems_PrintPage(object sender,
System.Drawing.Printing.PrintPageEventArgs e)
{
const int inch = 100; // 100 print units per inch.
RectangleF bounds = new RectangleF(
e.MarginBounds.X,
e.MarginBounds.Y,
4 * inch, 2 * inch);
float header_hgt = 50;
float margin = 0.05f * inch;
string[] headers = { "Item", "Price Each",
"Quantity", "Total" };
string[,] values =
{
{"Pencils, dozen", "$1.24", "4", "$4.96"},
{"Paper, ream", "$3.75", "3", "$11.25"},
{"Cookies, box", "$2.17", "12", "$26.04"},
{"Notebook", "$1.95", "2", "$3.90"},
{"Pencil sharpener", "$12.95", "1", "$12.95"},
{"Paper clips, 100", "$0.75", "1", "$0.75"},
};
StringAlignment[] header_alignments =
{
StringAlignment.Center,
StringAlignment.Far,
StringAlignment.Far,
StringAlignment.Far,
};
StringAlignment[] value_alignments =
{
StringAlignment.Near,
StringAlignment.Far,
StringAlignment.Far,
StringAlignment.Far,
};
StringAlignment[] line_alignments =
{
StringAlignment.Center,
StringAlignment.Center,
StringAlignment.Center,
StringAlignment.Center,
};
using (Font header_font =
new Font("Times New Roman", 12, FontStyle.Bold))
{
using (Font roman10 =
new Font("Times New Roman", 10, FontStyle.Regular))
{
using (Font courier8 =
new Font("Courier New", 8, FontStyle.Regular))
{
Font[] value_fonts = { roman10, courier8,
courier8, courier8 };
DrawValueGrid(e.Graphics, bounds, header_hgt,
margin, headers, values,
header_alignments, line_alignments,
value_alignments, line_alignments,
header_font, value_fonts,
Brushes.Blue, Brushes.Black,
Pens.Green, Pens.Blue, Pens.Red);
}
}
}
e.HasMorePages = false;
}
This event handler simply sets up a bunch of variables to give information to the DrawValueGrid method and then calls that method. The only tricky thing this code does is using the same line_alignments array to give the headers and values the same horizontal alignments. That ensures that a header is lined up over the values in its column.
You could add many variations to this basic printing technique. For example, you could:
- Draw an extra thick line below the headers
- Alternate the background colors of different rows
- Highlight specific cells with different foreground or background colors or fonts, for example if a value is negative
- Make the rows or columns auto-side instead of specifying the grid's total available area
- Print across multiple pages if the grid is too wide or long
Hopefully this example will get you started so you can make those changes if you need them. If you do make changes that you think others might like to see, please let me know.
Download the example to experiment with it and to see additional details.
|