在為客戶開發配套列印軟體的過程中,我深入研究了 C# 中列印機的枚舉與資訊獲取。.NET Framework 1.1 提供的 PrinterSettings.InstalledPrinters 雖能列出已安裝列印機名稱,但無法取得驅動程式類型等詳細資訊。由於客戶未提供 SDK,且出於商業授權考量,軟體必須僅在特定品牌列印機上運行,因此必須透過驅動程式名稱來識別硬體。
WMI (Windows Management Instrumentation) 提供了統一、可擴展的系統管理介面。.NET 中 System.Management 命名空間下的 ManagementObjectSearcher 可執行 WQL 查詢,輕鬆獲取 Win32_Printer 類別的屬性。
public StringCollection GetPrintersByDriverName(string driverName)
{
var printers = new StringCollection();
string condition = string.Empty;
if (!string.IsNullOrEmpty(driverName) && driverName != "*")
{
condition = " WHERE DriverName LIKE '" + driverName + "'";
}
string query = "SELECT Name FROM Win32_Printer" + condition;
using (var searcher = new ManagementObjectSearcher(query))
using (var collection = searcher.Get())
{
foreach (ManagementObject printer in collection)
{
printers.Add(printer.Properties["Name"].Value.ToString());
}
}
return printers;
}
雖然 WMI 功能強大,且透過 VBScript 幾乎能涵蓋所有 Windows 管理操作,但在實際部署中發現穩定性不足:偶爾會無法正確取得列印機清單,導致客戶投訴。參考 MSDN 文件可深入了解 WMI 架構。
EnumPrinters為了追求穩定與效能,轉而使用 winspool.drv 中的 EnumPrinters 函數。透過 P/Invoke (DllImport) 簽章如下:
[DllImport("winspool.drv", SetLastError = true, CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumPrinters(
[MarshalAs(UnmanagedType.U4)] PRINTER_ENUM flags,
[MarshalAs(UnmanagedType.LPStr)] string name,
uint level,
IntPtr pPrinterEnum,
uint cbBuf,
[MarshalAs(UnmanagedType.U4)] ref uint pcbNeeded,
[MarshalAs(UnmanagedType.U4)] ref uint pcReturned);
PRINTER_INFO_2 結構對應EnumPrinters 的 Level 參數決定回傳結構,需用 Level=2 才能取得驅動程式名稱 (pDriverName)。但 .NET 無內建 PRINTER_INFO_2 定義,且該結構包含嵌套的 DEVMODE 結構,手工對應極易出錯。
靈感來自於「用類別替代結構」的想法:定義 PRINTER_INFO_2 與 DEVMODE 為 class,將所有 DWORD 對應 Int32,指標類型 (LPTSTR, LPDEVMODE, PSECURITY_DESCRIPTOR) 對應 IntPtr。再利用 Marshal.PtrToStructure 將非受控記憶體區塊依序轉換為受控物件:
var pi = new PRINTER_INFO_2();
for (int i = 0; i < numPrinters; i++)
{
Marshal.PtrToStructure(ptr, pi);
string driver = Marshal.PtrToStringAuto(pi.pDriverName);
if (string.IsNullOrEmpty(targetDriver) || driver.ToLower().Contains(targetDriver))
{
// 符合授權條件,進行後續處理
}
// 移動指標至下一個結構起始位置
ptr = new IntPtr(ptr.ToInt32() + Marshal.SizeOf(typeof(PRINTER_INFO_2)));
}
fixed 關鍵字C# 2.0 引入 fixed 關鍵字,允許在 struct 中定義固定大小陣列緩衝區,簡化非受控記憶體對應。但需注意:fixed 僅能用於 struct,不能用於 class。
從 WMI 到 Win32 API 的遷移,解決了穩定性與授權檢查的核心需求。C# 與非受控程式碼的互操作 (Interop) 雖具挑戰,但掌握 Marshal 類別與記憶體佈局技巧後,能極大拓展 .NET 應用的邊界。