Introduction
The System.Drawing.Printing.PrinterSettings
class is powerful, but limited. Users who are interested in the advanced settings of their printer get stuck. Information that is available by clicking a PrinterDialog
is hidden, and the default settings of the printer are unavailable too. The .NET 2.0 framework provides a huge advance in the System.Printing
class, but for some actions, we still need basic Windows functions. Thus the idea of using SafeHandle
s to access and change advanced PrinterSettings
.
The SafeHandleZeroOrMinusOneIsInvalid
Many printer functions need a handle to an open printer. When this handle is no longer needed, it must be closed properly. This is an ideal situation to start using a SafeHandle
.
The .NET Framework 2.0 SafeHandleZeroOrMinusOneIsInvalid
class is a managed wrapper around an IntPtr
that knows:
- when it is invalid, (zero or minus one)
- when the handle is closed, (
this.IsClosed
) - how to close the handle, (
ReleaseHandle
) - how to handle asynchronous thread aborts safely (it derives from the
CriticalFinalizerObject
).
The moment we open our printer, we know for certain that the handle will be closed properly, even when the AppDomain
is unloaded by the host, or when normal finalizers block or take too long to clean up. Only two things have to be done:
- Use a constructor, specifying if the handle is to be released reliably.
- Override the
ReleaseHandle
method.
public sealed class qSafePrinterHandle : SafeHandleZeroOrMinusOneIsInvalid
{
internal qSafePrinterHandle()
: base(true)
{}
[return: MarshalAs(UnmanagedType.U1)]
protected override bool ReleaseHandle()
{
if (IsInvalid)
return true;
if (!ClosePrinter(this.handle))
return false;
this.SetHandle(IntPtr.Zero);
return true;
}
}
The IsClosed
property refers to the closing of our handle, not of our printer. It will be true only when the SetHandleAsInvalid
, Dispose
, or Close
method is called, and there are no references to the SafeHandle
object on other threads. Therefore, immediately after using the ClosePrinter
function, we have to set the handle to zero indicating that we do not have an open printer.
Additional Constructor
Providing an additional constructor, our class knows not only how to close our printer, but also how to open it:
...
private qPrinterDefaults _PrinterDefaults = new qPrinterDefaults(true);
internal qSafePrinterHandle(string printername)
: base(true)
{
OpenPrinter(printername, out this.handle, ref _PrinterDefaults) ;
}
...
We do not have to be careful with this constructor (a stored value can be useless if someone changed the name of our printer):
- Our
ReleaseHandle
only closes the printer when there is a valid handle. - Even a null value is accepted as a parameter of this function, the handle returned will be of the local printer server.
Using the same handle for a different printer causes no problems either, if we first call the Release
method.
public string PrinterName
{
get
{
if (this.IsInvalid) return String.Empty;
return new PrinterInfo5().PrinterName;
}
set
{
if (value != PrinterName)
{
this.ReleaseHandle();
OpenPrinter(value, out this.handle, ref _PrinterDefaults) ;
}
}
}
Caution is only needed when accessing properties. If our handle is valid (not zero, not minus one, handle not closed, and an open printer), we can query for data; if not, we have to return a null value.
public PrinterInfo5 PrinterInfo5
{
get
{
if (this.IsInvalid)
return null;
else
return PrinterInfo5(this);
}
}
Reading Unmanaged Memory
Let's first look at some code:
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
internal class PrinterInfo5
{
private string pPrinterName;
private string pPortName;
private int pAttributes;
private int pDeviceNotSelectedTimeout;
private int pTransmissionRetryTimeout;
internal PrinterInfo5(qSafePrinterHandle SafePrinterHandle)
{
int num1 = 0;
int num2 =
GetPrinter(SafePrinterHandle, 5, IntPtr.Zero, 0, out num1);
if (num1 <= 0)
throw new Win32Exception("First Call PrinterInfo5 Error");
IntPtr data = Marshal.AllocCoTaskMem(num1);
int num3 = 0;
num2 = GetPrinter(SafePrinterHandle, 5, data, num1, out num3);
if ((num2 != 1) | (num1 != num3))
throw new Win32Exception("Second Call PrinterInfo5 Error");
Marshal.PtrToStructure(data,this);
Marshal.FreeCoTaskMem(data);
}
}
We specify a class whose layout is sequential: first, we have the printer name, next comes the port name, then the attributes, the 'device not selected' timeout, and finally, the 'transmission retry' timeout. We are working in Unicode: we specify our charset in the structure layout of our class and in the calling of our GetPrinter
function. First, we call the GetPrinter
function to know the size of our answer. We allocate memory for our result, and call the function again with an IntPtr
to the allocated memory. If the function returns a zero value and the output parameter has the size of our allocated memory, our memory is easily marshaled into a structure. Last but not least, we free the memory we allocated.
Another way of doing the same thing:
...
int IntPtrSize = Marshal.SizeOf(typeof(IntPtr));
pPrinterName =
Marshal.PtrToStringUni(Marshal.ReadIntPtr(data, IntPtrSize * 0));
pPortName =
Marshal.PtrToStringUni(Marshal.ReadIntPtr(data, IntPtrSize * 1));
pAttributes = Marshal.ReadInt32(data, (IntPtrSize * 2));
pDeviceNotSelectedTimeout = Marshal.ReadInt32(data, (IntPtrSize * 3));
pTransmissionRetryTimeout = Marshal.ReadInt32(data, (IntPtrSize * 4));
...
When we call the Win32 function, we receive a pointer to something like {intptr, intptr, int32, int32, int32}
. The first IntPtr
points to a place in unmanaged memory where you can find the printer name, the second IntPtr
holds the port name, and next comes three integers. Knowing the size of an IntPtr
, it is easy to read on each offset the values we need.
The same done unsafely:
internal unsafe PrinterInfo5(qSafePrinterHandle SafePrinterHandle)
{
...
num1 = data.ToInt32();
pPrinterName = new string(*((char**)(num1 + (IntPtrSize * 0))));
pPortName = new string(*((char**)(num1 + (IntPtrSize * 1))));;
pAttributes = *(((int*)(num1 + (IntPtrSize * 2))));
pDeviceNotSelectedTimeout = *(((int*)(num1 + (IntPtrSize * 3))));
pTransmissionRetryTimeout = *(((int*)(num1 + (IntPtrSize * 4))));
...
Another Safehandle
Our pPrinterName
is the first IntPtr
to read. But in the PrinterInfo1
structure, the name of the printer comes on the third place, and in the PrinterInfo2
, it is on the second IntPtr
. Why not create a class that:
- stores what level of information we are querying,
- allocates memory for our answer,
- fills the memory with an answer,
- frees the allocated memory when no longer needed,
- can read strings and integers at different offsets.
Another implementation of a SafeHandleZeroOrMinusOneIsInvalid
:
public abstract class qSafeInfo : SafeHandleZeroOrMinusOneIsInvalid
{
private int IntPtrSize = Marshal.SizeOf(typeof(IntPtr));
internal int Level;
protected SafeHandle PrinterSafeHandle;
protected int BytesNeeded;
protected int Size;
protected abstract void RefreshInfo();
protected qSafeInfo(SafeHandle pSafeHandle, int infoLevel)
: base (true)
{
PrinterSafeHandle = pSafeHandle;
Level = infoLevel;
RefreshInfo();
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[return: MarshalAs(UnmanagedType.U1)]
protected override bool ReleaseHandle()
{
if (!IsInvalid)
{
if (Size > 0)
{
Marshal.FreeHGlobal(base.handle);
base.SetHandle(IntPtr.Zero);
}
}
return true;
}
protected void AllocMem()
{
if (Size >= BytesNeeded)
return;
if (Size != 0)
Marshal.FreeHGlobal(base.handle);
if (BytesNeeded != 0)
base.handle = Marshal.AllocHGlobal(BytesNeeded);
Size = BytesNeeded;
}
protected int GetIntField(int offset)
{
if (this.IsInvalid) return 0;
return Marshal.ReadInt32(this.handle, IntPtrSize * offset);
}
protected string GetStringField(int offset)
{
if (this.IsInvalid) return null;
IntPtr ptr = Marshal.ReadIntPtr(this.handle, IntPtrSize * offset);
string s = Marshal.PtrToStringUni(ptr);
if (s == null) s = string.Empty;
return s;
}
}
Here, we use the handle to point to a place in memory. The moment we close our handle, memory will be automatically freed. When the AllocMem
function is called and not enough memory is allocated, we simply allocate more memory (the possibility that other printers use the same driver is quite big, people tend to buy from one provider, and the PrinterInfo
structures are not too big).
Now, we are ready to read information from our printer. But not only printer information, we can use the same SafeHandle
to read ports, drivers, forms, print monitors,...
public abstract class qSafePrinterInfo : qSafeInfo
{
protected qSafePrinterInfo(SafeHandle PrinterHandle, int level)
:base(PrinterHandle, level)
{}
[PrintingPermission(SecurityAction.Demand, Level =
PrintingPermissionLevel.DefaultPrinting)]
protected override void RefreshInfo()
{
if (!GetPrinter(PrinterHandle, Level, handle, Size,
out BytesNeeded) && (BytesNeeded > 0))
{
AllocMem();
if (!GetPrinter(PrinterHandle, Level, handle, Size,
out BytesNeeded) && (BytesNeeded > 0))
throw new Win32Exception(GetType().FullName +
Level.ToString() + " Error");
}
}
public abstract class qSafeDriverInfo : qSafeInfo
{
protected qSafeDriverInfo(SafeHandle PrinterHandle, int level)
: base(PrinterHandle, level)
{ }
[PrintingPermission(SecurityAction.Demand, Level =
PrintingPermissionLevel.DefaultPrinting)]
protected override void RefreshInfo()
{
if (!GetPrinterDriver(PrinterHandle, IntPtr.Zero, Level, handle,
Size, out BytesNeeded) && (BytesNeeded > 0))
{
AllocMem();
if (!GetPrinterDriver(PrinterHandle, IntPtr.Zero, Level, handle,
Size, out BytesNeeded) && (BytesNeeded > 0))
throw new Win32Exception(GetType().FullName +
Level.ToString() + " Error");
}
}
}
Writing to Unmanaged Memory
If you can read, you can write. This universal truth is especially true in a situation where we know our handle is valid and not closed.
...
public abstract class qSafeInfo : SafeHandleZeroOrMinusOneIsInvalid
{
...
protected abstract bool Update();
protected bool SetIntField(int offset, int val)
{
if (this.IsInvalid) throw new InvalidOperationException();
Marshal.WriteInt32(this.handle, IntPtrSize * offset, val);
return Update();
}
protected bool SetStringField(int offset, string val)
{
if (this.IsInvalid) throw new InvalidOperationException();
IntPtr ptr1 = Marshal.StringToHGlobalUni(val);
Marshal.WriteIntPtr(this.handle, IntPtrSize * offset, ptr1);
bool b = Update();
Marshal.FreeHGlobal(ptr1);
return b;
}
}
public abstract class qSafePrinterInfo : qSafeInfo
{
...
[PrintingPermission(SecurityAction.Demand, Level =
PrintingPermissionLevel.AllPrinting)]
protected override bool Update()
{
if (this.IsInvalid)
throw new InvalidOperationException("Invalid Memory handle");
if (PrinterSafeHandle.IsInvalid)
throw new InvalidOperationException("Printer SafeHandle Invalid");
bool b = SetPrinter(PrinterHandle, Level, handle, 0);
if (!b) throw new Win32Exception("Printer NOT Updated.");
this.RefreshInfo();
return b;
}
}
Now, we can read and write to almost every field of our printer info. Our new PrinterInfo5
looks like this:
public sealed class PrinterInfo5 : qSafePrinterInfo
{
internal PrinterInfo5(SafeHandle PrinterHandle)
: base(PrinterHandle,5)
{ }
public string PrinterName
{
get { return GetStringField(0);}
set { SetStringField(0, value);}
}
public string PortName
{
get { return GetStringField(1);}
set { SetStringField(1, value);}
}
...
public int DeviceNotSelectedTimeout
{
get { return GetIntField(3); }
set { SetIntField(3, value);}
}
public int TransmissionRetryTimeout
{
get { return GetIntField(4); }
set { SetIntField(4, value);}
}
}
Enumeration Functions
A lot of printer functions return an array of data (EnumPrinters
, EnumForms
, EnumPrinterDrivers
,...). If the size of the structure is known, it is easy to calculate at which offset starts the next record.
public class qSafeDriverInfos : qSafeInfo
{
private string _servername;
private int recordcount;
internal qSafeDriverInfos(string ServerName, int level)
: base(IntPtr.Zero, level)
{
_servername = ServerName;
RefreshInfo();
}
[PrintingPermission(SecurityAction.Demand, Level =
PrintingPermissionLevel.DefaultPrinting)]
protected override void RefreshInfo()
{
if (!EnumPrinterDrivers(_servername, "All", Level, handle,
Size, out BytesNeeded, out recordcount) && (BytesNeeded > 0))
{
AllocMem();
if (!EnumPrinterDrivers(_servername, "All", Level, handle,
Size, out BytesNeeded, out recordcount) )
throw new Win32Exception("DriverInfo" +
Level.ToString() + " Error");
}
}
internal qSafeDriverInfo[] Drivers
{
get
{
qSafeDriverInfo[] _collection =
new qSafeDriverInfo[this.recordcount];
IntPtr ptr = base.handle;
for (int i = 1; i <= this.recordcount; i++)
{
switch (this.Level)
{
case 1:
_collection[i-1] = new DriverInfo1(ptr);
ptr = new IntPtr(base.handle.ToInt32() +
(i * this.IntPtrSize * 1));
break;
case 2:
_collection[i-1] = new DriverInfo2(ptr);
ptr = new IntPtr(base.handle.ToInt32() +
(i * this.IntPtrSize * 6));
break;
...
default:
return null;
}
}
return _collection;
}
}
}
We only have to provide an additional constructor to our DriverInfo
and qSafeInfo
classes. Because the memory is already allocated by the call of the Enum
function, we can use the constructor with a false argument: the memory will be freed the moment we close the qSafeDriverInfos
class.
public sealed class DriverInfo1 : qSafeDriverInfo
{
...
public DriverInfo1(IntPtr MemoryHandle)
: base(MemoryHandle, 1)
{ }
...
}
public abstract class qSafeInfo : SafeHandleZeroOrMinusOneIsInvalid
{
...
protected qSafeInfo(IntPtr pMemoryHandle, int infoLevel)
: base(false)
{
base.handle = pMemoryHandle;
Level = infoLevel;
}
...
}
Reading and Writing to the DevMode
The DevMode
is a structure inside the PrinterInfo2
class that holds the default printer settings. We can use the same ideas to read and update values:
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 1)]
public class DevMode
{
...
internal DevMode(IntPtr DevModeHandle)
{
Marshal.PtrToStructure(DevModeHandle,this);
}
public short Copies
{
get { return dmCopies; }
set
{
if (value != dmCopies)
{
dmCopies = value;
UpdateDevMode(0x100);
}
}
}
private bool UpdateDevMode(int field)
{
if (PrintComponent.SafePrinterHandle.IsInvalid) return false;
dmFields = field;
IntPtr ptr1 =
PrintComponent.SafePrinterHandle.PrinterInfo2.GetIntPtrField(7);
Marshal.StructureToPtr(this, ptr1, true);
return
PrintComponent.SafePrinterHandle.PrinterInfo2.SetIntPtrField(7, ptr1);
}
}
Of course, additional settings have to be checked; the value for our copies cannot be negative and has to be less than the maximum copies the printer can handle. We have to check if our printer supports setting this field. This is partly done in the code that I provide. In this article, I only wanted to show how easy life can be using SafeHandle
s.
Points of Interest
- It is possible to save
PrinterInfo
, DevMode
, PortInfo
, PrintJobInfo
, ... (Marshal.Copy(this.handle , new byte[Size] , 0 ,Size)
) - and of course, restore (
Size = b.Length; Marshal.Copy(b, 0, this.handle, Size);Update();
).
Part 2 explains how to monitor a printer using a SafeWaitHandle
.
History
- 2 July 2006: Initial version.
- 5 October 2006: Link added to part 2.
- 6 December 2006: Updated version, and minor changes to the
DevMode
class. - 15 March 2007: Links updated.
- 13 April 2007:
- Example of enumeration function added.
- Support for network printers added.
- Collection of
DriverInfo
by PrintServer
.