Академический Документы
Профессиональный Документы
Культура Документы
PrintingReportsinWindowsForms
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.
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.
//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.
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
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.
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.
Figure 6. The Trimming property can be used to provide an automatic ellipse when your string doesn't fit within the
layout rectangle.
Figure 7. The HotkeyPrefix property is quite useful when creating your own controls.
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.
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
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
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
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.
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.
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.
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