Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / multimedia / GDI+

Printable Reports Library for Windows Forms

4.29/5 (20 votes)
3 Jun 2007CPOL9 min read 1   3.9K  
An article on generating printable reports with C#

Sample Image - ReportLibrary.jpg

Introduction

The Report Library is a .NET class library which generates printable reports using GDI+. It takes a DataTable as its report source. The reports can be easily customized to suit business requirements.

Background

Although the .NET framework has support for viewing Crystal Reports, the report generation requires the installation of the Crystal Reports runtime on the client machine. In this article, I will explain how to create a C# class that reads a DataTable and renders the data in a printable format using GDI+.

Advantages of Using this Class Over Crystal Reports

  • The main advantage is portability. The class is fully coded in C#. Unlike Crystal Reports, no installation is required.
  • Ease of use. The report takes in a DataTable as its data source, and most formatting options have set default values which can be customized if required.

Brief Introduction to GDI+

GDI+ is an updated version of the old GDI (Graphics Device Interface) functions provided by the Windows API. GDI+ provides a new API for graphics functions, which then takes advantage of the Windows graphics library.

The GDI+ functions can be found in the System.Drawing namespace. Some of the classes and members in this namespace will look familiar if you have used the Win32 GDI functions. Classes are available for such items as pens, brushes, and rectangles.

Using the Code

Assuming that you have created a C# Windows Application project in VS, add the ReportLibrary Project files to your Solution. Compile the ReportLibrary project and add the ReportLibrary reference to your Windows Application project. Add a PrintPreviewDialog control to the Windows Form, and add the following code to the Click event of your Print Preview button. For demonstration purposes, I have created a simple Windows Form with a few radio buttons and text boxes that demonstrates a few different layout and formatting options available.

The following code demonstrates a few layout and formatting options that are available in the ReportClass. In this article, we will be using an XML file as the report source.

C#
ReportClass objRepLib = new ReportClass();

//Declare the PrintDocument
PrintDocument myDoc = new PrintDocument();

//Load Default properties
LoadProperties();

//Get the DataTable for the report data source
DataTable dt = new DataTable();
DataSet ds = new DataSet();
ds.ReadXml(DataSourceTxt.Text);
dt = ds.Tables[0];

//Set the PrintDocument from the ReportClass
myDoc = objRepLib.ReportPrintDocument;

//Set the report DataSource
objRepLib.ReportSource = dt;

//Set the PrintDocument to the applications PrintPreviewDialog 
//control
printPrevDiag.Document = myDoc;

//Set the ReportTitleFirstPageOnly property
objRepLib.ReportTitleFirstPageOnly = ShowTitleAllPages;

//Show or Hide the logo
objRepLib.ReportHasLogo = Showlogo;
Image img;
if (objRepLib.ReportHasLogo == true)
{
    img = Image.FromFile(LogoTxt.Text);
    objRepLib.ReportLogo = img;
}

//Show or Hide the report title
objRepLib.HasTitle = true;

//Show or Hide totals for all numeric columns
objRepLib.ReportShowTotals = ShowReportTotals;

//Set report page orientation to landscape or portrait
objRepLib.IsLandscape = PageOrientation;

//Show or hide the table border
objRepLib.HideGrid = ShowTableBorder;

//Set the report title text
objRepLib.ReportTitle = ReportTitleTxt.Text ;

//Show the print preview dialog
printPrevDiag.ShowDialog();

In this article, I will discuss most options, and it should be quite straightforward once I have explained the concept. The objective is to render a DataGrid using GDI+. The complexity, if any, arises when you have to implement 'paging', i.e., at what point do you stop printing and start on a new page? This cannot be a fixed value based on the row count as the reports page orientation, row height, and font size are all customizable.

Generating the Report

We first set the ReportSource property which takes a value of type DataTable. The report table is generated by looping through the DataTable and rendering the grid using the Graphics.DrawRectangles method for each cell in the table. To do this, we need the following information:

  1. Number of cells in a row: This would be the number of columns in the DataTable.
  2. Number of rows in the report table: This would be the number of rows in the DataTable.
  3. Total number of cells in the report table: This would be the product of the number of rows and columns + the number of columns (for the report header row) + the number of columns (for the report total row).
C#
int tabCol;
int tabRows;
int tabArrayMax;

//Store the number of cells in a row for the report table.
tabCol = dt.Columns.Count;
//Store the number of rows for the report table.
tabRows = dt.Rows.Count;
//Store the number of cells in the table which would be 
//the number of rectangles to render as table cells.
//Since the product of TabCol and TabRows represents the 
//number of cells in the table body, we need to add
//TabCol to the result to include the header row cells.
tabArrayMax = (tabCol * tabRows) + tabCol;

