December 13, 2011

How to P/Invoke PrintDlg (x86 & x64)

Do you know the PrintDlg function?

Well, it shows a Print Dialog and also allows the user to setup printer options which are stored in hDevMode and hDevNames. Long story short, this function has been used in C++ and I now need this function in a C# library which has been ported from an equivilant C++ library.

PrintDlg worked fine using this method signature:
[DllImport("comdlg32.dll", CharSet=CharSet.Auto)]
static extern bool PrintDlg([In, Out] PRINTDLG lppd);

And the definition of the PRINTDLG structure (yes, it's defined as a class, not as struct because struct is not a reference type as it is used here) as the following:
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto, Pack=1)]
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 IntPtr 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;
}

Now, if you are running your C# application under x86, you can use these lines of code to make the Print Dialog appear
PRINTDLG pd=new PRINTDLG();
pd.lStructSize=Marshal.SizeOf(pd);
PrintDlg(pd);

Nevertheless this won't work when running the same code on a x64 machine - the Print Dialog just won't show up and the function returns 0 which just means "failure".

Let's check that out. After some time I started to check the struct size in the C++ code, where it actually worked - also on x64. When I counted the bytes of the PRINTDLG structure I got 66 bytes on x86 architecture. Also the sizeof(pd) returned 66 bytes. OK, you now think this is boring because on x64 it would just be the same too.
I counted 114 bytes. But sizeof(pd) returned 120 bytes. So a difference of 6 bytes which causes the PrintDlg to fail because in my C# application the Marshal.SizeOf(pd) also returned 114 bytes which is not equal to 120 bytes.

Did you notice the Pack=1 in the StructLayout attribute?
You can find the definition on MSDN.
To keep it simple, this parameter tells the compiler how the structure should be aligned in memory. After playing a little with this setting, I found out that Pack=8 is required for the PRINTDLG structure to work under x64.
We are nearly finished, but we got one problem left. Since we wan't the class library to run either on x86 and x64 we would need to specify the Pack value depending on the current processor's architecture. We are out of luck here when trying to set the Pack to the IntPtr.Size. So what we need now is a wrapper.

First define the same structure as above one more time and name it PRINTDLGx64 and set the Pack=8. Also create another method signature for PrintDlg accepting the PRINTDLGx64 structure as input.

The wrapper should look like that:
class PrintDlgWrap
{
  PRINTDLG dlg;
  PRINTDLGx64 dlg64;

  public static readonly bool Is64Bit;

  static PrintDlgWrap()
  {
    Is64Bit=IntPtr.Size==8;  // this works for .net 3.5 and 4.0
  }

  public PrintDlgWrap()
  {
    if (Is64Bit)
    {
      dlg64=new PRINTDLGx64();
      dlg64.lStructSize=Marshal.SizeOf(dlg64);
    }
    else
    {
      dlg=new PRINTDLG();
      dlg.lStructSize=Marshal.SizeOf(dlg);
    }
  }

  public int lStructSize { get { return Is64Bit?dlg64.lStructSize:dlg.lStructSize; } }
  public int Flags { get { return Is64Bit?dlg64.Flags:dlg.Flags; } set { if (Is64Bit) dlg64.Flags=value; else dlg.Flags=value; } }
  // ...

  public PRINTDLG Getx86() { return dlg; }
  public PRINTDLGx64 Getx64() { return dlg64; }
}

And now you just create an instance of this wrapper in your class and use the properties to access the PRINTDLG's fields which has been created in the constructor depending on the processor's architecture :)

All you need now to call the print dialog are the following lines
PrintDlgWrap dp=new PrintDlgWrap();
if (PrintDlgWrap.Is64Bit) PrintDlg(pd.Getx64());
else PrintDlg(pd.Getx86());

Hope you liked my first post and feel free to post comments :)

2 comments:

  1. Nice post. Help me a lot. Thanks :)

    ReplyDelete
  2. Still very relevant if you need a modified print dialog. Thanks!

    ReplyDelete