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:
- http://www.dotnetmonster.com/Uwe/Forum.aspx/dotnet-general/9001/Changing-printer-setting-using-NET
- http://social.msdn.microsoft.com/forums/en-US/winforms/thread/5de19cbf-aadc-48cb-b1d3-2b700fef4540
- http://www.lessanvaezi.com/changing-printer-settings-using-the-windows-api/
- http://www.alexcrossley.net/?p=162
- http://social.msdn.microsoft.com/forums/en-US/vbgeneral/thread/8cc8ddfc-4822-4a64-974b-4fdb87eaeab1
- http://www.pinvoke.net/default.aspx/winspool/DocumentProperties.html
- http://nicholas.piasecki.name/blog/2008/11/programmatically-selecting-complex-printer-options-in-c-shar/
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)
- Imports System.Drawing.Printing
- Imports System.Runtime.InteropServices
- Public Class PrinterNative
- Private Declare Auto Function DocumentProperties Lib "winspool.drv" _
- (ByVal hWnd As IntPtr, ByVal hPrinter As IntPtr, ByVal pDeviceName As String, _
- ByVal pDevModeOutput As IntPtr, ByVal pDevModeInput As IntPtr, ByVal fMode As Int32) As Integer
- Public Declare Function GetPrinter Lib "winspool.drv" Alias "GetPrinterW" _
- (ByVal hPrinter As IntPtr, ByVal Level As Integer, ByVal pPrinter As IntPtr, _
- ByVal cbBuf As Integer, ByRef pcbNeeded As Integer) As Integer
- Private Declare Function SetPrinter Lib "winspool.drv" Alias "SetPrinterA" _
- (ByVal hPrinter As IntPtr, ByVal level As Integer, ByVal pPrinterInfoIn As IntPtr, _
- ByVal command As Int32) As Boolean
- <DllImport("winspool.drv", EntryPoint:="OpenPrinterA", ExactSpelling:=True, _
- SetLastError:=True, CallingConvention:=CallingConvention.StdCall, _
- CharSet:=CharSet.Ansi)> _
- Private Shared Function OpenPrinter(ByVal pPrinterName As String, _
- ByRef hPrinter As IntPtr, ByRef pDefault As PRINTER_DEFAULTS) As Boolean
- End Function
- <DllImport("winspool.drv", EntryPoint:="ClosePrinter", SetLastError:=True, ExactSpelling:=True, _
- CallingConvention:=CallingConvention.StdCall)> _
- Private Shared Function ClosePrinter(ByVal hPrinter As Int32) As Boolean
- End Function
- Declare Function GetDefaultPrinter Lib "winspool.drv" Alias "GetDefaultPrinterA" _
- (ByVal pszBuffer As System.Text.StringBuilder, ByRef pcchBuffer As Int32) As Boolean
- Declare Function SetDefaultPrinter Lib "winspool.drv" Alias "SetDefaultPrinterA" _
- (ByVal pszPrinter As String) As Boolean
- Public Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
- (ByVal hpvDest As IntPtr, ByVal hpvSource As IntPtr, ByVal cbCopy As Long)
- Private Structure PRINTER_DEFAULTS
- Dim pDatatype As String
- Dim pDevMode As Long
- Dim pDesiredAccess As Long
- End Structure
- Private Const STANDARD_RIGHTS_REQUIRED = &HF0000
- Private Const PRINTER_ACCESS_ADMINISTER = &H4
- Private Const PRINTER_ACCESS_USE = &H8
- Private Const PRINTER_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED Or PRINTER_ACCESS_ADMINISTER Or PRINTER_ACCESS_USE)
- Private Const DM_IN_BUFFER As Integer = 8
- Private Const DM_IN_PROMPT As Integer = 4
- Private Const DM_OUT_BUFFER As Integer = 2
- Private Structure PRINTER_INFO_9
- Dim pDevMode As IntPtr
- End Structure
- <StructLayout(LayoutKind.Sequential)> _
- Private Structure PRINTER_INFO_2
- <MarshalAs(UnmanagedType.LPTStr)> Public pServerName As String
- <MarshalAs(UnmanagedType.LPTStr)> Public pPrinterName As String
- <MarshalAs(UnmanagedType.LPTStr)> Public pShareName As String
- <MarshalAs(UnmanagedType.LPTStr)> Public pPortName As String
- <MarshalAs(UnmanagedType.LPTStr)> Public pDriverName As String
- <MarshalAs(UnmanagedType.LPTStr)> Public pComment As String
- <MarshalAs(UnmanagedType.LPTStr)> Public pLocation As String
- Public pDevMode As IntPtr
- <MarshalAs(UnmanagedType.LPTStr)> Public pSepFile As String
- <MarshalAs(UnmanagedType.LPTStr)> Public pPrintProcessor As String
- <MarshalAs(UnmanagedType.LPTStr)> Public pDatatype As String
- <MarshalAs(UnmanagedType.LPTStr)> Public pParameters As String
- Public pSecurityDescriptor As IntPtr
- Public Attributes As Integer
- Public Priority As Integer
- Public DefaultPriority As Integer
- Public StartTime As Integer
- Public UntilTime As Integer
- Public Status As Integer
- Public cJobs As Integer
- Public AveragePPM As Integer
- End Structure
- <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto)> _
- Public Structure DEVMODE
- <MarshalAs(UnmanagedType.ByValTStr, Sizeconst:=32)> Public pDeviceName As String
- Public dmSpecVersion As Short
- Public dmDriverVersion As Short
- Public dmSize As Short
- Public dmDriverExtra As Short
- Public dmFields As Integer
- Public dmOrientation As Short
- Public dmPaperSize As Short
- Public dmPaperLength As Short
- Public dmPaperWidth As Short
- Public dmScale As Short
- Public dmCopies As Short
- Public dmDefaultSource As Short
- Public dmPrintQuality As Short
- Public dmColor As Short
- Public dmDuplex As Short
- Public dmYResolution As Short
- Public dmTTOption As Short
- Public dmCollate As Short
- <MarshalAs(UnmanagedType.ByValTStr, Sizeconst:=32)> Public dmFormName As String
- Public dmUnusedPadding As Short
- Public dmBitsPerPel As Integer
- Public dmPelsWidth As Integer
- Public dmPelsHeight As Integer
- Public dmNup As Integer
- Public dmDisplayFrequency As Integer
- Public dmICMMethod As Integer
- Public dmICMIntent As Integer
- Public dmMediaType As Integer
- Public dmDitherType As Integer
- Public dmReserved1 As Integer
- Public dmReserved2 As Integer
- Public dmPanningWidth As Integer
- Public dmPanningHeight As Integer
- End Structure
- Private pOriginalDEVMODE As IntPtr
- Public Sub SavePrinterSettings(ByVal printerName As String)
- Dim Needed As Integer
- Dim hPrinter As IntPtr
- If printerName = "" Then Exit Sub
- Try
- If OpenPrinter(printerName, hPrinter, Nothing) = False Then Exit Sub
- 'Save original printer settings data (DEVMODE structure)
- Needed = DocumentProperties(Form1.Handle, hPrinter, printerName, Nothing, Nothing, 0)
- Dim pFullDevMode As IntPtr = Marshal.AllocHGlobal(Needed) 'buffer for DEVMODE structure
- DocumentProperties(Form1.Handle, hPrinter, printerName, pFullDevMode, Nothing, DM_OUT_BUFFER)
- pOriginalDEVMODE = Marshal.AllocHGlobal(Needed)
- CopyMemory(pOriginalDEVMODE, pFullDevMode, Needed)
- Catch ex As Exception
- MsgBox(ex.Message)
- End Try
- End Sub
- Public Sub RestorePrinterSettings(ByVal printerName As String)
- Dim hPrinter As IntPtr
- If printerName = "" Then Exit Sub
- Try
- If OpenPrinter(printerName, hPrinter, Nothing) = False Then Exit Sub
- Dim PI9 As New PRINTER_INFO_9
- PI9.pDevMode = pOriginalDEVMODE
- Dim pPI9 As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(PI9))
- Marshal.StructureToPtr(PI9, pPI9, True)
- SetPrinter(hPrinter, 9, pPI9, 0&)
- Marshal.FreeHGlobal(pPI9) 'pOriginalDEVMODE will be free too
- ClosePrinter(hPrinter)
- Catch ex As Exception
- MsgBox(ex.Message)
- End Try
- End Sub
- Function GetPrinterName() As String
- Dim buffer As New System.Text.StringBuilder(256)
- Dim PrinterName As String = String.Empty
- 'Get default printer's name
- GetDefaultPrinter(buffer, 256)
- PrinterName = buffer.ToString
- If PrinterName = "" Then
- MsgBox("Can't find default printer.")
- End If
- Return PrinterName
- End Function
- Sub SetTray(ByVal printerName As String, ByVal trayNumber As Integer)
- Dim hPrinter As IntPtr
- Dim Needed As Integer
- OpenPrinter(printerName, hPrinter, Nothing)
- 'Get original printer settings data (DEVMODE structure)
- Needed = DocumentProperties(IntPtr.Zero, hPrinter, printerName, Nothing, Nothing, 0)
- Dim pFullDevMode As IntPtr = Marshal.AllocHGlobal(Needed) 'buffer for DEVMODE structure
- DocumentProperties(IntPtr.Zero, hPrinter, printerName, pFullDevMode, Nothing, DM_OUT_BUFFER)
- Dim pDevMode9 As DEVMODE = Marshal.PtrToStructure(pFullDevMode, GetType(DEVMODE))
- ' Tray change
- pDevMode9.dmDefaultSource = trayNumber
- Marshal.StructureToPtr(pDevMode9, pFullDevMode, True)
- Dim PI9 As New PRINTER_INFO_9
- PI9.pDevMode = pFullDevMode
- Dim pPI9 As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(PI9))
- Marshal.StructureToPtr(PI9, pPI9, True)
- SetPrinter(hPrinter, 9, pPI9, 0&)
- Marshal.FreeHGlobal(pPI9) 'pFullDevMode will be free too
- ClosePrinter(hPrinter)
- End Sub
- End Class
I hope it can be useful to somebody and spare him or her a day or two looking for an answer.