Monitoring a print queue from Visual Basic.Net

Note: Some of 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

Although the VB.Net printer handling has improved immeasurably over that offered by Visual Basic 6 there is still a need to turn to the Windows API in order to monitor a print queue.

Get a handle to the printer you want to monitor

All of the API calls that access the printer or spooler need a printer handle. This is obtained by passing the unique printer device name to the OpenPrinter API call and must be released when no longer needed, by the ClosePrinter API call.

<DllImport("winspool.drv", EntryPoint:="OpenPrinterA", _
  SetLastError:=True, CharSet:=CharSet.Ansi, _
  ExactSpelling:=True, _
  CallingConvention:=CallingConvention.StdCall)> _
  Public Shared Function OpenPrinter(ByVal pPrinterName As String, _
        ByRef phPrinter As Int32, _
        ByVal pDefault As Int32 _
        ) As Boolean

  End Function

<DllImport("winspool.drv", EntryPoint:="ClosePrinter", _
  SetLastError:=True, _
  ExactSpelling:=True, _
  CallingConvention:=CallingConvention.StdCall)> _
  Public Shared Function ClosePrinter(ByVal hPrinter As Int32) As Boolean

  End Function

Ask for the notifications you are interested in

To minimise the impact of a printer watch on the system performance we can specify precisely which printer events we are interested in. This is done by passing a parameter to FindFirstPrinterChangeNotification using one or more of the following values:

  Public Enum Printer_Change_Notification_General_Flags
  End Enum

  Public Enum Printer_Change_Notification_Form_Flags
  End Enum

  Public Enum Printer_Change_Notification_Port_Flags
  End Enum

  Public Enum Printer_Change_Notification_Job_Flags
  End Enum

  Public Enum Printer_Change_Notification_Printer_Flags
  End Enum

  Public Enum Printer_Change_Notification_Processor_Flags
  End Enum

  Public Enum Printer_Change_Notification_Driver_Flags
  End Enum

Specify the information you want returned for the event

When an event occurs, for example if a job is added to the print queue, you will probably want to get information about the job that caused that event. Again, in order to minimise the impact on the system you specify exactly which fields you want information from.

For a print job event the possible fields are:

  Public Enum Job_Notify_Field_Indexes
  End Enum

To inform the print spooler that you want information on these fields you create a PRINTER_NOTIFY_OPTIONS structure which is passed to FindFirstPrinterChangeNotification and which holds a pointer to an array of PRINTER_NOTIFY_OPTIONS_TYPE, one for each of the above fields that you require. These structures are documented on MSDN

In VB.Net it is easy to translate these structures into classes which can be passed to the API by being marshalled as if they were structures:

<StructLayout(LayoutKind.Sequential)> _
Public Class PrinterNotifyOptionsType
  Public wType As Int16
  Public wReserved0 As Int16
  Public dwReserved1 As Int32
  Public dwReserved2 As Int32
  Public FieldCount As Int32
  Public pFields As IntPtr

  Private Sub SetupFields()

    '\\ Free up the global memory
    If pFields.ToInt32 <> 0 Then
    End If

    If wType = Printer_Notification_Types.JOB_NOTIFY_TYPE Then
      FieldCount = JOB_FIELDS_COUNT
      pFields = Marshal.AllocHGlobal((JOB_FIELDS_COUNT * 2) - 1)
      '\\ Put the field indexes in the unmanaged array
      Marshal.WriteInt16(pFields, 0, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_PRINTER_NAME))
      Marshal.WriteInt16(pFields, 2, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_MACHINE_NAME))
      Marshal.WriteInt16(pFields, 4, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_PORT_NAME))
      Marshal.WriteInt16(pFields, 6, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_USER_NAME))
      Marshal.WriteInt16(pFields, 8, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_NOTIFY_NAME))
      Marshal.WriteInt16(pFields, 10, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_DATATYPE))
      Marshal.WriteInt16(pFields, 12, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_PRINT_PROCESSOR))
      Marshal.WriteInt16(pFields, 14, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_PARAMETERS))
      Marshal.WriteInt16(pFields, 16, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_DRIVER_NAME))
      Marshal.WriteInt16(pFields, 18, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_DEVMODE))
      Marshal.WriteInt16(pFields, 20, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_STATUS))
      Marshal.WriteInt16(pFields, 22, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_STATUS_STRING))
      Marshal.WriteInt16(pFields, 24, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_SECURITY_DESCRIPTOR))
      Marshal.WriteInt16(pFields, 26, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_DOCUMENT))
      Marshal.WriteInt16(pFields, 28, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_PRIORITY))
      Marshal.WriteInt16(pFields, 30, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_POSITION))
      Marshal.WriteInt16(pFields, 32, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_SUBMITTED))
      Marshal.WriteInt16(pFields, 34, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_START_TIME))
      Marshal.WriteInt16(pFields, 36, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_UNTIL_TIME))
      Marshal.WriteInt16(pFields, 38, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_TIME))
      Marshal.WriteInt16(pFields, 40, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_TOTAL_PAGES))
      Marshal.WriteInt16(pFields, 42, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_PAGES_PRINTED))
      Marshal.WriteInt16(pFields, 44, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_TOTAL_BYTES))
      Marshal.WriteInt16(pFields, 46, CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_BYTES_PRINTED))
    End If

  End Sub

  Public Sub New(ByVal value As Printer_Notification_Types)

    wType = value
    Call SetupFields()

  End Sub

