Version 2.0 Released!
After many requests and many new features were 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 on their lines/items. See below for changes.
Update
Release 2.1: Property added for horizontal alignment
Release 2.2: Bugs fixed
- Extension problem when top position of a control is equal to the bottom position of the above control that grows. This problem arrives when docking is used.
- 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 the default values here.)
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 is 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 accepts 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 children
- 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 cases, 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 functions 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
automatically takes care of multi page functionality. It prints unit in the right page. Also, this class prevents for an element to be printed 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 printed at the top of the next page. Print units below it must be push down. This is handled by the FormPrinting
extension functionality. The MultiPageManagement
class doesn't change top or bottom margin to provide space for a print unit. Instead, it returns 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. On the 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
prints the document as many times as there are pages to print. At each pass, the MultiPageManagement
class prints only the print units contained in the current page. Pass #1 prints 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 finished.
This process also saves bitmap memory usage when printed to a Tif file. Each page uses 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 does page numbering.
Control Growing
I'm especially proud of this feature that works very fine. The code I wrote for this is fairly short and robust at the same time. It takes care of any controls position below, above or side by side with another one. It also calculates the new height of container controls like TabPage
and Panel
.
The Combination of Control expansion, recursive printing and MultiPageManagement
class produces a clean result in almost any case.
The basic logic is that a control is pushed 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 does 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 Scenes 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 returns the number of characters that can be printed 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 returns 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
holds an instance of a Graphics
object revived for each page as a parameter of the NewPage()
function. For each operation, it simply calculates the position in the current page of the object to draw, and then calls the appropriate drawing function. You can see this in the following sample of function DrawRectangle()
in MultiPageManagement
. Function _ConvertToPage()
computes vertical position in the current page of processed Print Unit . "_G
" holds 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 receives the painting of text, lines, square, ... Image device refers to a UI used as an output media. It can be a Form on a screen, a printer page, a bitmap. Depending on whether you want to print the report or to save it to a file, the main procedure of FormPrinting
creates for each page the corresponding Image
device, attaches it to the Graphics
object, and passes 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 has 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 contains many CheckBox
that you can use to try options. It contains two different kinds of controls that can expand. Some SpinBox
lets 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 demonstrates how you can provide your own print function to the class. I use this Form to test the FormPrinting
class.
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.