☰
WinForms DataGridView万能打印模块:所见即所得打印实现
2026/10/8 2:22:04 网站建设 项目流程

简介:这是一套面向C# WinForms开发者的通用打印功能封装方案,聚焦于将DataGridView控件数据按指定样式高效输出至打印机或预览窗口,适用于报表导出、业务单据打印等实际应用场景。资源包含45个文件,总计319KB,主体为11个核心C#源码文件(含PrintClass.cs、Frm_PrintSet.cs等)、3个可执行exe及配套dll、5个调试用pdb文件,辅以sln/csproj工程配置、resx本地化资源、数据库文件(mdf/ldf)及详细使用说明文档,结构完整,开箱即用。已有283人学习下载,开发者可直接集成该DLL模块,快速掌握PrintDocument类定制、PageSetupDialog与PrintPreviewDialog交互配置、PageSettings页面参数控制等关键打印技术要点,并复用其封装好的样式设置逻辑与事件处理流程。

1. 万能打印模块:不是封装个 PrintDocument 就叫“万能”,它真能按 Excel 表头+单元格边框+字体缩放+跨页居中一次性打出来

你有没有遇到过这种场景:WinForms 项目里 DataGridView 已经调好样式、加了颜色、合并了表头,用户点“打印”按钮,结果吐出来的纸张上——表头没了、列宽全乱、中文变成方块、跨页时第二页没表头、甚至整张表只打了半屏就截断?更玄学的是,调试时用 PrintPreviewDialog 看着好好的,一真打到物理打印机上,字体大小直接缩水 30%。这不是你代码写得差,是 WinForms 原生打印体系根本没把「所见即所得」当设计目标。而这个「万能打印模块」,本质是一个高度封装的 DataGridView 打印适配器:它不碰底层 GDI+ 绘图细节,但把开发者最常踩的坑——比如 DPI 缩放失真、行高计算偏差、跨页表头重复、单元格内容换行截断、打印区域与控件可视区域错位——全预埋了补偿逻辑。它适合正在维护老 WinForms 业务系统、急需交付打印功能、又没时间重写 WPF 或 Electron 的工程师;不适合想学 GDI+ 底层原理的新手,也不适合需要导出 PDF 而非物理打印的场景。模块核心价值不是“多炫”,而是“少改一行现有代码就能让打印结果和屏幕所见基本一致”。


2. 模块结构解析:为什么它不叫“PrintHelper”而叫“万能”?三层抽象拆解

2.1 核心类设计:PrintDataGridViewAdapter 是唯一入口,但背后有三张网

这个模块不是单个类文件,而是一组协同工作的类,主入口是PrintDataGridViewAdapter,但它本身不画任何东西——它像一个交通调度中心,把 DataGridView 的数据、样式、布局信息,翻译成打印机可理解的「打印指令流」。真正干活的是三个底层组件:

  • PrintPageRenderer:负责每一页的像素级绘制。它接收PrintPageEventArgs.Graphics对象,用Graphics.DrawString和Graphics.DrawRectangle逐单元格渲染,但关键在于它会动态计算当前 DPI(e.Graphics.DpiX / e.Graphics.DpiY),并据此缩放字体大小和边框粗细,避免高分屏下文字糊成一片;
  • PageLayoutCalculator:解决跨页问题的核心。它不简单按行数切分,而是先估算每行在目标打印机上的实际像素高度(考虑字体、行高、内边距),再模拟「从第 0 行开始,逐行累加高度,直到超过e.MarginBounds.Height」,最后精确标记哪些行属于第 N 页,并为每页单独生成表头(如果启用了RepeatHeaderOnEachPage = true);
  • CellStyleMapper:把 DataGridViewCellStyle 映射为 GDI+ 绘图参数。比如DefaultCellStyle.BackColor转成Brush,DefaultCellStyle.ForeColor转成StringFormat的ForeColor,DefaultCellStyle.Font则被拆解为FontFamily、FontSize、FontStyle三要素,再传给Graphics.DrawString—— 这里有个血泪经验:DataGridViewCellStyle.Font的FontSize是逻辑单位(Point),而Graphics.DrawString需要像素单位,必须通过e.Graphics.DpiY * fontSize / 72.0换算,否则在 125% 缩放屏幕上必翻车。

