Getting Started

Print Simple Reports With GDI+

Sometimes all you want to do is print the simple report you're already showing. Learn how to use GDI+ and the .NET Framework to print reports.

Technology Toolbox: C#

Sometimes all your customer wants to do is print some simple reports. You can do this easily in .NET without the overhead of a third-party reporting package. You simply need to familiarize yourself with GDI+ and the appropriate namespaces in the .NET Framework. I'll show you how to use GDI+ to build a dynamic report, as well as how to use the .NET Framework's PrintDocument and PrintPreviewDialog objects to handle the print-preview functionality (see Figure 1).

GDI+ is the latest version of the 2-D graphics subsystem used by Windows for almost everything you see displayed on the screen. It consists of three main parts: 2-D vector graphics, which you use to draw most basic shapes; imaging, which you use to display bitmaps and other complicated images; and typography, which you use to handle rendering fonts to the screen or a printer.

You can access all these functions in the .NET Framework through the managed classes in the System.Drawing namespace. Note that you access these classes through managed objects, but the classes actually use unmanaged resources. This means you must call the Dispose method of any GDI+ object after you finish using it, or your application will hang on to unused memory.

You usually want to use a GDI+ object in .NET in the context of a Graphics object. This object provides a surface for you to draw on. A Graphics object is associated with a specific object (or device context) that renders what has been drawn to the Graphics object.

For example, drawing a line on a form requires using the form's Graphics object in the Paint event of the form. This code draws a diagonal black line one pixel wide from the upper left-hand corner of the form to the point located at (100, 100):

private void Form1_Paint(object sender, 
   System.Windows.Forms.PaintEventArgs 
   e)
{
   Pen oPen = new Pen(Color.Black, 1);
   e.Graphics.DrawLine(oPen, 0,0, 
      100,100);
   oPen.Dispose();
}

Note that you call the Dispose method of the Pen object to clean up the unmanaged resources at the end of the event handler. I cannot emphasize the importance of this enough: As a general rule, if a framework object exposes a Dispose method, you should call it when you're finished with that object.

Create the Report
Imagine you have a form that contains a DataGrid, which displays some customer account balances. Begin by creating the object that will be the device context for your drawing. For example, use the System.Drawing.Printing.PrintDocument object:

PrintDocument oPrintDocument = new 
   PrintDocument();
oPrintDocument.DocumentName = 
   "Account Balances";
oPrintDocument.PrintPage +=new 
   PrintPageEventHandler(oPrintDocument_PrintPage);

The PrintPage event fires whenever you render a page to the printer or the screen. The actual report rendering occurs in the PrintPage event. Next, set up the PrintPreviewDialog object to enable previewing the document before rendering. This is even simpler:

PrintPreviewDialog oPrintDialog = 
   new PrintPreviewDialog();
oPrintDialog.Document = oPrintDocument;

The Document property is set to the PrintDocument object that you want to preview. Next, render the document by writing code in the PrintPage event of the PrintDocument object. You should decide how you want your report to look before you start. The example report in this article uses a basic setup, consisting of a title, some column headers, and the data itself. You also need to separate each section with a line, and place a page footer at the bottom of the report with the current date.

Begin by creating the drawing objects you will use to create the report:

//Create Drawing Objects
Point ptCursor = new Point(0,0);
SolidBrush brushText = new 
   SolidBrush(Color.Black);
SolidBrush brushTitle = new 
   SolidBrush(Color.DarkBlue);
Font fntTitle = new Font("Arial", 20);
Font fntHeader = new Font
   ("Arial", 14, FontStyle.Bold);
Font fntData = new Font("Arial", 12);
Font fntFooter = new Font("Arial", 10);

In this event, use the Point object to keep track of your drawing location, so that you know where to start drawing each new section. Use the brushes to draw strings onto your drawing surface, and the Font objects to render the text. Now draw the report header section of the report:

//Draw Title String
ptCursor.Offset(
   e.MarginBounds.Left,
   e.MarginBounds.Top);
e.Graphics.DrawString(
   grdData.CaptionText, fntTitle, 
   brushTitle, ptCursor);
Size oSize = 
   e.Graphics.MeasureString(
   grdData.CaptionText, 
   fntTitle).ToSize();
int iEndofSection = ptCursor.Y + 
   oSize.Height + 5;

//Title Line
e.Graphics.DrawLine(new Pen(brushTitle), 
   e.MarginBounds.Left, iEndofSection, 
   e.MarginBounds.Right, 
   iEndofSection);

This code moves the drawing point to the upper left-hand corner of the page margins. Using the margins rather than a hard-coded location enables the drawing to move automatically to the appropriate location on the page if the user changes the margins.

Next, call the DrawString method of the Graphics object that is passed into the event arguments. The DrawString method lets you draw a string at a particular location, using a specific font. Once you draw the string, the trick is to know where to start drawing next. Do you know how many pixels the string "Account Balances" is when drawn with a 20-point Arial font? Fortunately, you don't have to.