Report Total

The report has a ReportShowTotals property which displays the total for all numeric columns in the table (excluding the first column). This feature requires a numeric array to store the sum of the numeric columns. The array TotCol is defined outside the PrintPage method for public scope to preserve the values between pages. The TotCol array size is set while rendering the first page. The array defined has to be decimal to store numbers with precision.

C#
if (PageCount == 0)
{
    ReDim(ref TotCol,tabCol) ;
}

Rendering the Report

The report table width, header row height, and body row height have default values which can be customized using the following properties:

  • TableWidth: The default value is based on the page's inner width, i.e., the page width minus the left and right margin width. This means the report table will automatically span the max available area depending on the page orientation. This value can be customized. If the custom width is greater than the page's inner width, the custom value is ignored and the default value is used instead.

To get the individual cell width, we divide the table width with the number of columns. To start rendering the grid, we need to get the X and Y co-ordinates as the starting point, which is calculated based on the page's inner width and height.

C#
//Check if the table width specified for the report is greater than 
//the inner page width (excluding margins)
if (iTableWidth == 0 || iTableWidth > e.MarginBounds.Width)
{
    iTableWidth = e.MarginBounds.Width;
}

//Find out the avg width of each cell by dividing the total table width 
//by the number of columns.

int colWidth = Convert.ToInt32(iTableWidth / tabCol);
Pen vbPen = new Pen(Color.Black);
//Declare an Array of RectangleF to render the table grid using 
//the DrawReclangles method.
RectangleF[] rectCell = new RectangleF[tabArrayMax + tabCol];

//Declare a RectangleF to store the report title.
RectangleF titleRect = new RectangleF() ; 

//Declare a RectangleF to store the report logo.
RectangleF logoRect = new RectangleF(); 

//Store the X coordinate from where to start rendering.
int startX = e.MarginBounds.Top;

//Store the Y coordinate from where to start rendering.
int startY = e.MarginBounds.Top - 35;

Report Title and Logo

The title and logo are rendered by defining Rectangles as containers for the title and logo. The logo size defaults to the size of the logoRect rectangle. The title is rendered using the titleRect rectangle. This is done to ensure the title text would wrap within its container.

Typically, you would only need to set the ReportHasLogo, ReportLogo, HasTitle, and ReportTitle properties to display the logo and title, but they can be further customized by the following properties.

  • ReportLogo: Sets the report logo. The logo, by default, is located on the left side of the page. This property accepts a value of Image type.
  • ReportHasLogo: Sets the boolean property that shows or hides the report logo. The default value is false. Set this value to true to display the logo.
  • LogoHeight: Sets the height of the logo image. The default value is 75. Since the logo and title appear side by side, the logo height is restricted to the height set for the report title. If the logo height exceeds the title height, it defaults to the title height.
  • LogoWidth: Sets the width of the logo image. The default value is 150.
  • HasTitle: Sets the boolean property that shows or hides the report title. The default value is false.
  • ReportTitle: Sets the report title. The default value is a blank string. Setting this property does not automatically set the HasTitle property to true.
  • TitleFont: Sets the font for the report title. Accepts a value of type Font. The default value is Arial size 14.
  • TitleFontColor: Sets the font color for the report title. Accepts a value of type Brush. The default value is Brushes.Black.
  • TitleAlignment: Sets the alignment for the report title. Accepts a value of type StringAlignment. The default value is StringAlignment.Center.
  • TitleHeight: Sets the height for the report title. The default value is 40.
  • ReportTitleFirstPageOnly: This is a boolean property that shows or hides the report title on subsequent pages. The default value is true. Set this property to false to display the title on all pages.
C#
StringFormat modSf = new StringFormat();

int i  = 0;
int k  = 0;

//Check if Logo needs to be displayed
if (bReportHasLogo)
{
    //Check if the Logo height does not exceed the report title height.
    if (iReportLogoHeight > iReportTitleHeight)
    {
        iReportLogoHeight = iReportTitleHeight;
    }
    logoRect = new RectangleF(startX, startY, 
                iReportLogoWidth, iReportLogoHeight);
    //Render the logo.
    e.Graphics.DrawImage(imReportLogo, logoRect);
}
else
{
    iReportLogoWidth = 0;
    iReportLogoHeight = 0;
}

