Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

DataGridView Print/Print Preview Solution - Part II

5.00/5 (15 votes)
18 Jun 2009CPOL4 min read 78.6K   7.4K  
This article is the second one of two articles in which I want to show a solution for the Print Preview of the DataGridView object. One of the goals of my solution is to print the DataGridView keeping its styles automatically.

Introduction

This article is the second of two articles that I wrote about Print Preview for the DataGridView object. In the first one, I've written about how to use a new class, the GridPrintDocument, with the standard PrintPreviewDialog.

Now I'm going to show you how to create a personal preview dialog and how to use it.

The solution attached to the article contains the full library for the Print Preview (GridPrintPreviewLib) and a sample application with data (TestApp). Test data are in 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. There are also the GLPrintPreviewControl and frmPreviewDialog classes. They is the user interface for the preview and to handle the user interaction for page settings. To get the image for the preview, I've used the GetPreviewPageInfo() method of the GridPrintDocument. Please note that in the GridPrintDocument‘s PrintPreview, a PreviewPrintController is assigned to the PrintController of the GridPrintDocument. See the following code:

C#
private void PrintPreview(bool flagOnlyMeasure)
{
    PreviewPrintController preview = new PreviewPrintController();
    preview.UseAntiAlias = true;
    this.PrintController = preview;
    m_FlagOnlyMeasure = flagOnlyMeasure;
    base.Print();
    m_FlagOnlyMeasure = false;
}

In the GLPrintPreviewControl, you can see the code below to generate the images for the preview.

C#
public void GeneratePreview()
{
    Cursor oldCursor = this.Cursor;
    this.Cursor = Cursors.WaitCursor;
    m_PrintDocument.Print();
    PreviewPageInfo[] p = m_PrintDocument.GetPreviewPageInfo();
    previewPages = p;
    this.Cursor = oldCursor;
}

I've “replaced” the default Print method in GridPrintDocument of course. This is necessary to force preview and fit elaboration. Also, it means that if you want to print the result to a real printer, you need to generate the preview (it’s not necessary to show it) and then print the current result by using the base print method as below:

C#
m_PrintDocument.PrintController = null;
m_PrintDocument.PrinterSettings.PrinterName = printerName;
((PrintDocument)m_PrintDocument).Print();

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 frmPreviewDialog. The dialog will manage the interaction with the user.

C#
// dataGridView1 is the DataGridView to print
GridPrintDocument doc = new GridPrintDocument
			(this.dataGridView1, this.dataGridView1.Font, true);
doc.DocumentName = "Preview Test";
doc.StepForFit = 0.02f;
frmPreviewDialog frmDialog = new frmPreviewDialog();
frmDialog.ShowUnit = PrinterUnit.TenthsOfAMillimeter;
frmDialog.printDocument = doc;
frmDialog.ShowDialog(this);
frmDialog.Dispose();
doc.Dispose();

In order to use the frmPreviewDialog, you need to create the object and set ShowUnit and printDocument. printDocument is the GridPrintDocument to preview and ShowUnit is to set how you want to show the margins (millimeters, inches or display unit). In GridPrintDocument, there’s a new attribute named StepForFit. This value is used to reduce the fit factor when the fit action is not perfect. Let me explain. To fit the grid in n pages, I calc a reduce factor in this way:

C#
float scaleX = (m_PageSize.Width * m_FitToHorizontalPages) / m_MaxWidth;
float scaleY = (m_PageSize.Height * m_FitToVerticalPages) / m_MaxHeight;

Unfortunately this expression doesn't consider that a column cannot split into different pages, so it’s possible that when applying these reduce factors (scaleX and scaleY), the result is out of the requested range. To resolve this problem, I check if the output is inside the range: if it’s not, I reduce the factors by a percentage (StepForFit), while 0.02F means 2%, and then reapply the factor for generating a new preview. See the following piece of code:

C#
// Print for fit
bool flag = false;
do
{
    flag = false;
    ResetForRePrint();
    m_ScaleX = scaleX;
    m_ScaleY = scaleY;
    PrintPreview();
    if (m_XPageCount > m_FitToHorizontalPages)
    {
        flag = true;
        scaleX *= (1f - m_StepForFit);
    }
    if (m_YPageCount > m_FitToVerticalPages)
    {
        flag = true;
        scaleY *= (1f - m_StepForFit);
    }
} while (flag);