This is where the MeasureString method comes in. It gives you the exact size of a rendered string when using a given font on the current Graphics surface. It returns a floating-point value in the form of a SizeF object, which you can convert to an integer value to keep things simple.

The results from the MeasureString method tell you exactly where to draw the line that separates the report header from the next section. You'll want to draw the line from the left margin to the right margin, at the height of your current cursor location plus the height of the title string.

Draw Column Headers
Next, use the structure of the DataGrid to aid in drawing the column headers. Rather than duplicating the column names and widths specified in the DataGrid style objects, enumerate through the grid columns and render them to the report:

//Grid Header Row
ptCursor.X = e.MarginBounds.Left;
ptCursor.Y = iEndofSection + 10;
   foreach(DataGridColumnStyle oCol in 
      grdData.TableStyles[0]
      .GridColumnStyles)
{
   e.Graphics.DrawString(
      oCol.HeaderText, fntHeader, 
      brushText, ptCursor);
   oSize = e.Graphics.MeasureString(
      oCol.HeaderText, 
      fntHeader).ToSize();
   ptCursor.Offset(oCol.Width, 0);
}

First, move the drawing position back to the left margin and 10 pixels below the end of the last section. Next, loop through the GridColumnStyle objects of the data grid, and draw the column header text on the Graphics surface. At the end of each iteration, move the drawing position along the x-axis according to the width of the current column. Finally, draw the line that separates this section from the data:

//Header Line
iEndofSection = ptCursor.Y + 
   oSize.Height;
e.Graphics.DrawLine(new Pen(brushText), 
   e.MarginBounds.Left, iEndofSection, 
   e.MarginBounds.Right, 
   iEndofSection);

Next, you need to handle printing the data itself. Simply enumerate through each row, then through each column:

//Data
ptCursor.X = e.MarginBounds.Left;
ptCursor.Y = iEndofSection + 10;

foreach(DataRow oRow in oTable.Rows)
{
   foreach(DataGridColumnStyle oCol in 
      grdData.TableStyles[0]
      .GridColumnStyles)
   {
      e.Graphics.DrawString(
         oRow[oCol.MappingName]
         .ToString(), 
         fntData, brushText, ptCursor);
      oSize = e.Graphics.MeasureString(
         oRow[oCol.MappingName]
         .ToString(), 
         fntData).ToSize();
      ptCursor.Offset(oCol.Width, 0);
   }
   //Next Line
   ptCursor.X = e.MarginBounds.Left;
   ptCursor.Offset(0, oSize.Height + 
      10);
}

This code contains nothing new. The only part of interest is that you reset to the start of each line after you finish the row. Note that the column width uses a fixed value (the Width property of the DataGridColumn style) rather than the width of each string to move the drawing cursor along the x-axis. This helps you make sure your columns stay lined up in the report.

The final step is to render the page footer. For this, you want to right-align the current date on the right side of the report. You can do this easily with GDI+ using the StringFormat object:

//Page Footer
StringFormat oFormat = new 
   StringFormat();
oFormat.LineAlignment = 
   StringAlignment.Far;

e.Graphics.DrawString(
   DateTime.Now.ToShortDateString(
   ), fntData, brushText, 
   e.MarginBounds.Right, 
   e.MarginBounds.Bottom, oFormat);

Setting the LineAlignment property of the StringFormat object to "Far" enables GDI+ to render the string in a right-aligned format. The enumeration uses "Near" and "Far" rather than "Left" and "Right" to allow compatibility with languages that render from right to left. This means GDI+ renders the string as far from the normal direction (left) as possible. If you were writing an application using Hebrew text, "Far" would left-align the text.

Finally, dispose of your drawing objects:

//Dispose of objects
brushText.Dispose();
brushTitle.Dispose();
fntData.Dispose();
fntHeader.Dispose();
fntTitle.Dispose();
fntFooter.Dispose();

You've finished the code to render the report. Now you want to make the call to show the Print Preview dialog:

if(oPrintDialog.ShowDialog() == 
   DialogResult.OK)
{
   oPrintDocument.Print();
}

At this stage, you can simply print the report to the default printer.

You can spice up the visual appearance of your report in several ways. For example, you could add images of the customers, or a background watermark, or a graph showing each customer's account balance in comparison with the overall average. You could also use a PrintDialog object to allow the user to select a printer other than the default printer, or a PageSetup dialog to change margins and other page settings. GDI+ gives you the ability to do all of this and more. Just remember to always dispose of your drawing objects.

About the Author

Lee Falin is a software developer in the Roanoke Valley. He has worked in a variety of industries, including telecommunications, manufacturing, and defense. Reach Lee through his Weblog at www.leefalin.com.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube