How to Set Printer Default Paper Bin in .NET

Yesterday I was challenged with a task that seemed trivial but it was a real pain to solve.

The customer had a VB.NET application that spawns Adobe Reader via command line options to force a specific PDF file to be printed on a specified printer (using the “semi-clandestine” /t switch). The problem that they were facing was that the VB.NET Compatibility Power Pack’s Printer object (used to simulate the equivalent object in VB6) didn’t change the default paper bin (or tray) for the printer. The change just sticks to the application’s session lifetime.

I turned to Win32 API for the solution. Indeed, there is a winspool.drv library that has the necessary functions to change several printer parameters. I used many blogs and forum posts for a source, which I list here:

Finally, the solution involved coaxing the Win32 API structures into the .NET world using marshalling (using System.Marshal namespace and its methods). I had no success until I set the CharSet attribute to Auto encoding for the API structures. It seems that by default it uses Ansi encoding and the data is messed up.

The final code for the tray change routine is here (it’s in VB.NET but easily translated into C#). It has four main methods:

  • SetTray (sets the default printer tray to a tray number. In VB.NET it’s the Printer.PaperBin or RawKind property)
  • GetPrinterName (gets the default printer name)
  • SavePrinterSettings (copies the printer settings into memory)
  • RestorePrinterSettings (replaces the printer settings with previously saved ones)
Code Snippet
  1. Imports System.Drawing.Printing
  2. Imports System.Runtime.InteropServices
  3.  
  4.  
  5. Public Class PrinterNative
  6.  
  7.  
  8.     Private Declare Auto Function DocumentProperties Lib "winspool.drv" _
  9.      (ByVal hWnd As IntPtr, ByVal hPrinter As IntPtr, ByVal pDeviceName As String, _
  10.       ByVal pDevModeOutput As IntPtr, ByVal pDevModeInput As IntPtr, ByVal fMode As Int32) As Integer
  11.  
  12.     Public Declare Function GetPrinter Lib "winspool.drv" Alias "GetPrinterW" _
  13.      (ByVal hPrinter As IntPtr, ByVal Level As Integer, ByVal pPrinter As IntPtr, _
  14.       ByVal cbBuf As Integer, ByRef pcbNeeded As Integer) As Integer
  15.  
  16.     Private Declare Function SetPrinter Lib "winspool.drv" Alias "SetPrinterA" _
  17.      (ByVal hPrinter As IntPtr, ByVal level As Integer, ByVal pPrinterInfoIn As IntPtr, _
  18.       ByVal command As Int32) As Boolean
  19.  
  20.     <DllImport("winspool.drv", EntryPoint:="OpenPrinterA", ExactSpelling:=True, _
  21.        SetLastError:=True, CallingConvention:=CallingConvention.StdCall, _
  22.        CharSet:=CharSet.Ansi)> _
  23.     Private Shared Function OpenPrinter(ByVal pPrinterName As String, _
  24.   ByRef hPrinter As IntPtr, ByRef pDefault As PRINTER_DEFAULTS) As Boolean
  25.     End Function
  26.  
  27.     <DllImport("winspool.drv", EntryPoint:="ClosePrinter", SetLastError:=True, ExactSpelling:=True, _
  28.      CallingConvention:=CallingConvention.StdCall)> _
  29.     Private Shared Function ClosePrinter(ByVal hPrinter As Int32) As Boolean
  30.     End Function
  31.  
  32.     Declare Function GetDefaultPrinter Lib "winspool.drv" Alias "GetDefaultPrinterA" _
  33.      (ByVal pszBuffer As System.Text.StringBuilder, ByRef pcchBuffer As Int32) As Boolean
  34.  
  35.     Declare Function SetDefaultPrinter Lib "winspool.drv" Alias "SetDefaultPrinterA" _
  36.      (ByVal pszPrinter As String) As Boolean
  37.  
  38.     Public Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
  39.      (ByVal hpvDest As IntPtr, ByVal hpvSource As IntPtr, ByVal cbCopy As Long)
  40.  
  41.  
  42.     Private Structure PRINTER_DEFAULTS
  43.         Dim pDatatype As String
  44.         Dim pDevMode As Long
  45.         Dim pDesiredAccess As Long
  46.     End Structure
  47.  
  48.     Private Const STANDARD_RIGHTS_REQUIRED = &HF0000
  49.     Private Const PRINTER_ACCESS_ADMINISTER = &H4
  50.     Private Const PRINTER_ACCESS_USE = &H8
  51.     Private Const PRINTER_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED Or PRINTER_ACCESS_ADMINISTER Or PRINTER_ACCESS_USE)
  52.  
  53.     Private Const DM_IN_BUFFER As Integer = 8
  54.     Private Const DM_IN_PROMPT As Integer = 4
  55.     Private Const DM_OUT_BUFFER As Integer = 2
  56.  
  57.     Private Structure PRINTER_INFO_9
  58.         Dim pDevMode As IntPtr
  59.     End Structure
  60.  
  61.  
  62.  
  63.     <StructLayout(LayoutKind.Sequential)> _
  64.     Private Structure PRINTER_INFO_2
  65.         <MarshalAs(UnmanagedType.LPTStr)> Public pServerName As String
  66.         <MarshalAs(UnmanagedType.LPTStr)> Public pPrinterName As String
  67.         <MarshalAs(UnmanagedType.LPTStr)> Public pShareName As String
  68.         <MarshalAs(UnmanagedType.LPTStr)> Public pPortName As String
  69.         <MarshalAs(UnmanagedType.LPTStr)> Public pDriverName As String
  70.         <MarshalAs(UnmanagedType.LPTStr)> Public pComment As String
  71.         <MarshalAs(UnmanagedType.LPTStr)> Public pLocation As String
  72.  
  73.         Public pDevMode As IntPtr
  74.  
  75.         <MarshalAs(UnmanagedType.LPTStr)> Public pSepFile As String
  76.         <MarshalAs(UnmanagedType.LPTStr)> Public pPrintProcessor As String
  77.         <MarshalAs(UnmanagedType.LPTStr)> Public pDatatype As String
  78.         <MarshalAs(UnmanagedType.LPTStr)> Public pParameters As String
  79.  
  80.         Public pSecurityDescriptor As IntPtr
  81.         Public Attributes As Integer
  82.         Public Priority As Integer
  83.         Public DefaultPriority As Integer
  84.         Public StartTime As Integer
  85.         Public UntilTime As Integer
  86.         Public Status As Integer
  87.         Public cJobs As Integer
  88.         Public AveragePPM As Integer
  89.     End Structure
  90.  
  91.  
  92.     <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto)> _
  93.     Public Structure DEVMODE
  94.         <MarshalAs(UnmanagedType.ByValTStr, Sizeconst:=32)> Public pDeviceName As String
  95.         Public dmSpecVersion As Short
  96.         Public dmDriverVersion As Short
  97.         Public dmSize As Short
  98.         Public dmDriverExtra As Short
  99.         Public dmFields As Integer
  100.         Public dmOrientation As Short
  101.         Public dmPaperSize As Short
  102.         Public dmPaperLength As Short
  103.         Public dmPaperWidth As Short
  104.         Public dmScale As Short
  105.         Public dmCopies As Short
  106.         Public dmDefaultSource As Short
  107.         Public dmPrintQuality As Short
  108.         Public dmColor As Short
  109.         Public dmDuplex As Short
  110.         Public dmYResolution As Short
  111.         Public dmTTOption As Short
  112.         Public dmCollate As Short
  113.         <MarshalAs(UnmanagedType.ByValTStr, Sizeconst:=32)> Public dmFormName As String
  114.         Public dmUnusedPadding As Short
  115.         Public dmBitsPerPel As Integer
  116.         Public dmPelsWidth As Integer
  117.         Public dmPelsHeight As Integer
  118.         Public dmNup As Integer
  119.         Public dmDisplayFrequency As Integer
  120.         Public dmICMMethod As Integer
  121.         Public dmICMIntent As Integer
  122.         Public dmMediaType As Integer
  123.         Public dmDitherType As Integer
  124.         Public dmReserved1 As Integer
  125.         Public dmReserved2 As Integer
  126.         Public dmPanningWidth As Integer
  127.         Public dmPanningHeight As Integer
  128.  
  129.     End Structure
  130.  
  131.  
  132.  
  133.     Private pOriginalDEVMODE As IntPtr
  134.  
  135.  
  136.     Public Sub SavePrinterSettings(ByVal printerName As String)
  137.         Dim Needed As Integer
  138.         Dim hPrinter As IntPtr
  139.         If printerName = "" Then Exit Sub
  140.  
  141.         Try
  142.             If OpenPrinter(printerName, hPrinter, Nothing) = False Then Exit Sub
  143.             'Save original printer settings data (DEVMODE structure)
  144.             Needed = DocumentProperties(Form1.Handle, hPrinter, printerName, Nothing, Nothing, 0)
  145.             Dim pFullDevMode As IntPtr = Marshal.AllocHGlobal(Needed) 'buffer for DEVMODE structure
  146.             DocumentProperties(Form1.Handle, hPrinter, printerName, pFullDevMode, Nothing, DM_OUT_BUFFER)
  147.             pOriginalDEVMODE = Marshal.AllocHGlobal(Needed)
  148.             CopyMemory(pOriginalDEVMODE, pFullDevMode, Needed)
  149.         Catch ex As Exception
  150.             MsgBox(ex.Message)
  151.         End Try
  152.     End Sub
  153.  
  154.     Public Sub RestorePrinterSettings(ByVal printerName As String)
  155.         Dim hPrinter As IntPtr
  156.         If printerName = "" Then Exit Sub
  157.  
  158.         Try
  159.             If OpenPrinter(printerName, hPrinter, Nothing) = False Then Exit Sub
  160.             Dim PI9 As New PRINTER_INFO_9
  161.             PI9.pDevMode = pOriginalDEVMODE
  162.             Dim pPI9 As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(PI9))
  163.             Marshal.StructureToPtr(PI9, pPI9, True)
  164.             SetPrinter(hPrinter, 9, pPI9, 0&)
  165.             Marshal.FreeHGlobal(pPI9) 'pOriginalDEVMODE will be free too
  166.             ClosePrinter(hPrinter)
  167.  
  168.  
  169.         Catch ex As Exception
  170.             MsgBox(ex.Message)
  171.         End Try
  172.  
  173.  
  174.     End Sub
  175.  
  176.     Function GetPrinterName() As String
  177.         Dim buffer As New System.Text.StringBuilder(256)
  178.         Dim PrinterName As String = String.Empty
  179.         'Get default printer's name
  180.         GetDefaultPrinter(buffer, 256)
  181.         PrinterName = buffer.ToString
  182.         If PrinterName = "" Then
  183.             MsgBox("Can't find default printer.")
  184.         End If
  185.         Return PrinterName
  186.     End Function
  187.  
  188.     Sub SetTray(ByVal printerName As String, ByVal trayNumber As Integer)
  189.         Dim hPrinter As IntPtr
  190.         Dim Needed As Integer
  191.  
  192.         OpenPrinter(printerName, hPrinter, Nothing)
  193.  
  194.         'Get original printer settings data (DEVMODE structure)
  195.         Needed = DocumentProperties(IntPtr.Zero, hPrinter, printerName, Nothing, Nothing, 0)
  196.         Dim pFullDevMode As IntPtr = Marshal.AllocHGlobal(Needed) 'buffer for DEVMODE structure
  197.         DocumentProperties(IntPtr.Zero, hPrinter, printerName, pFullDevMode, Nothing, DM_OUT_BUFFER)
  198.  
  199.         Dim pDevMode9 As DEVMODE = Marshal.PtrToStructure(pFullDevMode, GetType(DEVMODE))
  200.  
  201.         ' Tray change
  202.         pDevMode9.dmDefaultSource = trayNumber
  203.  
  204.         Marshal.StructureToPtr(pDevMode9, pFullDevMode, True)
  205.  
  206.         Dim PI9 As New PRINTER_INFO_9
  207.         PI9.pDevMode = pFullDevMode
  208.  
  209.         Dim pPI9 As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(PI9))
  210.         Marshal.StructureToPtr(PI9, pPI9, True)
  211.         SetPrinter(hPrinter, 9, pPI9, 0&)
  212.         Marshal.FreeHGlobal(pPI9) 'pFullDevMode will be free too
  213.  
  214.         ClosePrinter(hPrinter)
  215.     End Sub
  216.  
  217. End Class

I hope it can be useful to somebody and spare him or her a day or two looking for an answer.