Click here to Skip to main content
15,879,326 members
Articles / Programming Languages / Visual Basic

Monitoring a Printer Queue from VB.NET

Rate me:
Please Sign up or sign in to vote.
4.61/5 (67 votes)
2 May 2014CPOL4 min read 1.6M   18.1K   188   518
How to monitor a printer queue from Visual Basic .NET

Introduction

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 VB.NET printer handling has improved immeasurably over that offered by Visual Basic 6 and the new extensions to System.Drawing.Printing have also helped, 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 by the ClosePrinter API call when it is no longer needed.

VB.NET
<DllImport("winspool.drv", EntryPoint:="OpenPrinter", _
SetLastError:=True, CharSet:=CharSet.Unicode, _
ExactSpelling:=False, _
CallingConvention:=CallingConvention.StdCall)> _
    Public Function OpenPrinter(<InAttribute()> ByVal pPrinterName As String, _
                            <OutAttribute()> ByRef phPrinter As IntPtr, _
                               <InAttribute(), MarshalAs(UnmanagedType.LPStruct)> ByVal pDefault As PRINTER_DEFAULTS _
                               ) As Boolean

    End Function

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

    End Function

Ask for the Notifications You are Interested in

To minimise the impact of a printer watch on 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:

VB.NET
Public Enum Printer_Change_Notification_General_Flags 
    PRINTER_CHANGE_FORM = &H70000 
    PRINTER_CHANGE_PORT = &H700000 
    PRINTER_CHANGE_JOB = &HFF00 
    PRINTER_CHANGE_PRINTER = &HFF 
    PRINTER_CHANGE_PRINT_PROCESSOR = &H7000000 
    PRINTER_CHANGE_PRINTER_DRIVER = &H70000000 
End Enum 

Public Enum Printer_Change_Notification_Form_Flags 
    PRINTER_CHANGE_ADD_FORM = &H10000 
    PRINTER_CHANGE_SET_FORM = &H20000 
    PRINTER_CHANGE_DELETE_FORM = &H40000 
End Enum 

Public Enum Printer_Change_Notification_Port_Flags 
    PRINTER_CHANGE_ADD_PORT = &H100000 
    PRINTER_CHANGE_CONFIGURE_PORT = &H200000 
    PRINTER_CHANGE_DELETE_PORT = &H400000 
End Enum 

Public Enum Printer_Change_Notification_Job_Flags 
    PRINTER_CHANGE_ADD_JOB = &H100 
    PRINTER_CHANGE_SET_JOB = &H200 
    PRINTER_CHANGE_DELETE_JOB = &H400 
    PRINTER_CHANGE_WRITE_JOB = &H800 
End Enum 

Public Enum Printer_Change_Notification_Printer_Flags 
    PRINTER_CHANGE_ADD_PRINTER = &H1 
    PRINTER_CHANGE_SET_PRINTER = &H2 
    PRINTER_CHANGE_DELETE_PRINTER = &H4 
    PRINTER_CHANGE_FAILED_CONNECTION_PRINTER = &H8 
End Enum 

Public Enum Printer_Change_Notification_Processor_Flags 
    PRINTER_CHANGE_ADD_PRINT_PROCESSOR = &H1000000 
    PRINTER_CHANGE_DELETE_PRINT_PROCESSOR = &H4000000 
End Enum 

Public Enum Printer_Change_Notification_Driver_Flags 
    PRINTER_CHANGE_ADD_PRINTER_DRIVER = &H10000000 
    PRINTER_CHANGE_SET_PRINTER_DRIVER = &H20000000 
    PRINTER_CHANGE_DELETE_PRINTER_DRIVER = &H40000000
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:

