首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

.NET 列印核心技術深度解析:從 PrintDocument 到分頁佈局與硬邊距處理

     編輯:關於C語言

前言:列印不只是「把內容丟給印表機」

在 .NET 中,System.Drawing.Printing 命名空間提供了強大的列印引擎,但要輸出專業級報表(頁首/頁尾、多欄、分頁、精確邊距),仍需深入理解事件模型、座標系統與 Win32 互操作。本文整理五大核心場景,附完整程式碼片段,助你快速掌握列印開發精髓。

一、 事件驅動模型:BeginPrint / PrintPage / EndPrint

PrintDocument 透過三個核心事件控制列印生命週期:

  • BeginPrint:列印開始前,配置字型、開啟資料源(檔案/資料庫連線)。
  • PrintPage:每一頁 觸發一次,繪製內容、計算分頁、設定 e.HasMorePages。
  • EndPrint:列印結束,釋放資源(關閉檔案、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();
}

二、 封裝列印邏輯:繼承 PrintDocument

將所有列印相關程式碼封裝於 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);
    }
}

三、 硬邊距:P/Invoke GetDeviceCaps 取得真實可列印區域

.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;
    }
}

在 PrintPage 中套用

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 定義頁首/主體/頁尾區塊

使用 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);

五、 PrintPage 核心邏輯:分頁、資料繫結、頁碼

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。

七、 實務提醒與最佳實踐

  1. 單位統一:Graphics.PageUnit = GraphicsUnit.Inch; PageScale = 0.01f 讓座標以 1/100 英吋運算,與 MarginBounds、硬邊距單位一致。
  2. 資源釋放:字型、筆刷、筆、StreamReader 必須在 EndPrint 或 Dispose 中釋放,避免 GDI 物件洩漏。
  3. 異常處理:GetHdc/ReleaseHdc 必須成對出現於 try/finally。
  4. 高 DPI 支援:.NET 4.7+ 設定 <dpiAwareness>PerMonitorV2</dpiAwareness>,列印輸出不會模糊。
  5. 複雜報表:超過本文範圍(多欄、跨頁表格、圖表)建議改用 RDLC、FastReport、QuestPDF 等專用報表引擎。

掌握上述核心機制,你就能在 .NET 中實現從簡單單據到複雜商業報表的各類列印需求,且程式碼結構清晰、資源管理嚴謹、可長期維護。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved