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.
ReportClass objRepLib = new ReportClass();
PrintDocument myDoc = new PrintDocument();
LoadProperties();
DataTable dt = new DataTable();
DataSet ds = new DataSet();
ds.ReadXml(DataSourceTxt.Text);
dt = ds.Tables[0];
myDoc = objRepLib.ReportPrintDocument;
objRepLib.ReportSource = dt;
printPrevDiag.Document = myDoc;
objRepLib.ReportTitleFirstPageOnly = ShowTitleAllPages;
objRepLib.ReportHasLogo = Showlogo;
Image img;
if (objRepLib.ReportHasLogo == true)
{
img = Image.FromFile(LogoTxt.Text);
objRepLib.ReportLogo = img;
}
objRepLib.HasTitle = true;
objRepLib.ReportShowTotals = ShowReportTotals;
objRepLib.IsLandscape = PageOrientation;
objRepLib.HideGrid = ShowTableBorder;
objRepLib.ReportTitle = ReportTitleTxt.Text ;
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:
- Number of cells in a row: This would be the number of columns in the
DataTable
. - Number of rows in the report table: This would be the number of rows in the
DataTable
. - 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).
int tabCol;
int tabRows;
int tabArrayMax;
tabCol = dt.Columns.Count;
tabRows = dt.Rows.Count;
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.
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.
if (iTableWidth == 0 || iTableWidth > e.MarginBounds.Width)
{
iTableWidth = e.MarginBounds.Width;
}
int colWidth = Convert.ToInt32(iTableWidth / tabCol);
Pen vbPen = new Pen(Color.Black);
RectangleF[] rectCell = new RectangleF[tabArrayMax + tabCol];
RectangleF titleRect = new RectangleF() ;
RectangleF logoRect = new RectangleF();
int startX = e.MarginBounds.Top;
int startY = e.MarginBounds.Top - 35;
Report Title and Logo
The title and logo are rendered by defining Rectangle
s 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.
StringFormat modSf = new StringFormat();
int i = 0;
int k = 0;
if (bReportHasLogo)
{
if (iReportLogoHeight > iReportTitleHeight)
{
iReportLogoHeight = iReportTitleHeight;
}
logoRect = new RectangleF(startX, startY,
iReportLogoWidth, iReportLogoHeight);
e.Graphics.DrawImage(imReportLogo, logoRect);
}
else
{
iReportLogoWidth = 0;
iReportLogoHeight = 0;
}
if (bReportHasTitle && (PageCount == 0 || !bReportTitleFirstPageOnly))
{
modSf.LineAlignment = saReportTitleAlignment;
modSf.Alignment = saReportTitleAlignment;
titleRect = new RectangleF(startX + ReportLogoWidth, startY,
e.MarginBounds.Width - iReportLogoWidth, iReportTitleHeight);
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 Rectangle
s 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
.
modSf.LineAlignment = saReportHeaderCellAlignment;
modSf.Alignment = saReportHeaderCellAlignment;
startY += Math.Max(iReportLogoHeight,iReportTitleHeight ) + 25;
object[] hString = new object[tabCol] ;
dt.Columns.CopyTo(hString,0);
iHeaderRowHeight = Convert.ToInt32(GetRowHeight
(hString,fntReportHeaderFont,colWidth,iHeaderRowHeight,e));
foreach (DataColumn dc in dt.Columns)
{
Rectangle r1 = new Rectangle(startX, startY, colWidth, iHeaderRowHeight);
e.Graphics.FillRectangle(brReportHeaderBackgroundColor,r1 );
if (!bReportHideGrid)
{
e.Graphics.DrawRectangle(vbPen,r1);
}
e.Graphics.DrawString(dc.ToString(), fntReportHeaderFont,
brReportHeaderFontColor, r1, modSf);
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 Rectangle
s 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
.
startX = e.MarginBounds.Top;
startY += iHeaderRowHeight;
int yPos = 0;
while (yPos <= e.MarginBounds.Bottom)
{
int j = 0;
DataRow dr ;
if (ReportRowCount == dt.Rows.Count)
{
if (bReportShowTotals)
{
foreach(DataColumn dc in dt.Columns)
{
rectCell[k] = new RectangleF(startX, startY, colWidth, iRowHeight);
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);
}
}
startX += colWidth;
j ++;
k ++;
}
}
e.HasMorePages = false;
ReportRowCount = 0;
break;
}
dr = dt.Rows[ReportRowCount];
iRowHeight = Convert.ToInt32(GetRowHeight(dr.ItemArray,
fntReportBodyFont,colWidth,iRowHeight ,e));
foreach (DataColumn dc in dt.Columns)
{
rectCell[k] = new RectangleF(startX, startY, colWidth, iRowHeight);
if (!bReportHideGrid)
{
e.Graphics.DrawRectangle(vbPen,startX,startY,colWidth,iRowHeight);
}
if (IsNumeric(dr.ItemArray[j].ToString()))
{
modSf.LineAlignment = StringAlignment.Near;
modSf.Alignment = saReportNumericAlignment;
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);
startX += colWidth;
j ++;
k ++;
}
startX = e.MarginBounds.Top;
startY += iRowHeight;
yPos = startY;
i ++;
ReportRowCount ++;
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.
ReportPrintDocument.DefaultPageSettings.Landscape = IsLandscape;
e.Graphics.DrawString(PageCount.ToString(),fntReportHeaderFont,
brReportHeaderFontColor,Convert.ToInt32 (Math.Round
(Convert.ToDouble (e.PageBounds.Width / 2))),
e.MarginBounds.Bottom + 30);
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