You are on page 1of 16

10/9/2016

PrintingReportsinWindowsForms

Printing Reports in Windows Forms

Duncan Mackenzie
Microsoft Developer Network
See Duncan's profile on GotDotNet.
July 2002
Applies to:
Microsoft .NET Framework
Windows Forms
Summary: How to create your own reports using the printing features of GDI+ in Microsoft .NET. 22 printed pages
Download Printwinforms.exe.

Contents
Introduction
Printing Features in the .NET Framework
Producing Real Reports
The TabularReport Class
Conclusion

Introduction
I won't start waxing philosophical about the paperless office, but it is sufficient to note that it hasn't arrived yet. I have built many
different systems that were designed to get rid of some part of a company's paperwork, turn it into data that is stored on the
computer, but regardless of how wonderful the system is one of the major requirements is always to get that information out
of the computer and back into paper form. Looking back, across systems built in Clipper, Microsoft FoxPro, Microsoft
Access, Microsoft Visual Basic and now Microsoft .NET, the one constant when developing business systems has been that
creating and testing reports is one of the largest elements of the project timetable. Assuming that this is true for other people,
not just me, I am going to show you how to use the drawing features of GDI+ and the GDI+ Printing classes to output a tabular
report. This report type, see Figure 1, covers a large percentage of the output you will ever have to develop.

https://msdn.microsoft.com/enus/library/ms996472(d=printer).aspx

1/16

10/9/2016

PrintingReportsinWindowsForms

Figure 1. Tabular reports are used to print out lists of information, such as financial accounts, order records, or other
material well suited for display in a grid style.
Although I will not be covering the other very common type of report, an invoice/form see Figure 2, many of the general
reporting concepts covered in this article would apply to either style.

Figure 2. Form style reports are often used to print out invoices, tax forms, or other similar types of documents.
NoteIf you have already been examining the features of Microsoft Visual Studio .NET, you will most likely be
aware that it ships with Crystal Reports, a fullblown report development package complete with a ton of
development tools, and you probably wondering why I am not discussing it in this article. Crystal Reports is a great
reporting tool, but it is not free to deploy, so I want to make sure you understand what is possible using just the
.NET Framework.

Printing Features in the .NET Framework


https://msdn.microsoft.com/enus/library/ms996472(d=printer).aspx

2/16

10/9/2016

PrintingReportsinWindowsForms

Before I dive into developing the two sample reports, I want to provide an overview of the printing functionality available in .NET.
The .NET Framework has provided a set of printing features that build on the existing GDI+ classes to allow you to print, preview,
and work with printers. These features are exposed for programmatic use through the System.Drawing.Printing classes and visual
components PrintDialog, PrintPreviewDialog, PrintDocument, and more... are provided for use on Windows Forms applications.
With these classes, producing some printed output can be accomplished with almost no code and using just a couple of the
Windows Forms components.

Trying It Out for Yourself


As a quick introduction to printing, follow these steps to create a small test application:
1. Create a new Visual Studio .NET Windows application, in either Microsoft Visual C# or Visual Basic .NET
2. On the new Form1 that is automatically created, add one of each of the following components from the toolbox:
PrintDocument, PrintPreviewDialog and Button.
3. Now, select the PrintPreviewDialog component, and view its properties. Set the Document property, which will be
currently equal to none, to PrintDocument1 'printDocument1' if you are using C#. This associates the two components
together so that when the preview dialog needs a page drawn, to the preview window or to the printer, it will call the
PrintPage event of this specific PrintDocument.
4. Add code to the PrintPage event of PrintDocument1. In either language, you can access this event by doubleclicking the
PrintDocument component. Here is a sample snippet of code you can add that will print the name of the user currently
logged on to the page.

