Introduction
This article demonstrates the ability to monitor a print queue on the local machine.
Background
There is a lot of code available in the internet (CodeProject as well as other sites) about the API calls to do print spool monitoring. However, I was not able to find any code that would help the user to enable monitoring one or more print queues with minimal code changes. So, I created a class (PrintQueueMonitor
) that would allow the user to monitor the print queue for job changes and raise an event as and when jobs get added to the queue or job status gets changed in the queue.
Using the code
The attached zip file contains two projects:
- PrintQueueMonitor
- PrintSpooler
The PrintQueueMonitor project has two main files:
- PrintSpoolAPI.cs
- PrintQueueMonitor.cs
PrintSpoolAPI.cs contains the data types and structures used by PrintQueueMonitor. This API was not created by me, and I pulled this from the internet (not sure of the location):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Globalization;
namespace Atom8.API.PrintSpool
{
public enum JOBCONTROL
......
......
}
PrintQueueMonitor.cs contains two classes - PrintJobChangeEventArgs
and PrintQueueMonitor
, and a delegate PrintJobStatusChanged
.
The PrintJobChangeEventArgs
is inherited from EventArgs
. This object is passed while raising the event for job status addition or change in status or deletion.
public class PrintJobChangeEventArgs : EventArgs
{
#region private variables
private int _jobID=0;
private string _jobName = "";
private JOBSTATUS _jobStatus = new JOBSTATUS();
private PrintSystemJobInfo _jobInfo = null;
#endregion
public int JobID { get { return _jobID;} }
public string JobName { get { return _jobName; } }
public JOBSTATUS JobStatus { get { return _jobStatus; } }
public PrintSystemJobInfo JobInfo { get { return _jobInfo; } }
public PrintJobChangeEventArgs(int intJobID, string strJobName,
JOBSTATUS jStatus, PrintSystemJobInfo objJobInfo )
: base()
{
_jobID = intJobID;
_jobName = strJobName;
_jobStatus = jStatus;
_jobInfo = objJobInfo;
}
}
The delegate PrintJobStatusChanged
is the type of event raised when the PrintQueueMonitor
class needs to send job status change notifications to the owner.
public delegate void PrintJobStatusChanged(object Sender,
PrintJobChangeEventArgs e);
The PrintQueueMonitor
class imports Win32 API functions, registers with the Win32 Spooler for change notifications, and waits for notificaitons. Upon receipt of notification, it checks to see if the notification is relevant to a job, and if so, raises the OnJobStatusChange()
event with the appropriate parameters:
public class PrintQueueMonitor
{
#region DLL Import Functions
[DllImport("winspool.drv",
EntryPoint = "OpenPrinterA",SetLastError = true,
CharSet = CharSet.Ansi,ExactSpelling = true,
CallingConvention = CallingConvention.StdCall)]
public static extern bool OpenPrinter(String pPrinterName,
out IntPtr phPrinter,
Int32 pDefault);
[DllImport("winspool.drv",
EntryPoint = "ClosePrinter",
SetLastError = true,
ExactSpelling = true,
CallingConvention = CallingConvention.StdCall)]
public static extern bool ClosePrinter
(Int32 hPrinter);
[DllImport("winspool.drv",
EntryPoint = "FindFirstPrinterChangeNotification",
SetLastError = true, CharSet = CharSet.Ansi,
ExactSpelling = true,
CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr FindFirstPrinterChangeNotification
([InAttribute()] IntPtr hPrinter,
[InAttribute()] Int32 fwFlags,
[InAttribute()] Int32 fwOptions,
[InAttribute(),
MarshalAs(UnmanagedType.LPStruct)]
PRINTER_NOTIFY_OPTIONS pPrinterNotifyOptions);
[DllImport("winspool.drv", EntryPoint =
"FindNextPrinterChangeNotification",
SetLastError = true, CharSet = CharSet.Ansi,
ExactSpelling = false,
CallingConvention = CallingConvention.StdCall)]
public static extern bool FindNextPrinterChangeNotification
([InAttribute()] IntPtr hChangeObject,
[OutAttribute()] out Int32 pdwChange,
[InAttribute(),
MarshalAs(UnmanagedType.LPStruct)]
PRINTER_NOTIFY_OPTIONS pPrinterNotifyOptions,
[OutAttribute()] out IntPtr lppPrinterNotifyInfo
);
#endregion
#region Constants
const int PRINTER_NOTIFY_OPTIONS_REFRESH = 1;
#endregion
#region Events
public event PrintJobStatusChanged OnJobStatusChange;
#endregion
#region private variables
private IntPtr _printerHandle = IntPtr.Zero;
private string _spoolerName = "";
private ManualResetEvent _mrEvent = new ManualResetEvent(false);
private RegisteredWaitHandle _waitHandle = null;
private IntPtr _changeHandle = IntPtr.Zero;
private PRINTER_NOTIFY_OPTIONS _notifyOptions =
new PRINTER_NOTIFY_OPTIONS();
private Dictionary<int, string> objJobDict =
new Dictionary<int, string>();
private PrintQueue _spooler = null;
#endregion
#region constructor
public PrintQueueMonitor(string strSpoolName)
{
_spoolerName = strSpoolName;
Start();
}
#endregion
#region destructor
~PrintQueueMonitor()
{
Stop();
}
#endregion
#region StartMonitoring
public void Start()
{
OpenPrinter(_spoolerName, out _printerHandle, 0);
if (_printerHandle != IntPtr.Zero)
{
_changeHandle = FindFirstPrinterChangeNotification(
_printerHandle, (int)PRINTER_CHANGES.PRINTER_CHANGE_JOB, 0,
_notifyOptions);
_mrEvent.Handle = _changeHandle;
_waitHandle = ThreadPool.RegisterWaitForSingleObject(_mrEvent,
new WaitOrTimerCallback(PrinterNotifyWaitCallback),
_mrEvent, -1, true);
}
_spooler = new PrintQueue(new PrintServer(), _spoolerName);
foreach (PrintSystemJobInfo psi in _spooler.GetPrintJobInfoCollection())
{
objJobDict[psi.JobIdentifier] = psi.Name;
}
}
#endregion
#region StopMonitoring
public void Stop()
{
if (_printerHandle != IntPtr.Zero)
{
ClosePrinter((int)_printerHandle);
_printerHandle = IntPtr.Zero;
}
}
#endregion
#region Callback Function
public void PrinterNotifyWaitCallback(Object state,bool timedOut)
{
if (_printerHandle == IntPtr.Zero) return;
#region read notification details
_notifyOptions.Count = 1;
int pdwChange = 0;
IntPtr pNotifyInfo = IntPtr.Zero;
bool bResult = FindNextPrinterChangeNotification(_changeHandle,
out pdwChange, _notifyOptions, out pNotifyInfo);
if ((bResult == false) || (((int)pNotifyInfo) == 0)) return;
bool bJobRelatedChange =
((pdwChange & PRINTER_CHANGES.PRINTER_CHANGE_ADD_JOB) ==
PRINTER_CHANGES.PRINTER_CHANGE_ADD_JOB) ||
((pdwChange & PRINTER_CHANGES.PRINTER_CHANGE_SET_JOB) ==
PRINTER_CHANGES.PRINTER_CHANGE_SET_JOB) ||
((pdwChange & PRINTER_CHANGES.PRINTER_CHANGE_DELETE_JOB) ==
PRINTER_CHANGES.PRINTER_CHANGE_DELETE_JOB) ||
((pdwChange & PRINTER_CHANGES.PRINTER_CHANGE_WRITE_JOB) ==
PRINTER_CHANGES.PRINTER_CHANGE_WRITE_JOB);
if (!bJobRelatedChange) return;
#endregion
#region populate Notification Information
PRINTER_NOTIFY_INFO info =
(PRINTER_NOTIFY_INFO)Marshal.PtrToStructure(pNotifyInfo,
typeof(PRINTER_NOTIFY_INFO));
int pData = (int)pNotifyInfo +
Marshal.SizeOf(typeof(PRINTER_NOTIFY_INFO));
PRINTER_NOTIFY_INFO_DATA[] data =
new PRINTER_NOTIFY_INFO_DATA[info.Count];
for (uint i = 0; i < info.Count; i++)
{
data[i] = (PRINTER_NOTIFY_INFO_DATA)Marshal.PtrToStructure(
(IntPtr)pData, typeof(PRINTER_NOTIFY_INFO_DATA));
pData += Marshal.SizeOf(typeof(PRINTER_NOTIFY_INFO_DATA));
}
#endregion
#region iterate through all elements in the data array
for (int i = 0; i < data.Count(); i++)
{
if ( (data[i].Field ==
(ushort)PRINTERJOBNOTIFICATIONTYPES.JOB_NOTIFY_FIELD_STATUS) &&
(data[i].Type == (ushort)PRINTERNOTIFICATIONTYPES.JOB_NOTIFY_TYPE)
)
{
JOBSTATUS jStatus = (JOBSTATUS)Enum.Parse(typeof(JOBSTATUS),
data[i].NotifyData.Data.cbBuf.ToString());
int intJobID = (int)data[i].Id;
string strJobName = "";
PrintSystemJobInfo pji = null;
try
{
_spooler = new PrintQueue(new PrintServer(), _spoolerName);
pji = _spooler.GetJob(intJobID);
if (!objJobDict.ContainsKey(intJobID))
objJobDict[intJobID] = pji.Name;
strJobName = pji.Name;
}
catch
{
pji = null;
objJobDict.TryGetValue(intJobID, out strJobName);
if (strJobName == null) strJobName = "";
}
if (OnJobStatusChange != null)
{
OnJobStatusChange(this,
new PrintJobChangeEventArgs(intJobID,strJobName,jStatus,pji));
}
}
}
#endregion
#region reset the Event and wait for the next event
_mrEvent.Reset();
_waitHandle = ThreadPool.RegisterWaitForSingleObject(_mrEvent,
new WaitOrTimerCallback(PrinterNotifyWaitCallback), _mrEvent, -1, true);
#endregion
}
#endregion
}
Sample source code and usage
The PrintSpooler project is a sample (with code) that uses the PrintQueueMonitor
class and appends a list box with job change notifications received from the PrintQueueMonitor
class.
pqm = new PrintQueueMonitor(cmbPrinters.Text.Trim());
pqm.OnJobStatusChange +=
new PrintJobStatusChanged(pqm_OnJobStatusChange);
........
........
........
void pqm_OnJobStatusChange(object Sender, PrintJobChangeEventArgs e)
{
MethodInvoker invoker = () => {
lbSpoolChanges.Items.Add(e.JobID + " - " +
e.JobName + " - " + e.JobStatus);
};
if (lbSpoolChanges.InvokeRequired)
Invoke(invoker);
else
invoker();
}
History
To build this class and sample, I have used pieces of code from various posts from CodeProject and other sites. Many thanks to the individuals who have contributed to this.