VB.NET
 Public Enum Job_Notify_Field_Indexes 
    JOB_NOTIFY_FIELD_PRINTER_NAME = &H0 
    JOB_NOTIFY_FIELD_MACHINE_NAME = &H1 
    JOB_NOTIFY_FIELD_PORT_NAME = &H2 
    JOB_NOTIFY_FIELD_USER_NAME = &H3 
    JOB_NOTIFY_FIELD_NOTIFY_NAME = &H4 
    JOB_NOTIFY_FIELD_DATATYPE = &H5 
    JOB_NOTIFY_FIELD_PRINT_PROCESSOR = &H6 
    JOB_NOTIFY_FIELD_PARAMETERS = &H7 
    JOB_NOTIFY_FIELD_DRIVER_NAME = &H8 
    JOB_NOTIFY_FIELD_DEVMODE = &H9 
    JOB_NOTIFY_FIELD_STATUS = &HA 
    JOB_NOTIFY_FIELD_STATUS_STRING = &HB 
    JOB_NOTIFY_FIELD_SECURITY_DESCRIPTOR = &HC 
    JOB_NOTIFY_FIELD_DOCUMENT = &HD 
    JOB_NOTIFY_FIELD_PRIORITY = &HE 
    JOB_NOTIFY_FIELD_POSITION = &HF 
    JOB_NOTIFY_FIELD_SUBMITTED = &H10 
    JOB_NOTIFY_FIELD_START_TIME = &H11 
    JOB_NOTIFY_FIELD_UNTIL_TIME = &H12 
    JOB_NOTIFY_FIELD_TIME = &H13 
    JOB_NOTIFY_FIELD_TOTAL_PAGES = &H14 
    JOB_NOTIFY_FIELD_PAGES_PRINTED = &H15 
    JOB_NOTIFY_FIELD_TOTAL_BYTES = &H16 
    JOB_NOTIFY_FIELD_BYTES_PRINTED = &H17 
End Enum 

To inform the print spooler that you want information on these fields, you create a PRINTER_NOTIFY_OPTIONS structure that 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 that can be passed to the API, by being marshaled as if they were structures:

VB.NET
<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 
            Marshal.FreeHGlobal(pFields) 
        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
  
End Class   

Starting the Watch

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

VB.NET
<DllImport("winspool.drv", EntryPoint:="FindFirstPrinterChangeNotification", _
 SetLastError:=True, CharSet:=CharSet.Unicode, _
 ExactSpelling:=False, _
 CallingConvention:=CallingConvention.StdCall)> _
Public Function FindFirstPrinterChangeNotification _
                (<InAttribute()> ByVal hPrinter As IntPtr, _
                 <InAttribute()> ByVal fwFlags As Int32, _
                 <InAttribute()> ByVal fwOptions As Int32, _
                 <InAttribute(), MarshalAs(UnmanagedType.LPStruct)> ByVal pPrinterNotifyOptions As PrinterNotifyOptions _
                    ) As Microsoft.Win32.SafeHandles.SafeWaitHandle

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. Thus, when the program was waiting for the printer notification, it was effectively locked up. In Visual Basic .NET, this is no longer necessary because it supports asynchronous events and threading.

The FindFirstPrinterChangeNotification API call returns a Windows synchronization wait handle. This can be used by the VB.NET Common Language Runtime to trigger a particular subroutine whenever that synchronisation object is signalled. This is done with the Threading.RegisteredWaitHandle object:

VB.NET
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)

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

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

Getting Information About the Event that Occurred

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

