Search

Monitoring a print queue from Visual Basic.Net

0 views

Getting a Handle to the Printer You Want to Monitor

Before you can start listening for print‑queue events you must obtain a printer handle. The Windows Print Spooler API exposes a simple function, OpenPrinter, that takes a device name and returns a HANDLE you use in all subsequent calls. On the surface the call looks like this:

Prompt
Dim printerHandle As IntPtr</p> <p>Dim result As Boolean = OpenPrinter(printerName, printerHandle, IntPtr.Zero)</p>

The printerName argument is the exact name that appears in the Devices and Printers control panel – for example "HP LaserJet 4200". If the name does not exist the function returns False and you can retrieve the error code with GetLastError. A good practice is to wrap this logic in a SafeHandle derived class so the operating system automatically frees the handle when the object is disposed.

Once you are finished monitoring, always call ClosePrinter to release the resource. Neglecting to close the handle can leave the spooler with stale references that may block other applications from accessing the printer. The pair of calls is tiny, but it is the cornerstone of every printer‑related API routine. In VB.NET you typically declare the API functions once in a module:

Prompt
Imports System.Runtime.InteropServices</p> <p>Module PrinterApi</p><DllImport("winspool.drv', SetLastError:=True, CharSet:=CharSet.Auto)> Public Function OpenPrinter(ByVal pPrinterName As String,</p> <p> ByRef phPrinter As IntPtr,</p> <p> ByVal pDefault As IntPtr) As Boolean</p> <p> End Function</p><DllImport("winspool.drv', SetLastError:=True)> Public Function ClosePrinter(ByVal hPrinter As IntPtr) As Boolean</p> <p> End Function</p> <p>End Module</p>

With the handle in hand, you are ready to describe the exact events you care about. That decision determines how much CPU time and memory the spooler will devote to your application, so be selective. The next step is to inform the spooler of the event types you wish to receive.

Specifying Which Printer Events to Watch

The Print Spooler can notify you about a handful of changes – jobs being added, deleted, paused, or completed, printer status changes, and even hardware failures. The FindFirstPrinterChangeNotification API lets you subscribe to any combination of these. Internally it accepts a flag set built from constants such as PRINTER_CHANGE_ADD_JOB and PRINTER_CHANGE_PAUSE_JOB. In VB.NET you can express the flags with a UInt32 variable:

Prompt
Const PRINTER_CHANGE_ADD_JOB As UInt32 = &H1</p> <p>Const PRINTER_CHANGE_DELETE_JOB As UInt32 = &H2</p> <p>Const PRINTER_CHANGE_PAUSE_JOB As UInt32 = &H4</p> <p>Const PRINTER_CHANGE_PRINT_JOB As UInt32 = &H8</p> <p>Const PRINTER_CHANGE_ALL As UInt32 = &HFFFF</p> <p>Dim changeMask As UInt32 = PRINTER_CHANGE_ADD_JOB Or PRINTER_CHANGE_PAUSE_JOB</p>

But the API goes further. It can return only the pieces of data you specify, such as job title, status, or number of pages. This is achieved with the PRINTER_NOTIFY_OPTIONS structure, which contains an array of PRINTER_NOTIFY_OPTIONS_TYPE entries. Each entry references a field you want back. A typical set might request the job ID, the name of the user who submitted the job, and the number of pages. The structure is passed to FindFirstPrinterChangeNotification as a pointer. In VB.NET you can model the two structures with classes marked StructLayout so the runtime marshals them automatically:

Public Class PrinterNotifyOptions

Public Flags As UInt32

Public Count As UInt32

Public Options() As PrinterNotifyOptionsType

End Class

Public Class PrinterNotifyOptionsType

Public NotifyType As UInt32

End Class

Once you build an instance of PrinterNotifyOptions and fill the Options array with the types you care about, you can hand it to the API:

Prompt
Dim notifyOpts As New PrinterNotifyOptions With {</p> <p> .Flags = 0,</p> <p> .Count = 3,</p> <p> .Options = { New PrinterNotifyOptionsType With {.NotifyType = 1}, ' JOB_ID</p> <p> New PrinterNotifyOptionsType With {.NotifyType = 2}, ' USER_NAME</p> <p> New PrinterNotifyOptionsType With {.NotifyType = 3} } ' PAGE_COUNT</p> <p>}</p> <p>Dim pNotifyOpts As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(notifyOpts))</p> <p>Marshal.StructureToPtr(notifyOpts, pNotifyOpts, False)</p>

With the mask and the options in place, the next stage is to call FindFirstPrinterChangeNotification. The function will return a handle that represents a waiting object. Under the hood the spooler creates an event that is signaled when any of the selected changes occur. The key point is that this event is a standard Windows synchronization object, so the .NET runtime can hook it into the asynchronous waiting mechanism provided by Threading.RegisteredWaitHandle

Starting the Watch and Waiting for Events

To avoid blocking the UI thread, you register the printer‑change event with the thread pool. The call looks like this:

Prompt
Dim printerEventHandle As IntPtr = FindFirstPrinterChangeNotification(</p> <p> printerHandle,</p> <p> changeMask,</p> <p> 0,</p> <p> pNotifyOpts</p> <p>)</p> <p>Dim waitHandle As New Threading.WaitHandle()</p> <p>waitHandle.SafeWaitHandle = New SafeWaitHandle(printerEventHandle, False)</p> <p>Dim registeredHandle As Threading.RegisteredWaitHandle = Threading.ThreadPool.RegisterWaitForSingleObject(</p> <p> waitHandle,</p> <p> New WaitOrTimerCallback(AddressOf PrinterNotifyCallback),</p> <p> Nothing,</p> <p> -1,</p> <p> True</p> <p>)</p>

When the spooler signals the event, the thread‑pool thread calls the PrinterNotifyCallback routine. The callback receives a state object and a Boolean indicating whether the event fired or a timeout occurred. Inside the callback you must perform the following actions:

  • Check the pdwChange value returned by FindNextPrinterChangeNotification to determine the exact type of event (e.g. PRINTER_CHANGE_ADD_JOB).
  • Retrieve the pointer to the PRINTER_NOTIFY_INFO data structure. The spooler allocates this buffer, and you free it with LocalFree after you are done.
  • Marshal the data into managed objects for easy consumption.

    The callback must be quick – the thread‑pool thread is part of the global pool, and holding it for long can starve other threads. A common pattern is to enqueue a work item or post a message to the UI thread so that heavy processing happens elsewhere.

    Handling a Notification and Extracting Job Information

    Inside PrinterNotifyCallback you first call FindNextPrinterChangeNotification with the printer handle and the same mask you used earlier. The function returns a 32‑bit flag set in pdwChange and a pointer to a PRINTER_NOTIFY_INFO buffer. The buffer contains a header followed by an array of PRINTER_NOTIFY_INFO_DATA structures, each holding one piece of data you requested.

    Prompt
    Dim changeFlags As UInt32</p> <p>Dim notifyInfoPtr As IntPtr</p> <p>Dim result As Boolean = FindNextPrinterChangeNotification(</p> <p> printerHandle,</p> <p> changeMask,</p> <p> 0,</p> <p> changeFlags,</p> <p> notifyInfoPtr</p> <p>)</p> <p>If result Then</p> <p> Dim notifyInfo As PRINTER_NOTIFY_INFO = Marshal.PtrToStructure(Of PRINTER_NOTIFY_INFO)(notifyInfoPtr)</p> <p> For i As Integer = 0 To notifyInfo.cData - 1</p> <p> Dim data As PRINTER_NOTIFY_INFO_DATA = Marshal.PtrToStructure(Of PRINTER_NOTIFY_INFO_DATA)(</p> <p> Marshal.ReadIntPtr(notifyInfoPtr, Marshal.SizeOf(notifyInfo) + i * IntPtr.Size)</p> <p> )</p> <p> ' Process data based on notifyInfo.pszDataType</p> <p> Next</p> <p> LocalFree(notifyInfoPtr)</p> <p>End If</p>

    The pszDataType field tells you which data type the current PRINTER_NOTIFY_INFO_DATA contains – job ID, job title, user name, page count, etc. You can map these numeric codes to meaningful names in a dictionary for readability. The data itself is stored in the data union. For numeric types you can read the ul field; for strings you read the psz pointer and call Marshal.PtrToStringUni to convert to a .NET string.

    For example, if you requested the job ID and user name, the loop might look like this:

    Prompt
    Dim jobId As UInt32 = 0</p> <p>Dim userName As String = ""</p> <p>For i = 0 To notifyInfo.cData - 1</p> <p> Dim infoData As PRINTER_NOTIFY_INFO_DATA = Marshal.PtrToStructure(Of PRINTER_NOTIFY_INFO_DATA)(</p> <p> Marshal.ReadIntPtr(notifyInfoPtr, Marshal.SizeOf(notifyInfo) + i * IntPtr.Size)</p> <p> )</p> <p> Select Case infoData.pszDataType</p> <p> Case 1 ' JOB_ID</p> <p> jobId = infoData.ul</p> <p> Case 2 ' USER_NAME</p> <p> userName = Marshal.PtrToStringUni(infoData.psz)</p> <p> End Select</p> <p>Next</p>

    After you have extracted the information you need, you can update a UI element, log to a file, or trigger any other business logic. Because the spooler may return multiple data blocks in a single event, you should iterate until you collect all required fields.

    Once you finish with the handle returned by FindFirstPrinterChangeNotification, call FindClosePrinterChangeNotification to let the spooler clean up its internal event object. Finally, when the application exits, release the printer handle with ClosePrinter and free any unmanaged memory you allocated for the options structure.

    With these pieces in place you have a robust, low‑overhead listener that reacts instantly to changes in the print queue. It works on Windows NT, 2000, XP and all .NET server releases, but not on legacy Windows 9x platforms. If you run into any quirks or need help tailoring the code to a specific printer model, feel free to reach out to the development team.

Suggest a Correction

Found an error or have a suggestion? Let us know and we'll review it.

Share this article

Comments (0)

Please sign in to leave a comment.

No comments yet. Be the first to comment!

Related Articles