Retrieving Printer Information

By Matt Hart

This article first appeared in Visual Developer Magazine.
(Note: This is my unedited original and may differ slightly from the published version.)

Visual Basic programmers usually find one of the more obscure bunch of API functions to be the Printer APIs. One reason for this is that Microsoft has provided VB with the Printer object and the Printers collection. You are expected to use these objects or perhaps some third party tool like Crystal Reports to print whatever you need. But what if you just want to change the default printer? This article will show you how to do that and more using a few basic Printer API functions.

The more adventurous programmer has probably already attempted to use the GetPrinter and SetPrinter API functions. They probably crashed Visual Basic in the attempt. While most of the API procedure and type declarations in the Win32api.txt file included with VB are accurate, the most needed printer type, PRINTER_INFO_2 (PI2), is wildly incorrect. The problem with that type stems from a basic inadequacy in VB – it’s almost impossible for a non-VB procedure to create strings that can be assigned to a VB variable. Indeed VB cannot accept any data types created in a DLL. If the function returns a string or a Type structure, VB must pre-allocate the variable and provide the function with the address to that variable’s buffer space. The variable itself cannot be "redirected" to point to one created by the DLL.

That is the problem with PI2 – it has all these As String declarations that cannot be pre-allocated, yet the GetPrinter API call expects to be able to do just that! The key to getting around this common API limitation in VB is to use a global memory block and copy to or from that memory into or out of VB structures and variables. Once they are copied from the memory block, they can be manipulated or read. Then the VB variables can be copied back into the memory block for transfer to the API. In essence, you have two copies of the information created by the DLL – the global memory block and the VB variables.

Copying to and from memory is accomplished with the RtlMoveMemory API function, although I always alias it to CopyMemory:

Private Declare Sub CopyMemory Lib "kernel32" Alias _

        "RtlMoveMemory" (hpvDest As Any, hpvSource As Any, _

        ByVal cbCopy As Long)

To copy to or from a Type structure or a non-variable length string, use:

CopyMemory fromType, toVar, lNumBytes

If you are copying using a global memory block, it must be locked memory in the form of a pointer. You pass it using the ByVal keyword, which passes the data contained in the variable (the pointer to the global memory block) rather than the pointer to the variable itself. With variable length strings, you must also use ByVal. That passes a pointer to the string data rather than a pointer to the string descriptor – that contains the string length and the string pointer.

CopyMemory ByVal aDat, ByVal hMem, lNumBytes

Allocating a global memory block in VB is easy – just use the GlobalAlloc API function with the GPTR flag – that tells GlobalAlloc to lock and clear the memory and return a pointer rather than a handle to the memory. That way you can skip the GlobalLock and GlobalUnlock API functions. Always remember to free the memory block when you are finished with it.

Private Declare Function GlobalAlloc Lib "kernel32" _

    (ByVal wFlags As Long, ByVal dwBytes As Long) As Long

Private Declare Function GlobalFree Lib "kernel32" _

    (ByVal hMem As Long) As Long

Private Const GMEM_FIXED = &H0

Private Const GMEM_ZEROINIT = &H40

Private Const GPTR = (GMEM_FIXED Or GMEM_ZEROINIT)



Dim hMem As Long

hMem = GlobalAlloc(GPTR, lSizeNeeded)

GlobalFree hMem
PRINTER INFO – 12345
There are five different levels of printer information you can retrieve or set. You can readily see the type of information returned by the other structures by glancing at their Type declarations in the Win32api.txt file. Some levels only work in Windows 9x, some only in Windows NT. Level two works in both Win9x and WinNT and has the most information. As I mentioned previously, the declaration in Win32api.txt is unusable. You need to change all the variable types to As Long. When you call the GetPrinter API function, it will create long integer pointers to the strings and Type structure elements contained in PI2. You then call CopyMemory to copy from the pointers into VB variables that you can use to manipulate or view the data (see Sidebar).

Sidebar
The process of retrieving information about a printer is:
1. OpenPrinter (uses the printer device name)
2. GlobalAlloc (allocate a global memory block)
3. GetPrinter (using the global memory)
4. CopyMemory (from global memory to VB variables)
5. (modify or view the VB variables)
6. CopyMemory (from VB variables to global memory)
7. SetPrinter (using the modified global memory)
8. GlobalFree (de-allocate the global memory)
9. ClosePrinter

The information returned in the PI2 structure is fairly self-explanatory. For complete descriptions, refer to the MSDN SDK Documentation – either the CD included with VB6 or online at http://msdn.microsoft.com/library/sdkdoc/gdi/prntspol_9otu.htm. Within the PI2 structure is a DEVMODE structure that has a lot of information about the way the printer is currently setup. You can find an online reference to DEVMODE at http://msdn.microsoft.com/library/sdkdoc/gdi/prntspol_8nle.htm. I did notice that one of the possible paper types is missing from this reference page – DMPAPER_USER for custom paper sizes. NT doesn't support custom paper sizes via the DEVMODE. Instead, you must create a custom Form via the AddForm API function. -Matt

I created two functions to facilitate transferring data from the global memory block to VB strings. Transferring to Type structures or other variables is as simple as CopyMemory, but VB strings must be allocated, copied, and truncated to the first encountered null character.

' Simple function to extract a null terminated

‘ string from a fixed length buffer.