VB.NET
<DllImport("winspool.drv", EntryPoint:="FindNextPrinterChangeNotification", _
SetLastError:=True, CharSet:=CharSet.Unicode, _
ExactSpelling:=False, _
CallingConvention:=CallingConvention.StdCall)> _
    Public Function FindNextPrinterChangeNotification _
                        (<InAttribute()> ByVal hChangeObject As Microsoft.Win32.SafeHandles.SafeWaitHandle, _
                         <OutAttribute()> ByRef pdwChange As Int32, _
                         <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 occurred. 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:

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

You can populate these classes from a pointer, using Marshal.PtrToStructure:

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

Using the code

The printer monitor code attached is implemented as a user control. This means you must compile the control project before Visual Studio can use it in the form designer or toolbox.

History

  • 16 Aug 2006
    • Release 2.0.3 of the component. Allows monitoring multiple printers and gives more detail on the print job events raised. The code is migrated to .NET 2.0.
  • 6 Nov 2006
    • Added new status TonerLow and properties Colour, Collate and PrintQuality to the PrinterInformation class
  • 15 August 2007
    • Updated source code: now uses the SafeWaitHandle class
  • 29 May 2009
    • New source code which is converted to .NET 2 and also uses the Unicode versions of the API calls for international support
  • 02 May 2014
    • Converted all code to be 32 and 64 bit compatible, and added a lot of new printer api calls

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Ireland Ireland
C# / SQL Server developer
Microsoft MVP (Azure) 2017
Microsoft MVP (Visual Basic) 2006, 2007

Comments and Discussions

 
QuestionBlock Job Pin
Bessax28-Jan-20 1:51
Bessax28-Jan-20 1:51 
QuestionForm is empty, I don't see any notification log window it's empty Pin
johnnycp19-Feb-19 23:35
johnnycp19-Feb-19 23:35 
BugUsePrinterOffline Pin
Member 1257133412-Aug-18 21:25
Member 1257133412-Aug-18 21:25 
QuestionWin32exception (and latest source version?) Pin
Member 1328507614-Sep-17 5:32
Member 1328507614-Sep-17 5:32 
AnswerRe: Win32exception (and latest source version?) Pin
Duncan Edwards Jones14-Sep-17 21:42
professionalDuncan Edwards Jones14-Sep-17 21:42 
QuestionGeting port info on job event on "enable printer pooling" active Pin
cristovaoOliveira20-Jul-17 0:53
cristovaoOliveira20-Jul-17 0:53 
AnswerRe: Geting port info on job event on "enable printer pooling" active Pin
Duncan Edwards Jones20-Jul-17 1:27
professionalDuncan Edwards Jones20-Jul-17 1:27 
GeneralRe: Geting port info on job event on "enable printer pooling" active Pin
cristovaoOliveira20-Jul-17 3:49
cristovaoOliveira20-Jul-17 3:49 
GeneralRe: Geting port info on job event on "enable printer pooling" active Pin
cristovaoOliveira20-Jul-17 5:48
cristovaoOliveira20-Jul-17 5:48 
PraiseRe: Geting port info on job event on "enable printer pooling" active Pin
Duncan Edwards Jones20-Jul-17 6:24
professionalDuncan Edwards Jones20-Jul-17 6:24 
QuestionComplete example Pin
Member 1271139711-Jul-17 14:44
Member 1271139711-Jul-17 14:44 
Bug"Wrong parameter" in InitJobInfo for ji1 and ji2 Pin
tweber20123-Jul-17 2:29
tweber20123-Jul-17 2:29 
GeneralRe: "Wrong parameter" in InitJobInfo for ji1 and ji2 Pin
Duncan Edwards Jones3-Jul-17 5:30
professionalDuncan Edwards Jones3-Jul-17 5:30 
GeneralRe: "Wrong parameter" in InitJobInfo for ji1 and ji2 Pin
tweber20126-Oct-17 9:54
tweber20126-Oct-17 9:54 
GeneralRe: "Wrong parameter" in InitJobInfo for ji1 and ji2 Pin
Duncan Edwards Jones6-Oct-17 23:42
professionalDuncan Edwards Jones6-Oct-17 23:42 
Can you try with the latest code from GitHub?

(That way at least we are working from the same source)
QuestionCan we also fetch raw data which is getting printed? Pin
chanonly12330-Oct-16 23:23
chanonly12330-Oct-16 23:23 
AnswerRe: Can we also fetch raw data which is getting printed? Pin
Duncan Edwards Jones31-Oct-16 0:14
professionalDuncan Edwards Jones31-Oct-16 0:14 
QuestionJobAdded and JobDeleted event is not fired Pin
santosh d7-Jun-16 4:21
santosh d7-Jun-16 4:21 
AnswerRe: JobAdded and JobDeleted event is not fired Pin
Duncan Edwards Jones7-Jun-16 4:37
professionalDuncan Edwards Jones7-Jun-16 4:37 
GeneralRe: JobAdded and JobDeleted event is not fired Pin
jose olvera9-Jun-16 6:24
jose olvera9-Jun-16 6:24 
GeneralRe: JobAdded and JobDeleted event is not fired Pin
santosh d9-Jun-16 23:29
santosh d9-Jun-16 23:29 
GeneralRe: JobAdded and JobDeleted event is not fired Pin
Mohammad Esfandiary Bayat12-Jul-16 10:57
Mohammad Esfandiary Bayat12-Jul-16 10:57 
QuestionError in project Pin
Member 1108038429-Oct-15 5:29
Member 1108038429-Oct-15 5:29 
QuestionJobAdded event is not fired Pin
Member 104897923-Feb-15 2:02
professionalMember 104897923-Feb-15 2:02 
AnswerRe: JobAdded event is not fired Pin
Duncan Edwards Jones3-Feb-15 2:50
professionalDuncan Edwards Jones3-Feb-15 2:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.