專案遇到批次列印 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 找到的範例程式:

private void 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
public static void 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; 
private static bool 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);
}

Comments

# by James

黑大你好, 最近剛好需要用到列印功能就試著來跑這個範例 可是遇到了點問題 (string)o.Properties["Document"].Value 這邊取出來的文件名稱 跟我要列印的文件名稱不一樣 我有debug去看 他取到的值是"列印文件" 而我的文件名稱是西元年月日 不知道黑大有沒有遇過這情形 能給點建議? -------------------------------------------------------------- 附上我的程式碼 string searchQuery = "SELECT * FROM Win32_PrintJob"; //抓取系統資訊 ManagementObjectSearcher printJobs = new ManagementObjectSearcher(searchQuery); foreach (ManagementObject mo in printJobs.Get()) { Job_Name = mo.Properties["Document"].Value.ToString(); } if (Job_Name == file) { return true; } else return false;

# by Jeffrey

to James, Document值由排入列印作業的軟體決定,以Word為例會是 "Microsoft Word - Review.doc" ( https://msdn.microsoft.com/en-us/library/aa394370(v=vs.85).aspx ),若列印程式沒提供檔案名稱就沒轍了。MSDN 論壇也有類似討論 https://social.msdn.microsoft.com/Forums/zh-TW/2a73e1ec-0a42-4a59-9156-c988e8f4f3aa/cprinter?forum=233

# by 老范

改用 PDF-XChange Viewer 列印就沒有需要結束程式的問題。

# by 米歐

黑大你好,首先先感謝黑大的分享,最近剛好也碰上相關問題。 我的環境(.net core 3.1)在使用黑大的範例程式碼時會產生 The specified executable is not a valid application for this OS platform. 的錯誤訊息。 這邊分享一下解決方式,幫助後來的人。 只需要在宣告 ProcessStartInfo info 變數時加入 info.UseShellExecute = true,即可解決問題。 參考來源:https://stackoverflow.com/questions/58102696/the-specified-executable-is-not-a-valid-application-for-this-os-platform

# by Jeffrey

to 米歐,謝謝你的回饋分享。

Post a comment