Private Function CString(aStr As String) As String

    Dim k As Long

    k = InStr(aStr, Chr$(0))

    If k Then

        CString = Left$(aStr, k - 1)

    Else

        CString = aStr

    End If

End Function



' Custom function to copy a string given the pointer

‘  and add it along with a description to the ListBox.

Private Sub ExL(ByVal sDesc As String, ByVal lpstr As Long)

    Dim m As String

    If lpstr Then

        m = Space$(256)		‘ 256 byte buffer

        CopyMemory ByVal m, ByVal lpstr, 256

        m = CString(m)

    End If

    lstDevmode.AddItem sDesc & m

End Sub

To load the ListBox with the information returned by GetPrinter (see Figure 1), I only need to pass the pointer to the referenced global memory block string and a description:

ExL "PI - PrintProcessor: ", PI2.pPrintProcessor

The PI2 type structure is my modified PRINTER_INFO_2, while pPrintProcessor is a long integer pointer to a string created by GetPrinter.

The sample program can:
    Enumerate the available printers
    Show the printer status (if the printer driver has the capability)
    Display the PRINTER_INFO_2 and DEVMODE settings
    Set the selected printer as the Windows Default
    Save the current printer settings
    Update the program with any changed settings
    Show the size and byte data of the Extra Driver data
    Compare the saved Extra Driver data with the current data.

Another bit of information returned by GetPrinter that I’ve found useful is the Extra Driver data. This is a variable length chunk of data pointed to by a DEVMODE element containing unspecified data. It will hold stuff like the current output tray selection, color mode options, and other printer specific settings. The sample program has a function that will save all the information returned by GetPrinter and then compare the extra driver data with the saved data. This will enable you to get the settings, save the data, change the printer specific setting, then automatically compare the current data with the saved data. If, for example, you need to force the paper to a specific output bin of your printer, you can easily discover the right bytes to set in the extra driver data using the sample program.

SETTING THE DEFAULT PRINTER
There are two different methods for setting the default printer – one for Windows NT and another for Windows 9x. Before you can set the default printer, you must determine which operating system the program is running under using the GetVersionEx API function (see Code Listing). While NT doesn’t need to use the messy Printer API functions, it still has its own little quirks. You must first retrieve registry information about the printer you want to set as default from the registry.

Open the key HCU\Software\Microsoft\Windows NT\CurrentVersion\Devices (HCU = HKEY_CURRENT_USER) and find the value name that matches your printer devicename. The value contained by that name has device specific information that needs to be concatenated onto the printer devicename, separated by a comma, and inserted into the key at HCU\Software\Microsoft\Windows NT\CurrentVersion\Windows using the value "Device". After modifying this registry key, you must alert all other Windows programs that the default device has been changed. Although the Registry is what was modified, Windows accomplishes this sending the WM_WININICHANGE to all windows using the HWND_BROADCAST window handle (see Code Listing). It is best to use SendMessageTimeout for this rather than SendMessage because if another application is locked or crashes and cannot respond to the message, your application will also halt. The timeout version of SendMessage returns control to your program without waiting for any others to finish.

Windows 9x operates differently, and, in my opinion, correctly. Part of the PI2 structure is an Attributes flag. One of the bits of this flag indicates whether the printer is the system default. Changing this bit sets the printer as the default and automatically alerts all other applications. You can follow the algorithm described in the Sidebar along with the Code Listing to see how I’m changing this bit. Note that when you open the printer, you must request adequate permissions:

Dim PD As PRINTER_DEFAULTS, PI2 As PRINTER_INFO_2Long, _

    lNeeded As Long, hMem As Long, hPrinter As Long

PD.DesiredAccess = PRINTER_ALL_ACCESS

OpenPrinter aName, hPrinter, PD

One thing I do a bit differently is to sort of request the buffer size that GetPrinter is going to need before actually allocating it. Calling GetPrinter with a null buffer and a zero size will cause it to return the needed size in the last parameter:

GetPrinter hPrinter, 2, ByVal 0, 0, lNeeded

If lNeeded Then

    hMem = GlobalAlloc(GPTR, lNeeded)

    lSize = lNeeded

    lNeeded = 0

    If GetPrinter(hPrinter, 2, ByVal hMem, lSize, lNeeded) Then

The global memory buffer created by GetPrinter contains the PI2 structure at the beginning. The location of everything else is subject to change, but will always follow PI2. Thus you need only copy from global memory into a PI2 structure and from the pointers in the PI2 structure to whatever you need.

        CopyMemory PI2, ByVal hMem, Len(PI2)

        PI2.Attributes = PI2.Attributes Or _

            PRINTER_ATTRIBUTE_DEFAULT

        CopyMemory ByVal hMem, PI2, Len(PI2)

        SetPrinter hPrinter, 2, ByVal hMem, 0

    End If

    GlobalFree hMem

End If

This article only scratches the surface of the capabilities of the Printer API functions. You can retrieve the paper options using the DeviceCapabilities API function. You can bypass the print driver using WritePrinter. You can enumerate open print jobs using EnumJobs. However, simply retrieving and modifying the printer settings using GetPrinter and SetPrinter are one of the most frustrating things that VB programmers attempt. I hope this article not only shows you how to do that but also how to use some of the more difficult API functions that require this technique.

Code listing for this article

Back to Articles
Copyright © 1999 by Matt E. Hart, All Rights Reserved Worldwide.
Nothing on this web site may be reproduced, in any form, without express written consent.