
Introduction
C# provides a standard PrintDialog which has all the basic print dialog functionality. Since it is declared as a sealed class, there is no way to extend it through C# native syntax. This article provides an alternate means to extend the functionality of the C# PrintDialog.
There are a lot of additional features that can be added to the common print dialog, one's imagination is only the limit. In this article, I will present an extended print dialog with a topmost and persistent location window capability.
Overview
PrintDialogEx serves as a wrapper to the system PrintDlg (common dialog), which uses P/Invoke methods to access the Win32 system's low-level APIs. Hook the dialog window mainproc, and add a topmost window style when it receives a WM_INIT_DIALOG event notification. This may sound complicated, but it's all under the hood.
Under the Hood
The Win32 PrintDlg(...) API exported by comdlg32.dll provides a way to customize the system's standard print dialog. Since, this API is only available in Win32, the trick is to use P/Invoke to map this API to make it available in C#.
[DllImport("comdlg32.dll", CharSet=CharSet.Auto)] static
extern bool PrintDlg([In,Out] PRINTDLG lppd);
The parameter to the API is a pointer to PRINTDLG. Below is the PRINTDLG structure from commdlg.h, ported to C#:
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto, Pack = 1)]
[System.Runtime.InteropServices.ComVisible(false)]
internal class PRINTDLG
{
public Int32 lStructSize;
public IntPtr hwndOwner;
public IntPtr hDevMode;
public IntPtr hDevNames;
public IntPtr hDC = IntPtr.Zero;
public Int32 Flags;
public Int16 FromPage = 0;
public Int16 ToPage = 0;
public Int16 MinPage = 0;
public Int16 MaxPage = 0;
public Int16 Copies = 0;
public IntPtr hInstance = IntPtr.Zero;
public IntPtr lCustData = IntPtr.Zero;
public PrintHookProc lpfnPrintHook;
public IntPtr lpfnSetupHook = IntPtr.Zero;
public IntPtr lpPrintTemplateName = IntPtr.Zero;
public IntPtr lpSetupTemplateName = IntPtr.Zero;
public IntPtr hPrintTemplate = IntPtr.Zero;
public IntPtr hSetupTemplate = IntPtr.Zero;
}
I will discuss only the important structure members necessary to achieve the goal of this article. For a detailed description of this structure, please refer to the PRINTDLG MSDN documentation.
hDevMode - Handle to a movable global memory object that contains a DEVMODE structure. Must be set to the current print document PrintSettings.GetHdevmode() member to make our customize dialog display our current printer settings.
hDevNames - Handle to a movable global memory object that contains a DEVNAMES structure. Must be set to the current print document PrintSettings.GetHdevmode() member to make our customize dialog display our current printer settings.
Flags - A set of bit flags that you can use to initialize the print dialog box. Setting the value of this flag enables us to enable/disable/show/hide some controls in the standard print dialog. But most importantly, the PD_ENABLEPRINTHOOK flag must be set in order to make our print dialog hook procedure implementation to be used. See lpfnPrintHook below.
lpfnPrintHook - a function pointer, set to our print dialog hook procedure(PrintHookProcImpl). The PD_ENABLEPRINTHOOK flag must be added to the Flags member to make this setting effective.
PRINTDLG _pd = new PRINTDLG();
_pd.lStructSize = Marshal.SizeOf(_pd);
_pd.hwndOwner = this.Parent;
_pd.hDevMode = printerSettings.GetHdevmode();
_pd.hDevNames = printerSettings.GetHdevnames();
_pd.Flags = PrintFlag.PD_ENABLEPRINTHOOK | PrintFlag.PD_NOPAGENUMS;
_pd.lpfnPrintHook = new PrintHookProc( PrintHookProcImpl );
bool bRet = PrintDlg(_pd);
printerSettings.SetHdevmode(_pd.hDevMode);
printerSettings.SetHdevnames(_pd.hDevNames);
Notice that lpfnPrintHook was set to an intance of PrintHookProc with the PrintHookProcImpl function as parameter. PrintHookProc is a delegate function which is equivalent to a function pointer in C++. You can create an instance of PrintHookProc with an argument equal to any method as long as the parameters match the PrintHookProc declaration. Below is the declaration:
internal delegate IntPtr PrintHookProc( IntPtr hWnd, UInt16 msg,
Int32 wParam, Int32 lParam );
Once, the PRINTDLG structure instance is properly intialized, the only next thing to do is to invoke our mapped Win32 PrintDlg(), passing our PRINTDLG as the argument. The code snippet above will display the system's standard print dialog, but prior to displaying will raise a WM_INIT_DIALOG event, which our hook procedure intercepts.
internal IntPtr PrintHookProcImpl( IntPtr hPrintDlgWnd,
UInt16 msg, Int32 wParam, Int32 lParam )
{
switch(msg)
{
case WndMsg.WM_INITDIALOG:
{
this.Handle = hPrintDlgWnd;
UInt32 nZOrder = (this.TopMost) ? WndZOrder.HWND_TOPMOST :
WndZOrder.HWND_TOP;
Win32.SetWindowPos(
this.Handle, nZOrder, 0, 0, 0, 0, WndPos.SWP_NOSIZE |
WndPos.SWP_NOMOVE );
break;
}
}
return IntPtr.Zero;
}
Using the code
The client application needs only to set the PrintDialogEx's TopMost property to true prior to calling ShowDialog() method.
Below is a sample code snippet to do this:
PrintDialogEx oPrintDlg = new PrintDialogEx();
oPrintDlg.Parent = this.Handle;
oPrintDlg.TopMost = true;
oPrintDlg.HandlePrintEvent += new EventHandler(OnPrintEvent);
oPrintDlg.Document = new System.Drawing.Printing.PrintDocument();
DialogResult nResult = oPrintDlg.ShowDialog();
References
- Printing Overview, MSDN documentation.
- Extend the Common Dialog Boxes, MSDN tutorials.
History
- 2006.11.25 - Original version.