
Introduction
A component that can change default printer settings should also be able to watch if some program or other is changing these settings. Even in the extreme case that someone deletes the printer, the handle of this watcher should be closed properly.
The easiest way of handling this, would be deriving a class from a SafeWaitHandle
and overriding the ReleaseHandle
function to close the printer change notification. But, like most classes in the System.Threading
namespace, the SafeWaitHandle
is sealed
. I first tried to write a non-sealed version of a SafeWaitHandle
, but I got stuck in Mutexes, ThreadAffinities, and LsaPolicies. It is much easier to wrap another SafeHandle
around the WaitHandle
.
A SafeWaitPrinterHandle
To obtain notification changes of a printer or a print server, you call the Windows function FindFirstPrinterChangeNotification
. This handle has to be closed with the FindClosePrinterChangeNotification
function.
sealed class qSafeWaitPrinterHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private ManualResetEvent _ManualResetEvent;
private RegisteredWaitHandle _PrinterChangeNotification;
internal qSafeWaitPrinterHandle(qSafePrinterHandle SafePrinterHandle)
: base(true)
{
if (SafePrinterHandle.IsInvalid) return;
this.handle = FindFirstPrinterChangeNotification(SafePrinterHandle,
(uint)Printer_Change.ALL, 0,
new qPrinterNotifyOptions(false));
}
protected override bool ReleaseHandle()
{
if (!base.IsInvalid)
{
if (!FindClosePrinterChangeNotification(base.handle))
return false;
base.handle = IntPtr.Zero;
}
return true;
}
}
Monitoring a PrintServer
Before using the qSafeWaitPrinterHandle
, we have to explain the difference between a printer and a print server. A server has forms, ports, drivers, ... and of course, printers and print jobs.

Printers use this information to print. If you add a port or form on the print server, all printers will be capable of using that port or form. It is not possible to add a form to only one printer. Therefore, if we want to monitor printer changes, we have to monitor the printer and also the server of this printer.
This is done by changing the second parameter of the FindFirstPrinterChangeNotification
function. One can opt for a combination of the following fields:
public enum Printer_Change : uint
{
ADD_PRINTER = 0x00000001,
SET_PRINTER = 0x00000002,
DELETE_PRINTER = 0x00000004,
FAILED_CONNECTION_PRINTER = 0x00000008,
ADD_JOB = 0x00000100,
SET_JOB = 0x00000200,
DELETE_JOB = 0x00000400,
WRITE_JOB = 0x00000800,
ADD_FORM = 0x00010000,
SET_FORM = 0x00020000,
DELETE_FORM = 0x00040000,
ADD_PORT = 0x00100000,
CONFIGURE_PORT = 0x00200000,
DELETE_PORT = 0x00400000,
ADD_PRINT_PROCESSOR = 0x01000000,
DELETE_PRINT_PROCESSOR = 0x04000000,
ADD_PRINTER_DRIVER = 0x10000000,
SET_PRINTER_DRIVER = 0x20000000,
DELETE_PRINTER_DRIVER = 0x40000000,
TIMEOUT = 0x80000000,
ALL = 0x7777FFFF
}
The information returned is limited: if there is a new print job, we will receive a signal that a job is added, no more. Which printer, which user, how many pages, ..., this information has to be queried otherwise.
Monitoring a Printer
If we want detailed information about which printer added what job, we have to fine-tune the fourth parameter of the FindFirstPrinterChangeNotification
function. Here, we have to specify in an array which printer and/or job changes we want to monitor:
[StructLayout(LayoutKind.Sequential)]
sealed class qPrinterNotifyOptionsType
{
public short wPrinterType;
public short wPrinterReserved0;
public int dwPrinterReserved1;
public int dwPrinterReserved2;
public int PrinterFieldCount;
public IntPtr pPrinterFields;
public short wJobType;
public short wJobReserved0;
public int dwJobReserved1;
public int dwJobReserved2;
public int JobFieldCount;
public IntPtr pJobFields;
public qPrinterNotifyOptionsType()
{
this.wPrinterType = 0;
this.PrinterFieldCount = 20;
this.pPrinterFields = Marshal.AllocCoTaskMem(42);
Marshal.WriteInt16(this.pPrinterFields, 0,
(short)Printer_Notify_Field_Indexes.PRINTER_NAME);
Marshal.WriteInt16(this.pPrinterFields, 2,
(short)Printer_Notify_Field_Indexes.SHARE_NAME);
Marshal.WriteInt16(this.pPrinterFields, 4,
(short)Printer_Notify_Field_Indexes.PORT_NAME);
...
Marshal.WriteInt16(this.pPrinterFields, 40,
(short)Printer_Notify_Field_Indexes.OBJECT_GUID);
this.wJobType = 1;
this.JobFieldCount = 22;
this.pJobFields = Marshal.AllocCoTaskMem(46);
Marshal.WriteInt16(this.pJobFields, 0,
(short)Job_Notify_Field_Indexes.PRINTER_NAME);
Marshal.WriteInt16(this.pJobFields, 2,
(short)Job_Notify_Field_Indexes.MACHINE_NAME);
...
Marshal.WriteInt16(this.pJobFields, 44,
(short)Job_Notify_Field_Indexes.BYTES_PRINTED);
}
~qPrinterNotifyOptionsType()
{
Marshal.FreeCoTaskMem(this.pJobFields);
Marshal.FreeCoTaskMem(this.pPrinterFields);
}
}
The SafeWaitHandle
To actually start monitoring our printer, we have to register a delegate and provide a callback function. We can use a ManualResetEvent
and a RegisteredWaitHandle
.
sealed class qSafeWaitPrinterHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private ManualResetEvent _ManualResetEvent;
private RegisteredWaitHandle _RegisteredWaitHandle;
...
internal qSafeWaitPrinterHandle(qSafePrinterHandle SafePrinterHandle)
: base(true)
{
if (SafePrinterHandle.IsInvalid) return;
this.handle = FindFirstPrinterChangeNotification(SafePrinterHandle,
(uint)Printer_Change.ALL, 0,
new qPrinterNotifyOptions(false));
if (!this.IsInvalid)
{
_ManualResetEvent = new ManualResetEvent(false);
_ManualResetEvent.SafeWaitHandle =
new SafeWaitHandle(this.handle,true);
_RegisteredWaitHandle =
ThreadPool.RegisterWaitForSingleObject(_ManualResetEvent,
new WaitOrTimerCallback(PrinterNotifyWaitCallback),
null, -1, true);
}
else
System.Windows.Forms.MessageBox.Show("Invalid handle");
}
protected override bool ReleaseHandle()
{
if (this.IsInvalid)
{
if (!this.IsInvalid)
{
_RegisteredWaitHandle.Unregister(_ManualResetEvent);
if (!FindClosePrinterChangeNotification(this.handle))
return false;
_ManualResetEvent.SafeWaitHandle.SetHandleAsInvalid();
_ManualResetEvent.Close();
}
return true;
}
}
}
The Callback Function
When the wait handle is signaled, some steps have to be taken:
- Check if we have a valid handle
- Call the Windows function
FindNextPrinterChangeNotification
- The print server:
- Check if we have a server change or not
- What change took place on the server
- The printer:
- Check if we have a printer change or not
- Read how many fields are updated
- Read for each field, the type: Job Changed or Printer Changed
- If the Job is changed, read which Job number
- Read for each field what has changed
- Read for each field the data that changed
- Free the
NotifyInfo
- Start waiting all over again
private void PrinterNotifyWaitCallback(object state, bool timedOut)
{
if (!this.IsInvalid)
{
IntPtr pni = new IntPtr();
uint uuu = 0;
string sss;
if (qStatic.FindNextPrinterChangeNotification(this,
out uuu, IntPtr.Zero, ref pni) != 0)
{
if (uuu != 0)
{
sss = "Server Changed : ";
if ((uuu & (uint)Printer_Change.ADD_FORM) ==
(uint)Printer_Change.ADD_FORM;
sss += "Form Added ";
if ((uuu & (uint)Printer_Change.ADD_PORT) ==
(uint)Printer_Change.ADD_PORT;
sss += "Port Added ";
...
if ((uuu & (uint)Printer_Change.WRITE_JOB) ==
(uint)Printer_Change.WRITE_JOB;
sss += "Job Written ";
System.Diagnostics.Debug.WriteLine(sss);
}
if (pni != IntPtr.Zero)
{
int count = Marshal.ReadInt32(pni,
(PrintComponent.IntPtrSize * 2));
int jobid;
int nnn;
for (int ii = 0; ii < count; ii++)
{
sss = "";
nnn = 0;
int j = (PrintComponent.IntPtrSize * 5) * ii;
short type = Marshal.ReadInt16(pni,
(j + (PrintComponent.IntPtrSize * 3)));
short field = Marshal.ReadInt16(pni,
(j + (PrintComponent.IntPtrSize * 3) + 2));
if (type == 0)
{
Printer_Notify_Field_Indexes fi =
(Printer_Notify_Field_Indexes)field;
sss = fi.ToString() + " ";
switch (field)
{
case (short)Printer_Notify_Field_Indexes.PRINTER_NAME:
sss +=
Marshal.PtrToStringUni(Marshal.ReadIntPtr(pni,
(j + (PrintComponent.IntPtrSize * 7))));
break;
case (short)Printer_Notify_Field_Indexes.DEVMODE:
DevMode d = new DevMode(Marshal.ReadIntPtr(pni,
(j + (PrintComponent.IntPtrSize * 7))));
break;
case (short)Printer_Notify_Field_Indexes.ATTRIBUTES:
nnn = Marshal.ReadInt32(pni, (j +
(PrintComponent.IntPtrSize * 6)));
sss += nnn.ToString();
break;
case (short)Printer_Notify_Field_Indexes.START_TIME:
nnn = Marshal.ReadInt32(pni,
(j + (PrintComponent.IntPtrSize * 6)));
TimeSpan ts = new TimeSpan(nnn / 60, nnn % 60, 0);
sss += ts.ToString();
break;
...
}
System.Diagnostics.Debug.WriteLine("Printer : " +
sss);
}
if (type == 1)
{
Job_Notify_Field_Indexes fi =
(Job_Notify_Field_Indexes)field;
sss = fi.ToString() + " ";
switch (field)
{
case (short)Job_Notify_Field_Indexes.PRINTER_NAME:
sss +=
Marshal.PtrToStringUni(Marshal.ReadIntPtr(pni,
(j + (PrintComponent.IntPtrSize * 7))));
break;
case (short)Job_Notify_Field_Indexes.DEVMODE:
DevMode d = new DevMode(Marshal.ReadIntPtr(pni,
(j + (PrintComponent.IntPtrSize * 7))));
sss += "Changed";
break;
case (short)Job_Notify_Field_Indexes.STATUS:
nnn = Marshal.ReadInt32(pni,
(j + (PrintComponent.IntPtrSize * 6)));
Job_Status stat = (Job_Status)nnn;
sss += stat.ToString();
break;
...
}
jobid = Marshal.ReadInt32(pni,
(j + (PrintComponent.IntPtrSize * 5)));
System.Diagnostics.Debug.WriteLine("Job : " +
jobid.ToString() + " " + sss);
}
FreePrinterNotifyInfo(pni);
}
}
_RegisteredWaitHandle =
ThreadPool.RegisterWaitForSingleObject(
(ManualResetEvent)_ManualResetEvent,
new WaitOrTimerCallback(PrinterNotifyWaitCallback),
null, -1, true);
}
}
Final Steps
We still have to update our qSafePrinterHandle
to properly initialize and close the qSafeWaitPrinterHandle
public sealed class qSafePrinterHandle : SafeHandleZeroOrMinusOneIsInvalid
{
...
private qSafeWaitPrinterHandle _SafeWaitPrinterHandle;
private qPrinterDefaults printerDefaults = new qPrinterDefaults(true);
internal qSafePrinterHandle(string printername)
: base(true)
{
if (_SafeWaitPrinterHandle != null)
_SafeWaitPrinterHandle.Close();
if (OpenPrinter(printername, out this.handle, ref printerDefaults))
_SafeWaitPrinterHandle = new qSafeWaitPrinterHandle(this);
}
public string PrinterName
{
...
set
{
if (value != PrinterName)
{
this.ReleaseHandle();
if (OpenPrinter(value, out this.handle, ref _PrinterDefaults))
_SafeWaitPrinterHandle = new qSafeWaitPrinterHandle(this);
}
}
}
protected override bool ReleaseHandle()
{
if (_SafeWaitPrinterHandle !=null)
_SafeWaitPrinterHandle.Close();
if (IsInvalid)
return true;
if (!qStatic.ClosePrinter(this.handle))
return false;
this.SetHandle(IntPtr.Zero);
return true;
}
}
Printing a local document produces the following output:
// Pause Printer
Printer : STATUS 1
// Print Document
Printer : CJOBS 1
Job : 2 PRINTER_NAME Adobe PDF
Job : 2 MACHINE_NAME \\QUIENSABE
Job : 2 PORT_NAME
Job : 2 USER_NAME Quiensabe
Job : 2 NOTIFY_NAME Quiensabe
Job : 2 DATATYPE NT EMF 1.008
Job : 2 PRINT_PROCESSOR WinPrint
Job : 2 PARAMETERS
Job : 2 DRIVER_NAME Adobe PDF Converter
Job : 2 DEVMODE Changed
Job : 2 STATUS SPOOLING
Job : 2 STATUS_STRING
Job : 2 DOCUMENT qSafeWaitPrinterHandle.cs
Job : 2 PRIORITY 1
Job : 2 POSITION 1
Job : 2 SUBMITTED 1/08/2006 12:39:01
Job : 2 START_TIME 0
Job : 2 UNTIL_TIME 0
Job : 2 TIME 0
Job : 2 TOTAL_PAGES 0
Job : 2 PAGES_PRINTED 0
Job : 2 TOTAL_BYTES 0
Job : 2 TOTAL_PAGES 1
Job : 2 TOTAL_BYTES 111872
Job : 2 TOTAL_PAGES 6
Job : 2 TOTAL_BYTES 632268
Job : 2 STATUS 0
// Resume Printer
Printer : STATUS 0
Job : 2 STATUS 0
Job : 2 PORT_NAME My Documents\*.pdf
Job : 2 STATUS PRINTING
Job : 2 PAGES_PRINTED 0
Job : 2 PAGES_PRINTED 0
Job : 2 PAGES_PRINTED 0
Job : 2 PAGES_PRINTED 1
Job : 2 PAGES_PRINTED 1
Job : 2 PAGES_PRINTED 2
Job : 2 PAGES_PRINTED 3
Job : 2 PAGES_PRINTED 3
Job : 2 PAGES_PRINTED 4
Job : 2 PAGES_PRINTED 5
Job : 2 PAGES_PRINTED 5
Job : 2 STATUS DELETING, PRINTING, PRINTED
Job : 2 STATUS DELETING, PRINTING, PRINTED
Job : 2 STATUS DELETING, PRINTING, PRINTED
Job : 2 PORT_NAME
Job : 2 STATUS DELETING, PRINTED
Job : 2 PAGES_PRINTED 6
Printer : CJOBS 0
Job : 2 STATUS DELETING, PRINTED, DELETED
History
- 5 August 2006: Initial version
- 1 September 2006: Source code added
- 13 April 2007: Network support added and code updated