Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Printing a Form in a Report Fashion (Release 2.2)

0.00/5 (No votes)
26 Oct 2004 1  
Printing a Form in a report fashion

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

  1. 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.
  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() 'Print to printer
fp.PrintToTifFile("myTiffFileName") 'Print to tif file

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;      
  // box around TextBox controls
public bool TabControlBoxed = true;      
  // box around TabControl controls
public bool LabelInBold = true;      
  // Print all labels in bold
public bool PrintPreview = true;     
  // enabled Print preview instead of direct printing
public bool DisabledControlsInGray = false;   
  // Color for disabled controls
public bool PageNumbering = false;      
  //If true, reserve space at the bottom of 
  //each page and print page number
public string PageNumberingFormat = "Page {0}";    
  // String format for page number
public OrientationENum Orientation = OrientationENum.Automatic;  
  // choose print orientation (Automatic, Protrait or Landscape)
public ControlPrinting DelegatePrintingReportTitle;    
  // Function that will print report title
public Single TopMargin = 0;        
  //If 0, use default margin, else use this (1/100 inch)
public Single BottomMargin = 0;        
  //If 0, use default margin, else use this (1/100 inch)

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")

Improvements

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.

// ************************************************************
// This loop over child controls calculate position of them.
// Algorithm take care of controls that expand besides and above.
// It keep an arraylist of original and printed (after expansion) bottom
// position of expanded control.
// So control is push down if it was originally below expanded controls
// *************************************************************
for (int i = 0; i < nbCtrl; i++)
{
  // Set y position of control depending on extension of controls above him
  Single pushDownHeight = 0;
  foreach (Element e in extendedYPos)
    if (controls[i].Location.Y > e.originalBottom) //completely under it
    {
      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)
  {
    //Keep extension with y position
    Element e = new Element();
    e.originalBottom = controls[i].Location.Y + controls[i].Height;
    e.printedBottom = cp + controls[i].Height + extendedHeight;
    extendedYPos.Add(e);
  }
}
// same computation for bottom of container control. Its bottom line is
// below all child controls. So it is extended the same as the most pushed
// child control.
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.

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