Quantcast
Channel: Darkthread
Viewing all articles
Browse latest Browse all 428

CODE-使用 C# 批次列印 PDF 檔案

$
0
0

專案遇到批次列印 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);
}

Viewing all articles
Browse latest Browse all 428

Trending Articles