//C#
privatevoidprintDocument1_PrintPage(objectsender,
System.Drawing.Printing.PrintPageEventArgse)
{
Graphicsg=e.Graphics;
Stringmessage=System.Environment.UserName;
FontmessageFont=newFont("Arial",
24,System.Drawing.GraphicsUnit.Point);
g.DrawString(message,messageFont,Brushes.Black,100,100);
}
'VisualBasic.NET
PrivateSubPrintDocument1_PrintPage(ByValsenderAsSystem.Object,_
ByValeAsSystem.Drawing.Printing.PrintPageEventArgs)_
HandlesPrintDocument1.PrintPage
DimgAsGraphics=e.Graphics
DimmessageAsString=System.Environment.UserName
DimmessageFontAsNewFont("Arial",24,_
System.Drawing.GraphicsUnit.Point)
g.DrawString(message,messageFont,Brushes.Black,100,100)
EndSub
5. Add code to the click event of your button doubleclick the button in the Form designer to get to the click event to
launch the print preview dialog. When the preview dialog needs to draw the page, the PrintPage event handler you just
finished will be called.

//C#
privatevoidbutton1_Click(objectsender,System.EventArgse)
{
printPreviewDialog1.ShowDialog();
}
https://msdn.microsoft.com/enus/library/ms996472(d=printer).aspx

3/16

10/9/2016

PrintingReportsinWindowsForms

'VisualBasic.NET
PrivateSubButton1_Click(ByValsenderAsSystem.Object,_
ByValeAsSystem.EventArgs)_
HandlesButton1.Click
PrintPreviewDialog1.ShowDialog()
EndSub
If everything worked out, running the application and clicking the button will open a dialog with your print preview, displaying a
mostly empty page with just your user name written onto it.

Producing Real Reports


The quick exercise described above is actually enough to get you writing your own reports, the rest of the work is really just
GDI+ drawing and some layout issues around margins and other settings; you could build complete datadriven reports simply
by adding code to the PrintPage event. Of course, that might be fine for one report that never changes, but if you are going to
produce reports on a regular basis, you will find putting all your code into PrintPage rather hard to maintain. Instead, what you
need to do is to break the task of printing a page up into its various subcomponents and then try to write code to perform each
of these bits of work. If you can make this code reusable, then you can produce all sorts of reports without having to write
anything new.
Having used a wide variety of reporting tools in the past, I have found that they all tend to divide up reports into the same
common elements:
Page Headers/Footers
Report Headers/Footers
Group Headers/Footers
Detail Rows
For the sake of simplicity, I decided not to implement Report or Group sections and stick to page headers and footers, and of
course the detail row. Starting off on a new Windows Form, with a PrintDocument added just like we did in the exercise, I
created a few different functions, PrintPageFooter, PrintPageHeader and PrintDetailRow, each of which returns the size the
height in pixels of the section they just created. I supplied each one of these functions with the PrintPageEventArgs object that
is passed to the PrintPage event, to give each function access to information about the printer settings and to the Graphics
object that they will need to do any drawing. I also passed in a rectangle, bounds, that described the area of the page on which I
wanted the section to appear and a final common argument, sizeOnly, which specifies if I wanted the section to actually print or
to just calculate and return its size. The PrintDetailRow function accepted several other arguments that I will discuss when we
get to that code. Once I had all of these individual sections written, I would be able to create a report quickly. Regardless of the
specific section I was developing, though, I found that there are a few key topics that are applicable for anyone printing out
reports, including resolution and handling multiple pages.

Resolution Issues: Converting Between Inches and Pixels


When you are drawing in GDI+, you are always working in a specific unit of measure, whether that is inches, pixels, points or
something else, but the relationship between these different units can be affected by the device such as a printer to which you
are drawing. I have found it simplest to do all my measurements in inches, which are deviceindependent an inch is an inch is an
inch, and avoid any resolution issues at all. To do this yourself, since an inch is not the default unit of measure, you need to set
the PageUnit property of your Graphics object at the start of each PrintPage event.

PublicSubPrintPage(ByValsenderAsObject,_
ByValeAsPrintPageEventArgs)
DimgAsGraphics=e.Graphics
https://msdn.microsoft.com/enus/library/ms996472(d=printer).aspx

4/16

10/9/2016

PrintingReportsinWindowsForms

g.PageUnit=GraphicsUnit.Inch

The only little hiccup in using inches as your unit of measure is when you are dealing with the Page and Printer settings, such as
margins, which are measured in 1/100ths of an inch. It is simple enough to convert between the two, but make sure you are
using Single data types so that you don't lose any precision when doing the conversion. At the start of my PrintPage, I grab the
margin values and convert them immediately to avoid any confusion.

DimleftMarginAsSingle=e.MarginBounds.Left/100
DimrightMarginAsSingle=e.MarginBounds.Right/100
DimtopMarginAsSingle=e.MarginBounds.Top/100
DimbottomMarginAsSingle=e.MarginBounds.Bottom/100
DimwidthAsSingle=e.MarginBounds.Width/100
DimheightAsSingle=e.MarginBounds.Height/100

Keeping Track of Multiple Pages


If your report could span more than one page, then you need to keep track of a few details, such as the current page number
and the current row of data, as each page results in its own call to the PrintPage event handler. In each case, creating a variable
that exists outside of the event handler code will allow you to remember your current position between calls to PrintPage.

DimcurrentPageAsInteger=0
DimcurrentRowAsInteger=0
PublicSubPrintPage(ByValsenderAsObject,_
ByValeAsPrintPageEventArgs)
...
currentPage+=1

Always remember to reset these values before every time you print, or else your second printing won't start with a page number
of 1. The best place to reset these values is in the BeginPrint event of the PrintDocument.

PrivateSubPrintDocument1_BeginPrint(ByValsenderAsObject,_
ByValeAsPrintEventArgs)_
HandlesPrintDocument1.BeginPrint
currentPage=0
currentRow=0
EndSub

When I first started playing around with the printing features in .NET, I was resetting my page count in my Print button, right
before printing the document. This worked fine until I decided to preview the document first before printing, the pages would go
from 1 to 3 in the preview, but print with page numbers of 4 to 6, since the document was really printed twice once to preview

https://msdn.microsoft.com/enus/library/ms996472(d=printer).aspx

5/16

10/9/2016

PrintingReportsinWindowsForms

and once to the printer. In fact, since a user can print as many times as they want from the Print Preview dialog box, the page
numbers could have kept increasing. Using the Begin Page event avoids all these worries.
It is also up to you to specify when your report is done or when you have more pages left to print, through the HasMorePages
property of PrintPageEventArgs. Before the end of the PrintPage event handler, you need to set this property to false if you are
done printing or true if you still have rows left to print when you reach the end of the page. Having the possibility of multiple
pages requires you to know when you have reached the end of your page, which is why each of my printing procedures
PrintDetailRow, for example has a "sizeOnly" argument. By passing True for the sizeOnly parameter, I can find out the size a
detail row will be before it is printed, allowing me to decide whether I have room left on the page for that row taking the footer
into account, of course. If I don't have enough room left, I don't print the row or increment the currentRow variable; instead I
set HasMorePages to True and it will be printed on the next page.
NoteIf a detail row returns a size greater than the size of your page, you will never be able to print it, and you'll
get stuck into an endless loop producing blank pages. After you get the size of a section, check to make sure it is
reasonable less than the available space on an otherwise empty page before trying to work with it, and throw an
exception if it is too big.

Outputting Text
Regardless of the underlying data type of your fields, you will be outputting text for every column of your report and for your
headers and footers, all through the DrawString method of the Graphics class. When drawing text for use in a report, you often
have to deal with size and positioning restrictions, and DrawString has provided the LayoutRectangle parameter for exactly
that reason. By specifying a LayoutRectangle, you are forcing the text into a specific area, with automatic word wrapping and
other features controllable through the StringFormat parameter.

Combining StringFormat Settings with a Layout Rectangle


Each of these images is the result of calling DrawString with the string, "The quick brown fox jumped high over the slow and
lazy black dog," and various string format settings. The layout rectangle is not normally visible, so I drew it afterwards using
DrawRectangle, to make the images easier to understand.

Figure 3. Output using the default StringFormat settings


The first image shows the default behavior of a layout rectangle, the text is wrapped to fit, partial lines are allowed to be visible,
and anything outside the rectangle is clipped not drawn.

