
Version 2.0 released!
After many requests and many new features added to the initial version, here
is version 2.0 of FormPrinting. A lot of improvements have been made in this
totally rewritten version. The main one is complete management of controls
growing over many pages. That is multiline TextBox,
ListBox, DataGrid,... can grow depending of their
lines/items. See
below for changes.
Update
Release 2.1 : property added for horizontal alignment.
Release 2.2 : bugs fixed. (1) Extension problem when top position of a control equal bottom position of above control that grow. This problem arrive when docking is used. (2) Little print error in multiline TextBox when text is less than one line.
Introduction
FormPrinting is useful to produce a report directly from a form containing
desired data. There are tools to print grids, trees, but I saw nothing to print
a form. It's possible to do a print screen, but this gives a cheap looking
report.
This tool prints any form in a manner that looks like a report. It create
pages by using a Graphics object to reproduce data appearing on the
form. Graphics object produced is print via a
PrintDocument or saved to a file as a multi-frame Tif image.
Using the code
To use it, just call the print() or the
PrintToTifFile() function of the class, like this :
Dim fp As New FormPrinting(Me)
fp.Print() fp.PrintToTifFile("myTiffFileName")
By passing Me as a parameter of the constructor, the
class uses a recursive function to scan each control of the form. It prints them
in different formats depending on the type of the control. Some controls are not
printed, like buttons. For TabControl, only the selected tab is
printed. For example :

