news 2026/10/12 4:59:47

WinForms DataGridView 打印模块设计:自绘样式与分页避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WinForms DataGridView 打印模块设计:自绘样式与分页避坑指南

简介:这是一套面向Windows窗体开发者的打印组件,主要解决DataGridView控件中的数据按照指定样式输出到纸张的问题。组件内部封装了打印文档、页面设置对话框、打印预览对话框和页面设置等关键类,调用后即可完成从页面参数配置、预览确认到实际打印的完整流程;同时提供动态库调用方式,开发者不需要关心底层打印逻辑,直接引用生成的动态库就能集成到自身项目中。压缩包共45个文件,整体大小约319KB,核心内容包含11个C#源代码、3个编译好的动态库和3个可执行演示程序,另有程序使用说明文档、解决方案与工程文件,以及配套的示例数据库文件。包内划分为动态库生成工程和打印调用工程两部分,方便读者对照源码理解封装思路与调用方法。目前已有283人学习下载,很适合需要快速为DataGridView添加打印功能,或者希望学习打印类封装的初中级开发者。

1. 万能打印模块到底解决了什么:DataGridView 打印样式全靠自己画

做过 WinForms 项目的人都有过这种经历:系统里到处是 DataGridView,业务方看了一眼说“把这张表打出来”,结果你用 PrintPreviewDialog 拖一拖,出来的东西连自己都嫌丑——表头没有底色、列宽全乱、超过一页的表头直接消失、中文字体糊成一片。所谓“万能打印模块(将 DataGridView 控件中的数据指定样式打印)”,解决的正是这个问题:把网格里的数据按指定样式输出到打印机或 PDF,表头、边框、行高、交替色、页码全部可控。适合做进销存、MES、后台管理系统的开发者,哪怕没有报表组件授权,也能在两天内落地一套够用的打印能力。很多人以为难在画线,实际难在分页和样式系统。

2. 设计思路:先定绘制协议,再谈样式和分页

2.1 为什么不能用 DataGridView 自带打印:三种常见做法都有硬伤

刚接触这个需求时,我踩过三条弯路。第一条是把 DataGridView 直接绘制到 Bitmap,然后用 PrintDocument 把图片打印出去。这种做法在数据量小的时候能看,但超过一页就露馅:整张图被等比缩放,字小到需要拿放大镜,而且打印机分辨率一般是 300DPI,屏幕截图只有 96DPI,打出来全是锯齿。第二条是导出 Excel 再让用户自己打印——这等于把问题踢给用户,而且导出 Excel 组件在服务器环境容易出权限问题。第三条是引入第三方报表控件,功能是强大,但授权费不便宜,小项目根本划不来。

所以最后选的路是:不碰 DataGridView 的绘图结果,直接读取它的数据模型,用 GDI+ 在 PrintDocument 的 PrintPage 事件里一列一列画出来。这个过程类似自己写一个微型报表引擎,绘制逻辑全在掌握中,样式做到什么程度完全由代码决定。用低代码平台的同学可能觉得这太原始,但低代码平台自带的打印组件在样式自由度和跨页逻辑上往往更不可控,出了问题连源码都调不到。自己画虽然多写几百行代码,但这套逻辑可以打包成通用组件,项目里所有 DataGridView 打印需求都能复用。

以下是一张对比表,说明为什么最终选择自绘方案:

方案跨页表现样式控制依赖风险实际落地成本
截图/转 Bitmap 打印差,整页缩放基本无低代码量最少,效果最差
导出 Excel 打印好受 Excel 限制高(服务器安装 Office)代码量中等,体验差
第三方报表控件好强高(授权费)学习成本高,费用高
自绘 PrintDocument可控完全可控低代码量中等,一次写好处处复用

2.2 模块的三层拆分:参数层、绘制层、输出层

通用打印模块最忌讳把什么都写在一个类里。我一般把代码拆成三层:第一层是样式参数层,用一个 GridPrintStyle 类承载所有可调项,包括字体、颜色、行高、列宽策略、页边距、页眉页脚格式;第二层是绘制层,核心是一个 DataGridViewPrinter 类,它在 PrintDocument 的 PrintPage 事件里按参数逐行绘制;第三层是输出层,负责把绘制层接到预览对话框、直接打印、或输出成 PDF。