Figure 4. Turning on LineLimit prevents partial lines


With the LineLimit option turned on, only complete lines are allowed to show, preventing the partial line that was visible in the
first image; the lack of this additional line changes the word wrapping slightly.

https://msdn.microsoft.com/enus/library/ms996472(d=printer).aspx

6/16

10/9/2016

PrintingReportsinWindowsForms

Figure 5. NoClip allows some text to appear outside of the layout rectangle.
As shown in Figure 5, turning off the line limit option and specifying NoClip will allow parts of the string that are partially cut off
by the layout rectangle to be visible.

Automatically Adding Ellipses


The StringFormat object can also be used through the Trimming property to add ellipses, when your string will be cut off by
the layout rectangle, by putting ellipses in place of the last few characters EllipseCharacter; after the last completely visible
word EllipseWord; or to replace the middle portion of the string so that the beginning and the end will both be visible
EllipsePath. All three options from left to right, EllipseCharacter, EllipseWord, and EllipsePath are shown in Figure 6, below.

Figure 6. The Trimming property can be used to provide an automatic ellipse when your string doesn't fit within the
layout rectangle.

Drawing Hotkey Indicators


Another configurable property of this class is HotkeyPrefix, which can come in very handy if you are doing customdrawn
Windows Forms controls. HotkeyPrefix affects how DrawString handles ampersands, and can be set to Hide, Show, or None. If
it is set to Show, an ampersand & before a letter in the string indicates that the letter is a hotkey, and the letter is drawn with a
line under it. Hide will prevent the underline from being shown, but also skips the ampersand itself, while None will just handle
the ampersand as if it was regular text. The image below shows the three possible settings Show, Hide, and None and the
effect on the string "&Print".

Figure 7. The HotkeyPrefix property is quite useful when creating your own controls.

Aligning Text Output


Finally, the StringFormat class can be used to specify alignment, using far, near and center instead of left, right and center so
that it is valid for any language, regardless of the direction of text flow. For English, flowing left to right, these values translate
into right aligned far and left aligned near, while center requires no translation. Whatever alignment setting you choose
affects the position of your text relative to the layout rectangle you specify. Without a layout rectangle specified, it affects how
DrawString interprets the x, y coordinates you provide for outputting the string. With no layout rectangle, specifying Alignment
= Center causes the string output to be centered on the x coordinate you supply, Alignment = Far causes the x coordinate to be
treated as the end point of the string's position, and Alignment = Near the default causes the string to start at the xposition.
Figure 8, below, shows the three alignment settings near, far, and center with a layout rectangle specified on top and then with
just an x, y position the upper left corner of the line that has been drawn specified for the bottom three examples. Note that the
results would be different in a system configured to use righttoleft flowing text.

https://msdn.microsoft.com/enus/library/ms996472(d=printer).aspx

7/16

10/9/2016

PrintingReportsinWindowsForms

Figure 8. The Alignment property has a different effect depending on whether or not you specify a layout rectangle.
All of the DrawString examples shown in this section are included as a second project, called PlayingWithDrawString, in the
code download for this article. I actually used that sample project to create all of the images for this section as well, so what you
see is truly what you get!

Handling Columns
In many reports, especially tabular ones, your detail row will consist of columns of information. For each column you will need to
determine a set of information including the source of the column a field in your data source for example, the width of the
column on the page, the font to use, the alignment of the column's contents, and more. For my tabular report, since columns are
the main content of the report, I decided to create a special ColumnInformation class and then use a collection of this type of
object to determine what columns each detail row should contain. My class includes all the information I need to correctly
output each column within my data row, and even some properties HeaderFont and HeaderText if I decide to add a header
row to my reporting code at some point in the future. Note that to simplify the code listing, I have removed the private members
for each property and the actual property procedure code, the full code is included in the download.

