C# PrintDocument 打印事件深度解析:3步实现自定义图文混排与分页
在Windows应用程序开发中,打印功能一直是业务系统不可或缺的组成部分。从简单的文本输出到复杂的报表生成,打印功能的实现质量直接影响用户体验。本文将聚焦PrintDocument控件的核心事件机制,通过实战演示如何构建支持图文混排、表格绘制和智能分页的打印解决方案。
1. PrintDocument事件模型解析
PrintDocument控件作为.NET打印体系的核心,其事件驱动模型决定了打印过程的每个关键节点。理解这三个核心事件的触发时机和作用,是构建复杂打印功能的基础。
BeginPrint事件:打印作业开始前的初始化工作站。典型应用场景包括:
- 初始化数据源(数据库连接、文件读取等)
- 计算总页数(用于进度显示)
- 设置打印作业的全局变量
private void printDocument_BeginPrint(object sender, PrintEventArgs e) { // 初始化数据读取器 dataReader = GetReportData(); currentPageIndex = 0; totalPages = CalculateTotalPages(); }PrintPage事件:每页内容的绘制中枢。开发者需要重点处理:
- 使用Graphics对象进行绘制操作
- 计算内容布局和分页位置
- 设置HasMorePages属性决定是否继续打印
EndPrint事件:打印作业的收尾处理。常见用途:
- 释放数据源等资源
- 打印完成提示
- 日志记录
三个事件的执行顺序形成完整的打印生命周期:
BeginPrint → (PrintPage × N) → EndPrint2. 图文混排绘制技术
Graphics对象是打印内容绘制的核心工具,它提供丰富的绘制方法支持各种内容类型的输出。下面通过具体示例展示混合内容绘制的最佳实践。
2.1 基础绘制方法
文本绘制:
e.Graphics.DrawString( "销售报表", new Font("微软雅黑", 16, FontStyle.Bold), Brushes.Black, new PointF(100, 50));图像绘制:
Image logo = Image.FromFile("company_logo.png"); e.Graphics.DrawImage(logo, new Rectangle(400, 50, 120, 60));表格绘制:
// 绘制表头 e.Graphics.DrawRectangle(Pens.Black, new Rectangle(100, 150, 600, 30)); e.Graphics.DrawString("产品名称", tableFont, Brushes.Black, new PointF(110, 155)); // 绘制表格内容 for(int i=0; i<products.Count; i++) { float yPos = 180 + i * 25; e.Graphics.DrawString(products[i].Name, tableFont, Brushes.Black, new PointF(110, yPos)); e.Graphics.DrawString(products[i].Price.ToString("C"), tableFont, Brushes.Black, new PointF(310, yPos)); }2.2 布局计算技巧
实现专业级打印输出的关键在于精确的布局计算:
边距处理:
float leftMargin = e.MarginBounds.Left; float topMargin = e.MarginBounds.Top; float printableWidth = e.MarginBounds.Width;内容高度计算:
float lineHeight = font.GetHeight(e.Graphics); float currentY = topMargin; foreach(var line in contentLines) { if(currentY + lineHeight > e.MarginBounds.Bottom) { e.HasMorePages = true; return; } e.Graphics.DrawString(line, font, Brushes.Black, leftMargin, currentY); currentY += lineHeight; }多列布局:
float columnWidth = (printableWidth - columnGap) / 2; // 第一列 DrawColumn(e.Graphics, leftMargin, currentY, columnWidth, leftColumnData); // 第二列 DrawColumn(e.Graphics, leftMargin + columnWidth + columnGap, currentY, columnWidth, rightColumnData);
3. 智能分页控制策略
复杂文档的分页处理需要综合考虑内容类型、页面剩余空间和用户偏好。以下是三种典型场景的实现方案。
3.1 文本流分页
处理长文本时的分页算法:
private void PrintTextWithPagination(PrintPageEventArgs e) { float yPos = topMargin; int linesPrinted = 0; while (linesPrinted < linesPerPage && currentLine < documentLines.Count) { string line = documentLines[currentLine]; SizeF size = e.Graphics.MeasureString(line, printFont, printableWidth); if(yPos + size.Height > e.MarginBounds.Bottom) { e.HasMorePages = true; return; } e.Graphics.DrawString(line, printFont, Brushes.Black, leftMargin, yPos); yPos += size.Height; linesPrinted++; currentLine++; } e.HasMorePages = currentLine < documentLines.Count; }3.2 表格跨页处理
当表格行数超过单页容量时,需要特殊处理:
- 记录已打印行索引
- 在下一页继续打印剩余行
- 重复表头(可选)
private void PrintTableWithPagination(PrintPageEventArgs e) { float yPos = topMargin; // 打印表头 PrintTableHeader(e.Graphics, ref yPos); // 打印表格内容 while(currentRow < dataTable.Rows.Count && yPos < e.MarginBounds.Bottom - rowHeight) { PrintTableRow(e.Graphics, dataTable.Rows[currentRow], ref yPos); currentRow++; } e.HasMorePages = currentRow < dataTable.Rows.Count; }3.3 图文混排分页
混合内容的分页需要更复杂的逻辑:
private void PrintMixedContent(PrintPageEventArgs e) { float yPos = topMargin; // 1. 打印标题和logo PrintHeader(e.Graphics, ref yPos); // 2. 打印文本内容 yPos += PrintTextContent(e.Graphics, yPos, e.MarginBounds.Bottom - 100); // 3. 检查剩余空间是否足够打印图表 if(e.MarginBounds.Bottom - yPos > chartHeight) { PrintChart(e.Graphics, yPos); } else { e.HasMorePages = true; return; } // 4. 打印页脚 PrintFooter(e.Graphics, e.MarginBounds.Bottom - 30); }4. 完整实现示例
下面是一个完整的销售报表打印实现,包含上述所有技术要点:
public class SalesReportPrinter { private List<SaleRecord> records; private int currentRecordIndex; private Font headerFont = new Font("Arial", 14, FontStyle.Bold); private Font bodyFont = new Font("Arial", 10); public void PrintReport(List<SaleRecord> data) { records = data; currentRecordIndex = 0; PrintDocument doc = new PrintDocument(); doc.BeginPrint += Doc_BeginPrint; doc.PrintPage += Doc_PrintPage; doc.EndPrint += Doc_EndPrint; PrintPreviewDialog preview = new PrintPreviewDialog(); preview.Document = doc; preview.ShowDialog(); } private void Doc_BeginPrint(object sender, PrintEventArgs e) { // 初始化打印作业 currentRecordIndex = 0; } private void Doc_PrintPage(object sender, PrintPageEventArgs e) { float leftMargin = e.MarginBounds.Left; float topMargin = e.MarginBounds.Top; float yPos = topMargin; // 打印报表标题 e.Graphics.DrawString("月度销售报表", headerFont, Brushes.Black, new PointF(leftMargin, yPos)); yPos += headerFont.GetHeight(e.Graphics) + 20; // 打印表格标题行 float[] columnWidths = { 150, 100, 100, 100 }; string[] headers = { "产品名称", "数量", "单价", "总价" }; DrawTableRow(e.Graphics, headers, columnWidths, leftMargin, ref yPos, true); yPos += 5; // 打印表格内容 while(currentRecordIndex < records.Count && yPos < e.MarginBounds.Bottom - 50) { var record = records[currentRecordIndex]; string[] row = { record.ProductName, record.Quantity.ToString(), record.UnitPrice.ToString("C"), record.TotalPrice.ToString("C") }; DrawTableRow(e.Graphics, row, columnWidths, leftMargin, ref yPos, false); currentRecordIndex++; } // 打印页脚 string footer = $"页码: {e.PageNumber}"; e.Graphics.DrawString(footer, bodyFont, Brushes.Black, new PointF(leftMargin, e.MarginBounds.Bottom - 30)); // 设置是否还有后续页面 e.HasMorePages = currentRecordIndex < records.Count; } private void DrawTableRow(Graphics g, string[] cells, float[] widths, float startX, ref float yPos, bool isHeader) { float xPos = startX; Font font = isHeader ? new Font(bodyFont, FontStyle.Bold) : bodyFont; Brush brush = isHeader ? Brushes.DarkBlue : Brushes.Black; for(int i=0; i<cells.Length; i++) { RectangleF cellRect = new RectangleF(xPos, yPos, widths[i], font.GetHeight(g) + 5); g.DrawString(cells[i], font, brush, cellRect); // 绘制单元格边框 g.DrawRectangle(Pens.Gray, Rectangle.Round(cellRect)); xPos += widths[i]; } yPos += font.GetHeight(g) + 5; } private void Doc_EndPrint(object sender, PrintEventArgs e) { // 清理资源 records = null; } }实际项目中遇到的典型问题包括图片分辨率适配、跨页表格的边框连贯性处理,以及特殊字符的打印异常等。通过合理设置Graphics对象的插值模式和文本渲染提示,可以显著提升输出质量:
// 优化打印质量设置 e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; e.Graphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit; e.Graphics.SmoothingMode = SmoothingMode.HighQuality;