Starting the watch

To start the printer watch you need to pass the printer handle to FindFirstPrinterChangeNotification:

  <DllImport("winspool.drv", EntryPoint:="FindFirstPrinterChangeNotification", _
  SetLastError:=True, CharSet:=CharSet.Ansi, _
  ExactSpelling:=True, _
  CallingConvention:=CallingConvention.StdCall)> _
  Public Shared Function FindFirstPrinterChangeNotification _
      (<InAttribute()> ByVal hPrinter As Int32, _
      <InAttribute()> ByVal fwFlags As Int32, _
      <InAttribute()> ByVal fwOptions As Int32, _
      <InAttribute(), MarshalAs(UnmanagedType.LPStruct)> ByVal pPrinterNotifyOptions As PrinterNotifyOptions _
      ) As Int32

  End Function

Waiting for a notification

In the Visual Basic 6 implementation of this a great deal of complexity was added by the fact that it is a single threaded system and so when the program was waiting for the printer notification it was effectively locked up. In Visual Basic .Net this is no longer neccessary as it supports asynchronous events and threading.

The FindFirstPrinterChangeNotification API call returns a Windows synchronisation wait handle. This can be used by the VB.Net common language runtime to trigger a particular subroutine whenever that synchronistation object is signalled. This is done with the Threading.RegisteredWaitHandle object:

  Private Shared _mhPrinterChangeNotification As RegisteredWaitHandle

  Dim wh As New ManualResetEvent(False)
  wh.Handle = mhWait
  _mhPrinterChangeNotification = ThreadPool.RegisterWaitForSingleObject(wh, New WaitOrTimerCallback(AddressOf PrinterNotifyWaitCallback), wh, -1, True)

Where PrinterNotifyWaitCallback is a public subroutine that has the correct signature for a WaitOrTimerCallback:

  Public Sub PrinterNotifyWaitCallback( _
    ByVal state As Object, _
    ByVal timedOut As Boolean)

Getting the information about the event that occured

When that wait object is triggered you have to call FindNextPrinterChangeNotification to find out what event triggered it and get the details.

  <DllImport("winspool.drv", EntryPoint:="FindNextPrinterChangeNotification", _
  SetLastError:=True, CharSet:=CharSet.Ansi, _
  ExactSpelling:=True, _
  CallingConvention:=CallingConvention.StdCall)> _
  Public Shared Function FindNextPrinterChangeNotification _
      (<InAttribute()> ByVal hChangeObject As Int32, _
      <OutAttribute()> ByRef pdwChange As IntPtr, _
      <InAttribute(), MarshalAs(UnmanagedType.LPStruct)> ByVal pPrinterNotifyOptions As PrinterNotifyOptions, _
       <OutAttribute()> ByRef lppPrinterNotifyInfo As IntPtr _
      ) As Boolean

  End Function

This returns a 32 bit number in pdwChange that indicates what event has occured. For example this will contain PRINTER_CHANGE_ADD_JOB when a job is added to the print queue. Additionally it returns a pointer to data allocated by the spooler in lppPrinterNotifyInfo which contains a PRINTER_NOTIFY_INFO structure followed by an array of PRINTER_NOTIFY_INFO_DATA structures. Again in VB.Net these can be represented by classes:

<StructLayout(LayoutKind.Sequential)> _
  Public Version As Int32
  Public Flags As Int32
  Public Count As Int32
End Class

And you can populate these classes from a pointer using the Marshal.PtrToStructure:

Private msInfo As New PRINTER_NOTIFY_INFO()
'\\ Read the data of this printer notification event
Marshal.PtrToStructure(lpAddress, msInfo)