PublicClassColumnInformation
PublicEventFormatColumn(ByValsenderAsObject,_
ByRefeAsFormatColumnEventArgs)
PublicFunctionGetString(ByValValueAsObject)
DimeAsNewFormatColumnEventArgs()
e.OriginalValue=Value
e.StringValue=CStr(Value)
RaiseEventFormatColumn(CObj(Me),e)
Returne.StringValue
EndFunction
PublicSubNew(ByValFieldAsString,_
ByValWidthAsSingle,_
ByValAlignmentAsStringAlignment)
m_Field=Field
m_Width=Width
m_Alignment=Alignment
EndSub
PublicPropertyField()AsString
https://msdn.microsoft.com/enus/library/ms996472(d=printer).aspx

8/16

10/9/2016

PrintingReportsinWindowsForms

PublicPropertyWidth()AsSingle
PublicPropertyAlignment()AsStringAlignment
PublicPropertyHeaderFont()AsFont
PublicPropertyHeaderText()AsString
PublicPropertyDetailFont()AsFont
EndClass
PublicClassFormatColumnEventArgs
InheritsEventArgs
PublicPropertyOriginalValue()AsObject
PublicPropertyStringValue()AsString
EndClass

In addition to the information describing my columns, I have also created a FormatColumn event, which provides a way to write
custom formatting code to convert between your database value and string output. An example handler that is designed to
produce currency output from a numeric database field is shown below.

PublicSubFormatCurrencyColumn(ByValsenderAsObject,_
ByRefeAsFormatColumnEventArgs)
DimincomingValueAsDecimal
DimoutgoingValueAsString
incomingValue=CDec(e.OriginalValue)
outgoingValue=String.Format("{0:C}",incomingValue)
e.StringValue=outgoingValue
EndSub

Before outputting my report, I populate an ArrayList with ColumnInformation objects, attaching FormatColumn handlers as
required.

DimColumnsAsNewArrayList()
PublicSubPrintDoc()
Columns.Clear()
DimtitleInfoAs_
NewColumnInformation("title",2,StringAlignment.Near)
Columns.Add(titleInfo)
DimauthorInfoAs_
NewColumnInformation("author",2,StringAlignment.Near)
Columns.Add(authorInfo)
DimbookPriceAs_
NewColumnInformation("author",2,StringAlignment.Near)
AddHandlerbookPrice.FormatColumn,AddressOfFormatCurrencyColumn
Columns.Add(bookPrice)
Me.PrintPreviewDialog1.ShowDialog()

https://msdn.microsoft.com/enus/library/ms996472(d=printer).aspx

9/16

10/9/2016

PrintingReportsinWindowsForms

EndSub

For each row in my data source, my PrintDetailRow code loops through this ArrayList and draws the contents of each column.
You can view the PrintDetailRow code in the download, and see how it uses the features described in Outputting Text to
produce each column.

The TabularReport Class


Up until this point, I have been showing you all the pieces of creating a report, but the end result is to take all of those pieces
and build a component that you can use to easily generate a tabularstyle see Figure 1 report. I created a new class that inherits
PrintDocument, to allow it to be used with all of the existing printing tools such as the PrintDialog, PrintPreviewDialog and
PrintPreviewControl. The complete code is relatively long, so I will cover the steps I followed in developing it, and then show you
how you could use a class like this in your application.

Adding Properties
To allow you to setup your tabular report, I needed to add a whole bunch of properties to my class to handle configuring the
Columns, providing a data source, and customizing the appearance of the overall report.

Configuring Columns
In addition to creating a ColumnInformation class, I could also have created a strongly typed collection class to hold multiple
ColumnInformation instances, which would have been relatively easy using Strongly Typed Collection Generator that is up on
the GotDotNet Web site. I didn't go that direction, as I wanted all column access to be through my class; instead, I decided to
just use an ArrayList and create some methods on my report class to provide strongly typed access to that list.

Protectedm_ColumnsAsNewArrayList()
PublicFunctionAddColumn(ByValciAsColumnInformation)AsInteger
Returnm_Columns.Add(ci)
EndFunction
PublicSubRemoveColumn(ByValindexAsInteger)
m_Columns.RemoveAt(index)
EndSub
PublicFunctionGetColumn(ByValindexAsInteger)AsColumnInformation
ReturnCType(m_Columns(index),ColumnInformation)
EndFunction
PublicFunctionColumnCount()AsInteger
Returnm_Columns.Count
EndFunction
PublicSubClearColumns()
m_Columns.Clear()
EndSub