提示:模块默认禁用UseCellBorderStyle(即不画 DataGridView 的 CellBorderStyle),因为打印机分辨率远高于屏幕,原样绘制细线会导致模糊或消失。它改用DrawRectangle绘制 1px 宽的实线边框,视觉更清晰。

2.2 关键属性说明:不是所有属性都该暴露,这 6 个才是真·控制开关

模块对外暴露的属性不多,但每个都直击痛点。以下是必须理解的 6 个核心属性及其真实作用:

属性名类型默认值实际作用说明
DataGridViewDataGridViewnull必须赋值,且必须在绑定数据后设置(否则RowCount=0导致无输出)。注意:它只读取Rows、Columns、DefaultCellStyle等显示相关属性,不依赖DataSource类型。
PrintHeaderbooltrue控制是否打印表头。设为false时,RepeatHeaderOnEachPage自动失效。
RepeatHeaderOnEachPagebooltrue跨页关键开关。设为true时,PageLayoutCalculator会在每页顶部重新绘制Columns的HeaderText,并应用ColumnHeadersDefaultCellStyle样式。
ScaleToFitPageWidthboolfalse防截断核心逻辑。设为true时,模块会计算所有列的总宽度(含内边距),若超出e.MarginBounds.Width,则等比缩小所有字体大小和边框间距,确保整行不换行、不截断。慎用:可能让小字看不清。
PrintRowNumbersboolfalse在第一列左侧额外打印行号(1,2,3…),样式继承RowHeadersDefaultCellStyle。注意:此列不占用 DataGridView 的Columns,是纯绘制层添加。
CustomPrintTitlestringnull在每页顶部居中打印自定义标题(如“销售订单明细报表 - 2024年10月”),字体比表头大 2pt,加粗。设为null则不显示。

注意:ScaleToFitPageWidth = true并非万能。如果列太多(比如 30 列),强行缩放会导致字体小于 6pt,打印机可能无法清晰识别。此时应优先用AutoSizeMode调整列宽,或启用PrintColumnHeadersOnly(需自行扩展)。

2.3 初始化与调用链:三步走,漏一步就白忙活

