ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WinForms DataGridView万能打印模块:从分页到样式全解析

WinForms DataGridView万能打印模块:从分页到样式全解析 简介一套面向C# WinForms开发者的DataGridView万能打印模块核心用途是将表格数据按指定样式输出到打印机。资源系统梳理了打印功能开发中的关键环节包括DLL文件建立方法、DataGridView控件属性设置、PrintDocument打印文档配置、PageSetupDialog页面设置、PrintPreviewDialog打印预览以及PageSettings页面参数调整能帮助具备基础开发能力的中级开发者快速实现自定义打印、页面排版和打印预览功能。资源包共45个文件压缩后仅319KB内容以C#源码、可执行程序、动态链接库和资源文件为主同时包含调试符号、项目解决方案、数据库及DOC使用说明结构清晰适合对照学习或集成复用。该模块已有281人学习下载。借助两个示例工程和配套说明读者可以完整掌握从建立DLL到调用打印模块的全过程理解打印设置与预览交互的实现细节同时借助数据库等附属内容进行模拟数据演练有效降低自行摸索的时间成本。 搞桌面开发的应该都遇到过这种需求业务系统跑得好好的DataGridView 里的数据也对得上客户突然说“把当前这个表格打印出来格式按我那个模板走”。如果你直接截屏输出或者用第三方表格控件导出打印十有八九会遇到列宽对不上、行数一多被截断、背景色丢失、打印预览和实际输出不一致这些糟心事。我花了几周时间把这个问题系统梳理了一遍沉淀成一个可复用的万能打印模块专门负责把 DataGridView 控件中的数据按照指定样式打印出来支持字体、颜色、边框、页眉页脚、分页方式等全部可控。这篇文章我会把模块的设计思路、核心实现、调用方式和踩坑经验全部说透特别适合用 WinForms DataGridView 做管理系统、需要频繁输出报表的开发者。1. 先把需求看清楚万能打印模块到底解决什么问题1.1 数据展示和纸张打印中间隔着一道鸿沟很多朋友第一次接到“打印 DataGridView”的需求时心里想的是“这有什么难的把表格里的数据循环一遍输出到打印机不就行了”。真正动手之后才发现DataGridView 是在屏幕上按像素绘制的控件而打印机是按物理尺寸工作的设备这两者的坐标系、分辨率、字体渲染方式完全不一样。屏幕上的列宽可能是 150 像素到了纸上就要换算成毫米或 1/100 英寸屏幕上字体显示很清楚打印出来却可能发虚甚至偏小DataGridView 自带滚动条一页显示不下会自动滚动而打印纸没有滚动条必须靠分页把内容切成一页一页。更麻烦的是用户说的“指定样式”往往不是只把文字打出来而是要把列标题、背景颜色、边框线、合计行、当前选中行的特殊标记等视觉元素都还原到纸上。如果这些细节都靠临时在 PrintPage 事件里手写绘制逻辑每次遇到新的表格样式都要重改一遍项目一多就变成灾难。万能打印模块的价值就是把“DataGridView 数据”和“纸张输出”之间这条链路标准化让调用方只需要设置参数就能得到一份样式可控的打印文档。1.2 模块边界控制到什么程度才算“万能”我设计这个模块时没有追求那种“传一个 DataGridView 就自动华丽排版”的过度自动化。那种方案在简单场景下很省事但遇到实际业务中的特殊样式时反而更难扩展。我这里所说的“万能”指的是对常见打印需求的覆盖能力列宽可以按页宽自动缩放也可以固定毫米数输出单元格字体、前景色、背景色、对齐方式能跟随 DataGridView 样式支持横向打印、纵向打印以及按行分页、按列分页页眉、页脚、页码、打印时间、标题行可自定义支持打印预览、直接打印、导出到 PDF 虚拟打印机。模块内部默认了一套规则但每个环节都暴露了可覆盖的入口。遇到特殊样式时你可以像做填空题一样只改一小块绘制代码而不是推翻整个流程。这样的设计既能快速接入普通表格又能应付那些“必须和领导给的 Excel 模板一模一样”的打印场景。2. 整体设计把表格控件变成一张可绘制的画布2.1 核心思路打印不是截屏而是重新绘制我最开始也试过用 Control.DrawToBitmap 把 DataGridView 直接画成一张位图再打印。这个方法在数据量小、页面刚好一屏时看起来挺省事但一旦数据超过一页就立刻露馅位图会被等比缩放字变得看不清列数多时整张图缩成邮票大小而且屏幕显示用的 96 DPI 和打印机常用的 300 DPI 根本不是一回事打印出来的清晰度完全不可控。后来我换成了标准的 PrintDocument GDI 绘制方案。思路其实很简单把纸张看作一块空白的画布把 DataGridView 看作数据的来源和样式模板在 PrintPage 事件里逐行、逐列绘制。你不需要真的在纸上“复刻”这个控件而是把控件里的数据值、单元格样式映射到纸张坐标系里。这样做的好处是显而易见的绘制过程完全由代码控制可以精确到毫米分页逻辑可以由我决定到底什么时候换页字体和颜色由 Graphics 对象实时渲染清晰度有保障。代价是工作量比截屏多不少但所有复杂的地方都可以被封装。2.2 技术选型为什么不用第三方报表控件市面上有很多现成的报表控件比如各种 Grid 控件的内置打印功能、商业报表组件甚至有人会选择把 DataGridView 导出到 Excel 再打印。这些方案在个别场景下确实更省力但我在实际项目里踩过不少坑第三方控件通常依赖自身的 Grid 控件要把现有 DataGridView 数据搬过去转换成本很高商业授权费用不低而且很多客户环境不允许额外安装运行时客户要求的“指定样式”往往非常本地化比如某个字段要加粗标红、某项要套用特定边框通用控件很难精确控制导出到 Excel 再打印依赖目标机器安装了 Office且打印效果会被 Excel 的页面设置干扰。所以我没有引入大型第三方库而是基于 .NET 自带的 System.Drawing.Printing 来做。这个命名空间里提供了 PrintDocument、PrintPreviewDialog、PrinterSettings 等类算是 WinForms 体系里最贴近打印需求的原始能力。用它开发代码量会多一点但完全可控不需要给客户多装任何东西。2.3 模块的主干结构模块大致分为三层数据包装层、绘制调度层、样式映射层。数据包装层负责把 DataGridView 的行列信息统一读取到一个内存模型中包括列名、列宽、单元格值、字体、颜色、对齐方式、是否可见等。这样做可以让打印模块不直接依赖 DataGridView 生命周期数据从数据源加载完毕后即使界面上的表格被刷新也不会影响正在打印的内容。绘制调度层是核心它负责计算分页、触发 PrintPage 事件、维护当前打印到第几行、判断还有没有下一页。它定义了打印的流程先画页眉再画表格内容最后画页脚和页码然后决定是否继续下一页。样式映射层则负责把 DataGridView 的各种 Style 属性翻译成 GDI 绘制参数。比如把 DataGridViewCellStyle 里的 BackColor 转换成 Brush把 ForeColor 转换成 Brush把 Alignment 转换成 StringFormat 的对齐方式。整个模块对外只暴露一个静态入口方法业务方一行代码就能调用。3. 核心细节剖析样式映射、分页与缩放3.1 样式映射让纸上的表格长得和屏幕上一样要打印出“指定样式”第一件事就是把 DataGridView 的样式规则完整读取出来。很多人只复制了数据丢掉了样式结果打印出来的是黑白表格客户立刻不满意。我的做法是遍历 DataGridView.Columns逐个读取 Visible、Width、HeaderText、DefaultCellStyle、HeaderCell.Style 等属性。对于单元格我会读取当前行每个单元格的 Value 和 Style。这里要注意DataGridViewCell.Style 里的 BackColor、ForeColor、Font、Alignment 在没有显式设置时可能会继承列样式、行样式或者表格默认样式。如果你只取 cell.Style.BackColor有可能拿到的是 Color.Empty。我在模块里封装了一个 GetRealBackColor(cell) 方法先判断单元格是否显式设置如果没有再向列、行、表格逐级回溯确保最终拿到的颜色和屏幕上实际显示的一致。字体方面更要小心。DataGridView 控件默认字体是 “Microsoft Sans Serif” 之类但单元格可能用宋体、微软雅黑打印时中文字体稍有偏差就会出现乱码或者方块。我的建议是在打印模块初始化时先扫描需要用到的所有字体系列然后调用 FontFamilies 做一次可用性检查遇到系统里不存在的字体就自动降级到宋体或微软雅黑。对齐方式映射比较直接DataGridViewContentAlignment 中的 MiddleLeft、MiddleRight、MiddleCenter 等枚举值需要翻译成 GDI 的 StringFormat 中的 Alignment 和 LineAlignment。左中右对应水平方向上中下对应垂直方向。我习惯用一个 switch 方法统一转换避免在绘制代码里到处写 if。3.2 分页算法保证行不截断、列不被挤丢分页是整个模块里最容易出 bug 的地方。按行分页时首先要计算“一页能放下多少行”。这个值不能直接用纸张高度除以行高因为还要减去页边距、页眉页脚高度和表头高度。公式大概是这样可用高度 纸张高度打印机单位 - 上下边距 - 页眉区高度 - 页脚区高度每页行数 可用高度 / 行高取整数当剩余空间小于一行高度时必须换页。这里面最容易忽略的是表头。如果表头只有一列标题通常放在第一页顶部后面各页不需要重复。但实际业务里很多表格需要每页都输出列标题我设计了一个 RepeatHeader 参数默认开启。开启后第一页先画表头每换一页再画一次表头这样跨页数据看起来仍然完整。按列分页比按行分页更复杂。当列数太多、横向一张纸放不下时有两种处理方式一是自动缩放所有列让总宽度适配页宽二是按固定列宽分栏打印每页显示一部分列。前者简单但可能导致文字过小后者更实用但需要处理跨页时行数据的连续性。我的模块里默认采用“等比缩放列宽”并提供一个 MaximumScale 参数如果缩放比例低于 0.6就提示调用方建议开启横向打印。横向打印能显著增加单页可容纳的列数在报表场景中应用非常多。分页状态用实例字段维护比如 _currentRow 表示当前打印到第几行每次 PrintPage 开始时保存当前 Graphics 状态绘制完成后根据 _currentRow 是否还有剩余行设置 HasMorePages。这里要特别提醒HasMorePages 必须在所有绘制完成后设置不能提前 return否则会出现“最后一页重复打印”或“漏行”的问题。3.3 页眉页脚、页码与打印预览完整的打印文档不能只有表格页眉页脚是必须的。我的模块里设计了 HeaderText、FooterText、ShowPageNumber、PrintDateTime 等属性。页眉通常放公司名称、报表标题、打印条件页脚放页码和打印日期。绘制页眉页脚时我统一使用 e.Graphics.DrawString并给页眉页脚区域预留固定的高度避免和表格内容重叠。页码的计算要额外注意。你不能在打印第一页时就写死“第 1 页共 N 页”因为事先不知道总页数。我的方案是分两遍打印第一遍模拟分页只统计总页数但不输出到打印机第二遍真正绘制时把总页数填充到页脚模板里。如果客户不太在意总页数也可以只显示“第 1 页”这样一遍就能完成。大多数业务场景里“第 1 页 / 共 N 页”比较正式多花一点时间也值得。打印预览我用的是 PrintPreviewDialog直接把刚才那个 PrintDocument 实例丢进去就行。预览和实际打印共用同一套绘制代码这样能最大限度保证“所见即所得”。不过预览窗口默认比例可能不适合大表格我建议在显示预览前把 UseAntiAlias 开启让预览效果更接近最终输出。4. 实操过程封装一个可以直接调用的打印模块4.1 最小化接入代码这个模块的核心入口我设计成一个静态方法业务代码里只需要两三行。下面是一段简化的调用示例using System.Drawing.Printing; PrintDocument doc DataGridViewPrinter.CreateDocument(dataGridView1); doc.DefaultPageSettings.Landscape true; doc.Print();如果你要打印预览就把最后一行换成PrintPreviewDialog dlg new PrintPreviewDialog(); dlg.Document doc; dlg.ShowDialog();是不是感觉很省事但省事的背后CreateDocument 内部做了很多事情。它会先读取 DataGridView 的列信息、样式信息生成一个内部的打印任务对象然后订阅 PrintPage 事件把分页状态初始化好。调用方不需要关心分页逻辑只要按需设置 PaperSize 和 PrinterSettings。如果要使用高度定制的样式可以在 CreateDocument 之后设置模块提供的一些参数DataGridViewPrinter.Options.PageTitle 销售出库单; DataGridViewPrinter.Options.RepeatHeader true; DataGridViewPrinter.Options.ShowRowIndex true; DataGridViewPrinter.Options.CellBorderWidth 0.5f;这种方式对业务团队非常友好因为大家不需要了解底层绘制细节。我封装好这个模块后项目里好几个窗口的打印功能几乎都是几行代码接好的差异只出现在个别需要额外定制样式的窗口上。4.2 参数调整与效果检查参数调整是打印模块后期维护的主要内容。我建议在正式接入前先拿一张数据量比较有代表性的表格做测试——比如单页能显示约 30 行、共 80 行的数据这样既能测试分页又能验证表头是否重复。初次跑通后重点检查三件事第一列宽是否与页面宽度匹配。如果默认等比缩放导致字号太小要立即手动指定列宽倍数。我的模块里提供了 ColumnWidthMode 枚举可以选 AutoFit自动适配页宽、FixedWidth固定毫米数、OriginalScale原始像素转毫米。实际项目里AutoFit 最常用但遇到某些“金额列必须比其他列宽”的需求时我会临时切到 FixedWidth 并把特殊列单独设置。第二检查颜色对比度。打印机的色彩还原度和屏幕还是有差别特别是浅色背景、灰色字体在预览里看着正常打出来可能看不清。经验是深色文字配浅色底色的表格打印效果最稳如果客户坚持要深色底、白字要提前告诉他打印效果会比较“糊”而且费墨水。第三检查是否有多余的空白页。这个问题通常由计算可用高度时的四舍五入导致。假设每页理论能放 28.7 行取整后可能是 28 行但实际绘制坐标到了第 29 行时已经超出页面导致最后一个空页被输出。我的解决办法是最后额外检查一次“当前页是否没有任何实际内容被绘制”如果是立即取消 HasMorePages。5. 常见问题与排查技巧实录5.1 打印出的内容总是偏离页面位置最常见的原因是打印机非打印区域也就是硬边距没有计算。很多打印机不能在纸张最边缘打印默认边距可能也有偏差。如果你按纸张左上角为原点绘制实际打印出来就会在左上角多出几毫米空白。我的处理方法是使用 PrinterSettings.HardMarginX 和 HardMarginY 作为初始偏移量并将页面大小换算成可打印区域大小。在 PrintPage 事件里设置 e.Graphics 的原点要综合考虑页边距和硬边距。这个坑很隐蔽因为预览窗口不受硬边距影响只有实际打印机才会体现出来。5.2 分页后某一行被硬生生截断分页算法里如果只按“行高乘行数”计算碰到内容超出单行高度的单元格比如一段很长的备注文本就会出现行被截断。虽然 DataGridView 单元格默认会截断显示但打印时客户往往希望完整打印。解决办法是给模块增加行高自适应逻辑。在分页之前先遍历当前页所有候选行使用 Graphics.MeasureString 计算每一行在当前页可用宽度下所需的实际高度取最大值作为该行的渲染高度。如果该高度导致当前页放不下就提前换页。这个逻辑增加了计算量但对内容不规则的表格非常必要。5.3 有背景色、图片的单元格打印不出来DataGridViewImageColumn 里的图片打印起来要比文字麻烦。首先要判断当前单元格的 Value 是不是 Image 类型其次Image 可能来自数据库的 byte[]需要先转成 MemoryStream 再加载最后绘制图片时要等比缩放不能直接拉伸到单元格宽高否则图片会变形。背景色绘制的问题通常出现在 DataGridView 启用了“交替行背景色”的场合。AlternatingRowsDefaultCellStyle 里的样式不会直接出现在每个单元格的 Style 上所以需要我专门判断当前是奇数行还是偶数行并获取对应的交替样式。这个细节很容易忽略一旦漏掉整张表就像被扒掉了衣服一样难看。5.4 大表格打印时性能卡顿甚至内存飙高当数据量达到几千行时如果每次 PrintPage 都重新解析整个 DataGridView速度会非常慢。我的优化方案是在 CreateDocument 阶段把需要用到的行数据全部拷贝到 List 内存模型中后续打印只从 List 里读取。这样界面线程对 DataGridView 的访问只需要一次后续打印过程全部基于内存数据既避免了跨线程问题又提升了速度。图片单元格的性能也要注意。如果表格里有大量图片建议在数据包装阶段统一转成合适分辨率的 Bitmap并缓存在列表里。不要每次绘制时都从数据库重新加载否则打印 50 页图片就重复加载 50 次。真正执行打印前还可以对图片做一次灰度二值化预览打印黑白文档时能减少传输数据量。6. 一些扩展思路与个人经验把这个模块做出来以后我又陆续给它加了几个功能支持自定义打印模板文本比如在页脚右侧加一个二维码或条形码支持把打印结果同时输出为 PDF 文件方便邮件发送支持对指定列做合并且不打印比如一些 ID 列只用于定位数据不出现在报表里。我个人最想提醒的一点是打印模块别等到项目上线再补。理想做法是在系统设计阶段就规划好哪些窗口需要打印统一预留打印按钮和样式配置入口。否则等项目后期让一个项目组成员临时加打印功能很容易做成“只在某个窗口能跑、换个表就崩”的代码后续维护成本比重新开发还高。还有一个经验之谈打印预览必须做得足够逼真。每次版本更新后不要把预览窗口当作可选功能尽量让用户在实际打印前先在预览里确认。因为对于业务系统使用者来说一张打印出来的单子可能直接会影响对账、入库、销售等流程预览能提前拦截大量格式问题。模块本身并不复杂但想让它“万能”就需要在实际业务里反复打磨把各种边角情况都补齐。希望这些思路能帮你在自己的项目里少走几条弯路。本文还有配套的精品资源点击获取
返回列表