Points of Interest

To show the current page in the preview window, I've done a set of operations in graphics and then used a PictureBox to show the result. This is necessary because if you show the image got from PreviewPageInfo methods, its size is greater than the real physical size of the paper. Then, it’s necessary to draw the image in a bitmap with the correct size and then show it in a PictureBox using the StrechImage mode. A scale factor for the zoom is also applied. So, please note the following action in order to show the image correctly:

  1. Create a bitmap using the physical size and pixel format of the page
  2. Get a graphics from the bitmap
  3. Clear the bitmap by drawing a white rectangle
  4. Draw the image of the current page
  5. Set size and size mode to the PictureBox
  6. Set the created bitmap as the image of the PictureBox
C#
// Show current page in preview
private void ShowCurrentPage()
{
    if (m_PreviewPages == null)
    {
        return;
    }
    // Dispose previous image
    if (pbPreviewImage.Image != null)
    {
        pbPreviewImage.Image.Dispose();
        pbPreviewImage.Image = null;
    }
    // Create the bitmap
    Bitmap bitmap = new Bitmap(m_PreviewPages[m_Currentpage].PhysicalSize.Width, 
		m_PreviewPages[m_Currentpage].PhysicalSize.Height, 
		m_PreviewPages[m_Currentpage].Image.PixelFormat);
    using (Graphics gr = Graphics.FromImage(bitmap))
    {
        // Empty white rectangle
        gr.FillRectangle(Brushes.White, 0, 0, bitmap.Width, bitmap.Height);
        // Draw preview image
        gr.DrawImage(m_PreviewPages[m_Currentpage].Image, 0, 0, 
				bitmap.Width, bitmap.Height);
    }
    double scale = CalcZoomFactorForFit(bitmap);
    double maxWidth = bitmap.Width;
    double maxHeight = bitmap.Height;
    maxWidth *= scale;
    maxHeight *= scale;
    pbPreviewImage.Width = (int)maxWidth;
    pbPreviewImage.Height = (int)maxHeight;
    pbPreviewImage.Left = (pPreview.Width - pbPreviewImage.Width) / 2;
    pbPreviewImage.Top = (pPreview.Height - pbPreviewImage.Height) / 2;
    pbPreviewImage.SizeMode = PictureBoxSizeMode.StretchImage;
    pbPreviewImage.Image = bitmap;
}

The most complicated elaborations are performed inside the GridPrintDocument‘s OnPrintPage. One of these is to check if there're more horizontal or vertical pages. To do that, I've used two different flags that I'll check at the end of the methods. Please see below:

C#
....
sizeF = onMeasureCell(e.Graphics, cell, s, m_Font);
// Check Y : use sizeF.Height instead of cellSize.Height 
// if you want to measure string and not cell
//if (y + sizeF.Height > scaleMarginRB.Y)
if (y + cellSize.Height > scaleMarginRB.Y)
{
         hasMoreYPage = true;
         break;
}
// Check X : use sizeF.Width instead of cellSize.Width 
// if you want to measure string and not cell
//if (x + sizeF.Width > scaleMarginRB.X)
if (x + cellSize.Width > scaleMarginRB.X)
{
         hasMoreXPage = true;
         maxCol = m_ColIndex;
         break;
}
....
// Set flag HasMorePages if there're more pages
e.HasMorePages = hasMoreXPage || hasMoreYPage;
....

One more elaboration is to draw the next horizontal page correctly. In this case, I've used 2 variables to know from which row and column I need to start in the next page. m_RowIndex is the current index of row, so I go back replacing its values with the value of m_PrevPageRowIndex. m_PrevPageColIndex is the last column in the previous page, so I set it with the maxCol (max column in this page). When I start the new page, I'll use the m_RowIndex and m_PrevPageColIndex values.

C#
....
// Start to check new page
while (m_RowIndex <= m_SelArea.EndRow)
{
         // Start row for first col
         m_ColIndex = m_PrevPageColIndex;
         while (m_ColIndex <= m_SelArea.EndCol)
....
....
// If more X page restore start row as this page and col from next col
if (hasMoreXPage)
{
         m_RowIndex = m_PrevPageRowIndex;
         m_PrevPageColIndex = maxCol;
}
....

History

  • 18th June, 2009: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)