//Check if report title needs to be displayed. 
if (bReportHasTitle && (PageCount == 0 || !bReportTitleFirstPageOnly))
{
    modSf.LineAlignment = saReportTitleAlignment;
    modSf.Alignment = saReportTitleAlignment;
    titleRect = new RectangleF(startX + ReportLogoWidth, startY, 
        e.MarginBounds.Width - iReportLogoWidth, iReportTitleHeight);
    //Render the title
    e.Graphics.DrawString(sReportTitle, fntReportTitleFont, 
                brReportTitleFontColor, titleRect, modSf);
}
else
{
    iReportTitleHeight = 0;
}

Report Header

The report header row is created by looping through the columns of the DataTable and rendered using the Graphics.DrawString method. The Graphics.DrawString method uses a Rectangle as the container for the string. This ensures the text remains within the rectangle bounds (table cell), and also gives us the option to display the grid lines by explicitly printing the Rectangles using the Graphics.DrawRectangles method.

The header row formatting can be customized by the following properties:

  • HeaderCellAlignment: Sets the text alignment for the header row. Accepts a value of type StringAlignment. The default value is StringAlignment.Near.
  • HeaderFont: Sets the font for the header row. Accepts a value of type Font. The default value is Arial size 10.
  • HeaderFontColor: Sets the font color for the header row. Accepts a value of type Brush. The default value is Brushes.Black.
  • HeaderBackgroundColor: Sets the background color for the report header. Accepts a value of type Brush. The default value is Brushes.White.
C#
modSf.LineAlignment = saReportHeaderCellAlignment;
modSf.Alignment = saReportHeaderCellAlignment;

//Set the Y coordinates for the next line.

startY += Math.Max(iReportLogoHeight,iReportTitleHeight ) + 25;

//create an array of header row values
object[] hString = new object[tabCol] ;
dt.Columns.CopyTo(hString,0);

//get the maximum header row height
iHeaderRowHeight = Convert.ToInt32(GetRowHeight
(hString,fntReportHeaderFont,colWidth,iHeaderRowHeight,e));

//Render the report table header.
//Loop through each column of the table and render the column title.
foreach (DataColumn dc in dt.Columns)
{
    Rectangle r1 = new Rectangle(startX, startY, colWidth, iHeaderRowHeight);
    //Renders the report table grid
    e.Graphics.FillRectangle(brReportHeaderBackgroundColor,r1 );
    if (!bReportHideGrid)
    {
        e.Graphics.DrawRectangle(vbPen,r1);
    }
    e.Graphics.DrawString(dc.ToString(), fntReportHeaderFont, 
                    brReportHeaderFontColor, r1, modSf);
    //Set X coordinates for the next cell
    startX += colWidth;
    k ++;
}

Report Body and Paging

The report body is generated by looping through each row of the DataTable and rendered using the Graphics.DrawString method. The Graphics.DrawString method uses a Rectangle as container for the string. This ensures the text remains within the rectangle bounds (table cell), and also gives us the option to display the grid lines by explicitly printing the Rectangles using the Graphics.DrawRectangles method.

This section also explains the "Total" and "Paging" features. The total of all numeric columns can be displayed by setting the following properties:

  • ReportShowTotals: Sets the boolean property which shows or hides the total for all numeric columns in the report. The default value is false.
  • NumericAlignment: Sets the text alignment for all numeric values in the report body. Accepts a value of type StringAlignment. The default value is StringAlignment.Near.

The total is calculated by looping through the columns for each row and storing the sum of numeric values into an array. If the ReportShowTotals property is set to true, the array values are displayed on the last page after the last row. The first column of this row will contain the word "Total".

The "Paging" feature is set by the Page's HasMorePages property which tells the printer if an additional page should be printed. This property should be set to true for all pages except the last one. To determine if the page rendered is not the last one, we keep track of the position of the last rendered row and check if its Y co-ordinate is equal to or greater than the bottom margin of the page. If the total number of rows in the report is greater than the current row count, the HasMorePages property is set to true.

C#
//Set the X coordinate for the next row.
startX = e.MarginBounds.Top;
//Set the Y coordinates for the next row.
startY += iHeaderRowHeight;