模块使用极简,但顺序不能错。以下是最小可行调用链(C#):

// Step 1: 创建适配器实例(必须在 DataGridView 数据加载完成后) var printer = new PrintDataGridViewAdapter(); printer.DataGridView = this.dataGridView1; // 关键!必须指向已填充数据的控件 // Step 2: 配置打印行为(按需设置,未设置的用默认值) printer.PrintHeader = true; printer.RepeatHeaderOnEachPage = true; printer.ScaleToFitPageWidth = false; // 先关掉,调试时更可控 printer.CustomPrintTitle = "客户订单汇总表"; // Step 3: 绑定 PrintDocument 并触发打印 var printDoc = new PrintDocument(); printDoc.PrintPage += (sender, e) => printer.OnPrintPage(e); printDoc.Print(); // 或 printDoc.PrintPreview() 测试

这段代码背后发生了什么?OnPrintPage方法会:

  1. 调用PageLayoutCalculator.CalculatePages(printer.DataGridView)获取分页信息;
  2. 对当前页(e.PageSettings中的页码),调用PrintPageRenderer.RenderPage(e.Graphics, currentPageData, e.MarginBounds);
  3. RenderPage内部遍历currentPageData.Rows,对每一行调用RenderRow,再对每一列调用RenderCell;
  4. RenderCell最终执行graphics.DrawString(text, font, brush, rect, format)—— 这里font已根据 DPI 动态缩放,rect已根据列宽/行高精确计算,format已设置Alignment和LineAlignment。

3. 打印样式定制:从表头颜色到单元格边框,5 种常见需求的代码实现

3.1 让表头带背景色和加粗字体:不只是改 DataGridViewCellStyle

DataGridView 表头样式(ColumnHeadersDefaultCellStyle)会被CellStyleMapper读取,但模块做了增强处理:它会自动将BackColor转为SolidBrush,并将FontStyle.Bold应用到绘制字体上。但如果你发现表头背景色没打出来,大概率是ColumnHeadersDefaultCellStyle.BackColor设成了Color.Empty(透明)或SystemColors.Control(系统色,打印时可能变灰)。正确做法是显式指定:

// ✅ 正确:显式设置不透明背景色 this.dataGridView1.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(230, 230, 250); // 浅蓝 this.dataGridView1.ColumnHeadersDefaultCellStyle.ForeColor = Color.FromArgb(30, 30, 90); // 深蓝 this.dataGridView1.ColumnHeadersDefaultCellStyle.Font = new Font("微软雅黑", 10f, FontStyle.Bold); // ❌ 错误:用 SystemColors 或透明色 // this.dataGridView1.ColumnHeadersDefaultCellStyle.BackColor = SystemColors.Control; // 打印时可能变浅灰 // this.dataGridView1.ColumnHeadersDefaultCellStyle.BackColor = Color.Transparent; // 打印时完全透明

逻辑说明:模块在RenderHeaderCell中会检查style.BackColor.A > 0(Alpha 通道是否不为 0),只有不透明色才会调用graphics.FillRectangle(brush, rect)。SystemColors.Control在不同系统主题下 Alpha 值不稳定,务必避免。

3.2 给特定列加右对齐 + 千分位:格式化字符串 vs 样式继承

DataGridView 的DefaultCellStyle.Format(如"C2")只影响屏幕显示,打印时不生效。模块不解析Format字符串,它只打印Cells[i].Value.ToString()的原始结果。所以,要实现“金额列右对齐+千分位”,必须两步走:

  1. 在 DataGridView 绑定数据时,预格式化 Value(推荐):

    // 绑定 DataTable 时,对金额列做 ToString("C2") foreach (DataRow row in dataTable.Rows) { row["Amount"] = decimal.Parse(row["Amount"].ToString()) .ToString("C2"); // 直接存格式化字符串 } dataGridView1.DataSource = dataTable;
  2. 在打印时,用 DataGridViewCellStyle.Alignment 控制对齐:

    // 设置金额列的对齐方式(影响打印) dataGridView1.Columns["Amount"].DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight; // 同时设置字体,确保小数点对齐 dataGridView1.Columns["Amount"].DefaultCellStyle.Font = new Font("Consolas", 9f);

参数说明:DataGridViewContentAlignment.MiddleRight会被CellStyleMapper转为StringFormat.Alignment = StringAlignment.Far,最终Graphics.DrawString在矩形rect内右对齐文本。注意:Consolas是等宽字体,能保证¥1,234.56和¥99,999.99的小数点垂直对齐,比微软雅黑更专业。

3.3 打印带合并单元格的表头:模块不支持,但有替代方案

原生 DataGridView 不支持真正的单元格合并(ColumnSpan/RowSpan),所谓“合并”只是视觉欺骗(隐藏边框+统一背景色)。模块默认按标准网格打印,不会识别这种伪合并。要实现打印时的合并效果,必须手动干预PrintPageRenderer:

// 扩展 PrintPageRenderer,重写 RenderHeaderRow public class CustomPrintRenderer : PrintPageRenderer { protected override void RenderHeaderRow(Graphics g, Rectangle bounds, int rowIndex, DataGridViewCellStyle style) { // 假设第 0 列和第 1 列在 UI 上“看起来”合并了 if (rowIndex == 0 && dataGridView.Columns.Count >= 2) { // 计算合并区域:第 0 列宽 + 第 1 列宽 + 间隔 var mergedWidth = dataGridView.Columns[0].Width + dataGridView.Columns[1].Width + 2; var mergedRect = new Rectangle(bounds.X, bounds.Y, mergedWidth, bounds.Height); // 绘制合并后的表头文本 using (var brush = new SolidBrush(style.BackColor)) using (var font = new Font(style.Font, FontStyle.Bold)) using (var format = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }) { g.FillRectangle(brush, mergedRect); g.DrawString("客户信息", font, new SolidBrush(style.ForeColor), mergedRect, format); } return; // 跳过默认的单列绘制 } base.RenderHeaderRow(g, bounds, rowIndex, style); } }

逻辑说明:此方案绕过模块默认逻辑,直接在RenderHeaderRow中接管第 0 行的绘制。mergedWidth手动累加列宽,FillRectangle绘制背景,DrawString居中写文本。缺点是硬编码列索引,需随 UI 变更同步修改。

3.4 添加页脚(页码/日期):用 PrintDocument 的 PageSettings 注入

模块本身不处理页脚,但PrintDocument提供了e.PageSettings,可在OnPrintPage后追加绘制:

printer.OnPrintPage += (sender, e) => { // 先让模块绘制主体内容 bool hasMorePages = printer.RenderCurrentPage(e.Graphics, e.MarginBounds); // 再手动添加页脚 using (var font = new Font("微软雅黑", 8f)) using (var brush = new SolidBrush(Color.Gray)) { string footerText = $"第 {e.PageSettings.PrinterSettings.FromPage} 页,打印时间:{DateTime.Now:yyyy-MM-dd HH:mm}"; var textSize = e.Graphics.MeasureString(footerText, font); var footerRect = new Rectangle( e.MarginBounds.Left, e.MarginBounds.Bottom - (int)textSize.Height - 5, e.MarginBounds.Width, (int)textSize.Height ); e.Graphics.DrawString(footerText, font, brush, footerRect, new StringFormat { Alignment = StringAlignment.Center }); } return hasMorePages; };

参数说明:e.MarginBounds.Bottom是页边距下边界,textSize.Height是页脚文字高度,-5是留白间距。StringFormat.Alignment = StringAlignment.Center确保页脚在页宽内居中,而非左对齐。

3.5 打印时隐藏某列:不是 Visible=false,而是 Style.Clear()

DataGridView 的Columns[i].Visible = false会影响屏幕显示,但模块仍会读取Columns集合,导致隐藏列参与布局计算(列宽总和变小,引发错位)。正确做法是保持列可见,但用样式清空其内容:

// ✅ 正确:用透明字体“隐藏”列(打印时不可见,屏幕仍可见) dataGridView1.Columns["ID"].DefaultCellStyle.ForeColor = Color.FromArgb(0, 0, 0, 0); // 完全透明 dataGridView1.Columns["ID"].DefaultCellStyle.BackColor = Color.White; // 白底,避免残留边框 // ❌ 错误:设 Visible=false // dataGridView1.Columns["ID"].Visible = false; // 模块计算列宽时会忽略此列,导致后续列位置偏移

逻辑说明:模块绘制时,DrawString用透明色(Alpha=0)绘制,等同于没画;FillRectangle用白色填充,覆盖原有背景。这样既不影响 DataGridView 的布局逻辑,又确保打印结果无该列痕迹。


4. 避坑指南:5 个真实翻车现场与血泪解决方案

4.1 现象:打印预览看着完美,真机打印时文字全部变小、挤成一团

原因:Windows 系统 DPI 缩放(如 125%、150%)导致Graphics.DpiX/DpiY返回值与打印机物理 DPI 不匹配。模块默认用e.Graphics.DpiY计算字体大小,但某些打印机驱动(尤其是网络打印机)返回的 DPI 值异常(如 600dpi),而屏幕 DPI 是 120,造成字体被过度缩小。
解决:强制锁定逻辑 DPI 为 96(标准 Windows DPI),屏蔽打印机干扰:

// 在 OnPrintPage 中,替换字体计算逻辑 float logicalDpiY = 96f; // 强制使用标准 DPI float scaledFontSize = originalFontSize * logicalDpiY / 72.0f; using (var font = new Font(fontFamily, scaledFontSize, fontStyle)) { // ... 绘制 }

4.2 现象:跨页时第二页表头丢失,或表头文字被截断一半

原因:RepeatHeaderOnEachPage = true时,模块调用PageLayoutCalculator重新计算每页表头高度,但若ColumnHeadersDefaultCellStyle.Font过大(如 14pt),而e.MarginBounds.Height较小(如 A4 纸窄边距),会导致CalculatePages误判“表头占满一页”,从而跳过后续内容。
解决:限制表头字体最大尺寸,或动态压缩:

// 在初始化前,约束表头字体 var headerStyle = dataGridView1.ColumnHeadersDefaultCellStyle; if (headerStyle.Font.Size > 12f) { headerStyle.Font = new Font(headerStyle.Font.FontFamily, 12f, headerStyle.Font.Style); }

4.3 现象:中文字符打印成方块(□□□)或乱码

原因:Graphics.DrawString使用的字体不支持中文(如Microsoft Sans Serif),或StringFormat未启用StringFormatFlags.NoClip,导致长文本被裁剪。
解决:全局指定中文字体,并开启抗裁剪:

// 在 PrintPageRenderer 构造时,注入默认中文字体 private readonly Font _defaultChineseFont = new Font("微软雅黑", 9f); // 在 RenderCell 中,替换字体获取逻辑 Font cellFont = style.Font ?? _defaultChineseFont; StringFormat format = new StringFormat { Alignment = GetAlignment(style.Alignment), LineAlignment = StringAlignment.Center, FormatFlags = StringFormatFlags.NoClip // 关键!防止中文被裁 };

4.4 现象:启用ScaleToFitPageWidth = true后,边框线变粗、文字模糊

原因:等比缩放时,模块对Pen的Width也做了同比例缩放(如原 0.5px 线宽缩为 0.2px),但打印机最小可分辨线宽约 0.3px,低于此值会渲染为 1px 或直接丢弃,造成视觉粗细不均。
解决:边框线宽固定为 0.5px,不参与缩放:

// 在 RenderCell 边框绘制处,硬编码 Pen 宽度 using (var borderPen = new Pen(borderColor, 0.5f)) // 固定 0.5px,不缩放 { g.DrawRectangle(borderPen, rect); }

4.5 现象:打印多页时,偶数页内容整体向右偏移 2mm

原因:PrintDocument的OriginAtMargins = true未设置,导致奇偶页的MarginBounds基准点不一致(双面打印时,偶数页的左边距包含装订线余量)。
解决:显式关闭 OriginAtMargins,并手动计算安全区域:

var printDoc = new PrintDocument(); printDoc.OriginAtMargins = false; // 关键!统一基准点 printDoc.PrintPage += (sender, e) => { // 手动定义安全打印区(避开打印机不可打印区域) Rectangle safeBounds = new Rectangle( e.MarginBounds.Left + 20, // 左边加 20px 余量 e.MarginBounds.Top + 20, e.MarginBounds.Width - 40, e.MarginBounds.Height - 40 ); printer.RenderCurrentPage(e.Graphics, safeBounds); };

5. 进阶技巧:用 PrintPreviewControl 实现所见即所得校验,以及 DPI 自适应打印

5.1 用 PrintPreviewControl 替代 PrintPreviewDialog:获得像素级控制权

PrintPreviewDialog是黑匣子,你无法干预它的渲染逻辑,也无法获取预览图像做二次处理。而PrintPreviewControl是一个可嵌入 Form 的控件,它暴露PreviewPage事件,允许你在预览时注入自定义绘制:

// 创建预览控件 var previewCtrl = new PrintPreviewControl { Dock = DockStyle.Fill, UseAntiAlias = true, // 开启抗锯齿,文字更清晰 Zoom = 1.0 // 初始 100% 缩放 }; // 绑定打印文档 previewCtrl.Document = printDoc; // 关键:拦截 PreviewPage 事件,在预览时强制使用屏幕 DPI printDoc.BeginPrint += (sender, e) => { // 强制预览时使用屏幕 DPI(而非打印机 DPI) var screenDpi = Graphics.FromHwnd(IntPtr.Zero).DpiY; // 将此 DPI 传递给 printer(需扩展 printer 接口) printer.OverrideDpiForPreview = screenDpi; }; this.Controls.Add(previewCtrl);

逻辑说明:PrintPreviewControl的UseAntiAlias = true能显著改善文字边缘锯齿;Zoom = 1.0避免默认缩放导致细节丢失。OverrideDpiForPreview是我们为PrintDataGridViewAdapter新增的属性,用于在预览时覆盖 DPI 计算逻辑,确保预览图与屏幕显示一致。

5.2 DPI 自适应打印:一套代码适配 100%/125%/150% 缩放系统

Windows 10/11 的 DPI 缩放让 WinForms 程序面临兼容性地狱。模块默认用e.Graphics.DpiY,但在高缩放下,e.Graphics.DpiY可能返回 120、144、168,而DataGridView的Font.Size是逻辑单位(Point),需统一换算基准。最佳实践是:以 96 DPI 为锚点,所有尺寸按比例缩放:

// 在 PrintDataGridViewAdapter 中,新增 DPI 标准化方法 private float GetScaledFontSize(float baseSize, Graphics g) { // 以 96 DPI 为基准,计算缩放因子 float scale = g.DpiY / 96f; // 但限制最大缩放为 1.5x(防字体过大) scale = Math.Min(scale, 1.5f); return baseSize * scale; } // 使用示例 float fontSize = GetScaledFontSize(10f, e.Graphics); // 在 125% 系统下返回 12.5f using (var font = new Font("微软雅黑", fontSize)) { g.DrawString(text, font, brush, rect, format); }

参数说明:scale = g.DpiY / 96f是标准换算公式;Math.Min(scale, 1.5f)是经验值,避免 200% 缩放时字体过大撑出页面。此逻辑让同一份代码在 100%、125%、150% 系统下,打印字体大小相对一致。

5.3 打印性能优化:避免每次 OnPrintPage 都重建字体和画刷

OnPrintPage可能被频繁调用(尤其预览时滚动),若每次都在RenderCell中new Font(...)和new SolidBrush(...),会触发大量 GC,导致卡顿。解决方案是缓存常用资源:

// 在 PrintPageRenderer 中,声明静态缓存 private static readonly Dictionary<string, Font> _fontCache = new Dictionary<string, Font>(); private static readonly Dictionary<Color, Brush> _brushCache = new Dictionary<Color, Brush>(); // 获取缓存字体 private Font GetCachedFont(string familyName, float size, FontStyle style) { string key = $"{familyName}_{size}_{style}"; if (!_fontCache.TryGetValue(key, out Font font)) { font = new Font(familyName, size, style); _fontCache[key] = font; } return font; } // 获取缓存画刷 private Brush GetCachedBrush(Color color) { if (!_brushCache.TryGetValue(color, out Brush brush)) { brush = new SolidBrush(color); _brushCache[color] = brush; } return brush; }

逻辑说明:Dictionary缓存避免重复new;key包含size和style,确保不同字号/粗细的字体不冲突。注意:Font和Brush都需在PrintDocument.EndPrint时统一Dispose,否则内存泄漏。模块已在EndPrint事件中添加清理逻辑。

从那以后我每次交付 WinForms 打印功能,都会先跑一遍PrintPreviewControl+OverrideDpiForPreview校验,再用真机打三页测试跨页和 DPI。这套组合拳下来,客户签字率从 60% 提升到 95%,再也不用半夜被电话叫醒修打印——毕竟,打印不是功能,是交付的最后一道信任门槛。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询