.Net Windows Forms: How to Create a Custom Print Dialog
By George Mihaescu
Summary: this article presents a solution for
customizing the print dialog in a .Net Windows Forms application. The solution
presented relies on P/Invoke (direct call into the Win32 API from .Net) and has
been tested with .Net runtime 1.0, 1.1 and 2.0 on Win 2000, XP and Vista.
A .zip file is available for download
with source code and the compiled binaries (.Net 2.0).
The problem
Your .Net Windows Forms application needs additional
controls in the standard print dialog. The example in the associated source
code adds a check box which allows the user the additional functionality to
"shrink the output to fit on a single page". However, the
object-oriented solution that immediately comes to mind (inherit from
System.Windows.Forms.PrintDialog and extend its functionality) cannot be
applied because the PrintDialog class is sealed. (As a side, it is sealed
because PrintDialog is just a thin wrapper over the standard Win32 Print Common
Dialog, and simply deriving from it would not have worked – although with
additional programming effort Microsoft could have provided set of methods to
allow customization of the dialog through inheritance, something similar to
what I'm doing in the sample code attached: see CustomPrintDialog.cs).
The solution
Derive your own class from System.Windows.Forms.CommonDialog
and use P/Invoke to call into the Win32 API PrintDlg. This function takes a
structure of type PRINTDLG, which has a field that allow a pointer to a
callback function specifically for customization. By creating a method in your
.Net code that matches the signature of the expected callback and specifying a
delegate wrapping it in the member of the structure, your method will be called
upon the standard Print Dialog initialization – from this point on you are free
to customize the dialog as you see fit (hide controls, add controls, etc).
This is how the customized dialog will look like using the
code in my example (note the additional checkbox at the bottom of the dialog,
which I dynamically extend vertically in my callback implementation in order to
make room for the new checkbox):

The core of the code is show below, but I strongly suggest
you download the full sample.
#region Imports from the Win32 API
private const int PD_ALLPAGES = 0x00000000;
private const int PD_NOSELECTION = 0x00000004;
private const int PD_NOPAGENUMS = 0x00000008;
private const int PD_ENABLEPRINTHOOK = 0x00001000;
[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Auto)]
private struct PRINTDLG
{
public Int32 lStructSize;
public IntPtr hwndOwner;
public IntPtr hDevMode;
public IntPtr hDevNames;
public IntPtr hDC;
public Int32 Flags;
public UInt16 nFromPage;
public UInt16 nToPage;
public UInt16 nMinPage;
public UInt16 nMaxPage;
public UInt16 nCopies;
public IntPtr hInstance;
public Int32 lCustData;
public PrintHookProc lpfnPrintHook;
public IntPtr lpfnSetupHook;
public string lpPrintTemplateName;
public string lpSetupTemplateName;
public IntPtr hPrintTemplate;
public IntPtr hSetupTemplate;
}
[DllImport("comdlg32.dll", CharSet = CharSet.Auto)]
private static extern bool PrintDlg(ref PRINTDLG
pdlg);
private const int WM_INITDIALOG = 0x0110;
private const int IDOK = 1;
private const int IDCANCEL = 2;
#endregion
#region Customization of the standard dialog through
a callback (print hook procedure)
private delegate IntPtr PrintHookProc(IntPtr hdlg, Int32
msg, UInt16 wparam, Int32
lparam);
/// <summary>
/// Signature matching the delegate above
which we instantiate and pass to
/// the print dialog as the customization
method.
/// </summary>
private IntPtr
PrintCustomizationMethod(IntPtr hdlg, Int32 msg, UInt16
wparam, Int32 lparam)
{
if (msg ==
WM_INITDIALOG)
{
//create checkbox
and attach it to the dialog
SetParent(m_cb.Handle, hdlg);
//get current
dialog size
RECT rect
= new RECT();
GetWindowRect(hdlg, ref
rect);
//place the
checkbox at the bottom of the dialog (using the top of the OK button as
reference)
RECT
button_rect = new RECT();
IntPtr
h_button = GetDlgItem(hdlg, IDOK);
GetWindowRect(h_button, ref button_rect);
POINT pt;
pt.x = button_rect.left;
pt.y = button_rect.top;
ScreenToClient(hdlg, ref pt);
m_cb.Location = new
Point(12,
pt.y);
m_cb.Size = new
Size(rect.Width - 12,
m_cb.Size.Height);
m_cb.Visible = true;
m_cb.Text = "Compress
output to fit in one page";
//set initial
state and attach handler to combo box check event
m_cb.Checked = this.ShrinkOutputToFitOnOnePage;
m_cb.CheckedChanged += new EventHandler(cb_CheckedChanged);
//extend the
dialog down with the height of the checkbox
SetWindowPos(hdlg, new
IntPtr(0), 0, 0, rect.Width,
rect.Height + m_cb.Bounds.Height,
SWP_NOZORDER | SWP_NOMOVE);
//move the OK and
Cancel buttons down
h_button = GetDlgItem(hdlg, IDOK);
GetWindowRect(h_button, ref rect);
pt.x = rect.left;
pt.y = rect.top;
ScreenToClient(hdlg, ref pt);
SetWindowPos(h_button, new IntPtr(0), pt.x, pt.y + m_cb.Bounds.Height, 0, 0,
SWP_NOZORDER | SWP_NOSIZE);
h_button = GetDlgItem(hdlg, IDCANCEL);
GetWindowRect(h_button, ref rect);
pt.x = rect.left;
pt.y = rect.top;
ScreenToClient(hdlg, ref pt);
SetWindowPos(h_button, new IntPtr(0), pt.x, pt.y + m_cb.Bounds.Height, 0, 0,
SWP_NOZORDER | SWP_NOSIZE);
}
//allow processing of
messages by default procedure by returning 0
return new IntPtr(0);
}
/// <summary>
/// Event handler for the custom combo box
check event
/// </summary>
private void
cb_CheckedChanged(object sender, EventArgs e)
{
CheckBox cb =
sender as CheckBox;
this.ShrinkOutputToFitOnOnePage
= cb.Checked;
}
#endregion
#region Implementation of abstract methods of the
base
protected override bool RunDialog(System.IntPtr
hwndOwner)
{
//initialize the
structure required by the PrintDlg Win32 API
PRINTDLG pdlg
= new PRINTDLG();
pdlg.lStructSize = Marshal.SizeOf(pdlg);
pdlg.hwndOwner = hwndOwner;
pdlg.Flags |= PD_ALLPAGES;
if (!this.AllowSelection)
{
pdlg.Flags |= PD_NOSELECTION;
}
if (!this.AllowSomePages)
{
pdlg.Flags |= PD_NOPAGENUMS;
}
//set the hook
procedure to point to our instance of the delegate designed
//for customizing the
dialog. Without this, the class will just show the standard print dialog.
pdlg.Flags |= PD_ENABLEPRINTHOOK;
pdlg.lpfnPrintHook = new
PrintHookProc(this.PrintCustomizationMethod);
//show the Win32
common print dialog (customized by the hook above)
bool f = false;
try
{
f = PrintDlg(ref
pdlg);
}
catch (Exception ex)
{
//any exception in
the direct Win32 call is most likely caused by a faulty
//printer driver
(we had one issue where the user provided a stack trace
//pointing to this
call that was resolved by uninstalling printer and installing
//with a new
printer driver)
throw new ApplicationException("The current printer caused a Windows error:\n ",
ex);
}
if (!f)
{
//closed with
Cancel, or an error showing the dialog
long err =
CommDlgExtendedError();
if (err != 0)
{
Debug.WriteLine("Windows common dlg error code: " + err);
}
}
return f;
}