在 .NET 中,System.Drawing.Printing 命名空間提供了強大的列印引擎,但要輸出專業級報表(頁首/頁尾、多欄、分頁、精確邊距),仍需深入理解事件模型、座標系統與 Win32 互操作。本文整理五大核心場景,附完整程式碼片段,助你快速掌握列印開發精髓。
PrintDocument 透過三個核心事件控制列印生命週期:
e.HasMorePages。Dispose() 字型/筆刷)。private PrintDocument _doc;
private Font _headerFont, _bodyFont;
private StreamReader _data;
private void Form_Load(object sender, EventArgs e)
{
_doc = new PrintDocument { DocumentName = "客戶聯繫清單" };
_doc.BeginPrint += Doc_BeginPrint;
_doc.PrintPage += Doc_PrintPage;
_doc.EndPrint += Doc_EndPrint;
_doc.QueryPageSettings += Doc_QueryPageSettings; // 可選:調整紙張/方向
}
private void Doc_BeginPrint(object sender, PrintEventArgs e)
{
_data = new StreamReader("contacts.csv");
_headerFont = new Font("Arial", 24, FontStyle.Underline);
_bodyFont = new Font("Arial", 12);
}
private void Doc_EndPrint(object sender, PrintEventArgs e)
{
_data?.Close();
_headerFont?.Dispose();
_bodyFont?.Dispose();
}
將所有列印相關程式碼封裝於 CustomPrintDocument : PrintDocument,覆寫 OnBeginPrint、OnPrintPage、OnEndPrint、OnQueryPageSettings,實現「單一職責、可複用、可測試」。
public class ContactListPrintDoc : PrintDocument
{
private readonly StreamReader _data;
private Font _headerFont, _bodyFont;
public ContactListPrintDoc(StreamReader data) { _data = data; }
protected override void OnBeginPrint(PrintEventArgs e)
{
base.OnBeginPrint(e);
_headerFont = new Font("Arial", 24);
_bodyFont = new Font("Arial", 12);
}
protected override void OnPrintPage(PrintPageEventArgs e)
{
base.OnPrintPage(e);
// 繪製邏輯...
e.HasMorePages = _data.Peek() >= 0;
}
protected override void OnEndPrint(PrintEventArgs e)
{
_data?.Close();
_headerFont?.Dispose();
_bodyFont?.Dispose();
base.OnEndPrint(e);
}
}
.NET e.MarginBounds 僅反映使用者設定的邊距,無法得知印表機物理不可列印區域。必須呼叫 Win32 GetDeviceCaps 取得 PHYSICALOFFSETX/Y、HORZRES/VERTRES、HORZSIZE/VERTSIZE,換算出真實硬邊距(單位:百分之一英寸)。
internal sealed class HardMargins
{
[DllImport("gdi32.dll", ExactSpelling = true)]
private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
private const int PHYSICALOFFSETX = 112, PHYSICALOFFSETY = 113;
private const int HORZRES = 8, VERTRES = 10, HORZSIZE = 4, VERTSIZE = 6;
public float Left { get; }, Top { get; }, Right { get; }, Bottom { get; };
public HardMargins(IntPtr hdc)
{
float offX = GetDeviceCaps(hdc, PHYSICALOFFSETX);
float offY = GetDeviceCaps(hdc, PHYSICALOFFSETY);
float resX = GetDeviceCaps(hdc, HORZRES);
float resY = GetDeviceCaps(hdc, VERTRES);
float szX = GetDeviceCaps(hdc, HORZSIZE) / 25.4f; // 公釐轉英寸
float szY = GetDeviceCaps(hdc, VERTSIZE) / 25.4f;
float ppiX = resX / szX;
float ppiY = resY / szY;
Left = (offX / ppiX) * 100f; // .NET 使用 1/100 英吋
Top = (offY / ppiY) * 100f;
Right = Left + szX * 100f;
Bottom = Top + szY * 100f;
}
}
IntPtr hdc = e.Graphics.GetHdc();
try
{
var hm = new HardMargins(hdc);
float left = e.MarginBounds.Left - hm.Left;
float top = e.MarginBounds.Top - hm.Top;
float right = e.MarginBounds.Right;
float bottom = e.MarginBounds.Bottom;
// 後續以 left/top/right/bottom 為基準佈局
}
finally { e.Graphics.ReleaseHdc(hdc); }
使用 RectangleF 切割頁面,配合 Graphics.DrawString、StringFormat 實現置中、換行、分頁。
float headerH = _headerFont.GetHeight(e.Graphics);
float footerH = _bodyFont.GetHeight(e.Graphics);
var headerRect = new RectangleF(left, top, pageWidth, headerH);
var footerRect = new RectangleF(left, bottom - footerH, pageWidth, footerH);
var bodyRect = new RectangleF(left, headerRect.Bottom, pageWidth,
pageHeight - headerH - footerH);
private int _currentPage = 0;
private void Doc_PrintPage(object sender, PrintPageEventArgs e)
{
_currentPage++;
// 1. 取得硬邊距、計算可列印區域(同上)
// 2. 繪製頁首
CenterText(e.Graphics, "北風貿易客戶聯繫清單", _headerFont, Brushes.Black, headerRect);
// 3. 計算每頁可容納行數
float lineH = _bodyFont.GetHeight(e.Graphics);
int linesPerPage = (int)(bodyRect.Height / lineH) - 1;
int currentLine = 2; // 頁首佔 2 行
// 4. 逐行讀取 CSV 並繪製
string line;
while (currentLine < linesPerPage && (line = _data.ReadLine()) != null)
{
var cols = line.Split(',');
float y = bodyRect.Top + currentLine * lineH;
e.Graphics.DrawString(cols[0], _bodyFont, Brushes.Black, bodyRect.Left, y);
e.Graphics.DrawString(cols[1], _bodyFont, Brushes.Black, bodyRect.Left + 20, y);
e.Graphics.DrawString("Phone: " + cols[2], _bodyFont, Brushes.Black, bodyRect.Left + 20, y + lineH);
e.Graphics.DrawString("Fax: " + cols[3], _bodyFont, Brushes.Black, bodyRect.Left + 20, y + 2 * lineH);
currentLine += 4;
e.Graphics.DrawLine(Pens.Black, left, y + 3 * lineH, right, y + 3 * lineH);
}
// 5. 繪製頁碼於頁尾
CenterText(e.Graphics, $"第 {_currentPage} 頁", _bodyFont, Brushes.Black, footerRect);
// 6. 決定是否續印
e.HasMorePages = _data.Peek() >= 0;
}
private void CenterText(Graphics g, string text, Font font, Brush brush, RectangleF rect)
{
var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
g.DrawString(text, font, brush, rect, sf);
}
PrintForm.cs:WinForms 表單,含「預覽」、「列印」按鈕、狀態列。HardMargins.cs:P/Invoke 封裝類別。contacts.csv:測試資料(放置於輸出目錄)。Form_Load,預覽使用 PrintPreviewDialog,列印使用 PrintDialog。Graphics.PageUnit = GraphicsUnit.Inch; PageScale = 0.01f 讓座標以 1/100 英吋運算,與 MarginBounds、硬邊距單位一致。StreamReader 必須在 EndPrint 或 Dispose 中釋放,避免 GDI 物件洩漏。GetHdc/ReleaseHdc 必須成對出現於 try/finally。<dpiAwareness>PerMonitorV2</dpiAwareness>,列印輸出不會模糊。掌握上述核心機制,你就能在 .NET 中實現從簡單單據到複雜商業報表的各類列印需求,且程式碼結構清晰、資源管理嚴謹、可長期維護。