资讯详情

资讯详情

UltraGrid打印深度指南:WinForms表格精准输出A4报表

1. 项目概述为什么UltraGrid打印不是“点一下就完事”的事在某高校实验室开发一套设备状态监控系统时我接手了一个遗留模块——用C# WinForms写的本地客户端核心数据展示全靠Infragistics的UltraGrid控件。客户提了个看似简单的需求“报表要能直接打出来格式跟屏幕上看到的一模一样带表头、页眉页脚、分页自动断行还要支持A4横向/纵向切换。”我心想不就是调个PrintDocument结果三天没跑通一页像样的纸——表格内容被截断、列宽错乱、合并单元格消失、字体缩放失真甚至有次打印预览里连网格线都消失了。这才意识到UltraGrid的打印机制根本不是WinForms原生控件那套逻辑它走的是自己封装的渲染管线底层依赖GDI绘图上下文重绘而这个重绘过程对DPI缩放、打印机物理分辨率、页面边距计算极其敏感。关键词UltraGrid打印、C# Infragistics、WinForms表格导出说白了这不是功能调用问题而是图形渲染上下文迁移问题。它解决的是“如何把屏幕上的视觉呈现无损映射到物理纸张坐标系”的工程难题。适合正在维护老系统、又不敢贸然替换UI框架的开发者也适合需要快速交付合规打印能力比如医疗设备日志、工业参数记录但预算有限、无法上WPF或Web方案的团队。如果你正对着UltraGrid.Print()方法发呆或者PrintPreviewDialog里看到一堆错位的方块这篇就是为你写的——不讲虚的API列表只说我在六个项目里踩出来的坑、算出来的参数、抄过来就能用的配置。2. UltraGrid打印机制深度拆解它到底在“打印”什么2.1 渲染路径的本质从屏幕像素到打印机点阵的两次坐标转换很多人以为UltraGrid.Print()是把当前Grid截图然后塞进打印机这是最大误区。实际流程是UltraGrid内部会创建一个虚拟打印设备上下文PrintDC然后调用其自身的DrawToGraphics()方法将Grid的每一行、每一列、每一个单元格样式逐元素重新绘制到这个DC上。这个过程完全脱离屏幕显存而是基于打印机的物理DPI通常96-600dpi和页面尺寸如A4210×297mm重新计算所有坐标。关键点在于屏幕坐标系是整数像素单位而打印机坐标系是逻辑英寸单位1英寸100逻辑单位UltraGrid内部用GraphicsUnit.Inch做换算。这意味着当你在设计器里设列宽为120像素在100% DPI下屏幕显示正常但打印时会被换算成120 / 96 ≈ 1.25英寸再乘以打印机DPI比如300最终输出375个点。如果打印机DPI是600同样列宽就变成750点——这直接导致列宽爆炸式增长撑破页面。我实测过同一台电脑连不同型号打印机UltraGrid打印效果差异极大根源就在这里。2.2 打印对象的三层结构PrintDocument、UltraGridPrintDocument、UltraGridPrintSettingsUltraGrid打印不是直连.NET Framework的PrintDocument而是通过Infragistics自研的包装层UltraGridPrintDocument继承自PrintDocument但它重写了OnPrintPage()方法。这里才是核心战场——它不直接画Grid而是调用UltraGridPrintSettings生成的布局信息。UltraGridPrintSettings这才是真正的“打印配置中心”。它包含PageSetup纸张大小、方向、HeaderFooterSettings页眉页脚文本、字体、对齐、PrintStyle是否打印选中行、是否打印隐藏列等。重点来了PrintStyle里的PrintMode属性有三个值PrintMode.Normal按屏幕当前视图打印但会忽略滚动条位置从第一行开始PrintMode.SelectedRowsOnly只打选中的行但要注意——如果用户用CtrlA全选它会打所有行而不是可视区域PrintMode.VisibleRowsOnly最常用也最容易出错的模式。它本意是只打当前滚动可见的行但UltraGrid有个Bug当Grid高度小于一页时它会错误地重复打印多页。我遇到过一次Grid只有5行却打了12页空白纸最后发现是VisibleRowsOnly在低DPI下误判了“可见”范围。UltraGridPrintPreviewDialog不是简单的预览窗口它内部会模拟打印机DC创建一个Metafile然后用GDI回放。所以预览里看到的效果基本等于真实打印效果——这也是为什么必须先调通预览再碰真机。提示不要试图用grid.Print()快捷方法。它内部默认用Normal模式且不暴露设置对象调试时你会失去所有控制权。正确姿势永远是var printDoc new UltraGridPrintDocument(grid); printDoc.PrintSettings mySettings;2.3 字体与缩放的隐性陷阱为什么你的宋体变成了黑体UltraGrid打印时字体处理有两套逻辑单元格内文本字体取自UltraGrid.DisplayLayout.Appearance.Font但打印时会强制应用PrintSettings.HeaderFooterSettings.Font覆盖页眉页脚而单元格字体则受PrintSettings.PrintStyle.CellFont控制。注意CellFont默认是null此时它会回退到DisplayLayout.Appearance.Font但如果这个Font是new Font(微软雅黑, 9f)而打印机驱动不支持该字体嵌入Windows会自动替换为MS Sans Serif——这就是你看到字体突变的原因。缩放策略UltraGrid提供PrintSettings.ScaleToFit属性设为true时它会计算整个Grid内容宽度与页面可用宽度的比值然后全局缩放。但这个缩放是等比缩放会导致行高、列宽、字体大小全部按比例压缩。我试过一个10列Grid设ScaleToFittrue后字体从9pt缩到6.2pt肉眼几乎无法识别。更糟的是某些打印机驱动在缩放后会丢失抗锯齿文字边缘出现毛刺。所以我的经验是永远手动计算列宽禁用ScaleToFit用PrintSettings.PageSetup.Margins精确控制留白。3. 实操全流程从零配置到稳定输出的七步法3.1 第一步初始化PrintSettings并锁定基础参数代码即配置private UltraGridPrintSettings SetupPrintSettings(UltraGrid grid) { var settings new UltraGridPrintSettings(); // 1. 纸张与方向必须显式指定不能依赖系统默认 settings.PageSetup.PaperSize new PaperSize(A4, 827, 1169); // 单位十分之一英寸 settings.PageSetup.Landscape false; // 横向设为true // 2. 页边距关键单位是“十分之一英寸”不是像素 // A4标准边距上2.54cm100左2.54cm100右2.54cm100下2.54cm100 settings.PageSetup.Margins new Margins(100, 100, 100, 100); // 3. 打印模式选VisibleRowsOnly但要配合后续的行高修正 settings.PrintStyle.PrintMode PrintMode.VisibleRowsOnly; settings.PrintStyle.PrintHiddenColumns false; settings.PrintStyle.PrintSelectedRowsOnly false; // 4. 字体必须显式指定可嵌入字体避免替换 // 宋体、黑体、楷体是Windows标配打印机兼容性最好 settings.PrintStyle.CellFont new Font(SimSun, 9f, FontStyle.Regular); settings.HeaderFooterSettings.Font new Font(SimSun, 10f, FontStyle.Bold); // 5. 网格线默认不打印必须开启 settings.PrintStyle.PrintGridLines true; settings.PrintStyle.GridLinePen new Pen(Color.Black, 0.5f); // 线宽0.5pt太粗会糊 return settings; }这段代码里藏着三个硬核细节PaperSize构造函数的宽高单位是十分之一英寸A4是210×297mm换算210mm ÷ 25.4 ≈ 8.27英寸 × 10 827同理297mm1169。如果填错预览里页面直接变形。Margins也是十分之一英寸单位1001英寸2.54cm。很多开发者填1000结果留白大得离谱。GridLinePen的宽度设为0.5f这是经验值。设1.0f在600dpi打印机上会变成一条粗黑线盖住文字设0.25f在96dpi屏幕上预览看着细但打印出来可能断线。3.2 第二步页眉页脚动态注入——让每页都带时间戳和标题客户要求“每页顶部显示‘设备运行日志’当前时间右下角显示页码”。UltraGrid的HeaderFooterSettings支持文本模板但{DateTime}是静态的只会取打印开始那一刻的时间。要实现“每页实时时间”必须用事件钩子private void ConfigureHeaderFooter(UltraGridPrintDocument printDoc, UltraGrid grid) { printDoc.PrintPage (sender, e) { // 获取当前页码从0开始 int currentPage e.PageNumber 1; // 页眉居中10号加粗宋体 string headerText $设备运行日志 — {DateTime.Now:yyyy年MM月dd日 HH:mm:ss}; using (var font new Font(SimSun, 10f, FontStyle.Bold)) { SizeF textSize e.Graphics.MeasureString(headerText, font); float x (e.MarginBounds.Width - textSize.Width) / 2 e.MarginBounds.Left; float y e.MarginBounds.Top - 20; // 上边距外20单位 e.Graphics.DrawString(headerText, font, Brushes.Black, x, y); } // 页脚右下角9号常规宋体 string footerText $第 {currentPage} 页; using (var font new Font(SimSun, 9f)) { SizeF textSize e.Graphics.MeasureString(footerText, font); float x e.MarginBounds.Right - textSize.Width - 10; float y e.MarginBounds.Bottom 10; e.Graphics.DrawString(footerText, font, Brushes.Black, x, y); } }; }这里的关键是e.MarginBounds返回的是扣除页边距后的可用绘图区域单位是像素不是逻辑英寸。所以页眉y坐标用e.MarginBounds.Top - 20确保在页边距上方20像素处画字。我试过用e.Graphics.PageUnit GraphicsUnit.Inch切换单位结果所有坐标全乱最终发现UltraGrid的PrintPage事件里e.Graphics的单位始终是像素强行改PageUnit会引发GDI异常。3.3 第三步列宽自适应算法——让10列表格稳稳落在A4纸上UltraGrid没有AutoResizeColumnsOnPrint属性必须手动计算。核心思路获取每列内容最大宽度含表头加上内边距再按比例缩放到页面可用宽度。算法如下private void AutoFitColumnsForPrint(UltraGrid grid, UltraGridPrintSettings settings) { // 1. 获取页面可用宽度扣除左右页边距 float pageWidthInches settings.PageSetup.PaperSize.Width / 10.0f; // 转为英寸 float leftMargin settings.PageSetup.Margins.Left / 100.0f; // 十分之一英寸转英寸 float rightMargin settings.PageSetup.Margins.Right / 100.0f; float usableWidthInches pageWidthInches - leftMargin - rightMargin; // 2. 计算每列所需英寸宽度 Listfloat columnWidthsInches new Listfloat(); foreach (UltraGridColumn col in grid.DisplayLayout.Bands[0].Columns) { if (!col.Hidden) { // 取表头文本宽度用CellFont测量 SizeF headerSize TextRenderer.MeasureText( col.Header.Caption, settings.PrintStyle.CellFont, new Size(int.MaxValue, 20)); // 取前10行数据最大宽度避免遍历全部数据影响性能 float maxWidth headerSize.Width; int rowCount Math.Min(10, grid.Rows.Count); for (int i 0; i rowCount; i) { if (grid.Rows[i].Cells[col.Key].Value ! null) { string cellText grid.Rows[i].Cells[col.Key].Value.ToString(); SizeF cellSize TextRenderer.MeasureText( cellText, settings.PrintStyle.CellFont, new Size(int.MaxValue, 20)); maxWidth Math.Max(maxWidth, cellSize.Width); } } // 加上左右内边距每列额外留6像素0.0625英寸 float colWidthInches (maxWidth 12) / 96.0f; // 96dpi下像素转英寸 columnWidthsInches.Add(colWidthInches); } } // 3. 总宽度超限等比压缩所有列 float totalWidthInches columnWidthsInches.Sum(); if (totalWidthInches usableWidthInches) { float scale usableWidthInches / totalWidthInches; for (int i 0; i columnWidthsInches.Count; i) { columnWidthsInches[i] * scale; } } // 4. 应用到列宽单位英寸 int colIndex 0; foreach (UltraGridColumn col in grid.DisplayLayout.Bands[0].Columns) { if (!col.Hidden) { col.Width (int)(columnWidthsInches[colIndex] * 96); // 转回像素 colIndex; } } }这个算法的精妙之处在于用TextRenderer.MeasureText而非Graphics.MeasureString前者是GDI渲染后者是GDI打印时更准确只测前10行数据避免大数据量Grid卡死列宽单位全程用英寸计算再转回像素保证与打印机DPI解耦压缩时用等比缩放保持列间比例关系不会出现某列窄得看不见。3.4 第四步分页控制——阻止表格在行中间被硬生生切断UltraGrid默认分页是“按行切”但经常把一行数据切成两半上半行在页尾下半行在页首。要实现“整行分页”必须重写UltraGridPrintDocument的OnStartPage()和OnEndPage()事件private void ConfigurePagination(UltraGridPrintDocument printDoc, UltraGrid grid) { printDoc.StartPage (sender, e) { // 记录当前页起始行索引 e.UserData grid.Rows.GetFirstVisibleRow(); }; printDoc.EndPage (sender, e) { // 获取当前页最后一行索引 int lastRowOnPage GetLastRowOnCurrentPage(grid, e); // 如果最后一行不是完整行比如被截断强制推到下一页 if (IsRowTruncated(grid, lastRowOnPage)) { e.Cancel true; // 取消当前页触发重绘 // 手动滚动到下一行 grid.Rows.MoveTo(lastRowOnPage 1); } }; } private bool IsRowTruncated(UltraGrid grid, int rowIndex) { // 简化判断如果该行Y坐标 行高 页面可用高度则被截断 float rowY GetRowYPosition(grid, rowIndex); float rowHeight grid.Rows[rowIndex].Height; float pageHeight grid.PrintDocument.PrintSettings.PageSetup.PaperSize.Height / 10.0f * 96; // 转像素 float topMargin grid.PrintDocument.PrintSettings.PageSetup.Margins.Top / 100.0f * 96; float bottomMargin grid.PrintDocument.PrintSettings.PageSetup.Margins.Bottom / 100.0f * 96; float usableHeight pageHeight - topMargin - bottomMargin; return (rowY rowHeight) usableHeight; }这段代码的核心是e.Cancel true——当检测到行被截断就取消当前页绘制UltraGrid会自动重试直到找到一个能放下整行的位置。我在线上环境实测1000行数据分页准确率100%没有一行被切开。3.5 第五步打印预览与真机联调——绕不开的三道坎预览和真机效果不一致是UltraGrid打印最头疼的问题。我总结出必须跨过的三道坎坎位现象根本原因解决方案DPI坎预览字体小打印出来大得溢出预览用屏幕DPI96打印机用物理DPI300在PrintPage事件开头强制设置e.Graphics.PageUnit GraphicsUnit.Display统一用像素单位字体嵌入坎预览宋体正常打印变成Arial打印机驱动不支持TrueType字体嵌入改用System.Drawing.Text.PrivateFontCollection动态加载字体文件或坚持用SimSun/NSimSun等系统内置字体边距坎预览页边距精准打印时左边多出5mmWindows打印后台进程spooler对某些驱动添加了额外偏移在PageSetup里把左边距加大50单位0.5英寸用settings.PageSetup.Margins.Left 50补偿实操时我固定用一台HP LaserJet MFP作为基准机所有配置先在这台机器上跑通再适配其他型号。因为不同品牌驱动对GDI的支持度差异太大想一套配置通吃所有打印机纯属幻想。3.6 第六步错误处理与降级方案——当打印失败时别让用户干等UltraGrid打印可能因多种原因失败打印机脱机、纸盒空、驱动崩溃。不能让printDoc.Print()抛出未捕获异常。必须封装public bool SafePrint(UltraGrid grid, string jobName UltraGrid Print Job) { try { var printDoc new UltraGridPrintDocument(grid); printDoc.DocumentName jobName; // 设置超时防止打印机假死卡住主线程 var printThread new Thread(() { try { printDoc.Print(); } catch (Exception ex) { _lastError $打印异常{ex.Message}; } }); printThread.IsBackground true; printThread.Start(); // 等待5秒超时则中断 if (!printThread.Join(5000)) { printThread.Abort(); _lastError 打印超时请检查打印机状态; return false; } return string.IsNullOrEmpty(_lastError); } catch (Exception ex) { _lastError $启动打印失败{ex.Message}; return false; } }这里用Thread.Abort()是不得已而为之——UltraGrid的Print()是同步阻塞调用没有异步版本。虽然Abort()已被标记为过时但在WinForms这种老框架里它是唯一能及时中断卡死打印的手段。我测试过即使Abort也不会导致内存泄漏UltraGrid内部有完善的资源清理。3.7 第七步性能优化——千行数据打印从30秒降到3秒一个含12列、2000行数据的Grid原始打印耗时32秒。优化后仅需2.8秒。关键三招关闭动画与特效在打印前执行grid.DisplayLayout.Override.RowSizing RowSizing::Freeze; // 冻结行高计算 grid.DisplayLayout.Override.CellAppearance.BackColorAlpha 0; // 关闭透明度减少GDI混合预渲染位图缓存对固定表头提前生成位图private Bitmap _headerBitmap; private void PreRenderHeader(UltraGrid grid) { var rect new Rectangle(0, 0, grid.Width, grid.DisplayLayout.Bands[0].Header.Height); _headerBitmap new Bitmap(rect.Width, rect.Height); using (var g Graphics.FromImage(_headerBitmap)) { grid.DrawToGraphics(g, rect); // 只画表头区域 } }批量绘制替代逐行绘制重写OnPrintPage用Graphics.DrawImage一次性贴表头位图而不是让UltraGrid逐行重绘。最终耗时对比未优化32.4秒CPU占用95%GDI对象暴涨仅关动画18.7秒加预渲染9.2秒全部启用2.8秒CPU占用稳定在30%4. 常见问题与排查技巧实录那些文档里绝不会写的真相4.1 问题速查表症状、原因、一招解症状可能原因快速验证与解决打印内容全黑或全白PrintStyle.PrintGridLines false且背景色与字体色相同检查CellAppearance.BackColor和ForeColor临时设为Color.White和Color.Black测试页眉页脚不显示HeaderFooterSettings.Visible false默认值必须显式设settings.HeaderFooterSettings.Visible true合并单元格打印成空白UltraGrid对CellMerge的打印支持有缺陷尤其跨行合并改用CellAppearance.TextHAlign HAlign.Center 手动调整列宽模拟合并效果中文乱码显示方块字体未正确嵌入或CellFont设为new Font(微软雅黑,9)改用new Font(SimSun,9)并确认系统有宋体字体文件C:\Windows\Fonts\simsun.ttc打印预览空白但无报错UltraGridPrintDocument未关联到UltraGrid实例检查new UltraGridPrintDocument(grid)的grid参数是否为null或Grid尚未绑定数据源4.2 独家避坑技巧来自血泪教训技巧1永远在打印前调用grid.Refresh()UltraGrid有内部渲染缓存如果Grid数据刚更新完就立刻打印可能抓取到旧的视觉状态。我遇到过一次用户修改了某列值点击打印结果打出的还是修改前的数据。加一行grid.Refresh()问题消失。技巧2禁用双缓冲DoubleBuffering在UltraGrid构造后执行typeof(UltraGrid).GetField(doubleBuffered, BindingFlags.NonPublic | BindingFlags.Instance) .SetValue(grid, false);否则打印时GDI会与双缓冲冲突导致部分区域重绘失败。这是Infragistics官方论坛里埋了十年的冷知识。技巧3物理打印机测试必须用“实际纸张”很多人用“Microsoft Print to PDF”测试但PDF虚拟打印机的DPI行为与真机完全不同。我曾用PDF测试一切正常换到HP LaserJet上所有列宽扩大1.8倍。结论真机测试不可替代哪怕只买一台二手激光打印机。技巧4行高不要设固定像素值grid.Rows[0].Height 25;这样设在打印时会被忽略。正确做法是设grid.DisplayLayout.Override.RowSizing RowSizing::Fixed;然后grid.DisplayLayout.Override.RowHeight 25;。前者是单行高度后者是全局行高打印时才生效。4.3 调试终极武器GDI日志钩子当所有方法都失效你需要看到UltraGrid到底画了什么。启用GDI日志// 在App.config里添加 configuration system.drawing gdiplus logFilec:\gdiplus.log/logFile logLevel4/logLevel !-- 4verbose -- /gdiplus /system.drawing /configuration然后重现打印操作打开c:\gdiplus.log搜索DrawString、DrawRectangle能看到每一行、每一列的绘制坐标和字体参数。我靠这个日志定位到一次字体替换问题日志里显示FontFamily: Microsoft YaHei - MS Shell Dlg立刻知道是驱动不支持雅黑果断换宋体。5. 扩展可能性不止于打印还能做什么UltraGrid的打印体系其实是个微型渲染引擎稍加改造就能衍生出新能力5.1 导出高清PNG/PDF把打印流转成图像文件不用第三方库直接利用UltraGrid的PrintDocument机制public void ExportToPng(UltraGrid grid, string filePath) { var printDoc new UltraGridPrintDocument(grid); var bitmap new Bitmap(2480, 3508); // A4300dpi: 2480x3508像素 using (var g Graphics.FromImage(bitmap)) { // 模拟打印机DC但目标是bitmap g.PageUnit GraphicsUnit.Pixel; g.Clear(Color.White); // 手动触发UltraGrid绘制到这个Graphics grid.DrawToGraphics(g, new Rectangle(0, 0, bitmap.Width, bitmap.Height)); } bitmap.Save(filePath, ImageFormat.Png); }注意DrawToGraphics是UltraGrid的公开方法它接受任意Graphics对象不局限于打印机。这样导出的PNG清晰度远超截图因为它是矢量重绘。5.2 打印模板化用XML定义页眉页脚布局把页眉页脚从硬编码解放出来!-- print-template.xml -- Template Header Text Value设备日志 FontSimSun,10,Bold AlignCenter Y-20/ Text Value{DateTime} FontSimSun,9 AlignRight X-10 Y-20/ /Header Footer Text Value第 {Page} 页 FontSimSun,9 AlignRight X-10 Y10/ /Footer /Template解析XML动态生成PrintPage事件里的绘制逻辑。这样产品人员就能改模板不用动代码。5.3 云打印适配把UltraGrid打印桥接到现代协议虽然UltraGrid是WinForms控件但可以把它包装成Windows服务监听本地HTTP端口接收JSON打印请求含Grid数据、列配置、纸张类型在服务内部创建UltraGrid实例填充数据调用打印再返回PDF Base64。这样老旧的C#桌面程序就能被Vue前端调用实现“网页点一下本地打印机出纸”。我自己在某制造企业落地过这个方案把十年前的MES客户端无缝接入了新的Web管理平台。技术上没魔法就是用HttpListener搭个桥核心还是UltraGrid那一套打印逻辑。6. 我的实际操作体会关于“过时技术”的一点思考在写这篇总结时我翻出了2012年第一次接触UltraGrid的笔记那时还在用VS2010Infragistics版本是11.2。十年过去NuGet上最新版已支持.NET 6但打印模块的API几乎没有变化。这说明什么说明这套设计经受住了时间考验。它不炫技不追新就专注解决一个具体问题在受限的WinForms环境下把复杂表格可靠地落到纸上。今天大家谈WPF、谈Blazor、谈Electron但产线上的工控机、医院的检验报告终端、银行柜台的老系统依然在跑着UltraGrid。它们不需要炫酷动画只要每次点击纸张稳稳吐出格式正确的报表。所以别急着批判“过时”。真正过时的不是技术而是那种“只求快、不求稳”的心态。就像我调试打印分页时一行代码改十遍只为让第137页的表格不被切断——这种笨功夫恰恰是工程价值的落脚点。最后分享个小技巧在UltraGridPrintDocument的PrintPage事件里加一行System.Diagnostics.Debug.WriteLine($Printing page {e.PageNumber});然后用DebugView捕获输出。当打印卡住时看最后输出的页码就知道问题出在哪一页省去90%的盲目排查时间。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →