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

Print Windows Forms w/o using API

0.00/5 (No votes)
27 Nov 2008 1  
Print Windows Forms using 100% managed code

Screenie

Preface

This article was written well before Visual Studio 2008. Visual Studio 2008 has a PrintForm component making this project moot. The component works very well. Visual Studio 2005 may have something as well (like a PrintForm class), but I have not checked this out. Look here for further information.

Introduction

This article describes how to send the image of a Form to a printer -- a screenshot, more or less, but using no API or other unmanaged code. This code will be able to print Forms using 2 lines of code. Generally, it redraws the Form and its controls on the printer's Graphics object.

This code is fairly limited but can print most Forms containing standard controls. Within the PrintForm class, a method is required to print each type of control. Included in this class are methods to print the following controls:

  • GroupBox
  • CheckBox
  • TextBox
  • RadioButton
  • Label
  • Button

Adding support for other controls is fairly easy. Add a method and code to draw the control. Add a couple of lines in the DrawControl method to call your new method.

Background

This solution came out of a work requirement. I needed to be able to print an entire Form. I saw many solutions that use the API to grab a screenshot, but I had to use managed code to perform the same task. Nothing I found helped.

Using the Code

To print a Form follow these steps:

  1. Add the PrintForm class into a new or existing Windows Forms project, or use the provided project.
  2. Instantiate a PrintForm object.
  3. Call one of the Print methods.

In the included test app, a button kicks off the printing process:

try
{
    this.Cursor = Cursors.WaitCursor;

    PrintForm pf = new PrintForm(this);
    pf.Print();
}
catch (Exception ex)
{
    MessageBox.Show(this, ex.Message);
}
finally
{
    this.Cursor = Cursors.Default;
}

The two lines of code in the try block are all that is required to print a Form.

All the work is contained in the single class PrintForm. I've provided two constructors:

public PrintForm()
{
}

public PrintForm(Form frm)
{
    _frm = frm;
}

The 2nd constructor sets the Class-level variable to the Form that you want to print.

There are 2 class-level variables that are needed. They are declared as:

private Form _frm = null;
private int _iCurrentPageIndex = 0;
  • _frm stores a reference to the Form to be printed.
  • _iCurrentPageIndex keeps track of the page which is being printed. Pagination is a little tricky due to the way the Framework is setup. This will be covered later.

There are two methods for printing the Form.

  • Print() - uses the Form that should have been set in the constructor.
  • Print(Form frm) - specifies the Form to print.

The latter contains the code which sets up and initiates the printing process. This is the method which will be examined.

public void Print(Form frm)
{
    if (frm == null)
        return;

    // Setup a PrintDocument

    PrintDocument pd = new PrintDocument();
    pd.BeginPrint += new PrintEventHandler(this.PrintDocument_BeginPrint);
    pd.PrintPage += new PrintPageEventHandler(this.PrintDocument_PrintPage);

    // Setup & show the PrintPreviewDialog

    PrintPreviewDialog ppd = new PrintPreviewDialog();
    ppd.Document = pd;
    ppd.ShowDialog();
}

The first block of code creates a PrintDocument object that is used to do the work. The two main PrintDocument events get hooked to matching PrintForm methods described below.

The second block of code creates a PrintPreviewDialog object used to display exactly what will be printed. This will save you lots of paper during testing!

private void PrintDocument_BeginPrint(object sender, PrintEventArgs e)
{
    _iCurrentPageIndex = 0;
}

This is the first of two methods that get hooked up in the Print method. All it does is reset the current page index. It also could have been done in the Print method, but this seems a more appropriate place.

private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e)
{
    // Set the page margins

    Rectangle rPageMargins = new Rectangle(e.MarginBounds.Location, 
      e.MarginBounds.Size);

    // Generate the offset origins for the printing window

    Point[] ptOffsets = GeneratePrintingOffsets(rPageMargins);

    // Make sure nothing gets printed in the margins

    e.Graphics.SetClip(rPageMargins);

    // Draw the rest of the Form using the calculated offsets

    Point ptOffset = new Point(-ptOffsets[_iCurrentPageIndex].X, 
      -ptOffsets[_iCurrentPageIndex].Y);
    ptOffset.Offset(rPageMargins.X, rPageMargins.Y);
    DrawForm(e.Graphics, ptOffset);

    // Determine if there are more pages

    e.HasMorePages = (_iCurrentPageIndex < ptOffsets.Length - 1);

    _iCurrentPageIndex++;
}

