Windows 業務應用程式的列印與 PDF 輸出 ── System.Drawing.Printing / WPF / 報表函式庫的取捨
· 小村 豪 · CSharp, .NET, WinForms, WPF, 列印, PDF, 報表, Windows 開發, 技術諮詢
Windows 業務應用程式的列印,並不是「用 PrintDocument 隨便畫一畫就結束」這麼簡單的事。分頁失效、留白跑掉、預覽和實際輸出不一致、只想要 PDF 卻硬是跳出列印對話框 ── 這些問題,大多是因為在還沒理解列印機制本身的情況下就動手實作所造成的。
本文將從「依需求該如何取捨」的角度,整理 WinForms 的 System.Drawing.Printing、WPF 的 FlowDocument/FixedDocument、PDF 輸出函式庫,以及透過 Excel/Word 產生報表這四種選項。
1. 先講結論
- 簡單的一覽表,或是既有 WinForms 資產的延伸,用
PrintDocument就足夠。 分頁的機制是重複呼叫PrintPage事件,直到HasMorePages變成false為止,忘記處理這一點,第二頁之後就不會輸出。12 - 列印的 DPI 和畫面的 DPI 是兩件不同的事。
PageSettings.PrinterResolution代表印表機的解析度,留白則分成兩層:PageSettings.HardMarginX/HardMarginY(印表機本身無法列印的物理區域)與Margins(應用程式指定的邏輯留白)。如果直接把畫面座標拿來套用在列印上,就會因為這兩套 DPI、座標系統的差異而使版面跑掉。345 - WPF 中,Windows 標準的列印路徑分成 GDI 列印路徑與 XPS 列印路徑兩套。 WPF 應用程式原本就使用 XPS 列印路徑,若要送到不支援 XPSDrv 的印表機,系統會自動轉換成 GDI 格式。67
- WPF 的文件在「流式排版」的
FlowDocument與「固定版面」的FixedDocument之間,設計思路完全不同。FlowDocument以可讀性優先,會依視窗大小、解析度重新排版;FixedDocument則以顯示、列印裝置的忠實度優先。89 - 紙張與紙匣的指定,在 WPF 中用
PrintDialog與PrintTicket/PrintQueue來處理。 列印前用PrintQueue.MergeAndValidatePrintTicket針對印表機的實際能力進行驗證與合併,是標準做法。1011 - Microsoft Print to PDF 是以互動式操作為前提設計的列印驅動程式。 它的定位是 OS 內建的列印佇列與驅動程式套件,基本架構是每次執行都會跳出對話框讓你選擇儲存位置,因此不適合無人值守的批次處理。12
- 從伺服器或服務進行的 Office COM 自動化,微軟官方明確表示不予支援、不建議使用。 Office 的設計前提是互動式桌面與登入使用者的設定檔,在無人值守、非互動式的環境中執行,可能發生不穩定行為或死結。13
- Windows 服務在 Session 0 中運作,因此無法顯示對話框,也無法依賴綁定在使用者工作階段上的預設印表機。 設計涉及列印的常駐處理時,請先意識到這個邊界。14
- 長時間運作時忘記釋放 GDI 物件(控制代碼),會達到每個行程的上限而導致當機。 這個上限是可透過登錄檔
GDIProcessHandleQuota調整的有限數值,並非可以無限取得。15
判斷表 ── 需求 × 手段
| 需求 | 適合的手段 | 理由 |
|---|---|---|
| 想列印簡單的一覽表、單據等少量頁面(WinForms) | PrintDocument |
只用標準類別即可完成,可直接沿用既有的 GDI+ 繪圖資產 |
| 罫線很多的報表、多種紙張、複雜版面 | 報表函式庫,或用 FixedDocument 做固定版面 |
自行撰寫座標計算、分頁控制、日文排版的成本很高 |
| 大量批次列印(夜間無人執行) | 以程式方式呼叫 PrintDocument.Print() / 直接送到 XpsDocumentWriter |
必須完全不顯示任何對話框,也要避免依賴預設印表機 |
| 只需要儲存 PDF(不使用列印功能) | 用 PDF 產生函式庫直接產生 PDF | 不依賴列印對話框、預設印表機、佇列狀態 |
| 必須有預覽、公司內部確認流程很重要 | PrintPreviewDialog(WinForms)、DocumentViewer(WPF) |
可直接沿用列印前確認的標準 UI |
| 想沿用既有的 Excel 範本 | 用 Open XML SDK 等方式直接產生,或有限度地使用 COM 自動化 | 可重複利用既有資產,但應避免在常駐服務中使用 COM 自動化 |
以下針對各項目詳細說明。
2. 認識 Windows 列印架構的最低限度知識
Windows 的列印架構由列印佇列和各印表機專用的驅動程式組成。應用程式只需要呼叫不依賴裝置的函式,就能把列印工作送到雷射印表機、向量繪圖機、點陣印表機、傳真機等各種輸出裝置。16
列印路徑大致分成兩套。
- GDI 列印路徑。 Win32 GDI 應用程式列印時,GDI 圖形引擎會把繪圖指令以 EMF(增強型元檔案)方式排入佇列,或是與印表機驅動程式配合,直接繪製可列印的影像後送到佇列。傳統的 WinForms 應用程式走的是這條路徑。166
- XPS 列印路徑。 這是給以 XPS(XML Paper Specification)為基礎的印表機驅動程式使用的路徑,WPF 應用程式原本就使用這條路徑,以 XPS 文件的格式送進佇列。如果目標印表機不是 XPSDrv 驅動程式,就會自動轉換成 GDI 格式後再送出。67
為什麼畫面上看到的樣子和列印結果會不一致? 最常見的原因是搞混了 DPI。畫面的 Graphics 通常以 96 DPI 前後建立座標系統,但印表機的 Graphics 是依 PageSettings.PrinterResolution 決定的,多數情況下是 600 DPI 前後、遠高於畫面的解析度。3 如果把為畫面計算好的像素座標直接丟給列印用的 Graphics,繪製出來的結果就會比預期小很多(或依高解析度設定而變大)。列印時應該一律以「百分之一英吋」或實際尺寸(公分、英吋)為座標基準,讓設計不依賴 Graphics 的解析度,這樣比較安全。
另一個容易被忽略的地方,是印表機本身具有的物理不可列印區域。多數印表機在紙張邊緣數毫米範圍內無法列印。這個物理限制可以透過 PageSettings.HardMarginX/HardMarginY 取得,官方文件也說明這代表「印表機所設定的物理留白」。4 應用程式側指定的邏輯留白則是 PageSettings.Margins(預設為上下左右各 1 英吋),但即使把它設為 0,也無法輸出到比印表機硬邊界更內側的位置。5 「明明把留白設成 0,邊緣卻被切掉/位置比預期偏移」這類問題,多半就是沒有意識到硬邊界的存在所造成的。
3. WinForms:System.Drawing.Printing 的實務
WinForms 的列印以 PrintDocument 元件為核心來搭建。基本流程是:建立 PrintDocument,設定 PrinterSettings 或 DefaultPageSettings,在 PrintPage 事件中進行實際繪圖,再呼叫 Print() 方法開始列印。1
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Printing;
using System.Windows.Forms;
public sealed class ReportPrinter
{
// 要列印的資料。假設每一行對應一筆單據明細
private readonly IReadOnlyList<string> _lines;
private int _lineIndex;
public ReportPrinter(IReadOnlyList<string> lines) => _lines = lines;
public void Print(string printerName)
{
using var document = new PrintDocument();
document.DocumentName = "訂單明細";
if (!string.IsNullOrEmpty(printerName))
{
document.PrinterSettings.PrinterName = printerName;
}
// 指定了不存在的印表機名稱時,這裡會變成 false(無法用來偵測離線狀態)
if (!document.PrinterSettings.IsValid)
{
throw new InvalidOperationException(
$"找不到指定的印表機: {printerName}");
}
_lineIndex = 0;
document.PrintPage += Document_PrintPage;
document.Print();
}
private void Document_PrintPage(object? sender, PrintPageEventArgs e)
{
// MarginBounds 是反映邏輯留白(Margins)後的可繪製區域。
// PageBounds 是整張紙,即使畫在硬邊界內側也可能被裁切
using var font = new Font("Meiryo", 10);
float lineHeight = font.GetHeight(e.Graphics);
float y = e.MarginBounds.Top;
while (_lineIndex < _lines.Count)
{
if (y + lineHeight > e.MarginBounds.Bottom)
{
// 這一頁畫到這裡為止,還有剩餘內容,換到下一頁
e.HasMorePages = true;
return;
}
e.Graphics.DrawString(_lines[_lineIndex], font, Brushes.Black,
e.MarginBounds.Left, y);
y += lineHeight;
_lineIndex++;
}
// 所有行都畫完了,這就是最後一頁
e.HasMorePages = false;
}
}
「第二頁沒有輸出」這種問題的典型模式,是忘記把 HasMorePages 明確設為 false(預設值就是 false),或者相反地,條件判斷寫錯,導致一直維持 false 就跳出去了。PrintPage 事件的機制是會不斷重複觸發,直到這個屬性變成 false 為止,請務必先明確判斷「是否還有尚未畫完的行」,再設定這個屬性。12 如果想針對不同頁面套用不同設定(如紙張尺寸或方向),除了 PrintPage 之外,也可以善用 QueryPageSettings 事件。
「留白跑掉」這種問題的典型模式,是以 e.Graphics.VisibleClipBounds 或 e.PageBounds 為基準來組合座標,而忽略了 MarginBounds(反映邏輯留白後的區域)與 HardMarginX/HardMarginY(印表機的物理限制)。4 特別容易發生的事故是:開發機的印表機硬邊界較小,剛好沒有出現問題,但交付對象換了另一款機型、硬邊界較大時,罫線或項目就會被切掉一部分看起來不完整。跨機型的列印驗證,正是開發階段容易被忽略的一個環節。
如果要提供預覽功能,可以使用 PrintPreviewDialog,只要把同一個 PrintDocument 指定給 Document 屬性,就能直接沿用 PrintPage 事件的邏輯。1 若要依印表機解析度切換輸出結果,可以從 PrinterSettings.PrinterResolutions 取得可選清單,再設定到 PageSettings.PrinterResolution。3
4. WPF:FlowDocument / FixedDocument 與 PrintDialog
在 WPF 中,該以 FlowDocument 還是 FixedDocument 為主軸,取決於文件本身的性質。
FlowDocument。 依視窗大小、字型設定等執行時期的變數,動態重新排版內容,是以可讀性為優先的文件,標準內建搜尋、分頁、切換顯示模式等功能。8FixedDocument。 由應用程式端完全掌控版面,是以忠實度優先的 WYSIWYG 文件,適合需要對顯示、列印裝置有高精度重現需求的情境。9
以流式排版優先考慮可讀性的公司內部報告,適合用 FlowDocument;需要把罫線位置、項目位置固定到像素單位的單據或標籤,則適合用 FixedDocument,這是基本的判斷準則。
基本的列印用 System.Windows.Controls.PrintDialog 進行。這個類別和 System.Windows.Forms.PrintDialog 是不同的東西,是 WPF 專用的類別。10
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
public static class FlowDocumentPrinter
{
public static void Print(FlowDocument document, string jobName)
{
var printDialog = new PrintDialog();
// 只有使用者按下確定時才進行列印
if (printDialog.ShowDialog() != true)
{
return;
}
// FlowDocument 在顯示用途與列印用途上,頁面尺寸的前提並不相同,
// 因此要把列印用的頁面尺寸、留白,對齊印表機的可列印區域
var paginator = ((IDocumentPaginatorSource)document).DocumentPaginator;
paginator.PageSize = new Size(
printDialog.PrintableAreaWidth, printDialog.PrintableAreaHeight);
printDialog.PrintDocument(paginator, jobName);
}
}
如果想細部指定紙張尺寸、進紙匣、雙面列印等設定,可以使用 PrintQueue 與 PrintTicket/PrintCapabilities。PrintTicket 代表對列印工作的指示,PrintCapabilities 代表印表機實際支援的功能,標準做法是先用 PrintQueue.MergeAndValidatePrintTicket 把指定的設定合併、驗證成該印表機專屬的有效值,再拿來使用。711
using System.Linq;
using System.Printing;
public static class DuplexPrintTicketBuilder
{
public static PrintTicket BuildDuplexTicket(PrintQueue printQueue)
{
PrintCapabilities capabilities = printQueue.GetPrintCapabilities();
// 事先確認印表機是否支援雙面列印(長邊裝訂)
bool supportsDuplex = capabilities.DuplexingCapability
.Contains(Duplexing.TwoSidedLongEdge);
var requestedTicket = new PrintTicket();
if (supportsDuplex)
{
requestedTicket.Duplexing = Duplexing.TwoSidedLongEdge;
}
// 針對使用者的預設 PrintTicket,只合併雙面列印這一項要求。
// 不支援的項目會自動被替換成有效的值
ValidationResult result = printQueue.MergeAndValidatePrintTicket(
printQueue.UserPrintTicket, requestedTicket);
return result.ValidatedPrintTicket;
}
}
如果要用 FixedDocument 組出固定版面的報表,直接送進 XPS 列印路徑,可以用 XpsDocumentWriter 的 Write/WriteAsync 方法把工作加進 PrintQueue。如果目標是不支援 XPSDrv 的印表機,系統內部會自動轉換成 GDI 格式後再送出,呼叫端不需要意識到路徑的差異。7
5. PDF 輸出的選項
「想要輸出 PDF」這個需求,實際上會因為指的是下面三種情境中的哪一種而讓設計大不相同。
- 作為列印的延伸,讓使用者可以手動選擇儲存為 PDF 即可
- 想從應用程式內,透過列印對話框產生 PDF
- 完全不需要列印,只需要在無人值守的批次作業中大量產生 PDF 檔案
Microsoft Print to PDF 是為情境 1 和 2 設計的功能。 這是 Windows 10 以後標準內建、定位為列印佇列與驅動程式套件的功能,從列印對象應用程式的角度看,它就是一台普通印表機。12 只不過它的實體是「每次列印都會詢問儲存檔名」這種以互動式操作為前提的機制,官方文件也只提供「管理員可以從映像中移除該列印佇列與驅動程式套件」這種單位的控制手段。12 若要在無人值守、大量的批次處理中固定檔案路徑、安靜地輸出 PDF,也就是情境 3 的用途,這個功能在設計上並不適合。要實現情境 3,比較直接的做法是不經過列印管線,改用直接產生 PDF 的函式庫。
選擇 PDF 產生函式庫時的比較觀點,建議在推薦特定產品之前,先依下列軸線整理自家公司的需求,選型時就不容易搖擺。
| 觀點 | 該確認的事項 |
|---|---|
| 授權 | 是 OSS(MIT/Apache 系列,還是 GPL/AGPL 系列)還是商業授權。如果自家應用程式要對外部散布、販售,務必以第一手資料(授權條款本文、提供方官方網站)確認函式庫的授權條件(是否有原始碼公開義務、再散布條件、能否商用)是否和自家的散布形式相牴觸 |
| 中文字型的處理 | 是否支援中文字型的子集嵌入,是否能在不嵌入字型的情況下讓 PDF 本身帶有字型實體(在瀏覽端沒有該字型時是否會出現亂碼),若業務需要直式排版或異體字則要確認其支援狀況(原文為日文情境,中文使用者可依需求對應繁簡字型與直式排版的支援度) |
| 報表相關功能 | 是否支援以範本為基礎的報表定義、條碼/二維條碼產生、電子簽章、PDF/A(長期保存規格)支援、能否輕鬆把既有紙本報表的版面直接移植過來 |
| 相依性、執行環境 | 能否在伺服器環境(無 GUI 的 Windows Server、容器)中運作,是否依賴原生函式庫,在 .NET Core/.NET(非 Framework)環境下的實際運作情況 |
| 效能 | 大量頁面、大量工作批次產生時的記憶體使用量、平行產生時的行為 |
授權條件會因產品和版本而異,務必在採用前確認提供方的官方文件或授權條文本身。本文不推薦特定函式庫名稱或產品,只呈現比較時該考慮的「觀點」。
6. 透過 Excel / Word 輸出報表的選項
如果已經有用 Excel 範本運作的既有報表,比起從零重新打造,沿用既有資產的架構有時反而更現實。方式大致分成兩種。
- 用 Open XML SDK 等方式直接產生、編輯 xlsx/docx。 不啟動 Excel/Word 的執行檔,直接讀寫檔案格式(ZIP + XML),因此在伺服器環境也能穩定運作。這種做法很適合沿用範本的罫線、格式,只置入數值的架構,詳細內容整理在「Excel 報表輸出該怎麼做 - COM 自動化 / Open XML / 範本方式的判斷表」。
- 用 Excel COM Interop 實際操作 Excel.Application。 這適合想直接沿用帶有巨集或複雜重新計算邏輯的既有 Excel 活頁簿,但容易因為 COM 參照沒有釋放而導致 EXCEL.EXE 行程殘留,對策與判斷基準整理在「C# 操作 Excel 時 EXCEL.EXE 殘留的問題」。
這裡重要的是,微軟官方明確表示,在伺服器端或 Windows 服務中進行 Office COM 自動化屬於「不建議、不支援」。Office 應用程式的設計前提是互動式桌面與已登入使用者的設定檔,若在無人值守、非互動式環境中執行,可能會發生不穩定行為或死結。13 在授權方面,透過伺服器端自動化把 Office 功能提供給用戶端使用的用法,也通常不在一般 EULA 的預期範圍內。13
因此,如果想把報表輸出納入常駐服務或批次工作,即使沿用既有的 Excel 範本,也應該避免在執行時真的啟動 Excel.exe 的架構(COM Interop),改以 Open XML SDK 等方式直接產生檔案較為安全。如果真的需要 COM Interop,比較現實的切分方式,是把它做成在互動式登入的使用者工作階段中運作的常駐應用程式(而非服務)。
7. 實務上的陷阱
從 Windows 服務、Session 0 進行列印
想把列印處理放進常駐服務的諮詢並不少見,但 Windows 服務是在Session 0 中運作的。Session 0 是 Windows Vista 以後保留給服務與不涉及互動使用者的行程專用的工作階段,與使用者的互動工作階段是分離的。14 因為這個分離機制,服務即使顯示對話框,使用者也看不到,如果寫了等待使用者輸入的程式碼(例如等待列印對話框顯示),看起來就會像卡住一樣。14
實務上另一個容易踩到的陷阱,是網路印表機或使用者個別對應印表機的可見性。使用者登入後連接的網路印表機,多半是綁定在該使用者的互動工作階段上才能看到,從別的工作階段(在 Session 0 運作的服務行程情境)看起來不一定相同。涉及列印的常駐處理,一定要先確認「用哪個帳戶、哪個工作階段,可以看到哪台印表機」。Session 分離的整體概念,整理在「如何理解 Windows 的工作階段分離」;服務的實作與維運基礎則整理在「Windows 服務的建立與維運」。
印表機不存在、離線時的行為
如果在 PrinterSettings.PrinterName 指定了不存在的印表機名稱,或是在沒有設定預設印表機的環境中執行,嘗試列印的當下就會發生例外或失敗。至少應該做到兩點:先用 PrinterSettings.IsValid 事先驗證,以及針對列印失敗設計好重試與錯誤通知的機制。網路印表機暫時離線時,即使工作已經排進佇列,實際輸出可能仍停滯不前,因此不要把列印結果只當成「已送出」就結束,必要時應設計確認工作狀態的機制。
依賴預設印表機的風險
不明確指定 PrinterSettings.PrinterName、一律依賴「預設印表機」的做法,隨著運作時間拉長,風險也會愈來愈高。原因是使用者可能把預設印表機改成別台,或是把筆電帶到別的地點,導致預設印表機發生變化,結果被送到非預期的輸出裝置。業務上重要的報表,應該用設定檔或應用程式內的設定畫面明確保存列印目標印表機名稱,設計成不會被預設印表機的變化所牽動。
長時間運作下的 GDI 控制代碼流失(Handle Leak)
如果在列印處理中沒有用 using 或 Dispose() 確實釋放 Graphics、Font、Brush、Pen 等 GDI/GDI+ 資源,長時間運作後 GDI 物件的控制代碼會耗盡,導致與繪圖相關的 API 呼叫開始失敗。GDI 物件的控制代碼在每個行程上都有預設上限,可以透過登錄檔的 GDIProcessHandleQuota 在 256 到 65,536 之間調整,是一種有限的資源。15 愈頻繁進行列印的常駐應用程式,這種洩漏問題就愈容易變成只在正式環境才會顯現的問題。控制代碼洩漏的排查與切分方法,在以工業相機長時間運作當機案例為題的「工業相機控制應用跑一個月後突然崩潰時(前篇) - handle leak 的找法與長時間運轉用的日誌設計」中有具體說明。
相關文章
- Excel 報表輸出該怎麼做 - COM 自動化 / Open XML / 範本方式的判斷表
- C# 操作 Excel 時 EXCEL.EXE 殘留的問題 ── COM 參照的釋放模式與替換的判斷
- Windows 服務的建立與維運 ── 從工作排程器的取捨到 BackgroundService 服務化
- 如何理解 Windows 的工作階段分離 ── Session 0、RDP、多使用者同時執行
- 工業相機控制應用跑一個月後突然崩潰時(前篇) - handle leak 的找法與長時間運轉用的日誌設計
相關諮詢領域
合同會社小村軟體(KomuraSoft LLC)承接 Windows 業務應用程式列印、報表輸出功能的設計、活用既有 Excel 資產重新打造報表,以及從常駐服務進行列印、輸出設計的技術諮詢。
參考連結
-
Microsoft Learn, PrintDocument Class。關於
PrintDocument的角色(設定DocumentName、PrinterSettings,再用Print()開始列印),以及從 WPF 列印時應使用System.Printing命名空間的說明。 ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, PrintPageEventArgs.HasMorePages Property。關於預設值為
false,以及PrintPage事件會重複觸發直到這個屬性變成false為止。 ↩ ↩2 -
Microsoft Learn, PageSettings.PrinterResolution Property。關於頁面列印解析度的預設值為印表機的預設解析度,以及可以從
PrinterSettings.PrinterResolutions取得可選解析度清單。 ↩ ↩2 ↩3 -
Microsoft Learn, PageSettings.HardMarginX Property。關於硬邊界代表印表機所設定的物理留白。 ↩ ↩2 ↩3
-
Microsoft Learn, PageSettings.Margins Property。關於頁面留白的預設值是上下左右各 1 英吋,以及在
PrintPage事件中可以搭配Bounds屬性計算可列印區域。 ↩ ↩2 -
Microsoft Learn, Windows Print Path Overview。關於 Windows 存在 GDI 列印路徑(源自 Win32 應用程式)與 XPS 列印路徑(源自 WPF 應用程式或 XPS Print API)兩條主要列印路徑。 ↩ ↩2 ↩3
-
Microsoft Learn, Printing documents overview。關於 WPF 應用程式使用 XPS 列印路徑、透過
PrintDialog進行的基本列印、透過PrintTicket/PrintCapabilities/PrintQueue/XpsDocumentWriter進行的進階列印,以及對不支援 XPSDrv 印表機自動轉換為 GDI 格式的說明。 ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Flow Document Overview。關於
FlowDocument是依視窗大小、解析度等執行時期變數重新排版內容、以可讀性為優先的文件。 ↩ ↩2 -
Microsoft Learn, FixedDocument Class。關於
FixedDocument是以忠實重現顯示、列印裝置為優先的 WYSIWYG 設計,與FlowDocument的設計思路不同。 ↩ ↩2 -
Microsoft Learn, PrintDialog Class。關於
System.Windows.Controls.PrintDialog是依使用者輸入設定PrintTicket與PrintQueue的對話框,以及它與System.Windows.Forms.PrintDialog是不同的類別。 ↩ ↩2 -
Microsoft Learn, How to: Validate and Merge PrintTickets。關於用
PrintQueue.GetPrintCapabilities確認印表機的支援功能,再用MergeAndValidatePrintTicket把要求內容合併、驗證成印表機專屬有效PrintTicket的步驟。 ↩ ↩2 -
Microsoft Learn, RemoveMPDW。關於 Microsoft Print to PDF 是預設安裝的選用功能,並以列印佇列與驅動程式套件為單位進行設定、移除。 ↩ ↩2 ↩3
-
Microsoft Support, Considerations for server-side Automation of Office。關於微軟不建議、不支援從包含 ASP/ASP.NET/DCOM/NT 服務的無人值守、非互動式用戶端應用程式進行 Office 自動化,以及 Office 的設計前提是互動式桌面與使用者設定檔。 ↩ ↩2 ↩3
-
Microsoft Learn, Service Changes for Windows Vista - Session 0 Isolation。關於 Windows Vista 以後,Session 0 保留給服務與不涉及互動使用者的應用程式專用,服務也因此無法直接顯示對話框。 ↩ ↩2 ↩3
-
Microsoft Learn, GDI Objects。關於 GDI 物件的控制代碼在每個行程上有預設上限,可以透過登錄檔
GDIProcessHandleQuota在 256 到 65,536 之間調整上限。 ↩ ↩2 -
Microsoft Learn, Introduction to printing。關於 Windows 的列印架構由列印佇列與印表機驅動程式組成,以及 Win32 GDI 應用程式的繪圖指令以 EMF 方式排入佇列的機制。 ↩ ↩2
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
用 PerfView 與 dotnet-trace 找出「變慢」的原因 ── .NET 效能調查實務入門
當商用應用程式「變慢」「CPU 卡住不動」「偶爾當掉」時,該用哪個工具看什麼?本文整理 PerfView 與 dotnet-trace 的角色分工、CPU 取樣的讀法(inclusive/exclusive)、用 ThreadTime 調查阻塞時間,以及與 EventSou...
不只是 appsettings.json ── Windows 商用應用程式的組態管理實務(環境別設定・機密資訊・寫入位置)
本文從實務角度整理 Windows 商用應用程式的組態管理,涵蓋 appsettings.json 的分層、IConfiguration 與 IOptions/IOptionsSnapshot/IOptionsMonitor 的選用方式、環境別設定、可寫入設定的存放位置、機...
在 WinForms/WPF 應用程式中導入 Entra ID 驗證 ── MSAL.NET 與 WAM 代理的實務架構
從實務角度整理在 WinForms/WPF 桌面應用程式中導入 Entra ID(原 Azure AD)驗證的做法。內容涵蓋公用客戶端的概念、ROPC 廢除現況、應用程式註冊、MSAL.NET 的 AcquireTokenSilent 模式、WAM 代理、權杖快取持久化,以...
委外・委託開發 Windows 應用程式前該整理的事項
在委外・委託開發 Windows 應用程式之前,整理既有軟體改版、設備整合、COM/ActiveX、發布與更新、維護等應留意的重點。
把 Generic Host / BackgroundService 帶進桌面應用程式的理由 - 啟動・壽命・graceful shutdown 的整理會輕鬆很多
整理把 .NET Generic Host 與 BackgroundService 帶進 WPF / WinForms 桌面應用程式的理由,把啟動、lifetime、graceful shutdown 集中於入口管理。透過 StartAsync / ExecuteAsync...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
UI 執行緒 & 計時器
整理 WPF / WinForms UI 執行緒、非同步流程、Dispatcher 使用、計時器判斷的主題頁面。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
技術諮詢 & 設計審查
協助釐清設計方向、架構邊界、生命週期責任,以及既有 Windows 資產的處理方式。
常見問題
整理諮詢這個主題時常見的問題。
- PrintDocument 和 WPF 的列印,該用哪一種來做?
- 如果畫面是 WinForms,就用 PrintDocument;如果是 WPF,就用 FlowDocument/FixedDocument,這是最直接的判斷。沒有必要混用兩種技術,配合既有應用程式的畫面框架才是實務上的判斷基準。若是全新選用 WPF,建議先決定報表是以流式排版為主、還是以固定版面為主,再決定要以 FlowDocument 還是 FixedDocument 為主軸,之後就不會猶豫。
- 報表函式庫該不該買?
- 如果罫線很多的複雜報表、需要支援多種紙張尺寸、需要嵌入日文字型或直式排版等,自行用 GDI+/WPF 繪圖來做的成本很高,那麼導入報表函式庫的成本通常反而更划算。反過來說,如果只是 A4 一覽表這種簡單報表,用 PrintDocument 或 FixedDocument 自行實作往往就足夠了,建議先評估需求的複雜度,再做判斷比較安全。
- 只產生 PDF、不使用列印功能的架構可行嗎?
- 完全可行。PDF 終究只是輸出格式的一種,不依賴列印對話框或預設印表機、直接用 PDF 函式庫產生檔案的做法,通常和批次處理、自動化的相容性更好。Microsoft Print to PDF 是以互動式操作為前提設計的驅動程式,並不適合無人值守的 PDF 產生作業。
- 可以支援標籤印表機或收據印表機嗎?
- 可以支援,但單靠通用的 PrintDocument/FixedDocument 延伸做法,有時會難以應付。多數標籤印表機擁有自己的指令語言或 SDK,往往需要透過與 Windows 標準列印路徑不同的管道來控制。建議先確認目標機型是以 Windows 標準印表機驅動程式的方式運作,還是需要透過專用 SDK 來控制,再決定後續設計。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。