專案遇到批次列印 PDF 檔需求。
Acrobat Reader 或 Foxit Reader 等常用 PDF 軟體本身就具備傳參數直接列印功能,例如 Acrobat Reader 直接列印 PDF 之語法為:AcroRd32.exe /p /h "pdf路徑" "印表機名稱"(印表機名稱省略時由預設印表機輸出)
基於以上資訊,最直覺的做法是找出 Acrobat Reader EXE 檔(AcroRd32.exe)路徑,在 .NET 程式透過 Process.Start() 傳入 PDF 路徑及 /p /h 參數呼叫 Acrobat Reader 列印檔案。但這個做法有個小缺點,它限制使用者必須安裝特定 PDF 閱讀軟體,再不然程式就得夠彈性,支援各種可列印 PDF 的軟體,如此尋找及識別 PDF 軟體邏輯將複雜化。
在 Stackoverflow 看到一個好方法,由於 Windows 多半會預設 PDF 開啟程式,並且還會註冊開啟、列印等動作,方便使用者透過檔案總管右鍵選單直接列印:
探索其背後原理,是 Acrobat Reader 先在 .pdf 副檔名註冊 UserChoice/ProgId = AcroExch.Document.11
而 AcroExch.Document.11 註冊了 Print/Command 對應到先前說過的列印指令: AcroRd32.exe /p /h "%1":
透過以上 Registry,當我們對 PDF 檔下達 Print Verb 時,Windows 便會找到對應程式並執行列印,不管它是 Acrobat Reader 還是 Foxit Reader,遠比指定並尋找特定軟體的做法更具彈性。以下為 Stackoverflow 找到的範例程式:
privatevoid SendToPrinter()
{
ProcessStartInfo info = new ProcessStartInfo();
info.Verb = "print";
info.FileName = @"c:\output.pdf";
info.CreateNoWindow = true;
info.WindowStyle = ProcessWindowStyle.Hidden;
Process p = new Process();
p.StartInfo = info;
p.Start();
p.WaitForInputIdle();
System.Threading.Thread.Sleep(3000);
if (false == p.CloseMainWindow())
p.Kill();
}
仿照上述方法寫好第一版,丟給使用者測試後馬上被打槍-程式在列印多頁報表時會掉頁,例如 6 頁只印完 4 頁就沒了。
推敲其原因,由於 AcroRd32 非標準的命令列程式,無法等待程式執行結束,啟動程式後控制權即回到呼叫端,故範例程式的做法是等待三秒,假設文件已列印完畢即強制關閉 PDF 程式,造成 AcroRd32 6 頁只列了 4 頁就被關掉的狀況。(飄向北方才唱到咀嚼爆肚涮羊就被卡歌來著)
把 3 秒等待時間加長是種鋸箭做法,但魔術數字註定要糾結於「空等 vs 不足」的兩難。最後,我想出一個好方法-監測列印佇列(PrintQueue)。呼叫 AcroRd32 後先等待列印文件出現在 PrintQueue,再等待其列印完畢從佇列消失,最長等待時間則拉長到 180 秒,確保每個 PDF 都印好印滿,如此既沒有無謂等待,也沒有過早中止程式掉頁風險,新做法美妙到我想為自己起立鼓掌 XD(捻鬚而笑)
完整程式範例如下供大家參考:
//REF:https://stackoverflow.com/a/6106155/288936
publicstaticvoid Print(string filePath)
{
Status = PrintJobStatus.Printing;
Message = string.Empty;
try
{
logger.Debug($"Printing... {filePath}");
ProcessStartInfo info = new ProcessStartInfo();
info.Verb = "print";
info.FileName = filePath;
info.CreateNoWindow = true;
info.WindowStyle = ProcessWindowStyle.Hidden;
Process p = new Process();
p.StartInfo = info;
p.Start();
p.WaitForInputIdle();
//以下邏輯克服無法得知Acrobat Reader或Foxit Reader是否列印完成的問題
//最多等待180秒(假設所有檔案可在3分鐘內印完)
var timeOut = DateTime.Now.AddSeconds(180);
bool printing = false; //是否開始列印
bool done = false; //是否列印完成
//取純檔名部分,跟PrintQueue進行比對
string pureFileName = Path.GetFileName(filePath);
//限定最大等待時間
while (DateTime.Now.CompareTo(timeOut) < 0)
{
if (!printing)
{
//未開始列印前發現檔名相同的列印工作
if (CheckPrintQueue(pureFileName))
{
printing = true;
Console.WriteLine($"[{pureFileName}]列印中...");
}
}
else
{
//已開始列印後,同檔名列印工作消失表示列印完成
if (!CheckPrintQueue(pureFileName))
{
done = true;
Console.WriteLine($"[{pureFileName}]列印完成");
break;
}
}
System.Threading.Thread.Sleep(100);
}
try
{
//若程序尚未關閉,強制關閉之
if (false == p.CloseMainWindow())
p.Kill();
}
catch
{
}
if (!done)
{
Console.WriteLine($"無法確認報表[{pureFileName}]列印狀態!");
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {DateTime.Now:HH:mm:ss} {ex.Message}");
}
}
//需查詢 WMI 記得加入參照及 using System.Management;
privatestaticbool CheckPrintQueue(string file)
{
//尋找PrintQueue有沒有檔案相同的列印工作
string searchQuery =
"SELECT * FROM Win32_PrintJob";
var printJobs =
new ManagementObjectSearcher(searchQuery).Get();
return printJobs.Any(o => (string)o.Properties["Document"].Value == file);
}