This is the second of two methods that get hooked up in the Print method. This method is called before each page is printed. It provides a PrintPageEventArgs object which contains the Graphics object on which the Form will be drawn. This is where pagination gets tricky. The page and its margins do not move. Therefore, we have to offset our drawing each time this method gets called.

First, we setup a Rectangle object to the print margins we want. The PrintPageEventArgs contains several properties that we can use. For this project I chose the MarginBounds property which contains the page margins.

Next, pagination is calculated using the GeneratePrintingOffets method which is described below. The data this method generates is the same every time for a given Form, so it is rather inefficient to make this calculation each time this gets called. The reason it cannot be pre-calculated is the page margins aren't known outside this method (they come from the PrintPageEventArgs object). If anyone knows of a different way to determine the page margins, please let me know and I'll update this. In such a case, the functionality could be moved to the PrintDocument_BeginPrint method and the offsets stored in a class-level Point array.

Next, Graphics.SetClip sets the bounds of the drawing area. Anything drawn outside this rectangle will not be displayed.

Then, draw the entire Form using the calculated offsets. Effectively, this divides the Form into rectangles the size of the printing area. This method is described below.

Next, the PrintPageEventArgs must be told if there are more pages to be printed. Each offset that is calculated is a page that will be printed, so it's easy to determine if there are more pages.

Finally, don't forget to increment the page index.

private Point[] GeneratePrintingOffsets(Rectangle rMargins)
{
    // Setup the array of Points

    int x = (int)Math.Ceiling((double)(_frm.Width) / 
      (double)(rMargins.Width));
    int y = (int)Math.Ceiling((double)(_frm.Height) / 
      (double)(rMargins.Height));
    Point[] arrPoint = new Point[x * y];

    // Fill the array

    for (int i = 0; i < y; i++)
        for (int j = 0; j < x; j++)
            arrPoint[i * x + j] = new Point(j * rMargins.Width, 
              i * rMargins.Height);

    return arrPoint;
}

This method calculates the offsets for each page based on the page margins. It essentially divides the Form into rectangles of the specified size. The array of points stores the upper-left corner of each rectangle. To print each page, the Form is drawn so that this offset is located at the origin of the page margins.

private void DrawForm(Graphics g, Point ptOffset)
{
    // Calculate the Title Bar rectangle

    int iBarHeight = (int)g.MeasureString(_frm.Text, _frm.Font).Height;
    Rectangle rTitleBar = new Rectangle(ptOffset.X, ptOffset.Y,
         _frm.Width, iBarHeight + 2);

    // Draw the rest of the Form under the Title Bar

    ptOffset.Offset(0, rTitleBar.Height);
    g.FillRectangle(new SolidBrush(_frm.BackColor), ptOffset.X, 
        ptOffset.Y, _frm.Width, _frm.Height);

    // Draw the rest of the controls

    DrawControl(_frm, ptOffset, g);

    // Draw the Form's Title Bar

    Bitmap bmp = Bitmap.FromHicon(_frm.Icon.Handle);
    g.FillRectangle(new SolidBrush(SystemColors.ActiveCaption), rTitleBar);
    g.DrawImage(bmp,
        rTitleBar.X,
        rTitleBar.Y,
        rTitleBar.Height,
        rTitleBar.Height);
    g.DrawString(_frm.Text,
        _frm.Font,
        new SolidBrush(SystemColors.ActiveCaptionText),
        rTitleBar.X + rTitleBar.Height, // adding the width of the icon

        rTitleBar.Y + (rTitleBar.Height / 2) - 
          (g.MeasureString(_frm.Text, _frm.Font).Height) / 2);

    // Draw the Title Bar buttons

    Size s = new Size(16, 14); // size determined from graphics program

    ControlPaint.DrawCaptionButton(g,
        ptOffset.X + _frm.Width - s.Width,
        ptOffset.Y + (rTitleBar.Height / 2) - 
            (s.Height / 2) - rTitleBar.Height,
        s.Width,
        s.Height,
        CaptionButton.Close,
        ButtonState.Normal);
    ControlPaint.DrawCaptionButton(g,
        ptOffset.X + _frm.Width - (s.Width * 2) - 1,
        ptOffset.Y + (rTitleBar.Height / 2) - (s.Height / 2)
        - rTitleBar.Height,
        s.Width,
        s.Height,
        (_frm.WindowState == FormWindowState.Maximized ? 
            CaptionButton.Restore : CaptionButton.Maximize),
        ButtonState.Normal);
    ControlPaint.DrawCaptionButton(g,
        ptOffset.X + _frm.Width - (s.Width * 3 - 1),
        ptOffset.Y + (rTitleBar.Height / 2) - (s.Height / 2) - 
        rTitleBar.Height,
        s.Width,
        s.Height,
        CaptionButton.Minimize,
        ButtonState.Normal);

    // Draw a rectangle around the entire Form

    g.DrawRectangle(Pens.Black, ptOffset.X, 
       ptOffset.Y - rTitleBar.Height, _frm.Width, 
       _frm.Height + rTitleBar.Height);
}

The bulk of the code in this method prints the parts of the Form itself -- the title bar, the Form's window, and the title bar buttons (maximize, minimize, and close). An important point is that the parts are drawn in a certain order. The main form is the bottom most control and should be drawn first. All controls contained in the Form are drawn next using the DrawControl method (described below). Then the title bar and its buttons. All these Form related pieces are purely for an accurate printout and may be excluded for simplicity.

private void DrawControl(Control ctl, Point ptOffset, Graphics g)
{
    // Cycle through each control on the form and paint

    // it on the graphics object

    foreach (Control c in ctl.Controls)
    {
        // Skip invisible controls

        if (!c.Visible)
            continue;

        // Calculate the location offset for the control - this offset is

        // relative to the original offset passed in

        Point p = new Point(c.Left, c.Top);
        p.Offset(ptOffset.X, ptOffset.Y);

        // Draw the control

        if (c is GroupBox)
            DrawGroupBox((GroupBox)c, p, g);
        else if (c is Button)
            DrawButton((Button)c, p, g);
        else if (c is TextBox)
            DrawTextBox((TextBox)c, p, g);
        else if (c is CheckBox)
            DrawCheckBox((CheckBox)c, p, g);
        else if (c is Label)
            DrawLabel((Label)c, p, g);
        else if (c is ComboBox)
            DrawComboBox((ComboBox)c, p, g);
        else if (c is RadioButton)
            DrawRadioButton((RadioButton)c, p, g);
        else
            return;

        // Draw the controls within this control

        DrawControl(c, p, g);
    }
}

This method is the "meat & potatoes" of the project. It draws the contents of the specified Control on the specified Graphics object at the specified offset. This is where you'd add a couple lines of code to draw additional controls. The magic of this method lies in recursion. Note that it calls itself after the current control is drawn. This way, child controls are drawn on top.

Let's take a peek at one of the methods used to draw a particular control. I'll pick the DrawRadioButton as an example:

private void DrawRadioButton(RadioButton rdo, Point p, Graphics g)
{
    // Setup the size of a RadioButton

    Rectangle rRadioButton = new Rectangle(p.X, p.Y, 12, 12);

    ControlPaint.DrawRadioButton(g, p.X,
        p.Y + (rdo.Height / 2) - (rRadioButton.Height / 2),
        rRadioButton.Width,
        rRadioButton.Height,
        (rdo.Checked ? ButtonState.Checked : ButtonState.Normal));

    // RadioButton's text left justified & centered vertically

    g.DrawString(rdo.Text,
        rdo.Font,
        new SolidBrush(rdo.ForeColor),
        rRadioButton.Right + 1,
        p.Y + (rdo.Height / 2) - (g.MeasureString(rdo.Text, 
            rdo.Font).Height / 2));
}

In this case, the Framework already provides a method to draw the Radio Button itself within the ControlPaint object. We have to provide the size and location of the Radio Button. Using MSPaint, I determined that a standard Radio Button is 12x12, which is why I hard-coded it here. After the Radio Button itself is drawn, the text must be drawn. For simplicity, I assume the text should be left-justified and centered vertically.

It's a bit of work to draw a Form in this way, but if you don't want to use unmanaged code it's the only solution I've found for the .NET environment. In the end, it's up to you how accurately you want your forms to print.

Summary

I found it interesting that the framework provides a handful of methods to draw controls but leaves most of the work to the programmer. You must supply the size and location, and in some cases the state of the control. It doesn't simply allow you to pass the control itself. In retrospect, this was more than I had intended to tackle.

History

  • Version 1.0 - initial release

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