Reading the jobs in the print queue from Visual Basic.NET

What is the print queue?

 Once you know how to get additional information about the printer you will notice that one of the bits of information returned is the Pending Jobs count. This tells you how many jobs are queued up on that printer waiting to print out.
 Of course just having a count of the number of pending jobs is only partly useful. It would be more useful to get a list of the jobs pending and some information about them, such as the document name, user name, pages printed so far etc..
The EnumPrintJobs API call provides this functionality.

<DllImport("winspool.drv", EntryPoint:="EnumJobs", _
 SetLastError:=True, CharSet:=CharSet.Ansi, _
 ExactSpelling:=False, _
 CallingConvention:=CallingConvention.StdCall)> _
 Public Function EnumJobs _
       (<InAttribute()> ByVal hPrinter As IntPtr, _
        <InAttribute()> ByVal FirstJob As Int32, _
        <InAttribute()> ByVal NumberOfJobs As Int32, _
        <InAttribute(), MarshalAs(UnmanagedType.U4)> ByVal Level As JobInfoLevels, _
        <OutAttribute()> ByVal pbOut As IntPtr, _
        <InAttribute()> ByVal cbIn As Int32, _
        <OutAttribute()> ByRef pcbNeeded As Int32, _
        <OutAttribute()> ByRef pcReturned As Int32 _
       ) As Boolean

 End Function

 This takes the handle to a printer in hPrinter that is open (see how to get additional information about the printer) and fills the buffer passed in with an array of JOB_INFO_n structures.
The Level parameter passed in tells the API which kind of structure you want returned.

Getting all the current jobs

This is significantly different from the Visual basic 6 version of this code as instead of having to write our own code to take the buffer returned from EnumJobs and turn it into the relevant JOB_INFO_n structures we can use the built in .NET functionality provided by System.Runtime.InteropServices.Marshal

To do this we need to define a STRUCTURE that matches the API declaration of JOB_INFO_1 thus:-

' Import a reference to System.Runtime.InteropServices
Imports System.Runtime.InteropServives
<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto)> _
 Friend Class JOB_INFO_1
    Public JobId As Int32
     <MarshalAs(UnmanagedType.LPStr)> Public pPrinterName As String
     <MarshalAs(UnmanagedType.LPStr)> Public pMachineName As String
     <MarshalAs(UnmanagedType.LPStr)> Public pUserName As String
     <MarshalAs(UnmanagedType.LPStr)> Public pDocument As String
     <MarshalAs(UnmanagedType.LPStr)> Public pDatatype As String
     <MarshalAs(UnmanagedType.LPStr)> Public pStatus As String
     <MarshalAs(UnmanagedType.U4)> Public Status As Int32
     Public Priority As Int32
     Public Position As Int32
     Public TotalPage As Int32
     Public PagesPrinted As Int32
     <MarshalAs(UnmanagedType.Struct)> Public Submitted As SYSTEMTIME

End Class

Now we have a structure that has enough information for the System.Runtime.InteropServices.Marshal class to populate it from a memory address (also known as a pointer). It makes sense to add a constructor for this class that takes the pointer and does just that thus:-

 Public Sub New(ByVal lpJob As IntPtr)
    Marshal.PtrToStructure(lpJob, Me)
 End Sub

So using this constructor if we pass the address of a JOB_INFO_1 the members will all be populated from it by the PtrToStructure call.

  In order to allocate the buffer needed for these JOB_INFO_n we need to know how many of them we expect to be returned. To get this information we call EnumPrintJobs with insufficient buffer and it will return a value in pcbSizeRequired to inform us how much buffer is needed:

Dim pcbNeeded As Int32 '\\ Holds the requires size of the output buffer (in bytes)
Dim pcReturned As Int32 '\\ Holds the returned size of the output buffer (in bytes)
Dim pJobInfo As IntPtr


If Not EnumJobs(mhPrinter, 0, JobCount, JobInfoLevels.JobInfoLevel1, New IntPtr(0), 0, pcbNeeded, pcReturned) Then
  If pcbNeeded > 0 Then
'\\ Allocate an unmanaged buffer of memory big enough to fit all the returned JOB_INFO_1 structures into
    pJobInfo = Marshal.AllocHGlobal(pcbNeeded)
    Dim pcbProvided As Int32 = pcbNeeded

Then we populate this unmanaged buffer by calling EnumJobs again and passing it in to be filled

  If EnumJobs(mhPrinter, 0, JobCount, JobInfoLevels.JobInfoLevel1, pJobInfo, pcbProvided, _
    pcbTotalNeeded, pcTotalReturned) Then
    If pcTotalReturned > 0 Then
      Dim item As Int32
      Dim pnextJob As IntPtr = pJobInfo
      For item = 0 To pcTotalReturned - 1
        Dim jiTemp As New JOB_INFO_1(pnextJob)
         '\\ Use the JOB_INFO_1 data here....
        pnextJob = New IntPtr(pnextJob.ToInt32 + 64)
      Next
    End If

Having got what we want from the unmanaged memory we need to free the buffer. Note that unmanaged memory is not collected by the garbage collector so you must explicitly free any memory you allocate

  Marshal.FreeHGlobal(pJobInfo)