Monitoring a print queue from Visual Basic

Note: The API calls used in this example are only supported on Windows NT, 2000, XP and .Net server therefore this technique does not apply to windows 95, Windows 98 or Windows Me

Get a handle to the printer you want to monitor

As with any operation involving the printer API, the first step is to get a handle to the printer. You do this by passing the printer device name to the OpenPrinter API call and you must remeber to release this handle with closeprinter when you are done with it.

Private Declare Function OpenPrinter Lib "winspool.drv" _
    Alias "OpenPrinterA" (ByVal pPrinterName As String, _
    phPrinter As Long, pDefault As PRINTER_DEFAULTS) As Long

Private Declare Function ClosePrinter Lib "winspool.drv" _
    (ByVal hPrinter As Long) As Long

See the Getting the status of the selected printer from Visual Basic article for more details about this.

Ask for the notifications you are interested in

There are a vast number of events that can happen to a printer or to a print job. You can request notification whenever one or more of them happens by creating a PRINTER_NOTIFY_OPTIONS variable which you pass to the initial request to set up a notification object using the FindFirstPrinterChangeNotification API call.

The types of printer notification are:

Public Enum PrinterChangeNotifications
End Enum

And the types of print job notification are:

Public Enum JobChangeNotificationFields
End Enum

However in order to prevent unnecessary notifications which would potentially slow down your system, the notification events only trigger for the events which you have set them to monitor. You do this by creating a PRINTER_NOTIFY_OPTIONS_TYPE record for the printer events you need and one for the job events you need and putting these in a PRINTER_NOTIFY_OPTIONS variable that we pass to the notification API calls. For example, if we wish to be notified when the printer name, share name and status change or when a job is printed we woud do so thus:

'\\ Declarations
    Version As Long '\\should be set to 2
    Flags As Long
    Count As Long
    lpPrintNotifyOptions As Long
End Type

    Type As Integer
    Reserved_0 As Integer
    Reserved_1 As Long
    Reserved_2 As Long
    Count As Long
    pFields As Long
End Type

Private PrinterNotifyOptions(0 To 1) As PRINTER_NOTIFY_OPTIONS_TYPE

'\\ Initialising the PrintOptions
Private Sub InitialiseNotifyOptions

With PrintOptions
  .Version = 2 '\\ This must be set to 2
  .Count = 2 '\\ There is job notification and printer notification
  '\\ The type of printer events we are interested in...
  With PrinterNotifyOptions(0)
    ReDim pFieldsPrinter(0 To 19) As Integer
    '\\ Add the list of printer events you are interested in being notified about
    '\\ to this list. Note that the fewer notifications you ask for the less of a
    '\\ burden your app place upon the system.

    .Count = (UBound(pFieldsPrinter) - LBound(pFieldsPrinter)) + 1 '\\ Add one as the array is zero based
    .pFields = VarPtr(pFieldsPrinter(0))
  End With
  '\\ The type of print job events we are interested in...
  With PrinterNotifyOptions(1)
    '\\ Add the list of print job events you are interested in being notified about
    '\\ to this list. Note that the fewer notifications you ask for the less of a
    '\\ burden your app place upon the system.

    ReDim pFieldsJob(0 To 22) As Integer
    pFieldsJob(10) = JOB_NOTIFY_FIELD_STATUS
    pFieldsJob(18) = JOB_NOTIFY_FIELD_TIME
    .Count = (UBound(pFieldsJob) - LBound(pFieldsJob)) + 1 '\\ Add one as the array is zero based
    .pFields = VarPtr(pFieldsJob(0))
  End With
  .lpPrintNotifyOptions = VarPtr(PrinterNotifyOptions(0))
End With

End Sub

Starting the watch

You start watching the printer queue by passing the printer handle and notify options to the FindFirstPrinterChangeNotification API function:

'\\ Declaration
Public Declare Function FindFirstPrinterChangeNotificationLong Lib "winspool.drv" Alias "FindFirstPrinterChangeNotification" _
  (ByVal hPrinter As Long, ByVal fdwFlags As Long, ByVal fdwOptions As Long, ByVal lpPrinterNotifyOptions As Long) As Long
'\\ Use
mEventHandle = FindFirstPrinterChangeNotificationLong(mhPrinter, 0, 0, VarPtr(PrintOptions))

This returns a printer watch handle that uniquely identifies this printer notification object and is a waitable object. To wait for a printer event to occur you can use the WaitForSingleObject API call.

'\\ Declaration
Declare Function WaitForSingleObject Lib "kernel32" (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long
Const INFINITE = &HFFFF ' Infinite timeout
'\\ Use
Call WaitForSingleObject(mEventHandle, INFINITE)
'\\ Code will only get to here after a printer event occurs

This will wait forever, unless a printer event occurs. Note that because Visual Basic is a single threaded application no user interface updates can take place while the wait is active - if you are putting this code in an application that has a user interface you might do well to put that in a totally seperate application and use registered windows messages to communicate between them.

Getting the information about the event that occured

Once your code gets past the WaitForSingleObject line you know that a printer event has occured. However you also need to find out the information about the notification and also to get ready to wait again for the next printer event. This is done with the FindNextPrinterChangeNotification API call:

'\\ Declaration
Declare Function FindNextPrinterChangeNotificationByLong Lib "winspool.drv" Alias "FindNextPrinterChangeNotification" _
    (ByVal hChange As Long, pdwChange As Long, pPrinterOptions As PRINTER_NOTIFY_OPTIONS, ppPrinterNotifyInfo As Long) As Long
'\\ Use
Dim lpPrintInfoBuffer As Long
Call FindNextPrinterChangeNotificationByLong(mEventHandle, pdwChange, PrintOptions, lpPrintInfoBuffer)

This resets the printer notification and returns a pointer to an area of memory, allocated by the system, that contains the detail of that change event. You need to get the detail from this pointer and then ask the system to release the memory used.

'\\ Declarations
  Type As Integer
  Field As Integer
  Reserved As Long
  id As Long
  adwData(0 To 1) As Long
End Type

  dwVersion As Long
  dwFlags As Long
  dwCount As Long
End Type

Declare Function FreePrinterNotifyInfoByLong Lib "winspool.drv" Alias "FreePrinterNotifyInfo" (ByVal pInfo As Long) As Long

Declare Sub CopyMemoryPRINTER_NOTIFY_INFO Lib "kernel32" Alias "RtlMoveMemory" (Destination As PRINTER_NOTIFY_INFO, ByVal Source As Long, ByVal Length As Long)
Declare Sub CopyMemoryPRINTER_NOTIFY_INFO_DATA Lib "kernel32" Alias "RtlMoveMemory" (Destination As PRINTER_NOTIFY_INFO_DATA, ByVal Source As Long, ByVal Length As Long)

'\\ Use
Call CopyMemoryPRINTER_NOTIFY_INFO(mData, lpPrintInfoBuffer, Len(mData))
'\\ mData contains a valid PRINTER_NOTIFY_INFO structure
If mData.dwCount > 0 Then
  ReDim aData(1 To mData.dwCount) As PRINTER_NOTIFY_INFO_DATA
  '\\ Copy the structure in full
  Call CopyMemoryPRINTER_NOTIFY_INFO_DATA(aData(1), lpPrintInfoBuffer + Len(mData), Len(aData(1)) * mData.dwCount)
  '\\ Operate on the changes

  '\\ and clear out the buffer
  Erase aData
  Call FreePrinterNotifyInfoByLong(lpPrintInfoBuffer)
End If

Once you have the data you call WaitForSingleObject to wait for the next event

Cleaning up when you finish

When you close the program that has been watching th eprinter you need to release the printer notification object. You do this by passing it to the FindClosePrinterChangeNotification API call:

'\\ Declarations
Declare Function FindClosePrinterChangeNotification Lib "winspool.drv" (ByVal hChange As Long) As Long
'\\ Use
Call FindClosePrinterChangeNotification(mEventHandle)