//Store current coordinate on the page.
int yPos = 0;
//Render report table body till the bottom margin of the page
while (yPos <= e.MarginBounds.Bottom)
{
    int j  = 0;
    DataRow dr ;
    //Check if current rendered row count = the total number of rows 
    //and set the HasMorePages property to false.
    if (ReportRowCount == dt.Rows.Count)
    {
        //Write the total row
        if (bReportShowTotals)
        {
            foreach(DataColumn dc in dt.Columns)
            {
                rectCell[k] = new RectangleF(startX, startY, colWidth, iRowHeight);
                //Renders the report table grid
                if (!bReportHideGrid)
                {
                    e.Graphics.DrawRectangle(vbPen,startX,startY,colWidth,iRowHeight);
                }
                if (j == 0)
                {
                    modSf.LineAlignment = StringAlignment.Near;
                    modSf.Alignment = StringAlignment.Near;
                    e.Graphics.DrawString("Total", fntReportBodyFont, 
                    Brushes.Black, rectCell[k], modSf);
                }
                else
                {
                    if(TotCol[j] != -1)
                    {
                        modSf.LineAlignment = StringAlignment.Near;
                        modSf.Alignment = saReportNumericAlignment;
                        e.Graphics.DrawString(TotCol[j].ToString(), 
                        fntReportBodyFont, Brushes.Black, rectCell[k], modSf);
                    }
                }
                //Set the X coordinate for the next cell.
                startX += colWidth;
                j ++;
                k ++;
            }
        }
        e.HasMorePages = false;
        ReportRowCount = 0;
        break;
    }
    dr = dt.Rows[ReportRowCount];

    //gets the maximum height required for the row.
    iRowHeight = Convert.ToInt32(GetRowHeight(dr.ItemArray, 
                    fntReportBodyFont,colWidth,iRowHeight ,e));

    //Loop through each row and column to render the cells.
    foreach (DataColumn dc in dt.Columns)
    {
        rectCell[k] = new RectangleF(startX, startY, colWidth, iRowHeight);
        //Renders the report table grid
        if (!bReportHideGrid)
        {
            e.Graphics.DrawRectangle(vbPen,startX,startY,colWidth,iRowHeight);
        }
        //Right align numeric values
        if (IsNumeric(dr.ItemArray[j].ToString()))
        {
            modSf.LineAlignment = StringAlignment.Near;
            modSf.Alignment = saReportNumericAlignment;
            //Get totals for all numeric columns
            TotCol[j] = TotCol[j] + Convert.ToDecimal(dr.ItemArray[j]);
        }
        else
        {
            modSf.LineAlignment = StringAlignment.Near;
            modSf.Alignment = StringAlignment.Near;
            TotCol[j] = -1;
        }
    
        e.Graphics.DrawString(dr.ItemArray[j].ToString(), fntReportBodyFont, 
                        Brushes.Black, rectCell[k], modSf);
        //Set the X coordinate for the next cell.
        startX += colWidth;
        j ++;
        k ++;
    }
    //Set the X coordinate for the next row.
    startX = e.MarginBounds.Top;
    //Set the Y coordinate for the next row.
    startY += iRowHeight;
    //Set the last rendered coordinate.
    yPos = startY;
    i ++;
    ReportRowCount ++;
    //Check if current rendered row count < the total number of rows 
    //and set the HasMorePages property to true.
        if (ReportRowCount < dt.Rows.Count)
        {
            e.HasMorePages = true;
        }
    }
    PageCount ++;

The report can be further customized by the following properties:

  • BodyFont: Sets the font for the table body. Accepts a value of type Font. The default value is Arial size 8.
  • HideGrid: Sets the boolean property which shows or hides the table borders. Default value is true.
  • IsLandscape: Sets the boolean property which switches the orientation of the page from the default Portrait to Landscape. The default value is false. Set this to true if the report requires more page width.
C#
//Sets the page orientation
ReportPrintDocument.DefaultPageSettings.Landscape = IsLandscape;

//Renders the page count
e.Graphics.DrawString(PageCount.ToString(),fntReportHeaderFont,
    brReportHeaderFontColor,Convert.ToInt32 (Math.Round 
    (Convert.ToDouble (e.PageBounds.Width / 2))), 
    e.MarginBounds.Bottom + 30);

//Reset properties after final page
if (!e.HasMorePages)
{
    PageCount = 0;
    ReportTitleHeight = 40;
}

Limitations

This library is suitable for creating simple reports. It does not have advanced reporting features available in Crystal Reports. Its main advantage over Crystal Reports is portability and ease of use.

Points of Interest

Calling the print preview dialog will not print the document; it's the print icon on the preview dialog that re-runs the print event and sends output to the printer. The variables in the print event need to be re-initialised after previewing the last page; otherwise, the printed page count and formatting based on the page count will be incorrect.

Reference

  • Introduction on GDI+ taken from Professional .NET 2003 Third Edition by Wrox

License

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