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:
- Add the
PrintForm
class into a new or existing Windows Forms project, or use the provided project.
- Instantiate a
PrintForm
object.
- 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;
PrintDocument pd = new PrintDocument();
pd.BeginPrint += new PrintEventHandler(this.PrintDocument_BeginPrint);
pd.PrintPage += new PrintPageEventHandler(this.PrintDocument_PrintPage);
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)
{
Rectangle rPageMargins = new Rectangle(e.MarginBounds.Location,
e.MarginBounds.Size);
Point[] ptOffsets = GeneratePrintingOffsets(rPageMargins);
e.Graphics.SetClip(rPageMargins);
Point ptOffset = new Point(-ptOffsets[_iCurrentPageIndex].X,
-ptOffsets[_iCurrentPageIndex].Y);
ptOffset.Offset(rPageMargins.X, rPageMargins.Y);
DrawForm(e.Graphics, ptOffset);
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)
{
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];
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)
{
int iBarHeight = (int)g.MeasureString(_frm.Text, _frm.Font).Height;
Rectangle rTitleBar = new Rectangle(ptOffset.X, ptOffset.Y,
_frm.Width, iBarHeight + 2);
ptOffset.Offset(0, rTitleBar.Height);
g.FillRectangle(new SolidBrush(_frm.BackColor), ptOffset.X,
ptOffset.Y, _frm.Width, _frm.Height);
DrawControl(_frm, ptOffset, g);
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,
rTitleBar.Y + (rTitleBar.Height / 2) -
(g.MeasureString(_frm.Text, _frm.Font).Height) / 2);
Size s = new Size(16, 14);
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);
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)
{
foreach (Control c in ctl.Controls)
{
if (!c.Visible)
continue;
Point p = new Point(c.Left, c.Top);
p.Offset(ptOffset.X, ptOffset.Y);
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;
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)
{
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));
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