Today I got a question on twitter asking how to enumerate
printers in Silverlight, so I put together this quick blog
post.
Silverlight lacks the PrinterSettings.InstalledPrinters
collection. So, how do you go about getting a list of installed
printers in Silverlight 5? PInvoke to the Win32 API, of course.
Alexandra Rusina on the Silverlight team wrote a
great article on PInvoke. If you're interested in how PInvoke
in Silverlight compares to PInvoke in .NET in general, stop right
now and
read her post. Be sure to subscribe to the blog while you're
there.
Turning to my trusty friend PInvoke.net, I dug up the
API reference for EnumPrinters. They were even so helpful as to
provide a nice C# example. Unfortunately, some of the functions
don't work exactly the same, so you need to do a bit of tweaking.
My thanks to Alexandra for helping with some tips about how to
refactor this code to work around missing Marshal overloads in
Silverlight. I also made a number of changes to make it more
Silverlight-friendly (observable collection, for example).
When done, here's what the application will look like:
Nothing fancy, but it shows the list of printers on my system in
beautiful highlighter yellow.
Now let's look at how it's constructed. Warning, lots of
Win32 API cruft in here. Of course, that's the good part too
:)
First of all, make sure you create the application as an
elevated trust application. PInvoke requires that. I did mine as
out-of-browser, but in-browser should work fine if you prefer
that.
I named my application "SilverlightEnumPrinters". Now, to the
structure of the app.
The PrinterInformation Class
The sample needs something to hold the information for a single
printer. This class is my friendly version of the
PRINTER_INFO_2 structure used by the Win32 API. I removed the
fields I wasn't using (including those that required another call
to marshal an aggregated structure, as I didn't want to fill the
code with that), and made the remaining fields into actual
properties that could be used for binding. The actual
PRINTER_INFO_2 class is also in the same file. Note that it
is a class, not a structure. That's to allow
Marshal.PtrToStructure to work (thanks to Alexandra for help on
that one).
Marshal.PtrToStructure in Silverlight doesn't include the
overload to return the instance of the type. So, the sample on the
pinvoke site which looks like this:
printerInfo2[i] = (PRINTER_INFO_2)Marshal.PtrToStructure(new
IntPtr(offset), type);
Must be changed to look like this:
Marshal.PtrToStructure(new IntPtr(offset), printerInfo2[i]);
Note that you don't pass a type, you pass an actual instance of
the object you want to fill. I made some additional changes which
eliminate the use of the array, so the code below will look
slightly different. The rest of the code follows.
using System;
using System.Runtime.InteropServices;
namespace SilverlightEnumPrinters
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
internal class PRINTER_INFO_2
{
[MarshalAs(UnmanagedType.LPTStr)]
public string pServerName;
[MarshalAs(UnmanagedType.LPTStr)]
public string pPrinterName;
[MarshalAs(UnmanagedType.LPTStr)]
public string pShareName;
[MarshalAs(UnmanagedType.LPTStr)]
public string pPortName;
[MarshalAs(UnmanagedType.LPTStr)]
public string pDriverName;
[MarshalAs(UnmanagedType.LPTStr)]
public string pComment;
[MarshalAs(UnmanagedType.LPTStr)]
public string pLocation;
public IntPtr pDevMode;
[MarshalAs(UnmanagedType.LPTStr)]
public string pSepFile;
[MarshalAs(UnmanagedType.LPTStr)]
public string pPrintProcessor;
[MarshalAs(UnmanagedType.LPTStr)]
public string pDatatype;
[MarshalAs(UnmanagedType.LPTStr)]
public string pParameters;
public IntPtr pSecurityDescriptor;
public uint Attributes;
public uint Priority;
public uint DefaultPriority;
public uint StartTime;
public uint UntilTime;
public uint Status;
public uint cJobs;
public uint AveragePPM;
}
public class PrinterInformation
{
public string ServerName { get; set; }
public string PrinterName { get; set; }
public string ShareName { get; set; }
public string PortName { get; set; }
public string DriverName { get; set; }
public string Comment { get; set; }
public string Location { get; set; }
public string SeparatorPageFileName { get; set; }
public string PrintProcessor { get; set; }
public string DefaultPrintProcessorParameters { get; set; }
public PrinterAttributes Attributes { get; set; }
public uint Priority { get; set; }
public uint DefaultPriority { get; set; }
public uint StartTime { get; set; }
public uint UntilTime { get; set; }
public PrinterStatus Status { get; set; }
public uint QueuedJobCount { get; set; }
public uint AveragePagesPerMinute { get; set; }
}
}
Printer Attributes
The printer attributes enum is a bunch of flag constants
repackaged into an enum.
using System;
namespace SilverlightEnumPrinters
{
[Flags]
public enum PrinterAttributes
{
Queued = 0x1,
Direct = 0x2,
Default = 0x4,
Shared = 0x8,
Network = 0x10,
Hidden = 0x20,
Local = 0x40,
WorkOffline = 0x400,
EnableBidi = 0x800
}
}
Printer Status
Similarly, PrinterStatus is also a repackaging of constants
using System;
namespace SilverlightEnumPrinters
{
[Flags]
public enum PrinterStatus
{
Busy = 0x200,
DoorOpen = 0x400000,
Error = 0x2,
Initializing = 0x8000,
IOActive = 0x100,
ManualFeed = 0x20,
NoToner = 0x40000,
NotAvailable = 0x1000,
Offline = 0x80,
OutOfMemory = 0x200000,
OutputBinFull = 0x800,
PagePunt = 0x80000,
PaperJam = 0x8,
PaperOut = 0x10,
PaperProblem = 0x40,
Paused = 0x1,
PendingDeletion = 0x4,
Printing = 0x400,
Processing = 0x4000,
TonerLow = 0x20000,
UserIntervention = 0x100000,
Waiting = 0x2000,
WarmingUp = 0x10000
}
}
Finally, it's time to create the function which actually
enumerates the printers.
Printer Service
The PrinterService class is where everything happens. It exposes
the collection of printers, and includes a public method which
loads the collection using the EnumPrinters API call. It even
includes my ugly L2R code to transform a PRINTER_INFO_2 into a
PrinterInformation object.
using System;
using System.Runtime.InteropServices;
using System.Collections.ObjectModel;
namespace SilverlightEnumPrinters
{
[Flags]
public enum PrinterEnumFlags
{
Default = 0x00000001,
Local = 0x00000002,
Connections = 0x00000004,
Favorite = 0x00000004,
Name = 0x00000008,
Remote = 0x00000010,
Shared = 0x00000020,
Network = 0x00000040,
Expand = 0x00004000,
Container = 0x00008000,
IconMask = 0x00ff0000,
Icon1 = 0x00010000,
Icon2 = 0x00020000,
Icon3 = 0x00040000,
Icon4 = 0x00080000,
Icon5 = 0x00100000,
Icon6 = 0x00200000,
Icon7 = 0x00400000,
Icon8 = 0x00800000,
Hide = 0x01000000
}
public class PrinterService
{
[Flags]
private enum LocalMemoryFlags
{
LMEM_FIXED = 0x0000,
LMEM_MOVEABLE = 0x0002,
LMEM_NOCOMPACT = 0x0010,
LMEM_NODISCARD = 0x0020,
LMEM_ZEROINIT = 0x0040,
LMEM_MODIFY = 0x0080,
LMEM_DISCARDABLE = 0x0F00,
LMEM_VALID_FLAGS = 0x0F72,
LMEM_INVALID_HANDLE = 0x8000,
LHND = (LMEM_MOVEABLE | LMEM_ZEROINIT),
LPTR = (LMEM_FIXED | LMEM_ZEROINIT),
NONZEROLHND = (LMEM_MOVEABLE),
NONZEROLPTR = (LMEM_FIXED)
}
[DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool EnumPrinters(PrinterEnumFlags Flags, string Name, uint Level, IntPtr pPrinterEnum, uint cbBuf, ref uint pcbNeeded, ref uint pcReturned);
[DllImport("kernel32.dll", EntryPoint = "LocalAlloc")]
private static extern IntPtr LocalAlloc_NoSafeHandle(LocalMemoryFlags uFlags, IntPtr sizetdwBytes);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr LocalFree(IntPtr hMem);
private const int ERROR_INSUFFICIENT_BUFFER = 122;
private ObservableCollection<PrinterInformation> _printers = new ObservableCollection<PrinterInformation>();
public ObservableCollection<PrinterInformation> Printers
{
get { return _printers; }
}
public void LoadPrinters(PrinterEnumFlags Flags)
{
Printers.Clear();
uint cbNeeded = 0;
uint cReturned = 0;
if (EnumPrinters(Flags, null, 2, IntPtr.Zero, 0, ref cbNeeded, ref cReturned))
{
// nothing
return;
}
int lastWin32Error = Marshal.GetLastWin32Error();
if (lastWin32Error == ERROR_INSUFFICIENT_BUFFER)
{
IntPtr pAddr = LocalAlloc_NoSafeHandle(LocalMemoryFlags.LMEM_FIXED, (IntPtr)cbNeeded);
if (EnumPrinters(Flags, null, 2, pAddr, cbNeeded, ref cbNeeded, ref cReturned))
{
int offset = pAddr.ToInt32();
int increment = Marshal.SizeOf(typeof(PRINTER_INFO_2));
for (int i = 0; i < cReturned; i++)
{
PrinterInformation printer = new PrinterInformation();
PRINTER_INFO_2 pinfo = new PRINTER_INFO_2();
Marshal.PtrToStructure(new IntPtr(offset), pinfo);
printer.Attributes = (PrinterAttributes)pinfo.Attributes;
printer.AveragePagesPerMinute = pinfo.AveragePPM;
printer.Comment = pinfo.pComment;
printer.DefaultPrintProcessorParameters = pinfo.pParameters;
printer.DefaultPriority = pinfo.Priority;
printer.DriverName = pinfo.pDriverName;
printer.Location = pinfo.pLocation;
printer.PortName = pinfo.pPortName;
printer.PrinterName = pinfo.pPrinterName;
printer.PrintProcessor = pinfo.pPrintProcessor;
printer.Priority = pinfo.Priority;
printer.QueuedJobCount = pinfo.cJobs;
printer.SeparatorPageFileName = pinfo.pSepFile;
printer.ServerName = pinfo.pServerName;
printer.ShareName = pinfo.pShareName;
printer.StartTime = pinfo.StartTime; // should xform this
printer.Status = (PrinterStatus)pinfo.Status;
printer.UntilTime = pinfo.UntilTime; // should xform this
Printers.Add(printer);
offset += increment;
}
LocalFree(pAddr);
}
else
{
lastWin32Error = Marshal.GetLastWin32Error();
throw new Exception("Win32 Error: " + lastWin32Error);
}
}
}
}
}
The code is pretty straight-forward. Make an API call to see if
there are any printers. If so, allocate some memory for the buffer,
get the list of printers into that buffer, and start reading the
printers out of it. Once complete, free the allocated buffer.
With all that in place, it's time to create the UI.
Main Page
The main page has a simple ListBox with a really basic template
used to show the properties of the printers. I only surface a few
properties; you could certainly add more.
<UserControl x:Class="SilverlightEnumPrinters.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Background="White">
<ListBox x:Name="PrinterList"
Margin="10">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="10" Background="Yellow">
<StackPanel>
<TextBlock Text="{Binding ServerName}" />
<TextBlock Text="{Binding ShareName}" />
<TextBlock Text="{Binding PrinterName}" />
<TextBlock Text="{Binding DriverName}" />
<TextBlock Text="{Binding Location}" />
<TextBlock Text="{Binding Comment}" />
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>
Code-behind
The code-behind instantiates the service and binds the ListBox
on the UI.
namespace SilverlightEnumPrinters
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
Loaded += new RoutedEventHandler(MainPage_Loaded);
}
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
var service = new PrinterService();
PrinterList.ItemsSource = service.Printers;
service.LoadPrinters(PrinterEnumFlags.Local);
}
}
}
That's it! Run it and it should rattle off the list of printers
attached to your system. If you want to change from, say, local to
network printers, just change the parameter you pass into
LoadPrinters.
Full source code attached. Warning: Works on my machine
(Windows 7 x64, one attached printer).