层之间通过接口和实体类传递数据,不互相引用控件实例。样式层不知道数据从哪来,绘制层不知道最终输出到打印机还是 PDF,输出层不关心表头什么颜色。好处是替换任何一个环节都不影响另外两个。比如后来项目里要支持打印 List 类型的数据,只需在数据接入层加一个适配器,绘制层一行不用改。

2.3 数据源接入:从 DataGridView 到打印行模型的转换

绘制层不能直接依赖 DataGridView 的 Cell 对象,因为 Cell 里存的是对象,显示什么文本取决于列的 ValueType 和 Format,直接 ToString 会得到一堆奇怪的结果。正确做法是先抽出一份打印行模型:每列输出什么文本、对齐方式是什么、列宽原始值是多少像素。以下代码展示如何从 DataGridView 抽取这些信息:

public class PrintColumnInfo { public string HeaderText { get; set; } public string PropertyName { get; set; } public int WidthPixel { get; set; } public DataGridViewContentAlignment Alignment { get; set; } public string FormatString { get; set; } } public class PrintRowData { public List<string> CellTexts { get; set; } } public static List<PrintColumnInfo> ExtractColumns(DataGridView grid) { var list = new List<PrintColumnInfo>(); foreach (DataGridViewColumn col in grid.Columns) { if (!col.Visible) continue; // 隐藏列不打印,这是最常见的需求 var info = new PrintColumnInfo { HeaderText = col.HeaderText, PropertyName = col.DataPropertyName, WidthPixel = col.Width, Alignment = col.DefaultCellStyle.Alignment, FormatString = col.DefaultCellStyle.Format }; list.Add(info); } return list; }

这段代码里有两个关键点。其一是 Visibility 过滤,界面上为了操作方便隐藏的列(比如主键 ID)不该出现在打印结果里。其二是 DataPropertyName 的保留,这个属性在后续从数据行取值时要用。注意这里没有处理按钮列、图片列这类特殊列,它们需要单独过滤,后面避坑章节会细说。

从 DataGridView 取显示文本时,不要直接读 Cell.Value,因为有些列经过了 CellFormatting 事件格式化。安全做法是调用 grid.Rows[i].Cells[j].FormattedValue,这个属性返回的就是界面上看到的文本,已经应用了格式:

public static PrintRowData ExtractRow(DataGridView grid, int rowIndex) { var row = new PrintRowData(); row.CellTexts = new List<string>(); foreach (DataGridViewColumn col in grid.Columns) { if (!col.Visible) continue; var cell = grid.Rows[rowIndex].Cells[col.Index]; object val = cell.FormattedValue ?? ""; row.CellTexts.Add(val.ToString()); } return row; }

用 FormattedValue 代替 Value,是避免样式翻车的关键细节。比如某列存的是 DateTime,界面上格式化成 “2025-06-01”,如果直接用 Value,拿到的是 DateTime 对象,ToString 出来可能是 “2025/6/1 0:00:00”。用 FormattedValue,打印出来的内容和屏幕上看到的一致,业务方不会因为“屏幕和打印不一样”来找你麻烦。

2.4 分页与跨页:一页画不下时怎么断行

分页是整个模块里最容易想简单的地方。新手写打印逻辑,通常是在 PrintPage 里数着行高一行一行画,画到页面底部就设置 e.HasMorePages = true。这个思路没错,但有三个细节容易被忽略:表头必须在每页重复绘制、行高固定模式与自适应模式的分页判断不同、最后一页可能出现只有表头没有数据的情况。

我在绘制层里维护一个_currentRowIndex字段,记录已经画到第几行。每次 PrintPage 触发时,先画表头,然后从 _currentRowIndex 开始画数据行,每画一行更新一次当前 Y 坐标,画到超出页面可用高度时就结束本页,把 e.HasMorePages 设为 true,等待下一次事件继续画。如果当前页第一行数据还没画出来就已经超出高度,说明这个表格一页根本放不下一行数据,这时候应该自动切换横向打印,而不是把内容裁掉。

还有一个细节是动态行高。默认情况下所有数据行等高,分页判断就是简单的除法。但某些列文本很长需要换行时,行高会变化,这个情况下不能提前算出每页行数,只能边画边算。用 Graphics.MeasureString 测量文本换行后的高度,然后累加,判断是否超出边界。这个做法性能略差,但打印本身不是高频操作,完全能接受。

3. 核心绘制循环:从最小可用代码到完整打印器

3.1 最小可用的 PrintDocument 绘制循环

这一节给出一套可以直接抄走的完整实现。先定义样式参数类,把频率最高的可调项都做成公开属性:

public class GridPrintStyle { public Font HeaderFont { get; set; } public Font CellFont { get; set; } public Color HeaderBackColor { get; set; } public Color HeaderForeColor { get; set; } public Color BorderColor { get; set; } public Color CellBackColor { get; set; } public Color AlternatingRowColor { get; set; } public bool UseAlternatingRowColor { get; set; } public int HeaderHeightPixel { get; set; } public int RowHeightPixel { get; set; } public Margins PageMargin { get; set; } public string PageTitle { get; set; } public bool ShowPageNumber { get; set; } public bool AutoFitColumns { get; set; } public float MinColumnScale { get; set; } public GridPrintStyle() { HeaderFont = new Font("微软雅黑", 9, FontStyle.Bold); CellFont = new Font("微软雅黑", 9); HeaderBackColor = Color.FromArgb(45, 62, 80); HeaderForeColor = Color.White; BorderColor = Color.FromArgb(120, 120, 120); CellBackColor = Color.White; AlternatingRowColor = Color.FromArgb(242, 245, 248); UseAlternatingRowColor = true; HeaderHeightPixel = 30; RowHeightPixel = 24; PageMargin = new Margins(50, 50, 50, 60); ShowPageNumber = true; AutoFitColumns = true; MinColumnScale = 0.5f; } }

这里把样式参数全部集中在一个类里,后续要给不同项目不同打印需求做预设值就非常方便。比如库存类单据用紧凑样式,就把 RowHeightPixel 调小;对外报表把 HeaderBackColor 改成深蓝。这个类没有任何输出逻辑,纯数据,测试也好写。

然后是打印器主类。它接收 DataGridView 实例和样式参数,提供三个对外方法:PrintToPreview()、PrintDirect()、PrintToPdf()。所有方法共用一个内部方法 PrepareAndPrint():

public class DataGridViewPrinter { private DataGridView _grid; private GridPrintStyle _style; private List<PrintColumnInfo> _columns; private int _currentRowIndex; private int _pageNo; public DataGridViewPrinter(DataGridView grid, GridPrintStyle style) { _grid = grid; _style = style; _columns = ExtractColumns(grid); } public void Preview() { using (var doc = BuildPrintDocument()) using (var dialog = new PrintPreviewDialog()) { dialog.Document = doc; dialog.ShowDialog(); } } private PrintDocument BuildPrintDocument() { var doc = new PrintDocument(); doc.DefaultPageSettings.Margins = _style.PageMargin; doc.DefaultPageSettings.Landscape = IsLandscapeNeeded(); doc.PrintPage += OnPrintPage; return doc; } private bool IsLandscapeNeeded() { // 粗略判断:全部列宽总和超过纵向页面宽度就打横向 int totalWidth = _columns.Sum(c => c.WidthPixel); using (var g = _grid.CreateGraphics()) { float pageWidth = g.DpiX * 8.27f; // A4 纵向约 8.27 英寸 return totalWidth > pageWidth; } } private void OnPrintPage(object sender, PrintPageEventArgs e) { Graphics g = e.Graphics; g.Clear(Color.White); _pageNo++; // 绘制页眉 float yPos = DrawPageHeader(g, e); // 绘制表头 yPos = DrawTableHeader(g, e, yPos); // 绘制数据行 yPos = DrawDataRows(g, e, yPos); // 绘制页脚 DrawPageFooter(g, e); e.HasMorePages = _currentRowIndex < _grid.Rows.Count; } }

这段代码是整个模块的骨架。有几个耐人寻味的地方:IsLandscapeNeeded 在构造时算一次,而不是每次翻页都算;页眉绘制的返回值是结束时的 Y 坐标,因为页眉区域高度会影响表头和数据行的起始位置。这些细节如果没处理,到了第二页就会出现页眉和表头重叠的问题。

3.2 表头、数据行、边框线的绘制顺序与 GDI+ 细节

绘制顺序不能乱。先画背景填充矩形,再画文字,最后画边框线,这样文字不会被线条覆盖。表头部分代码:

private float DrawTableHeader(Graphics g, PrintPageEventArgs e, float yPos) { float xPos = e.MarginBounds.Left; float[] colWidths = CalculateColumnWidths(e); // 表头背景 using (var bgBrush = new SolidBrush(_style.HeaderBackColor)) using (var fgBrush = new SolidBrush(_style.HeaderForeColor)) { RectangleF headerRect = new RectangleF( xPos, yPos, e.MarginBounds.Width, _style.HeaderHeightPixel); g.FillRectangle(bgBrush, headerRect); // 逐个画列标题 float colX = xPos; for (int i = 0; i < _columns.Count; i++) { RectangleF cellRect = new RectangleF( colX, yPos, colWidths[i], _style.HeaderHeightPixel); StringFormat sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center, Trimming = StringTrimming.EllipsisCharacter }; g.DrawString(_columns[i].HeaderText, _style.HeaderFont, fgBrush, cellRect, sf); colX += colWidths[i]; } // 表头下边框线 using (var pen = new Pen(_style.BorderColor, 1f)) { g.DrawLine(pen, xPos, yPos + _style.HeaderHeightPixel, xPos + e.MarginBounds.Width, yPos + _style.HeaderHeightPixel); } } return yPos + _style.HeaderHeightPixel; }

注意 StringFormat 里设置了 Trimming = EllipsisCharacter。这个设置保证列宽不够时,超长表头显示为省略号而不是溢出到相邻列。很多打印模块的翻车现场就是忘了这个属性,表头文字串到隔壁列上,整个表看起来像花屏。

数据行的绘制理论上和表头是同一套代码,只是画笔颜色换成数据样式。但这里有个小陷阱:交替行背景色在跨页时依然要连续,不能每页从第一行重新算颜色。因为 _currentRowIndex 是全局递增的,只要用_currentRowIndex % 2 == 0判断当前行是否是奇数行,就能保持整个表格的颜色连续性。这个问题不仔细很容易踩,后面避坑章节再展开。

3.3 列宽策略:按比例缩放、固定宽度、自动换行

列宽是最能体现“指定样式”的地方。DataGridView 里的列宽是像素,屏幕渲染基本按 96DPI 计算,但打印机可能是 300DPI 或 600DPI,直接搬像素数画出来会严重偏窄。正确做法是把列宽统一视为相对权重,然后按页面可用宽度重新分配:

private float[] CalculateColumnWidths(PrintPageEventArgs e) { float totalWidthPixel = _columns.Sum(c => c.WidthPixel); float pageWidth = e.MarginBounds.Width; if (!_style.AutoFitColumns) { // 固定宽度模式:像素按 96DPI 换算成 1/100 英寸再乘缩放系数 float scale = e.Graphics.DpiX / 96f; float[] fixedWidths = _columns .Select(c => c.WidthPixel * scale) .ToArray(); return fixedWidths; } // 按比例缩放模式 float[] result = _columns .Select(c => c.WidthPixel / totalWidthPixel * pageWidth) .ToArray(); // 如果按比例算出的某列太窄,做最小宽度保护 float minWidth = pageWidth / _columns.Count * 0.6f; for (int i = 0; i < result.Length; i++) { if (result[i] < minWidth) { result[i] = minWidth; } } return result; }

这段代码提供了两种列宽策略。AutoFitColumns 为 true 时是常用的“自适应铺满页面”模式,列之间宽度比例保持和屏幕一致。为 false 时是“所见即所得”模式,列宽严格按屏幕像素等比放大,适合用户手动调整过列宽的场景。最小宽度保护很关键,因为如果某列特别窄,按比例计算会得到几像素的宽度,文字全变成竖排省略号。

4. 样式参数与打印设置:一张表讲清所有可调项

4.1 样式参数说明表

把 GridPrintStyle 中每个字段的含义、默认值、注意事项列成一张表,方便团队协作时参考:

参数默认值取值范围说明
HeaderFont微软雅黑 9pt 粗体系统安装字体表头字体,中英文混排优先选微软雅黑
CellFont微软雅黑 9pt系统安装字体数据单元格字体
HeaderBackColor深灰蓝 (45,62,80)任意 Color表头背景,建议与公司 VI 色一致
UseAlternatingRowColortruetrue/false是否启用交替行色
AlternatingRowColor浅灰 (242,245,248)任意 Color交替行背景,打印时建议用浅色
RowHeightPixel2416-60数据行行高;内容多时调大防止文字被截断
AutoFitColumnstruetrue/false列宽自动铺满页面
MinColumnScale0.50.3-0.9列宽最小缩放比,防止列太窄
PageTitle空任意字符串页眉显示的大标题
ShowPageNumbertruetrue/false是否显示第 x / 共 y 页

字体这一项最值得多说两句。很多人直接写new Font("宋体", 9),在 Windows 7 上没问题,换到 Windows 10/11 上宋体渲染偏细,打印出来笔画很淡。微软雅黑在屏幕和打印机上的表现更一致。如果客户机器没有微软雅黑,可以加一个回退逻辑:用FontFamily.GenericSansSerif做兜底。

4.2 页边距、纸张大小与横向打印设置

打印设置通常在 PrintDialog 里让用户自己调,但代码里要提供一套智能默认值。最常见的需求是:数据列多时自动切横向,列少时保持纵向。这个判断不能只依赖列数,应该依赖 2.4 里算出的总列宽。

doc.DefaultPageSettings.Landscape = IsLandscapeNeeded(); doc.DefaultPageSettings.Margins = new Margins(40, 40, 50, 60);

页边距单位是 1/100 英寸。PrintDocument 的页边距和 Graphics 坐标系是两套单位,PrintPage 事件里 e.MarginBounds 已经帮你算好了扣除页边距后的可用区域,单位是 1/100 英寸。但 Graphics 默认坐标单位是像素,也就是 DPI 相关。所以用 e.MarginBounds.Left 作为 X 起点不存在问题,但如果自己用e.PageBounds去做坐标计算,要先把 1/100 英寸乘以 DpiX / 100 换算成像素。这里是最容易闹出“打印结果整体偏移”的地方。

4.3 页眉页脚:页码、总页数、标题文本的自定义模板

页眉页脚看起来简单,做不好会显得很业余。我一般把页眉页脚做成支持占位符的模板字符串,运行期替换变量。常见的占位符有 {TITLE}、{PAGENO}、{PAGECOUNT}、{DATETIME}。这样项目里每个窗口可以拼出自己的模板,比如“出货单 - 第 {PAGENO} / {PAGECOUNT} 页”。

private string FormatTemplate(string template, int pageNo, int pageCount) { return template .Replace("{TITLE}", _style.PageTitle) .Replace("{PAGENO}", pageNo.ToString()) .Replace("{PAGECOUNT}", pageCount.ToString()) .Replace("{DATETIME}", DateTime.Now.ToString("yyyy-MM-dd HH:mm")); }

总页数怎么算?最自然的做法是先完整模拟打印一遍,统计出总页数,再正式打印。打印本身很快,几千行数据模拟一次最多几十毫秒,值得做。用一个CalculatePageCount()方法走一遍分页逻辑,不触发真正的绘制,只统计PrintPage事件里的 _currentRowIndex 推进次数。这样在预览界面就能直接显示“第 2 页 / 共 5 页”,体验好很多。

5. 避坑实录:从乱码到列宽错位,5 个最常见的排查与解决

5.1 打印出来中文变成方块或问号

现象:数据里中文在屏幕上显示正常,打印出来一部分变成 “????” ,或者变成空心方块。这个现象在针式打印机上尤其常见,激光打印机偶尔也有。

原因:两种情况。一种是代码里用了Font但没指定中文字体,GDI+ 在找不到字符时用默认字体渲染,结果字形缺失。另一种是打印机的自带字体库不支持 Unicode,针式打印机的内置字库只覆盖 GB2312,遇到生僻字就显示问号,这个不是代码问题。

解决:代码里优先使用“微软雅黑”或“宋体”这类明确支持中文的字体,不要用SystemFonts.DefaultFont。同时把 PrintDocument 的PrinterSettings里字体替换功能打开,在打印事件里自查一遍文本是否包含超出 GB2312 范围的字符,有的话提示用户更换字体。

5.2 打印结果列宽与预览对不上

现象:预览时表格宽度刚好,打印出来整体向左偏移,或者右边多出一大块白边。

原因:像素和 1/100 英寸混用了。预览控件使用的是屏幕 DPI(通常 96),打印机是设备 DPI(常见 300/600),直接拿像素数当打印坐标就会整体缩小。很多人在 PrintPage 里写死x = 100,这个 100 在屏幕上等于 1 英寸多,在打印机上只有零点几英寸。

解决:所有坐标计算必须基于e.MarginBounds,不要用e.PageBounds。列宽换算用 96DPI 做基准:屏幕像素数 × (设备 DpiX / 96) = 打印像素数。把这段换算封装成一个工具方法,所有绘制逻辑都走它,避免各处零散转换导致漏算。

5.3 跨页后表头消失

现象:第一页有表头,第二页开始直接从数据行开始,没有表头,读者翻页后不知道每一列是什么。

原因:PrintPage 事件里只画了一次表头,把表头绘制写在分页判断之前,但没有在每次进入事件时都重新画。

解决:把表头绘制逻辑放进 OnPrintPage 里,每次翻页后第一件事就是重新绘制表头。第二步是处理好表头和数据行的间距,不然表头粘贴在数据行第一行的上边缘,分辨不出来谁是谁。按照 3.2 的写法,每次事件都先 DrawTableHeader 再 DrawDataRows 就能解决问题。

5.4 数据量很大时预览卡死

现象:DataGridView 里几千行数据,打开打印预览要卡 5-10 秒,而且预览时滚轮拖动不流畅。

原因:打印逻辑里反复调用MeasureString,每调用一次都要做字体测量,几千行乘几千次测量,性能就崩了。另外有些代码在绘制时,遍历所有行提前算好所有文本像素尺寸,这完全是浪费。

解决:固定行高模式不做 MeasureString,直接按行高整数除法算出每页最大行数。动态行高模式只在当前页真实绘制的行上调用 MeasureString,而且缓存同一列中相同文本的结果。另外,把打印数据的获取从 DataGridView.Rows 改成绑定的 DataTable,避免每次访问 Row 对象触发控件内部的重绘逻辑。

5.5 导出 PDF 时边框线丢失或颜色变浅

现象:直接打印没问题,输出到 PDF 后,边框线很淡,有的细得几乎看不见,表头深色背景变成浅灰。

原因:系统 PDF 虚拟打印机在处理 GDI+ 线条时,会把 1f 宽度 Pen 按先到先得的原则抖动抗锯齿,颜色对比度被压缩。这不是打印代码的问题,是 PDF 虚拟打印机的渲染策略。

解决:绘制边框时把 Pen 宽度改成 1.2f 到 1.5f,颜色比预期更深一些(如边框灰从 120,120,120 改为 80,80,80)。表头背景色用暗色系时,输出到 PDF 会有轻微色偏,属于正常现象,可按项目要求决定是否单独调整 PDF 模式下的调色板。

6. 进阶:输出 PDF、合并多表打印与预览缩放

6.1 把打印作业输出到系统 PDF 虚拟打印机

不需要引入任何第三方库。直接把 PrintDocument 的 PrinterSettings.PrinterName 设为系统预装的“Print to PDF”虚拟打印机,调用 Print() 时会弹出另存为对话框。代码实现如下:

public void PrintToPdf() { var doc = BuildPrintDocument(); string pdfPrinter = GetPdfPrinterName(); if (string.IsNullOrEmpty(pdfPrinter)) { MessageBox.Show("未检测到 PDF 虚拟打印机,请先安装系统自带组件。"); return; } doc.PrinterSettings.PrinterName = pdfPrinter; doc.Print(); } private string GetPdfPrinterName() { foreach (string name in PrinterSettings.InstalledPrinters) { if (name.Contains("PDF")) { return name; } } return null; }

虚拟打印机在不同 Windows 版本上的名称有差异,有的是“Microsoft Print to PDF”,有的是“PDF 打印机”。用 Contains("PDF") 匹配最保险。这段代码在服务端(IIS 部署)里不可用,因为虚拟打印机依赖桌面会话,建议只在客户端程序里提供导出 PDF 功能,服务端导出用别的手段。

6.2 一张纸上打印主表加明细表:二次绘制坐标偏移技巧

实际业务中经常遇到“主表是订单头、明细表是多行商品”的打印需求。最简单的方式是把表格绘制方法从 PrintPage 事件里抽出来,接收一个 Y 偏移参数。先画完主表后获得主表末尾 Y,再把明细表以这个 Y 为起点继续画。同时设置明细表的当前行索引为 0,明细表分页逻辑从“第 2 页是否继续主表数据”改成“第 2 页重新绘制明细表”。实现时注意清除主表占用的高度,避免两表交界处重叠或留白过大。

6.3 预览窗口的缩放与鼠标滚轮支持

PrintPreviewDialog 自带缩放功能,但缩放到 100% 以上时文字边缘偶尔会出现明显的毛边。换成 PrintPreviewControl 放在自定义窗体里,把它的 ZoomMode 设为 Auto,再用 ShowGrid 属性显示边距参考线,交互体验会好很多。缩放操作时应该调用 PrintPreviewControl.InvalidatePreview() 而不是设置 Size 来拉伸,后者只是把图放大,不会重新渲染,效果就是虚的。

6.4 性能优化的一个硬习惯

最后说一个踩坑踩出来的习惯。每次 PrintPage 事件里绘制文本前,检查一下当前行的所有 CellTexts 是否有内容。整行都是空字符串的行,直接跳过不画,不跳高度。DataGridView 中常出现空行占位的情况——比如用户按了几次 Ctrl+End,Rows.Count 已经很大,但实际数据只有前几行。如果不加这个判断,打印结果会有大量空白页,光是排查这个问题就能耗掉半天。另一个习惯是把公共的字体和画刷对象放在 using 块外创建,复用而不是在每行里 new,减少 GDI 对象句柄泄漏的风险。

等项目跑过一两个真正把打印需求丢过来的客户后,你会发现这套模块的价值不只是省了买报表控件的钱,而是给团队留了一个所有打印规律全都知道的内部工具。AI 编程助手能帮你生成一版能跑的打印代码,但列宽换算的坑、跨页表头的坑、PDF 虚拟打印机的渲染差异,这些只有踩过一遍才能变成你的潜意识。我的习惯是每次新接到打印需求,先打开这个模块的样式默认值,对着需求清单改十分钟参数,再跑一次预览截图发给业务方确认,基本一轮就能定稿。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 4:56:47

外贸老商家升级台式双屏翻译机的实战价值

在外贸档口摸爬滚打多年的老商家&#xff0c;大多都有过这样的经历&#xff1a;生意刚起步时&#xff0c;手里攥个手机或简易手持翻译机&#xff0c;觉得能比划着沟通就足够了。那时候单量不大&#xff0c;客户三三两两&#xff0c;设备递来递去也不觉得麻烦。可随着口碑积累&a…

作者头像 李华
网站建设 2026/10/12 4:56:33

UE多敌人AI性能优化:从GameThread瓶颈到行为树调度实战

前阵子做一个 UE FPS 玩法验证&#xff0c;我在场景里放了 40 个敌人 AI&#xff0c;一进场帧率直接从 120 掉到 35。当时第一反应是显卡吃不消&#xff0c;结果打开性能面板一看&#xff0c;GPU 占用还不到一半&#xff0c;卡死的是 CPU 的 GameThread。后来我花了两天把多敌人…

作者头像 李华
网站建设 2026/10/12 4:55:42

SCA逐次凸近似:非凸优化问题的工程实践与避坑指南

简介&#xff1a;这份资源聚焦SCA&#xff08;Sequential Convex Approximation&#xff09;凸优化算法的实现&#xff0c;面向具备一定数学优化基础、希望将非凸问题转化为连续凸近似求解的学习者与工程人员&#xff0c;可用于无线通信、信号处理、能源系统优化等场景的算法验…

作者头像 李华
网站建设 2026/10/12 4:55:38

用着自家的招牌界面,后台却悄悄把客户送去老对手那里干活

用着自家的招牌界面&#xff0c;后台却悄悄把客户送去老对手那里干活 你在屏幕前打开一个挂着谷歌商标的对话框&#xff0c;输入一段复杂的业务需求&#xff0c;按下回车。你以为后台正在全力运转谷歌最骄傲的自家模型&#xff0c;但水面之下&#xff0c;这个系统却在悄悄做出判…

作者头像 李华
网站建设 2026/10/12 4:55:24

基于BP神经网络的电力负荷预测:从特征工程到PyTorch实现

简介&#xff1a;基于BP神经网络实现电力负荷预测的Matlab学习资料包&#xff0c;面向本硕博及科研入门者&#xff0c;聚焦电力负荷序列的非线性映射与短期预测建模问题&#xff0c;可用于算法编程学习、课程设计、论文验证或教学演示。资源共3个文件&#xff0c;包含可直接运行…

作者头像 李华
网站建设 2026/10/12 4:52:58

手写Tool Use的三种方案:从正则解析到Agent循环的完整实战指南

面试官让手写一个 Tool Use&#xff0c;这事我在模拟面试里遇到过不止一次。说实话&#xff0c;第一次听到这个题的时候我愣了一下——不是不会&#xff0c;是没想到对方会把问题压得这么具体。Tool Use 说白了就是让模型调用外部函数&#xff0c;把“只动嘴”的大模型变成“能…

作者头像 李华