Introduction
This article is the first of two articles that I am writing about Print Preview for the DataGridView
object. I’ve seen different articles and code about this, and I hope to give some contribution to it. When I’ started to write this piece of code, I had three different goals in my mind:
- To have a class that prints a
DataGridView
keeping its styles; - To manage a set of possibilities like margins, fit to pages, print a part of the table, and so on;
- To have a custom window for the preview (I’ll show that in the second article).
Also, I’ve tried to write a class that is possible to extend and customize in order to change the output result in a way like you can override the Paint
method in DataGridViewCell
to change the painting. The solution attached to the article contains the library for the Print Preview (GridPrintPreviewLib) and a sample application with data (TestApp). Test data are on an XML file, so no settings are necessary to use the sample.
Background
The basic class for the Print and Print Preview is the PrintDocument
that I’ve extended to create the GridPrintDocument
. The GridPrintDocument
is the real engine for the printing. In the OnPrintPage
overridden method, you can see the logic to check if it’s possible to print in the page and then print it. Currently, the engine doesn’t manage images inside the DataGridView
. Only the text is printed using its styles. You can override my methods to create a new class that prints images if you need this option.
Using the code
The library can be used inside every project. It’s only necessary to add references and nothing more. However, you need to have a DataGridView
in your project, of course. Below, you can see the basic code to show the Print Preview of a DataGridView
using the PrintPreviewDialog
.
GridPrintDocument doc = new GridPrintDocument(this.dataGridView1,
this.dataGridView1.Font, true);
doc.DocumentName = "Preview Test";
PrintPreviewDialog printPreviewDialog = new PrintPreviewDialog();
printPreviewDialog.ClientSize = new Size(400, 300);
printPreviewDialog.Location = new Point(29, 29);
printPreviewDialog.Name = "Print Preview Dialog";
printPreviewDialog.UseAntiAlias = true;
printPreviewDialog.Document = doc;
printPreviewDialog.ShowDialog();
doc.Dispose();
doc = null;
Print a selected part of the grid
The engine is designed to print with different options. One of them is the possibility to print just a part of the grid. To do that, I’ve created the GridSelectedArea
class that represents an area of the Grid. You define the start row and column and the final row and column as well. You can change the previous example by adding the use of the GridSelectedArea
and passing it to the GridPrintDocument
, setting the SelectedArea
attribute. Please note that this attribute is never set to null
; the GridPrintDocument
constructor fills it with a GridSelectedArea
that covers all the grid. For this reason, you cannot use the same instance of GridPrintDocument
if you change the DataGridView
linked with it. Better to say, you can, but you need to manually change the SelectedArea
attribute as well. See an example of the GridSelectedArea
below.
GridPrintDocument doc = new GridPrintDocument(this.dataGridView1,
this.dataGridView1.Font, true);
GridSelectedArea selArea = new GridSelectedArea(1, 2, 10,
dataGridView1.RowCount - 3);
doc.SelectedArea = selArea;
doc.DocumentName = "Preview Test";
PrintPreviewDialog printPreviewDialog = new PrintPreviewDialog();
printPreviewDialog.ClientSize = new Size(400, 300);
printPreviewDialog.Location = new Point(29, 29);
printPreviewDialog.Name = "Print Preview Dialog";
printPreviewDialog.UseAntiAlias = true;
printPreviewDialog.Document = doc;
printPreviewDialog.ShowDialog();
doc.Dispose(); doc = null;
Fit the grid into one page
The GridPrintDocument
can fit the grid in one or more pages. Unfortunately, to calculate the x and y factors for the fit, the engine needs to print the grid once before. So, the example below shows the normal grid at the beginning and then shows the grid fitting to a page. A better result is obtained with a custom preview that you’ll see in the second part of this article.
GridPrintDocument doc = new GridPrintDocument(this.dataGridView1,
this.dataGridView1.Font, true);
doc.DocumentName = "Preview Test";
PrintPreviewDialog printPreviewDialog = new PrintPreviewDialog();
printPreviewDialog.ClientSize = new Size(400, 300);
printPreviewDialog.Location = new Point(29, 29);
printPreviewDialog.Name = "Print Preview Dialog";
printPreviewDialog.UseAntiAlias = true;
printPreviewDialog.Document = doc;
printPreviewDialog.ShowDialog();
float scale = doc.CalcScaleForFit();
doc.ScaleFactor = scale;
printPreviewDialog = new PrintPreviewDialog();
printPreviewDialog.ClientSize = new Size(400, 300);
printPreviewDialog.Location = new Point(29, 29);
printPreviewDialog.Name = "PrintPreviewDialog1";
printPreviewDialog.UseAntiAlias = true;
printPreviewDialog.Document = doc;
printPreviewDialog.ShowDialog();
doc.Dispose();
doc = null;
However, for fitting to pages, you have the following methods: CalcScaleForFit
, if you want to fit to n1
horizontal pages and n2
vertical pages, and CalcScaleForFit
, if you want to fit all in one page. The factors obtained from these methods should be passed on to the engine using ScaleFactorX
, ScaleFactorY
, or ScaleFactor
.
Other print options
Using the DefaultPageSetting
of the GridPrintDocument
, you can change the print orientation, the margins, and the pages (from/to) that you want to print as well.
GridPrintDocument doc = new GridPrintDocument(this.dataGridView1,
this.dataGridView1.Font, true);
doc.DocumentName = "Preview Test";
doc.DefaultPageSettings.Landscape = true;
doc.DefaultPageSettings.PrinterSettings.FromPage = 1;
doc.DefaultPageSettings.PrinterSettings.ToPage = 3;
PrintPreviewDialog printPreviewDialog = new PrintPreviewDialog();
printPreviewDialog.ClientSize = new Size(400, 300);
printPreviewDialog.Location = new Point(29, 29);
printPreviewDialog.Name = "Print Preview Dialog";
printPreviewDialog.UseAntiAlias = true;
printPreviewDialog.Document = doc;
printPreviewDialog.ShowDialog();
doc.Dispose();
doc = null;
Points of interest
The print process is performed on the OnPrintPage
method in GridPrintDocument
class. The logical flow of the print process is described as follows:
- If the page comes before the
DefaultPageSettings.PrinterSettings.FromPage
value, skip it; - Apply scale;
- Calculate the vertical space for the header (if it’s present) and leave it on the page;
- Draw a horizontal cell checking if more pages in the horizontal axes are necessary;
- Draw the next row checking if more pages in the vertical axes are necessary;
- On first page, draw the header (if it’s present);
- If the next page comes after the
DefaultPageSettings.PrinterSettings.ToPage
value, skip it.
The flow calls for a set of private
and protected
methods in order to resolve all the calculations and perform the print process. The protected virtual
methods are designed to permit overriding them in order to customize the print output. In detail, these methods are:
onMeasureCell | To measure the cell size |
onPrepareDrawCell | To prepare all the necessary data for drawing the cell |
onDrawCell | To draw the cell |
onMeasureColumnHeaderHeight | To measure the vertical size of the header |
onPrepareColumnHeader | To prepare all the necessary data for drawing the header |
onDrawColumnHeader | To draw the header |
I suggest not overriding the onPrepare
methods because it should be necessary to implement a set of logic inside. Also, the onMeasure
and onDraw
methods could be enough to customize printing. Anyway, you have them if you want some different results! Below, you can see the code for the onDraw
methods.
protected virtual void onDrawCell(Graphics g, string s, RectangleF layoutRect,
StringFormat format, Font font, Brush brush, Brush brushBack,
DataGridViewAdvancedBorderStyle borderStyle)
{
g.FillRectangle(brushBack, layoutRect);
g.DrawString(s, font, brush, layoutRect, format);
}
As you can see, the method just draws the cell using the passed parameters. In detail, it draws the background, then the string inside, and the box around if it’s required.
protected virtual void onDrawColumnHeader(Graphics g, string s, RectangleF layoutRect,
StringFormat format, Font font, DataGridViewAdvancedBorderStyle borderStyle)
{
g.DrawString(s, font, Brushes.Black, layoutRect, format);
drawBox(g, layoutRect, borderStyle);
}
The method for drawing the header is called for every header label, and it draws the header using the passed parameters. In detail, it draws the string inside and the box around if it’s required. Please find below an example of the onDrawCell
override:
protected override void onDrawCell(System.Drawing.Graphics g, string s,
System.Drawing.RectangleF layoutRect, System.Drawing.StringFormat format,
System.Drawing.Font font, System.Drawing.Brush brush, System.Drawing.Brush brushBack,
System.Windows.Forms.DataGridViewAdvancedBorderStyle borderStyle)
{
borderStyle.Left = System.Windows.Forms.DataGridViewAdvancedCellBorderStyle.Single;
borderStyle.Right = System.Windows.Forms.DataGridViewAdvancedCellBorderStyle.Single;
base.onDrawCell(g, s, layoutRect, format, font, brush, brushBack, borderStyle);
}