Introduction
In many Windows applications, it could be useful to add some kind of print functionality. The DataGrid
included in Windows Form
s does not include any support for this. In this article I will show how to use a custom DataGrid
written by me in Visual Basic .NET inheriting from class System.Windows.Froms.DataGrid
to implement an application with print preview and obviously print support. If you already have a window with a DataGrid,
you can go directly to the section - Using the custom DataGrid
.
Print support is written to work, if the DataSource
used in the DataGrid
is a DataTable
or a DataView
. There is no support if the DataGrid
is bound to a DataSet
or some other kind of object (Array
s and so on). Modifications in source code for other DataSource
objects would not be very difficult.
The custom DataGrid
has also other useful features like the possibility to print column headers using more then one row (enabled by default), export data in XML and HTML formats and send data by E-mail.
This code is written in Visual Basic mainly because I was writing it for a true application in VB.NET. There is no reason why you cannot use it in a C# or a J# application. The following code is also written in VB.NET but with some minor changes (mainly because the syntax is different). It will also work fine in other .NET languages.
The basic application
The first thing you have to do is to create a new Windows Application Project with a F
orm
hosting a standard DataGrid
and a MainMenu
object. Then you can add (using the designer or with code if you like), some menus in this form. Normally the main form can have a Sizable FormBorderStyle
. The DataGrid
will have the Dock
property set to Fill
.
Now you can add a database connection, a DataAdapter
and a DataSet
(typed or untyped) you will use, to fill the DataGrid
(for the sake of simplicity this code is omitted here).
In the demo project, you will find a test file called Demo.xml used to fill the DataSet
with its data. In the sample application in the Form
constructor, I simply use the method ReadXml
to read a table named orders
, in a DataSet
named dsOrders
.
Public Sub New()
MyBase.New()
InitializeComponent()
dsOrders.ReadXml("..\Demo.xml")
objDataGrid.DataSource = dsOrders.Tables(0).DefaultView
End Sub
If you compile and run your project now, you will obtain a basic Windows application showing some data in a DataGrid
.
Using the custom DataGrid
The first thing to do is to add to your project, a reference to the library hosting the custom DataGrid
(customcontrols.dll) or to add to your solution the existing project with the DataGrid
(customcontrols.vbproj). If you choose this second way, you must add a reference in your main project, to the second project you just added.
To transform the normal DataGrid
to my custom DataGrid
, the simplest way is by using the Search and Replace feature in Visual Studio. Open your Form
in Code View and then use the Replace function (CTRL+H). As source text, you will insert System.Windows.Forms.DataGrid
and as replace text insert CustomControls.DataGridEx
.
There is another way to use my custom DataGrid
if you start by scratch, with a new Form
. Simply you can customize the Visual Studio toolbox, adding my custom DataGrid
(the class name is DataGridEx
and is hosted in customcontrols.dll). Then instead of dragging in the designer, the standard DataGrid
, you will drag the DataGridEx
control. The result will be the same as in the previous approach.
If you try to compile and run the application now, you will obtain a runtime exception. This is because, to obtain print preview support and to support column headers using more then one row, you have to create a TableStyle
, with information about every column (especially the column width). Looking in MSDN, you can find this kind of information, but this can be a long and extremely tedious process. To skip this, in the form constructor, you can call the method AdjustColumnWidths
of the DataGrid
(this method evaluates an optimum column width for each column, based on the table data). Alternatively you can invoke the method AdjustColumnWidthToTitles
(that evaluates column widths based on column header text). This code can also be placed in the form Load
event handler. If you call either of the two methods, you also have some interesting features like the possibility to display boolean
columns in colored cells or to use custom cell controls.
Public Sub New()
...
objDataGrid.AdjustColumnWidths(dsOrders.Tables(0))
End Sub
To use custom column header text, you can use the method SetColumnName
like in the following example.
Public Sub New()
...
objDataGrid.SetColumnName(dsOrders.Tables(0), "OrderID", "Order Number")
objDataGrid.SetColumnName(dsOrders.Tables(0), "OrderDate", "Order Date")
End Sub
To add the Page Setup code, simply you will invoke the PageSetup
method (a static
/shared
method) of the PageSetup
class contained in the CustomControls
namespace.
Private Sub mnuPageSetup_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles mnuPageSetup.Click
CustomControls.PageSetup.PageSetup()
End Sub
To add Print and Print Preview support, you can simply call the methods Print
and PrintPreview
of the DataGrid
. The following code is quite more complex because it works also if the DataSource
used is a DataTable
and not only if it is a DataView
.
Private Sub mnuPrintPreview_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles mnuPrintPreview.Click
Dim obj, obj2 As Object
obj = objDataGrid.DataSource
If TypeOf (obj) Is DataView Then
obj2 = CType(obj, DataView).Table
Else
obj2 = obj
obj = Nothing
End If
Me.objDataGrid.PageSettings = CustomControls.PageSetup.PageSettings
objDataGrid.PrintPreview(CType(obj, DataView), CType(obj2, DataTable), _
CType(Me.BindingContext(objDataGrid.DataSource), CurrencyManager), _
25, "Do you wish to continue?")
End Sub
Private Sub mnuPrint_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles mnuPrint.Click
Dim obj, obj2 As Object
obj = objDataGrid.DataSource
If TypeOf (obj) Is DataView Then
obj2 = CType(obj, DataView).Table
Else
obj2 = obj
obj = Nothing
End If
Me.objDataGrid.PageSettings = CustomControls.PageSetup.PageSettings
objDataGrid.Print(CType(obj, DataView), CType(obj2, DataTable), _
CType(Me.BindingContext(objDataGrid.DataSource), CurrencyManager))
End Sub
Adding custom columns
To add custom columns in a DataGrid
, you have to create your own ColumnStyle
class inheriting from the class System.Windows.Forms.DataGridColumnStyle
(or other classes inheriting from it).
In the CustomControls library you can find two sample classes usable in your applications. The first one is DataGridBoolColumnEx
(a normal boolean
column with a background painted in a custom color if the value in its cell is true
) and the second is DataGridPushPinColumn
(a boolean
column with custom painting). In the sample application after calling AdjustColumnWidths
, I added a call to a private
function called AddOtherColumns
. This function in the first 11 rows simply obtains a reference to the table loaded from the XML file and adds two new boolean
columns initializing their values. Then two new ColumnStyles
are created and initialized supplying the column header, the column name in the DataTable
and the column width in the DataGrid
. Finally these column styles are added to the table style used by the DataGrid
.
Public Sub New()
MyBase.New()
...
objDataGrid.AdjustColumnWidths(dsOrders.Tables(0))
AddOtherColumns()
...
End Sub
Private Sub AddOtherColumns()
Dim myType As System.Type
myType = System.Type.GetType("System.Boolean")
Dim tbl As DataTable
tbl = dsOrders.Tables(0)
tbl.Columns.Add(New System.Data.DataColumn("BoolColumn", myType))
tbl.Columns.Add(New System.Data.DataColumn("PinnedColumn", myType))
Dim i As Integer
For i = 0 To tbl.Rows.Count - 1
tbl.Rows(i).Item("BoolColumn") = IIf(i Mod 3 <> 0, True, False)
tbl.Rows(i).Item("PinnedColumn") = IIf(i Mod 3 = 0, True, False)
Next
Dim tst As DataGridTableStyle
tst = objDataGrid.GetTblStyle(tbl)
Dim cs As New CustomControls.DataGridBoolColumnEx(Color.Blue)
cs.HeaderText = "Sample Boolean Column"
cs.MappingName = "BoolColumn"
cs.Width = 50
tst.GridColumnStyles.Add(cs)
Dim cs2 As New CustomControls.DataGridPushPinColumn()
cs2.HeaderText = "Pinned Column"
cs2.MappingName = "PinnedColumn"
cs2.Width = 100
tst.GridColumnStyles.Add(cs2)
End Sub
The following images show the form when the application is started and when the print preview is invoked.
Low level details
As mentioned earlier, the class DataGridEx
inherits from the standard .NET DataGrid
. It offers some more functionality and these new features are spread among classes contained in a DLL called CustomControls. The DataGridEx
class offers many new methods. Mainly there are methods used for print support, to extend basic functionality and to work with table styles.
I will not explain every method and its functionality but I will only cite the most important and useful ones. If you look at the code, I think you will find many other features.
There is a method called HitCellTest
usable to raise an event called CellHitTest
. This event is fired based on mouse position and tells the user what is the cell the mouse pointer is over. A property called MouseOverNotificationEnabled
is usable to enable or disable the notification of current cell based on mouse pointer position. If the notification is enabled, every second the mouse position is checked and an event called MouseOverNotification
is eventually fired (this can be useful to display custom tooltips based on mouse position).
When you call the method AdjustColumnWidthToTitles
a new TableStyle
is created and for each column in the table passed as parameter, a ColumnStyle
is created. The column width is evaluated based on the header font. The method AdjustColumnWidths
simply calls AdjustColumnWidthToTitles
and then makes a cycle on the table for each row and each column eventually enlarging the column width. These methods create column styles based on the datatype hosted in the columns. They try to create extended column styles (the ones written by me) so if there is for example a boolean
column a DataGridBoolColumnEx
is created.
The class PrintPreviewDialogEx
inherits from the standard print preview dialog. The code in this class is quite interesting because the standard PrintPreviewDialog
has a private toolbar. If I simply inherit from this class, I cannot obtain a standard reference to this toolbar and so I couldn't add any more toolbar buttons. The solution to this problem was to use the features of .NET reflection to obtain a reference to the private variable. The true print preview dialog usable by users is TablePrintPreviewDialog
. It inherits from PrintPreviewDialogEx
and adds some functionality as the export of data in XML or HTML and the possibility to send the data by E-mail.
Conclusion
Please E-mail me for upgrades, questions, bugs found, etc. Thanks.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.