However, you can pass any type of Windows control to the constructor. For
example, you can send only a TapPage or a GroupBox. In
this case FormPrinting will use GroupBox.Text as report title and
will process only controls contained in the GroupBox . No need to
"print" the whole Form.
The class takes care of some control properties like Visible,
enabled or Horizontal alignment. But not all
properties are considered.
The following properties of the FormPrinting class can be set to
customize the result: (You can see here default values)
public bool TextBoxBoxed = false;
public bool TabControlBoxed = true;
public bool LabelInBold = true;
public bool PrintPreview = true;
public bool DisabledControlsInGray = false;
public bool PageNumbering = false;
public string PageNumberingFormat = "Page {0}";
public OrientationENum Orientation = OrientationENum.Automatic;
public ControlPrinting DelegatePrintingReportTitle;
public Single TopMargin = 0;
public Single BottomMargin = 0;
It's possible to activate printing of customized controls that have a
text property without modifying the code. Just add a part of the
description of the type of the control by calling the function
AddTextBoxLikeControl(). Example:
fp.AddTextBoxLikeControl("DateTimeSlicker")
This a list of addition made in version 2.0. Most of them are discussed in
this article :
- Version 2.0 is in C# (1.0 was written with VB.NET. Demo Form still in
VB.NET)
- Constructor accept control other than a Form
- Can provide your own print functions for specific controls and for report
title formatting
- Controls can expand
- Expandable
TextBox multiline added
- Expandable
ListBox added
- Expandable
FlexGrid added (Grid from Component One : code in
comments only [no references] )
DataGrid printing
- More comments in source code
- Report can continue on many pages
- Can display a trace of controls printed (Call function
GetTrace() )
- Can print page number using your own
String.Format
- Report can be print to a multi-frame Tif file (example: for faxing)
- Better algorithm to compute extension of container controls with height
growing of side by side childs
- Can set top and bottom margin
- This article is elaborated more
Providing your own printing function
Printing functions for each control type, including FormPrinting
report header, are called via a common delegate signature.
FormPrinting provide printing functions for most used controls.
However, you can provide your own personalized printing function to replace
internal one or to add printing of new type of control. To use you own function,
record it using the function AddDelegateToPrintControl like in this
VB sample :
fp.AddDelegateToPrintControl("FlexGrid", _
AddressOf FlexGridPrinting.PrintIt)
This will cause FormPrinting to call PrintIt()
function for controls with Type ending with "FlexGrid".
To replace the internal function used to print the title of the report,
assign yours to the public variable DelegatePrintingReportTitle :
fp.DelegatePrintingReportTitle = AddressOf MyOwnPrintReportTitle
Printing function must correspond to the following signature :
public delegate void ControlPrinting(
System.Windows.Forms.Control c,
ParentControlPrinting typePrint,
MultiPageManagement mp,
Single x, Single y,
ref Single extendedHeight, out bool ScanForChildControls);
In most case, set ScanForChildControls to false and you don't
have to worry about typePrint parameter. These are used for
controls containing children to be printed, like Panel and
TapPage . In their case, printing function are called before and
after printing of children. The reason is to adjust size according to growing
children like ListBox.
- c : control to draw
- x,y : Top left printing position of control to draw (relative to parent
control)
- extendedHeight : growing height of the control if needed. For example, if
a
TextBox.Height control is 200 and it needs 250 to be printed,
return 50. Returned value must be 0 or more
- mp : the FormPrinting drawing manager (
MultiPageManagement ).
Use it to draw
MultiPage management
The MultiPageManagement class in FormPrinting take
automatically care of multi page functionality. It print unit in the right page.
Also, this class prevent for an element to be print over a page break.
Drawing must always be done in a print unit. If there is not enough space at
the bottom of the current page to print the unit, this one will be print at the
top of next page. Print units below it must be push down. This is handled by the
FormPrinting extension functionality. The
MultiPageManagement class don't change top or bottom margin to
provide space for a print unit. Instead, it return the height missing to print
unit at the bottom of the page. So this value is simply used by the controls
extension management of FormPrinting to push down controls below
it.
Print of more than one page is pretty tricky with PrintDocument.
Pages are printing one by one. So it's not possible to create and use many pages
at the same time. In other hand, it would be too complicated to
FormPrinting to remember position of all controls on each pages. To
bypass this problem, print engine in FormPrinting print the
document as many time as there is pages to print. At each pass, the
MultiPageManagement class print only the print units contained in
the current page. Pass #1 print page 1, Pass #2 print page 2, and so on. After
each pass, MultiPageManagement class returns a boolean
indicating if another page is needed. When all controls fit in current or
previous pages, job is finish.
This process also save bitmap memory usage when print to a Tif file. Each
page use the same bitmap. Frames (pages) are added to the tif file at each
pass.
Look at this code snippet demonstrating printing of an item in a
ListBox (lb) using MultiPageManagement class :
extendedHeight = mp.BeginPrintUnit(yItem, lb.ItemHeight);
mp.DrawString(lb.Text, printFont, _Brush, x, yItem,
lb.Width, lb.ItemHeight);
mp.EndPrintUnit();
The MultiPageManagement class also do page numbering.
Control growing
I'm specially proud of this feature that works very fine. The code I wrote
for this is fairly short and robust at the same time. It take care of any
controls position below, above or side by side with another one. It also
calculate the new height of container controls like TabPage and
Panel.
The Combination of Control expansion, recursive printing and
MultiPageManagement class produce a clean result in almost any
case.
The basic logic is that a control is push down (Y position increased) to keep
his vertical distance with the bottom of the nearest control above him.
Here is the only section of code that do all of this. It's in
PrintControls() function.
Note : "Controls" is an array of contained controls sorted by Y position. For
example, it can refer to all controls inside a Panel control.
for (int i = 0; i < nbCtrl; i++)
{
Single pushDownHeight = 0;
foreach (Element e in extendedYPos)
if (controls[i].Location.Y > e.originalBottom) {
if (e.totalPushDown > pushDownHeight)
pushDownHeight = e.totalPushDown;
}
Single cp = controls[i].Location.Y + pushDownHeight;
Single extendedHeight;
PrintControl(controls[i], mp,
x + controls[i].Location.X, y + cp, out extendedHeight);
if (extendedHeight > 0)
{
Element e = new Element();
e.originalBottom = controls[i].Location.Y + controls[i].Height;
e.printedBottom = cp + controls[i].Height + extendedHeight;
extendedYPos.Add(e);
}
}
globalExtendedHeight = 0;
foreach (Element e in extendedYPos)
if (e.totalPushDown > globalExtendedHeight)
globalExtendedHeight = e.totalPushDown;
}
private class Element
{
public Single originalBottom;
public Single printedBottom;
public Single totalPushDown
{get {return printedBottom - originalBottom;} }
}
Behind the scene of control printing
Some type of control was more difficult to format than other. In this section
I explain how I solved some problems.
For multiline TextBox , the problem was to separate text
into lines that fit in the width of the control. This is necessary to print it
line by line, so that page break can be handled properly. I used
Graphics.MeasureString(). This method return the number of
characters that can be print for a font in a specific rectangle. I used a
rectangle corresponding to one line of the TextBox.
For ListBox, I only found one way to obtain the text of items. A
Text property return the text of the selected Item. So I save
selected position, and in the loop I change the selected index to get the text
of each items.
For DataGrid, cells content are private elements. So I get the
DataSource of the control. If a DataTable is found, it
is used to retrieve cells content. For column header caption and width, the
DataGridTableStyle object isn't directly accessible. The trick is
to create an instance of object DataGridTableStyle and link it to
the DataGrid. Then we can access column properties :
DataGridTableStyle myGridTableStyle;
if (dg.TableStyles.Count == 0)
{
myGridTableStyle = new DataGridTableStyle();
dg.TableStyles.Add(myGridTableStyle);
}
string caption = dg.TableStyles[0].GridColumnStyles[i].HeaderText;
Another job to do was juggling with alignment. For TextBox,
there are 3 values in Enum HorizontalAlignment. For Labels, there
are 9 values from ContentAlignment. And with the
graphic object, we use StringAlignment Enum, which
contain values Near, Far and Center.
These values must be converted before they can be used. For example, the
absolute value of Center in StringAlignment is not the
same that in HorizontalAlignment.
Drawing
Drawing of controls is made via a Graphics object. This is the
tool provided in .NET to draw text, lines, square on an Image device. The class
MultiPageManagement in FormPrinting hold an instance
of a Graphics object revived for each page as a parameter of the
NewPage() function. For each operation, it simply calculate the
position in the current page of the object to draw, and then call the
appropriate drawing function. You can see this in the following sample of
function DrawRectangle() in MultiPageManagement.
Function _ConvertToPage() compute vertical position in the current
page of processed Print Unit . "_G" hold the Graphics object.
public void DrawRectangle(Pen pen, Single x,
Single y, Single w, Single h)
{
if (PrintUnitIsInCurrentPage())
{
Single yPage = _ConvertToPage(y);
_G.DrawRectangle(pen, x, yPage, w, h);
}
}
The Graphics object is linked to an Image device who receive the
painting of text, lines, square, ... Image device refer to a UI used as an
output media. It can be a Form on a screen, a printer page, a bitmap. Depending
if you want to print the report or to save it to a file, the main procedure of
FormPrinting create for each page the corresponding Image device,
attach it to the Graphics object, and pass it to the
MultiPageManagement class.
To save an image, a Bitmap is used. The Graphics
object is created with the Graphics.FromImage() method :
Graphics g = Graphics.FromImage(bitmapAllPages);
To obtain a multiframe image, bitmaps are added together with
Image.SaveAdd() method. To do this we need to set different values
in an Imaging.EncoderParameters.
To print the report, a PrintDocument is used. When the
Print method is called, a PrintPage event is triggered
for each pages. The handler received a printer page object already linked with a
Graphics object. At the end of each page, the handler just have to
set the HasMorePages property to true to continue with a new
page.
Limitations
Not all properties are considered when printing controls. As an example,
FormPrinting doesn't look for the Font of separate elements inside
controls. ListView and other controls are not implemented.
Using the Demo
The Demo Form contain many CheckBox that you can used to try
options. It contains too different kind of controls that can expand. Some
SpinBox let you change the number of items in ListBox
and in the DataGrid. Here is the function of test buttons :
- "Print Me" : Print the form
- "Tif me" : Save report in a tif file
- "Print Tab" : Print only the selected tab, not the whole Form
- "Trace" : after printing, display a build-in trace of controls
printed
The Demo also demonstrate how you can provide your own print function to the
class. I use this Form to test the FormPrinting class.