Customizing the Report Appearance


By allowing the users of my report class to configure all of the fonts and brushes used, along with the height of each report
section, they have a fair degree of customization available. For all of these options, I provided public property procedures and
internal variables that are initialized to some default values. I also created DefaultReportFont and DefaultReportBrush
https://msdn.microsoft.com/enus/library/ms996472(d=printer).aspx

10/16

10/9/2016

PrintingReportsinWindowsForms

properties to allow users to set a single Font or Brush that would be used through the report, unless the more specific properties
had been set. The code for the Font properties is listed below, but the Brush properties are omitted as they are essentially doing
the same work but with Brush objects.

Protectedm_DefaultReportFontAsFont=_
NewFont("Arial",12,FontStyle.Bold,GraphicsUnit.Point)
Protectedm_HeaderFontAsFont
Protectedm_FooterFontAsFont
Protectedm_DetailFontAsFont
PublicPropertyDefaultReportFont()AsFont
Get
Returnm_DefaultReportFont
EndGet
Set(ByValValueAsFont)
IfNotValueIsNothingThen
m_DefaultReportFont=Value
EndIf
EndSet
EndProperty
PublicPropertyHeaderFont()AsFont
Get
Ifm_HeaderFontIsNothingThen
Returnm_DefaultReportFont
Else
Returnm_HeaderFont
EndIf
EndGet
Set(ByValValueAsFont)
m_HeaderFont=Value
EndSet
EndProperty
PublicPropertyFooterFont()AsFont
Get
Ifm_FooterFontIsNothingThen
Returnm_DefaultReportFont
Else
Returnm_FooterFont
EndIf
EndGet
Set(ByValValueAsFont)
m_FooterFont=Value
EndSet
EndProperty
PublicPropertyDetailFont()AsFont
Get
Ifm_DetailFontIsNothingThen
Returnm_DefaultReportFont
Else
Returnm_DetailFont
EndIf
EndGet
https://msdn.microsoft.com/enus/library/ms996472(d=printer).aspx

11/16

10/9/2016

PrintingReportsinWindowsForms

Set(ByValValueAsFont)
m_DetailFont=Value
EndSet
EndProperty

In addition to the Fonts and Brushes, I also implemented properties to allow the user to control the height of the various report
sections setting both a minimum and maximum for the Detail section.

Protectedm_HeaderHeightAsSingle=1
Protectedm_FooterHeightAsSingle=1
Protectedm_MaxDetailRowHeightAsSingle=1
Protectedm_MinDetailRowHeightAsSingle=0.5
PublicPropertyHeaderHeight()AsSingle
Get
Returnm_HeaderHeight
EndGet
Set(ByValValueAsSingle)
m_HeaderHeight=Value
EndSet
EndProperty
PublicPropertyFooterHeight()AsSingle
Get
Returnm_FooterHeight
EndGet
Set(ByValValueAsSingle)
m_FooterHeight=Value
EndSet
EndProperty
PublicPropertyMaxDetailRowHeight()AsSingle
Get
Returnm_MaxDetailRowHeight
EndGet
Set(ByValValueAsSingle)
m_MaxDetailRowHeight=Value
EndSet
EndProperty
PublicPropertyMinDetailRowHeight()AsSingle
Get
Returnm_MinDetailRowHeight
EndGet
Set(ByValValueAsSingle)
m_MinDetailRowHeight=Value
EndSet
EndProperty

Setting up the Data Source

https://msdn.microsoft.com/enus/library/ms996472(d=printer).aspx

12/16

10/9/2016

PrintingReportsinWindowsForms

To be able to produce my report, I need access to data, so I added a property that accepts a DataView instance, and then I loop
through its rows in my overridden version of OnPrintPage.

