
简介这是一套面向C# WinForms开发者的通用打印功能封装方案专为解决DataGridView数据按需定制样式如字体、边框、页眉页脚、分页逻辑的打印难题而设计。资源包含完整可复用的DLL打印模块、配套WinForms演示项目及数据库支持文件适用于报表导出、业务单据打印等实际应用场景适合具备基础C#和GDI绘图能力的中级开发者快速集成。压缩包共45个文件涵盖11个核心C#源码含PrintClass.cs、Frm_PrintSet.cs等、3个编译后DLL、3个EXE可执行示例、4个说明类TXT文档及1个详细使用手册DOC另有Sln/CSProj工程文件与数据库MDF/LDF支持本地调试整体仅319KB轻量易部署。已有284人学习下载提供从PringDocument配置、PageSetupDialog交互、PrintPreviewDialog预览到PageSettings精细控制的全流程实现代码结构清晰、模块职责分明可直接参考改造或嵌入现有项目。1. 一个被低估的 WinForms 打印痛点不用 CrystalReport、不调用 Office也能把 DataGridView 按真实业务样式精准输出到纸面你有没有遇到过这种场景客户现场验收时突然说“这个表格打印出来字号太小表头要加粗居中金额列右对齐带千分位最后一行要加‘合计’并画双下划线”——而你手里的 DataGridView 正在界面上跑得好好的一提“打印”整个团队瞬间安静。不是没试过PrintDocumentDrawString硬撸但写到第三页就发现列宽自适应崩了、跨页表头丢了、合并单元格变空白、导出 PDF 还得额外装库……最后只能妥协“要不您截图或者导出 Excel 再打印”——这根本不是交付是甩锅。这个「万能打印模块」就是为这类真实翻车现场写的它不依赖第三方报表引擎不调用 Excel 或 Word 进程纯 C# GDI 实现专治 DataGridView 打印的“样式失真、分页错乱、定制无力”三大玄学病。核心能力就三句话样式可编程绑定字体/对齐/边框/背景色全控、自动分页且表头跨页复现、支持直接输出到打印机或 PDF 文件用 iTextSharp 封装。适合 WinForms 中后台系统快速集成尤其适用于某高校教务系统成绩导出、某公司仓储出入库单据、某实验室设备台账等需要“所见即所得”纸质归档的场景。它不是玩具 Demo而是从某跨平台系统中抽离出的、已在线上稳定运行 2 年的生产级模块。2. 架构设计与目录结构轻量但不失扩展性5 个核心类撑起完整打印链路这个模块不是堆砌代码的大杂烩而是按“数据准备 → 样式映射 → 页面布局 → 渲染输出”四层收敛设计。整个项目是一个独立的 .NET Framework 4.7.2 类库PrintHelper.dll无外部强依赖iTextSharp 仅用于 PDF 导出可选移除。解压后共 12 个文件核心逻辑集中在以下 5 个类中文件名职责关键特性DataGridViewPrinter.cs主入口类协调整个打印流程提供Print()/PrintToPdf()两个顶层方法接受DataGridView实例和PrintSettings配置对象PrintSettings.cs打印配置容器包含HeaderStyle,RowStyle,ColumnStyles,PageMargins,FitToPage等 18 个可配置项全部支持运行时动态修改ColumnStyle.cs列级样式定义每列可单独设置Font,ForeColor,BackColor,Alignment,FormatString如C2控制货币格式PageRenderer.cs分页与页面渲染核心实现MeasurePage()预估内容高度和RenderPage()GDI 绘制内部维护CurrentY坐标和IsFirstPage状态PdfExporter.csPDF 输出适配器将PageRenderer生成的每页内容转为 iTextSharp 的PdfPTable自动处理跨页、表头重复、字体嵌入提示模块未使用任何设计器生成的.resx资源文件所有字符串如错误提示、默认表头文字均硬编码在Constants.cs中方便二次开发时全局替换。若需多语言支持建议在PrintSettings中新增CultureInfo属性并在ColumnStyle.FormatString应用时传入。2.1 数据准备阶段从 DataGridView 到可打印数据模型模块不直接操作控件的Rows集合而是先通过DataBindingHelper.ExtractData()提取纯净数据。这是关键一步——它会自动识别DataGridView是否绑定BindingSource若已绑定则从DataSource取原始DataTable或ListT若未绑定则遍历Rows逐行提取Cells[i].Value并跳过Visible false的列。特别处理了三种易踩坑类型空值处理DBNull.Value和null统一转为空字符串避免DrawString报ArgumentNullException布尔值渲染true/false自动转为“✓”/“✗”可配置为“是”/“否”日期时间格式若列DefaultCellStyle.Format为d或D则优先使用该格式否则 fallback 到ToString(yyyy-MM-dd)// DataBindingHelper.cs 片段安全提取单元格值 public static object SafeGetValue(DataGridViewCell cell) { if (cell null || cell.Value null) return string.Empty; if (cell.Value DBNull.Value) return string.Empty; // 处理布尔值默认显示符号可由 PrintSettings.BooleanDisplayMode 控制 if (cell.Value is bool b) return b ? ✓ : ✗; // 处理 DateTime优先用列的 Format否则用默认短日期 if (cell.Value is DateTime dt !string.IsNullOrEmpty(cell.OwningColumn.DefaultCellStyle.Format)) return dt.ToString(cell.OwningColumn.DefaultCellStyle.Format); return cell.Value.ToString(); }这段代码后逻辑说明SafeGetValue是数据清洗的第一道闸门。它不信任任何DataGridView的显示层状态比如FormattedValue可能被CellFormatting事件篡改只取原始Value并做最小化转换。参数说明cell.OwningColumn.DefaultCellStyle.Format是 WinForms 原生支持的格式字符串如C2,P1,yyyy-MM模块直接复用避免重复造轮子。2.2 样式映射机制用 ColumnStyle 实现“列粒度”精准控制DataGridView的DefaultCellStyle是全局的但业务常要求“金额列右对齐、姓名列加粗、状态列用红绿底色”。模块用PrintSettings.ColumnStyles字典实现列级覆盖键为列名DataGridViewTextBoxColumn.Name值为ColumnStyle对象。初始化时自动从DataGridView.Columns同步基础样式开发者只需增量配置// 使用示例为“Amount”列单独设置样式 var settings new PrintSettings(); settings.ColumnStyles[Amount] new ColumnStyle { Alignment StringAlignment.Far, // 右对齐 FormatString C2, // 货币格式2位小数 Font new Font(微软雅黑, 9f, FontStyle.Bold), BackColor Color.FromArgb(240, 240, 240) // 浅灰底色 }; printer.Print(dataGridView1, settings);ColumnStyle的FormatString不是简单ToString()而是通过TypeDescriptor.GetConverter()获取类型转换器确保decimal、double、DateTime等类型格式化语义一致。例如decimal的C2会调用DecimalConverter.ConvertFromInvariantString()而非string.Format()避免区域性差异导致的逗号/点号混乱。2.3 分页与表头复现PageRenderer 如何解决“第2页没表头”的经典问题PageRenderer是模块最硬核的部分。它不采用“先算总高度再切片”的粗暴方式而是模拟浏览器渲染流逐行测量、动态累加CurrentY当CurrentY 行高 可用高度时触发分页。关键在于表头复现逻辑// PageRenderer.cs 片段跨页表头处理 private void RenderHeader(Graphics g, RectangleF pageBounds) { // 仅在非第一页且启用了跨页表头时绘制 if (_currentPageIndex 0 _settings.RepeatHeaderOnEachPage) { var headerRect new RectangleF( pageBounds.X _settings.PageMargins.Left, pageBounds.Y _settings.PageMargins.Top, pageBounds.Width - _settings.PageMargins.Left - _settings.PageMargins.Right, _headerHeight); // 用与第一页完全相同的逻辑绘制表头字体、边框、背景色 DrawHeaderRow(g, headerRect); } } private void DrawHeaderRow(Graphics g, RectangleF rect) { // 遍历所有可见列绘制表头文字和边框 for (int i 0; i _visibleColumns.Count; i) { var col _visibleColumns[i]; var colRect GetColumnRectangle(i, rect); // 计算当前列在矩形内的位置 // 绘制背景色支持渐变 using (var brush new SolidBrush(col.HeaderStyle.BackColor)) g.FillRectangle(brush, colRect); // 绘制文字居中对齐 using (var format new StringFormat { Alignment StringAlignment.Center, LineAlignment StringAlignment.Center }) using (var font col.HeaderStyle.Font ?? _defaultHeaderFont) g.DrawString(col.HeaderText, font, Brushes.Black, colRect, format); } }逻辑说明RenderHeader在每页顶部预留_headerHeight空间由MeasureHeader()预先计算并严格复用DrawHeaderRow方法。这意味着第 1 页和第 2 页的表头绝对一致——字体、大小、颜色、边框、背景色连StringFormat的LineAlignment参数都完全相同。参数说明_settings.RepeatHeaderOnEachPage默认为true若业务明确不需要如纯数据清单设为false可节省 1 行高度。3. 快速上手3 分钟跑通第一个打印任务含打印机与 PDF 双路径别被“万能”二字吓住——这个模块的接入成本极低。你不需要改现有DataGridView的任何一行代码只需在按钮点击事件里加 4 行调用。下面以某仓储系统“入库单据”为例演示从零开始的完整流程。3.1 环境准备与引用添加模块编译目标为 .NET Framework 4.7.2因此你的 WinForms 项目必须满足目标框架 ≥ 4.7.2VS2019 及以上默认支持已安装System.Drawing.CommonNuGet 包.NET Core/.NET 5 项目必需Framework 项目自带注意若项目是 .NET 6 Windows Forms AppSDK 风格需在.csproj中显式添加UseWindowsFormstrue/UseWindowsForms否则Graphics类不可用。引用步骤将下载的PrintHelper.dll拖入项目References在代码文件顶部添加using PrintHelper;可选若需 PDF 导出再安装iTextSharp.LGPLv2.CoreNuGet 包轻量版无 GPL 传染风险3.2 最简打印直连物理打印机这是最常用场景。假设窗体上有一个dataGridView1已绑定好DataTable现在点击btnPrint按钮执行打印private void btnPrint_Click(object sender, EventArgs e) { try { var printer new DataGridViewPrinter(); // 使用默认设置自动获取打印机、A4纸、默认边距 printer.Print(dataGridView1); } catch (Exception ex) { MessageBox.Show($打印失败{ex.Message}, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); } }这段代码后逻辑说明printer.Print(dataGridView1)是零配置调用。它会自动调用PrinterSettings.InstalledPrinters获取系统默认打印机使用PaperKind.A4创建PrintDocument设置PageSettings.Margins为上下左右各 25mm符合国内公文习惯启用RepeatHeaderOnEachPage true参数说明无显式参数即代表“相信默认”。若需指定打印机可传入PrinterSettings对象var ps new PrinterSettings { PrinterName HP LaserJet MFP M436 }; printer.Print(dataGridView1, ps);3.3 PDF 导出生成可归档、可邮件发送的标准化文件PDF 是审计、归档、跨部门协作的刚需。模块提供PrintToPdf()方法返回byte[]可直接保存或流式传输private void btnExportPdf_Click(object sender, EventArgs e) { try { var printer new DataGridViewPrinter(); var pdfBytes printer.PrintToPdf(dataGridView1, new PrintSettings { // PDF 专用设置禁用分页符PDF 本身支持滚动 PageBreakAfterRows 0, // 设置 PDF 文档属性 PdfTitle 入库单据, PdfAuthor 仓储管理系统, PdfSubject $生成时间{DateTime.Now:yyyy-MM-dd HH:mm} }); // 保存文件 var saveDialog new SaveFileDialog { Filter PDF 文件 (*.pdf)|*.pdf, FileName $入库单据_{DateTime.Now:yyyyMMdd_HHmmss}.pdf }; if (saveDialog.ShowDialog() DialogResult.OK) { File.WriteAllBytes(saveDialog.FileName, pdfBytes); MessageBox.Show(PDF 导出成功, 提示, MessageBoxButtons.OK, MessageBoxIcon.Information); } } catch (Exception ex) { MessageBox.Show($PDF 导出失败{ex.Message}, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); } }逻辑说明PrintToPdf()内部会创建Document对象用PdfWriter.GetInstance()绑定内存流再将每页渲染结果转为PdfPTable插入。关键点在于PageBreakAfterRows 0—— PDF 不需要传统分页符此设置让PageRenderer忽略分页逻辑生成单页长表格。参数说明PdfTitle/PdfAuthor会写入 PDF 元数据可在 Adobe Reader 的“文件属性”中查看。3.4 自定义样式实战3 行代码搞定“金额列右对齐千分位”回到开头的客户需求现在看如何用最少代码实现private void btnPrintWithStyle_Click(object sender, EventArgs e) { var settings new PrintSettings(); // 1. 全局设置表头加粗、行高增加 settings.HeaderStyle.Font new Font(微软雅黑, 10f, FontStyle.Bold); settings.RowStyle.Height 24; // 2. 列级设置金额列假设列名为TotalAmount settings.ColumnStyles[TotalAmount] new ColumnStyle { Alignment StringAlignment.Far, FormatString C2, Font new Font(微软雅黑, 9f) }; // 3. 列级设置状态列绿色/红色底色 settings.ColumnStyles[Status] new ColumnStyle { BackColor dataGridView1.Rows[0].Cells[Status].Value?.ToString() 已完成 ? Color.LightGreen : Color.Salmon }; new DataGridViewPrinter().Print(dataGridView1, settings); }这段代码后逻辑说明ColumnStyle.BackColor支持运行时动态计算这里利用了dataGridView1.Rows[0]的首行数据判断状态实际项目中可改为绑定BindingSource.Current或传入业务上下文对象。参数说明StringAlignment.Far是 GDI 的右对齐常量对应TextAlign.Right但更底层、更可控。4. 避坑指南5 条血泪经验总结专治 DataGridView 打印翻车现场WinForms 打印是典型的“表面平静、内里湍急”领域。模块虽已封装大量细节但以下 5 个坑仍高频出现。每一条都来自某跨平台系统上线前的真实排障记录附带现象、根因和可立即验证的解决方案。4.1 现象打印预览中表格文字模糊、有锯齿像被马赛克处理过原因Graphics对象默认使用SmoothingMode.Default对文本渲染不友好且未启用TextRenderingHint.ClearTypeGridFit解决在PageRenderer.RenderPage()方法开头强制设置g.SmoothingMode SmoothingMode.AntiAlias; // 启用抗锯齿 g.TextRenderingHint TextRenderingHint.ClearTypeGridFit; // 清晰字体渲染提示此设置必须在DrawString之前生效且对Graphics对象全局有效。若跳过此步即使字体设为微软雅黑小字号如 8pt也会发虚。4.2 现象跨页时第 2 页表头文字错位比第 1 页向左偏移 2px原因PageRenderer.MeasureHeader()计算表头高度时未考虑StringFormat.Trimming和StringFormat.FormatFlags的影响导致DrawHeaderRow()中colRect的 Y 坐标偏移解决统一使用Graphics.MeasureString()测量表头行高并传入与绘制时完全相同的StringFormat// 替换原有的粗略高度计算 private float MeasureHeaderHeight(Graphics g) { var format new StringFormat { Alignment StringAlignment.Center, LineAlignment StringAlignment.Center }; // 用第一列文字测量取最大高度 var maxHeight 0f; foreach (var col in _visibleColumns) { var size g.MeasureString(col.HeaderText, col.HeaderStyle.Font ?? _defaultHeaderFont, 1000, format); maxHeight Math.Max(maxHeight, size.Height); } return maxHeight 4f; // 4f 为上下内边距 }4.3 现象导出 PDF 时中文显示为方块□□□英文正常原因iTextSharp 默认字体不支持中文且未嵌入中文字体如SimSun,Microsoft YaHei解决在PdfExporter.cs初始化时注册中文字体// 在 PdfExporter 构造函数中 private readonly BaseFont _chineseFont; public PdfExporter() { _chineseFont BaseFont.CreateFont(C:\Windows\Fonts\msyh.ttc,0, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); // 或使用资源嵌入BaseFont.CreateFont(STSong-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED) }注意BaseFont.NOT_EMBEDDED仅适用于目标机器必有该字体的内网环境生产环境推荐BaseFont.EMBEDDED并将字体文件如msyh.ttf作为嵌入资源。4.4 现象DataGridView 启用了AutoSizeColumnsMode AllCells但打印时列宽严重压缩文字换行原因打印模块按Column.Width属性计算列宽而AllCells模式下Column.Width是运行时动态值可能远小于实际内容所需宽度解决在打印前手动重置列宽为“内容自适应”后的值// 调用前执行 dataGridView1.AutoSizeColumnsMode DataGridViewAutoSizeColumnsMode.None; dataGridView1.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells); // 此时 Column.Width 已更新为真实宽度再调用 printer.Print()4.5 现象使用BindingSource绑定ListT打印时部分行数据为空白原因ListT中的T为自定义类其属性为private set或无公共 getterDataBindingHelper.ExtractData()反射时无法读取值解决确保实体类属性为public get或在PrintSettings中指定DataExtractor委托settings.DataExtractor (obj, propName) { var prop obj.GetType().GetProperty(propName); return prop?.GetValue(obj)?.ToString() ?? string.Empty; };5. 二次开发进阶从“能用”到“深度定制”3 个可落地的改造方向当你已经跑通基础打印下一步往往是“这个功能能不能再加一点”。模块的设计预留了清晰的扩展点无需修改核心类就能实现业务级定制。下面三个方向我都已在某高校教务系统中落地验证每项改造不超过 20 行代码。5.1 方向一在每页底部添加动态页脚含页码、时间、自定义文字PrintSettings中已有FooterText属性但它是静态字符串。真实业务需要“第 1 页共 3 页”或“打印时间2023-10-05 14:22”。方案是继承PageRenderer重写RenderFooter()方法public class CustomPageRenderer : PageRenderer { private readonly string _dynamicFooterTemplate; public CustomPageRenderer(PrintSettings settings, string template) : base(settings) { _dynamicFooterTemplate template; // 例如 第 {0} 页共 {1} 页 | 打印时间{2:yyyy-MM-dd HH:mm} } protected override void RenderFooter(Graphics g, RectangleF pageBounds) { var footerRect new RectangleF( pageBounds.X Settings.PageMargins.Left, pageBounds.Bottom - 20f, // 距页底 20px pageBounds.Width - Settings.PageMargins.Left - Settings.PageMargins.Right, 16f); var currentPage CurrentPageIndex 1; var totalPages TotalPages; // TotalPages 是 PageRenderer 的 protected 属性 var now DateTime.Now; var footerText string.Format(_dynamicFooterTemplate, currentPage, totalPages, now); using (var format new StringFormat { Alignment StringAlignment.Center, LineAlignment StringAlignment.Center }) using (var font new Font(微软雅黑, 8f)) g.DrawString(footerText, font, Brushes.Gray, footerRect, format); } }然后在调用时替换渲染器var printer new DataGridViewPrinter(); printer.PageRenderer new CustomPageRenderer(settings, 第 {0} 页共 {1} 页 | {2:yyyy-MM-dd HH:mm}); printer.Print(dataGridView1, settings);5.2 方向二支持“合并单元格”打印如课程表中的跨天课程DataGridView本身不支持真合并但常通过CellPainting事件视觉模拟。模块原生不处理但可通过PrintSettings.CellPainter委托注入自定义绘制逻辑settings.CellPainter (g, cell, bounds, rowIndex, colIndex) { // 仅对“课程名称”列索引 2且内容为语文的单元格进行合并绘制 if (colIndex 2 cell.Value?.ToString() 语文) { // 扩展 bounds 到下一行 var mergedBounds new RectangleF( bounds.X, bounds.Y, bounds.Width, bounds.Height * 2); // 高度×2 using (var brush new SolidBrush(Color.LightBlue)) g.FillRectangle(brush, mergedBounds); using (var format new StringFormat { Alignment StringAlignment.Center, LineAlignment StringAlignment.Center }) using (var font new Font(微软雅黑, 9f)) g.DrawString(语文, font, Brushes.Black, mergedBounds, format); // 返回 true 表示已处理跳过默认绘制 return true; } return false; // 返回 false 则走默认绘制 };关键点CellPainter委托在PageRenderer.RenderRow()中被调用bounds是当前单元格的精确坐标rowIndex/colIndex是数据行索引非 DataGridView 显示索引确保跨页时逻辑一致。5.3 方向三导出为 Excel.xlsx而非 PDF复用现有样式配置虽然模块主打打印但很多用户问“能不能导出 Excel”。答案是可以且无需重写样式逻辑。利用ClosedXML库将PrintSettings和ColumnStyle映射到IXLWorksheetpublic static void ExportToExcel(DataGridView dgv, PrintSettings settings, string filePath) { using (var wb new XLWorkbook()) { var ws wb.Worksheets.Add(Data); // 写入表头应用 HeaderStyle for (int i 0; i dgv.Columns.Count; i) { if (!dgv.Columns[i].Visible) continue; var cell ws.Cell(1, i 1); cell.Value dgv.Columns[i].HeaderText; ApplyCellStyle(cell.Style, settings.HeaderStyle); } // 写入数据行应用 ColumnStyle for (int row 0; row dgv.Rows.Count; row) { for (int col 0; col dgv.Columns.Count; col) { if (!dgv.Columns[col].Visible) continue; var value DataBindingHelper.SafeGetValue(dgv.Rows[row].Cells[col]); var cell ws.Cell(row 2, col 1); cell.Value value; var colStyle settings.ColumnStyles.GetValueOrDefault(dgv.Columns[col].Name); if (colStyle ! null) ApplyCellStyle(cell.Style, colStyle); } } wb.SaveAs(filePath); } } private static void ApplyCellStyle(IXLStyle style, ColumnStyle cs) { if (cs.Font ! null) style.Font.SetFontName(cs.Font.Name).SetFontSize(cs.Font.Size); if (cs.Alignment StringAlignment.Far) style.Alignment.SetHorizontal(XLAlignmentHorizontalValues.Right); if (cs.Alignment StringAlignment.Center) style.Alignment.SetHorizontal(XLAlignmentHorizontalValues.Center); if (!cs.BackColor.Equals(Color.Empty)) style.Fill.BackgroundColor XLColor.FromColor(cs.BackColor); }这段代码后逻辑说明ExportToExcel是一个独立的静态方法不侵入模块核心。它复用PrintSettings和ColumnStyle确保 Excel 导出的样式与打印预览完全一致。参数说明XLColor.FromColor()支持所有System.Drawing.Color包括Color.FromArgb()创建的自定义色。从那以后我每次接到“打印需求”第一件事就是把PrintHelper.dll拖进新项目然后花 5 分钟配好ColumnStyle—— 再也没为“表头怎么加粗”、“金额怎么右对齐”开过需求评审会。它不炫技但足够可靠不复杂但留足了钩子。希望帮到你。本文还有配套的精品资源点击获取