Protectedm_DataViewAsDataView
PublicPropertyDataView()AsDataView
Get
Returnm_DataView
EndGet
Set(ByValValueAsDataView)
m_DataView=Value
EndSet
EndProperty
ProtectedFunctionGetField(ByValrowAsDataRowView,ByValfieldNameAsString)AsObject
DimobjAsObject=Nothing
IfNotm_DataViewIsNothingThen
obj=row(fieldName)
EndIf
Returnobj
EndFunction
'relevantsnippetoutofOnPrintPage
DimrowCounterAsInteger
e.HasMorePages=False
ForrowCounter=currentRowToMe.DataView.Count1
DimcurrentRowHeightAsSingle=_
PrintDetailRow(leftMargin,_
currentPosition,Me.MinDetailRowHeight,_
Me.MaxDetailRowHeight,width,_
e,Me.DataView(rowCounter),True)
IfcurrentPosition+currentRowHeight<footerBounds.YThen
'itwillfitonthepage
currentPosition+=_
PrintDetailRow(leftMargin,currentPosition,_
MinDetailRowHeight,MaxDetailRowHeight,_
width,e,Me.DataView(rowCounter),False)
Else
e.HasMorePages=True
currentRow=rowCounter
ExitFor
EndIf
Next

Printing the Individual Report Sections


Just having a lot of properties is not enough, I actually have to output the report, but I will be making extensive use of all the
properties to make sure that what I print out conforms to the options the user set. Outputting the report is controlled by the
code in the OnPrintPage procedure, which I override from the base class PrintDocument, and that procedure in turn calls
each of the individual section printing routines PrintPageHeader, PrintPageFooter, and PrintDetailRow. As with some of the
https://msdn.microsoft.com/enus/library/ms996472(d=printer).aspx

13/16

10/9/2016

PrintingReportsinWindowsForms

earlier samples, the code for these routines is quite long so I am not going to include it all inline in this article, but instead I
suggest you download the code and run the sample application.

Using the TabularReport Class


Once I had finished creating the TabularReport class, building a sample report was very easy and consisted of only a few steps.

Step 1: Create an instance of TabularReport


First, create an instance of my class, which you could do directly in your code, but since I inherited from PrintDocument, you can
add my component to your toolbox and then drag it onto your Windows Forms application.

Step 2: Retrieve your data


To provide the report with data, you need a DataView instance, which you can get by filling up a DataTable with the results of a
stored procedure or a SQL query.

PrivateFunctionGetData()AsDataView
DimConnAsNewOleDbConnection(connectionString)
Conn.Open()
'AccessVersion
DimgetOrdersSQLAsString=_
"SELECTCustomers.ContactName,Orders.OrderID,Orders.OrderDate,
Orders.ShippedDate,Sum([UnitPrice]*[Quantity])ASTotalFROM(Customers
INNERJOINOrdersONCustomers.CustomerID=Orders.CustomerID)INNERJOIN
[OrderDetails]ONOrders.OrderID=[OrderDetails].OrderIDGROUPBY
Customers.ContactName,Orders.OrderID,Orders.OrderDate,
Orders.ShippedDateORDERBYOrders.OrderDate"
DimgetOrdersAsNewOleDbCommand(getOrdersSQL,Conn)
getOrders.CommandType=CommandType.Text
DimdaOrdersAsNewOleDbDataAdapter(getOrders)
DimordersAsNewDataTable("Orders")
daOrders.Fill(orders)
Returnorders.DefaultView
EndFunction

In my sample, I am retrieving data from an Access database, so I am using the OleDB classes, but you could use any type of
database you wish, since all my report needs is the resulting DataView.

Step 3: Configure the Columns of the Report


This step consists of creating the ColumnInformation objects for each of the columns in your report and adding each of those
objects to the TabularReport column collection. I put this code into a routine called SetupReport that handles setting up the
columns along with a variety of other appearance related details. I covered the concept of these ColumnInformation objects
earlier in Handling Columns, but here is how the sample application sets them up.

PrivateSubSetupReport()
'GetData
DimordersAsDataView
orders=GetData()
'SetupColumns
https://msdn.microsoft.com/enus/library/ms996472(d=printer).aspx

14/16

10/9/2016

PrintingReportsinWindowsForms

DimcontactName_
AsNewColumnInformation("ContactName",2,_
StringAlignment.Near)
DimorderID_
AsNewColumnInformation("OrderID",1,_
StringAlignment.Near)
DimorderDate_
AsNewColumnInformation("OrderDate",1,_
StringAlignment.Center)
AddHandlerorderDate.FormatColumn,AddressOfFormatDateColumn
DimshippedDate_
AsNewColumnInformation("ShippedDate",1,_
StringAlignment.Center)
AddHandlershippedDate.FormatColumn,AddressOfFormatDateColumn
Dimtotal_
AsNewColumnInformation("Total",1.5,_
StringAlignment.Far)
AddHandlertotal.FormatColumn,AddressOfFormatCurrencyColumn
WithTabularReport1
.ClearColumns()
.AddColumn(contactName)
.AddColumn(orderID)
.AddColumn(orderDate)
.AddColumn(shippedDate)
.AddColumn(total)
.DataView=orders
.HeaderHeight=0.5
.FooterHeight=0.3
.DetailFont=NewFont("Arial",_
12,FontStyle.Regular,_
GraphicsUnit.Point)
.DetailBrush=Brushes.DarkKhaki
.DocumentName="OrderSummaryFromNorthwindsDatabase"
EndWith
EndSub

As part of setting up the columns, there is an AddHandler call for each of the three columns, orderDate, shippedDate, and total,
which associates these columns with routines to format their output as currency or date strings as appropriate.

PublicSubFormatCurrencyColumn(ByValsenderAsObject,_
ByRefeAsFormatColumnEventArgs)
DimincomingValueAsDecimal
DimoutgoingValueAsString
IfNotIsDBNull(e.OriginalValue)Then
incomingValue=CDec(e.OriginalValue)
Else
incomingValue=0
EndIf
outgoingValue=String.Format("{0:C}",incomingValue)
e.StringValue=outgoingValue
https://msdn.microsoft.com/enus/library/ms996472(d=printer).aspx

15/16

10/9/2016

PrintingReportsinWindowsForms

EndSub
PublicSubFormatDateColumn(ByValsenderAsObject,_
ByRefeAsFormatColumnEventArgs)
DimincomingValueAsDate
DimoutgoingValueAsString
IfNotIsDBNull(e.OriginalValue)Then
incomingValue=CDate(e.OriginalValue)
outgoingValue=incomingValue.ToShortDateString()
Else
outgoingValue=""
EndIf
e.StringValue=outgoingValue
EndSub

Whatever you write in your FormatColumn event handler, make sure you make it as quick as possible, as it will be called twice
per row for each column it is attached to. Even a small amount of delay will be noticeable in that situation. Implementing my
column formatting using this method has some pros and cons. On the positive side, you can implement formatting as complex
as you may need, making the report more flexible, while on the negative side it is relatively difficult to perform even simple
formatting. As an alternative that you may wish to implement, you could build the ColumnInformation class so that simple
formats such as currency could be set using a property. With that level of formatting built in, providing a FormatColumn event
handler would be used only for advanced formatting, and overall performance could be improved.

Step 4: Print or Preview the Document


Using either the Print method of the TabularReport or one of the several Windows Forms controls such as the PrintDialog,
PrintPreviewDialog or PrintPreviewControl that are capable of interacting with a PrintDocument object, you can now print
or preview your configured report. Any control that has a property type of PrintDocument will happily work with an instance of
TabularReport, since it inherits from PrintDocument.

Conclusion
Creating reports is a very common task, so you don't want to write all the code every single time you have to build one. Instead, I
suggest you create a minireport engine like my TabularReport class, and try to make it as flexible as you can. Take a look at my
sample to see how exposing fonts, brushes and dimensions allow my single set of code to support a large amount of
customization. If your report engine cannot quite handle a specific report, you could always create another report class that
inherits from PageDocument again, or from TabularDocument, and either way you can build in the specific functionality you
need for your report.
2016 Microsoft

https://msdn.microsoft.com/enus/library/ms996472(d=printer).aspx

16/16