
简介一份面向C#/.NET桌面开发者的Winform自定义打印设计工具资源解决报表、单据、标签等场景中动态排版与打印输出的需求。基于System.Drawing.Printing命名空间围绕PrintDocument、PrintPage事件、Graphics绘制等核心机制提供从设计界面、打印预览到实际打印的完整实现思路并支持文本、图片、二维码等元素的自定义布局与代码动态调用。压缩包共563个文件约56.26MB以dll运行库、xml配置与注释文档、cs源码、pdb调试文件为主另含nupkg依赖包、exe示例、sln工程及少量png、ico等资源结构清晰便于直接打开工程研读。目前已有3692人学习下载。通过学习Print.Test示例项目可以掌握自定义控件的元素管理、PrintPage事件中的绘制逻辑、预览渲染方式以及按钮触发打印/预览的调用流程适合需要为Winform应用集成灵活打印能力的初中级开发者参考。1. 不是报表控件是 WinForm 自定义打印设计工具我在做一套 winform 进销存系统客户三天两头改发货单版式加一列、挪一行、换个公司名位置。最初写死 PrintPage 事件里的 DrawString 代码改一次版式就要重新编译、发版实在撑不住于是动手做了一套自定义打印设计工具模板在画布上拖拽设计、存成文件运行时通过代码动态调用进行打印操作。这是很多 c# .net 桌面项目都会撞上的需求——报表控件太重、授权费高裸写 PrintDocument 又不想每次被版式改动折腾。这套资源把打印拆成两块设计时控件拖拽、属性配置和运行时模板加载、数据替换、渲染输出中间用模板文件解耦。适合两类人一类是 winform 项目里打印需求多变、想摆脱写死坐标的开发者另一类是刚接触 windows 打印组件、想彻底搞懂 Graphics 和单位换算的新手。工具交付后客户改版式只需要在设计器里改一次保存业务代码一行调用打印版式的维护成本基本归零。2. 设计器核心控件模型、画布坐标与拖拽落位2.1 从打印需求反推控件模型做设计器第一步不是写界面而是定义“能放什么”。订单打印里最常见的元素就五类文本、横线、矩形框、图片 Logo、条码再加一个明细表格基本覆盖发货单、送货单、标签纸九成需求。我先把所有元素的公共属性收敛到一个基类里而不是给每种控件单独建类——这样序列化、命中检测、属性面板绑定都只需写一遍。// 打印控件基类所有可放置元素统一走这一套属性 public class PrintControlBase { public string Name { get; set; } // 控件名用于数据绑定时识别 public PrintControlType Type { get; set; } // 文本/直线/矩形/图片/表格等 // 位置与尺寸统一用 1/100 英寸不用像素这是后面所有换算的地基 public float X { get; set; } public float Y { get; set; } public float Width { get; set; } public float Height { get; set; } // 文本相关 public string FontName { get; set; } 微软雅黑; public float FontSize { get; set; } 9f; // 单位点(pt) public bool Bold { get; set; } public string Text { get; set; } public int ZIndex { get; set; } // 叠放次序绘制/命中都用它 // 对齐方式绘制时通过 StringFormat 转换 public TextAlign Align { get; set; } TextAlign.Left; }单位选择是这里最需要说清楚的决策。WinForm 控件尺寸习惯用像素但打印机的物理分辨率是 300dpi 或 600dpi屏幕 96dpi 下的 96 像素到了打印机上代表完全不同的物理尺寸。Windows 打印组件里PrintPageEventArgs.Graphics的默认单位是GraphicsUnit.Display也就是 1/100 英寸所以模板坐标直接用 1/100 英寸设计时换算一次打印时零换算是最省事的设计。换算公式就一条1 英寸 100 单位 96 屏幕像素96dpi 时像素转模板单位要乘 100/96 ≈ 1.0417。很多设计器做出来“打印尺寸不对”根源就是这里直接用像素值当模板坐标。2.2 画布交互鼠标拖拽与属性编辑画布本质是一个 PictureBox纸张显示在中间。我维护一个缩放比例把模板坐标映射到屏幕像素private float _scale 0.8f; // 画布缩放比例支持 50%~200% 调整 // 模板单位 - 屏幕像素 private PointF TemplateToScreen(float x, float y) { float px x * ScreenDpiX / 100f * _scale; float py y * ScreenDpiY / 100f * _scale; return new PointF(px, py); } // 屏幕像素 - 模板单位拖拽和命中检测都用它 private PointF ScreenToTemplate(Point p) { float x p.X * 100f / (ScreenDpiX * _scale); float y p.Y * 100f / (ScreenDpiY * _scale); return new PointF(x, y); }拖拽移动控件时我把 MouseMove 里的像素差换算回模板单位再更新控件坐标。命中检测按 ZIndex 倒序查找选中最上层元素核心代码就三段事件private ListPrintControlBase _selected new ListPrintControlBase(); private PointF? _dragStart; // 拖拽开始时控件左上角的模板坐标 private Point _mouseStart; // 鼠标按下位置用于算位移差 private void canvas_MouseDown(object sender, MouseEventArgs e) { var pos ScreenToTemplate(e.Location); // 关键从最上层开始找避免叠放的控件永远选不到 var hit _template.Controls .OrderByDescending(c c.ZIndex) .FirstOrDefault(c pos.X c.X pos.X c.X c.Width pos.Y c.Y pos.Y c.Y c.Height); if (hit null) return; _selected.Clear(); _selected.Add(hit); _dragStart new PointF(hit.X, hit.Y); _mouseStart e.Location; RefreshPropertyGrid(hit); // 选中后把属性灌进 PropertyGrid } private void canvas_MouseMove(object sender, MouseEventArgs e) { if (_selected.Count 0 || _dragStart null) return; // 鼠标位移先换算成 1/100 英寸再叠加到起始坐标上 float dx (e.X - _mouseStart.X) * 100f / (ScreenDpiX * _scale); float dy (e.Y - _mouseStart.Y) * 100f / (ScreenDpiY * _scale); foreach (var c in _selected) { c.X _dragStart.Value.X dx; c.Y _dragStart.Value.Y dy; } canvas.Invalidate(); // 只重绘不重建位图 } private void canvas_MouseUp(object sender, MouseEventArgs e) { _selected.Clear(); _dragStart null; }这里有三个细节值得注意。第一MouseMove 里用“位移差”而不是直接把鼠标坐标赋给控件否则鼠标按下时控件会“跳”一下——因为鼠标落在控件内部不在控件左上角。第二命中检测按 ZIndex 倒序这是叠放布局的底线不然上层元素永远拖不动。第三拖动结束不要立刻刷新 PropertyGrid否则客户每拖一格就闪一次属性面板体验很差。属性编辑我直接复用 WinForm 自带的 PropertyGrid 控件基类上的公共属性会自动生成编辑界面配合[DisplayName(字体大小)]之类的特性修饰显示名。这样“自定义设计”能力就完整落地了拖拽摆位置、属性面板调样式、画布即时重绘。整套设计器代码量不大但控件模型必须一次想清楚后面加二维码、加水印控件时改动面才最小。3. 模板持久化XML 序列化与占位符数据绑定3.1 模板文件用什么格式设计器里摆好的模板最终要落成文件传给运行时。我选 XML 有三个理由可读、可 diff、c# 序列化简单。用 XmlSerializer 不需要额外引用直接定义根对象即可// 模板根对象页尺寸 控件列表 [XmlRoot(PrintTemplate)] public class PrintTemplate { [XmlAttribute] public string Version { get; set; } 1.0; [XmlAttribute] public float PageWidth { get; set; } // A4: 827 单位(210mm) [XmlAttribute] public float PageHeight { get; set; } // A4: 1169 单位(297mm) [XmlElement(Control)] public ListPrintControlBase Controls { get; set; } }页宽 827、页高 1169 怎么来的210mm 除以 25.4 再乘 100约 827 单位297mm 同理约 1169 单位。模板里存页尺寸的意义很大打印时按模板指定的纸型设置 PaperSize而不是跟着打印机当前默认纸走这是后面“换打印机模板不乱”的前提。序列化与反序列化的封装要做到“文件坏了给明确错误”public static class TemplateFile { // 保存模板到磁盘 public static void Save(PrintTemplate tpl, string path) { var serializer new XmlSerializer(typeof(PrintTemplate)); using var fs File.Create(path); serializer.Serialize(fs, tpl); } // 加载模板解析失败要抛业务错误而不是内部异常 public static PrintTemplate Load(string path) { try { var serializer new XmlSerializer(typeof(PrintTemplate)); using var fs File.OpenRead(path); return (PrintTemplate)serializer.Deserialize(fs); } catch (Exception ex) { // 常见原因模板由其他版本工具保存或手工改坏节点 throw new InvalidDataException($模板文件无法解析: {ex.Message}); } } }序列化出来的文件大致长这样?xml version1.0? PrintTemplate Version1.0 PageWidth827 PageHeight1169 Control NameCompanyLogo/Name TypeImage/Type X50/XY30/YWidth100/WidthHeight40/Height ImageData/9j/4AAQ...Base64/ImageData /Control Control NameOrderNo/Name TypeText/Type X50/XY80/YWidth200/WidthHeight30/Height FontName微软雅黑/FontName FontSize9/FontSize Text订单编号:{OrderNo}/Text /Control /PrintTemplate图片控件有个特别容易踩的坑如果存文件路径模板换台电脑就失效。我把图片序列化成 Base64 字符串塞进控件属性让模板文件自包含拷到哪台机器都能打。代价是文件体积会大一些一张 Logo 也就多几 KB完全值得。3.2 数据绑定占位符替换的约定模板里写死的“订单编号:”没有意义运行时每个订单编号都不同这就是“代码动态调用”要解决的事。我用最朴素的约定文本里写{OrderNo}打印前用数据字典统一替换// 运行时数据示例订单实体转成字典 var data new Dictionarystring, string { { OrderNo, SO20240618001 }, { Customer, 广州某某贸易有限公司 }, { TotalAmount, 12850.00 }, { Date, 2024-06-18 } }; // 替换所有 {占位符}找不到的保持原样方便定位问题 public static string RenderText(string raw, Dictionarystring, string data) { if (string.IsNullOrEmpty(raw)) return raw; return Regex.Replace(raw, \{(\w)\}, m { string key m.Groups[1].Value; return data.TryGetValue(key, out string? v) ? v : m.Value; }); }用正则\{(\w)\}一次性匹配所有占位符而不是写循环逐个 Replace有两个好处所有字段一趟替换完未命中的字段保留{OrderNo}原样打印出来一眼就能看出是数据没传进来而不是悄无声息变空白。这一点在联调时极其有用——空白文本你会怀疑字体颜色保留占位符则直接定位到数据字典缺 key。细节表格要单独说。表头是模板里固定的明细行运行时才知道行数。我的做法是在模板里放一个 Table 控件定义列宽和表头运行时往 Data 属性里挂 DataTable// Table 控件打印时逐行绘制表头由模板固定 public class PrintTableControl : PrintControlBase { // 每列宽度1/100英寸逗号分隔如 100,200,100 public string ColumnWidths { get; set; } 100,200,100; // 表头文本竖线分隔如 商品|数量|金额 public string HeaderText { get; set; } 商品|数量|金额; // 运行时填充的数据源标 XmlIgnore 避免序列化 [XmlIgnore] public DataTable? Data { get; set; } }表头存成“商品|数量|金额”这种竖线分隔字符串比存数组更利于手工编辑模板文件。运行时行高按字号估算逐行绘制超出可用高度就要分页——分页逻辑在第 4 章和 PrintPage 事件一起讲。3.3 动态调用入口“代码动态调用进行打印操作”的入口我封装成一个静态方法业务侧一行调用不关心模板内部结构public static class PrintService { // 最常用入口模板路径 数据 可选的表格数据 public static void PrintTemplate(string templatePath, Dictionarystring, string data, DataTable? tableData null) { var tpl TemplateFile.Load(templatePath); // 替换文本占位符表格数据挂到 Table 控件上 foreach (var c in tpl.Controls) { if (c is PrintTableControl t) t.Data tableData?.Copy(); else c.Text RenderText(c.Text, data); } // 用模板里的页尺寸初始化 PrintDocument不跟打印机默认纸走 using var doc new PrintDocument(); doc.DefaultPageSettings.PaperSize new PaperSize( Custom, (int)tpl.PageWidth, (int)tpl.PageHeight); doc.PrinterSettings.PrinterName SelectPrinter(); doc.PrintPage (s, e) RenderPage(e.Graphics, tpl); doc.Print(); } }注意PaperSize构造函数第二、三参数的单位就是 1/100 英寸和模板坐标天然一致这里不需要再换算。很多初学者在打印入口不设置纸张结果模板设计成 827 单位宽、打印机默认纸却是 Letter右边整排控件被裁掉。模板里存页尺寸、打印时强制写回 PaperSize是保证“所见即所得”的第一道防线这条在避坑章还会再展开。4. 打印执行PrintDocument 渲染与单位换算4.1 为什么说 Graphics 是打印的核心PrintDocument 触发 PrintPage 事件时事件参数的 Graphics 就是画布——屏幕上怎么用 GDI 画图打印机上就怎么画。但两者有一个关键差异打印机 Graphics 的默认单位是 Display1/100 英寸而屏幕 Bitmap 的 Graphics 默认单位是 Pixel。这个差异直接决定了渲染代码怎么写。网上很多教程教你“先按屏幕 DPI 把像素换算成打印机像素”代码里到处乘 3.125300/96最后打出来尺寸永远差一点。我踩过一次之后彻底放弃手动换算既然设计时已经用 1/100 英寸存坐标了渲染阶段直接取坐标值绘制不做任何 DPI 乘法唯一要做的就是显式声明 PageUnit防止调用方改过状态// 核心渲染遍历模板控件逐类绘制屏幕预览和打印机共用 private static void RenderPage(Graphics g, PrintTemplate tpl) { // 关键显式声明 1/100 英寸单位和模板坐标完全一致 g.PageUnit GraphicsUnit.Display; foreach (var c in tpl.Controls) { var rect new RectangleF(c.X, c.Y, c.Width, c.Height); switch (c.Type) { case PrintControlType.Text: DrawText(g, c, rect); break; case PrintControlType.Line: g.DrawLine(Pens.Black, c.X, c.Y, c.X c.Width, c.Y c.Height); break; case PrintControlType.Rect: g.DrawRectangle(Pens.Black, c.X, c.Y, c.Width, c.Height); break; case PrintControlType.Image: // c.ImageData 是反序列化时从 Base64 解出来的 g.DrawImage(c.ImageData, c.X, c.Y, c.Width, c.Height); break; case PrintControlType.Table: DrawTable(g, (PrintTableControl)c, tpl); break; } } }所有控件共用同一个矩形计算规则类型分支只负责“怎么画”。后面加二维码、加水印只加一个 case 分支不动其他代码。这是整个资源里我最坚持的结构设计渲染核心写成与设备无关的静态方法而不是散落在 PrintPage 事件里。关于单位建议直接记下面这张对照表场景默认单位100 单位对应的物理尺寸屏幕 BitmapPixel100 像素 ≈ 1.04 英寸96dpi打印机 GraphicsDisplay1/100 英寸1 英寸 2.54 厘米显式设置 PageUnitDisplay 的 BitmapDisplay96 像素 1 英寸这张表解决的是 90% 的“预览和打印尺寸不一致”问题。凡是代码里出现dpi变量的乘除先问一句这个换算到底有没有必要。坐标统一为 1/100 英寸之后dpi 只出现在画布显示换算和 Bitmap 预览的像素宽高计算里。4.2 文本绘制与对齐文本是所有控件里最容易画歪的。DrawString 的矩形区域和字体度量之间有偏差中文格外明显所以我把文本绘制单独封成一个方法private static void DrawText(Graphics g, PrintControlBase c, RectangleF rect) { using var font new Font(c.FontName, c.FontSize, c.Bold ? FontStyle.Bold : FontStyle.Regular); using var brush new SolidBrush(Color.Black); var format new StringFormat(); format.Alignment c.Align switch { TextAlign.Center StringAlignment.Center, TextAlign.Right StringAlignment.Far, _ StringAlignment.Near }; format.LineAlignment StringAlignment.Center; // 垂直居中简单保险 // 绘制前先量一行字高防止文字被矩形裁剪 var lineH g.MeasureString(高, font).Height; if (rect.Height lineH) rect.Height lineH; g.DrawString(c.Text, font, brush, rect, format); }垂直对齐固定用 Center因为打印模板里的文本框通常只放一行垂直居中视觉最稳。MeasureString(高, font)是经验值——很多字符在 LineAlignment 为 Near 时基线以下部分会被裁掉提前补一行高度能规避绝大多数文字截断。注意字体字号单位是 pt点1 点 1/72 英寸它和模板坐标的 1/100 英寸之间只在进行行高换算时才发生关系// 行高(模板单位) 字号(点) × 100 / 72 × 行距系数 float rowHeight c.FontSize * 100f / 72f * 1.4f;1.4 的行距系数是中文打印“看起来不挤”的关键这个系数在宋体 9pt 到微软雅黑 12pt 范围内都验证过不是拍脑袋取的。4.3 分页与 HasMorePages明细表格行数不定一个 PrintPage 画不下就要分页。PrintDocument 的分页机制靠PrintPageEventArgs.HasMorePages事件里画完当前页如果还有内容就把它设为 truePrintDocument 会再触发一次 PrintPage。关键是分页游标的状态要跨多次事件保存// 渲染上下文游标跨页保存避免用静态字段 private class RenderContext { public int RowCursor { get; set; } // 表格画到第几行 public int PageIndex { get; set; } // 当前第几页用于页脚 } private static void DrawTable(Graphics g, PrintTableControl t, PrintTemplate tpl, RenderContext ctx, Actionbool setHasMorePages) { if (t.Data null) return; float rowH t.FontSize * 100f / 72f * 1.4f; // 行高转成模板单位 float pageBottom tpl.PageHeight - 50f; // 底部留白 50 单位 for (int i ctx.RowCursor; i t.Data.Rows.Count; i) { float y t.Y i * rowH; // 简化示意实际要加表头占位 if (y rowH pageBottom) { ctx.RowCursor i; // 记住下一行下页继续 setHasMorePages(true); return; } DrawRow(g, t.Data.Rows[i], y, t); } ctx.RowCursor 0; // 画完重置 }这段代码反复提醒我的是HasMorePages 为 true 时 PrintPage 会再次触发但局部变量全部重建唯一能跨页保存的就是上下文对象或事件参数。用静态字段保存游标也能跑但两个 PrintDocument 实例并发打印时会互相污染。分页真正复杂的部分是表格跨页重复表头、底部不断半行资源的实现把表头绘制从 DrawTable 里拆出来每页先画表头再画明细这是分页的教科书做法照着写就行。5. 避坑记录单位、边距、预览与打印不一致5.1 现象设置宽度 100打出来不是 1 英寸设计器里一个文本框宽度设 100按逻辑应该是 1 英寸2.54 厘米结果量出来要么 0.96 英寸要么 2.4 英寸。原因分两种。第一种设计器画布上根本没按 1/100 英寸换算鼠标拖了 100 像素就存了 100而像素和模板单位在 96dpi 下差约 4%。第二种是打印时 Graphics 的 PageUnit 被改成 Pixel或者开发者手动用 DpiX 换算多乘了一个系数。我见过最离谱的写法是同时做了这两种换算尺寸差出 8%打印出来对不齐表格线。解决设计器里所有鼠标位移先经过px * 100f / (dpi * scale)换算成模板单位再赋给控件打印渲染时保持GraphicsUnit.Display不动。从那以后我给自己定了一条规矩代码里出现dpi变量的地方必须带注释说明用途不允许无头绪的乘除。5.2 现象预览正常打印出来整体往右下偏模板在设计器画布上位置正确打印预览也对但真正打出来内容整体偏移距离可能一两毫米位置不固定换台打印机偏移量还不一样。这个问题一度被我当成玄学。原因PrintDocument 的绘制原点不在纸张物理边缘而在打印机“可打印区域”的左上角这个位置由硬件决定对应e.PageSettings.HardMarginX和HardMarginY。模板坐标 (0,0) 画下去实际被硬体边距推走了。解决两选一。要么在 PrintPage 开头用e.Graphics.TranslateTransform(-e.PageSettings.HardMarginX, -e.PageSettings.HardMarginY)抵消硬边距让 (0,0) 回到纸张物理原点要么干脆让模板设计坐标自带边距第一行内容留出至少 50 单位约 1.27 厘米。我推荐后者TranslateTransform 在不同驱动上表现不稳定而“模板自带边距”是任何打印机都不会错的方案。5.3 现象换一台打印机模板左右错位、右边内容被裁同一份模板在公司电脑打得好好的拿到客户那边打印机打出来要么整体变小要么右边界被切掉一截。原因Windows 打印组件在驱动层做了很多自动处理最讨厌的是“缩放以适合页面”。当模板页尺寸和打印机当前默认纸不一致时部分驱动会悄悄缩放内容模板单位就失真了。另外如果打印流程里用过 PageSetupDialog用户一旦勾选过“调整比例”PrintDocument 会写入一个非 100% 的缩放因子。解决打印入口强制写入纸张尺寸和缩放因子// 强制模板页尺寸不跟打印机默认纸走 doc.DefaultPageSettings.PaperSize new PaperSize( Custom, (int)tpl.PageWidth, (int)tpl.PageHeight); doc.DefaultPageSettings.Margins new Margins(0, 0, 0, 0); // 关键把潜在的缩放因子拉回 100% doc.OriginalMargins new Margins(0, 0, 0, 0);OriginalMargins是很多人没注意的属性PrintDocument 内部拿它判断是否补偿边距。模板既然自带边距Margins 就设 0让渲染坐标自己掌控布局。个别旧驱动对自定义 PaperSize 支持差还需要把PaperSize.RawKind设为(int)PaperKind.Custom这属于驱动兼容性的最后一道保险遇到再补。5.4 现象中文文本底部被截、字距比设计器里挤设计器画布上显示正常的文本框打印出来中文下面被切了一点点或者一行字看起来字与字贴在一起。原因WinForm 的 DrawString 走 GDI 字体度量和 TextRendererGDI渲染结果有偏差。中文的 ascent/descent 在 GDI 里计算偏小矩形高度不够时底部像素被舍弃。行距如果直接用 FontSize 当行高中文本身不自带行距视觉上就是贴着的。解决矩形高度给足绘制前rect.Height Math.Max(rect.Height, g.MeasureString(高, font).Height)行高公式统一为FontSize * 100f / 72f * 1.4f。1.4 倍行距是我从宋体 9pt 到微软雅黑 12pt 都验证过的经验系数。如果项目里所有文本都走 GDI 绘制可以把 DrawString 换 TextRenderer.DrawText但 GDI 和 GDI 在字体度量上不一致两种方案混用必乱要么全用一套要么就按上面的方式补高度。5.5 现象数据里有特殊字符替换后打印出空或错乱数据字典里的值带了$或{号替换后文本中间多出一段空白或者$直接消失。原因如果替换实现用的是string.Replace({OrderNo}, value)不会有问题但很多人习惯在正则的 replacement 参数里直接拼接 value——正则 replacement 字符串里$是特殊符号$1表示分组引用内容就被吃掉了。解决用 MatchEvaluator 而不是在 replacement 里拼字符串就是 3.2 节 RenderText 的写法——它在委托里直接返回字典值不走正则 replacement 解析$、\都会原样输出。想偷懒的话先Regex.Escape(value)再拼 replacement 也能绕过去但可读性差不推荐。6. 打印预览的硬核技巧先渲染 Bitmap再复用同一套绘制核心打印开发最烦的不是写绘制代码而是每次调试都要废一张纸。我的习惯是开发阶段的打印入口走一条特殊通道先把模板渲染成 Bitmap 显示在界面上确认无误再放行到打印机。这个方案能成立前提是渲染核心设备无关——也就是前面反复强调的坐标全部用 1/100 英寸绘制时统一设Graphics.PageUnit GraphicsUnit.Display。// 把模板渲染成 Bitmap用于屏幕预览 public static Bitmap RenderToBitmap(PrintTemplate tpl, float zoom 1f) { float dpi 96f; // 屏幕基准 DPI int w (int)(tpl.PageWidth * dpi / 100f * zoom); int h (int)(tpl.PageHeight * dpi / 100f * zoom); var bmp new Bitmap(w, h); using (var g Graphics.FromImage(bmp)) { g.Clear(Color.White); // 纸张打底 g.PageUnit GraphicsUnit.Display; // 关键和打印机一致的坐标 RenderPage(g, tpl); // 与 PrintPage 共用同一方法 } return bmp; }这段代码的精髓在g.PageUnit GraphicsUnit.Display。Bitmap 的 Graphics 默认单位是像素如果不改RenderPage 里的坐标会被当成像素整个模板缩小到原尺寸的约 1/96。改成 Display 后GDI 按设备逻辑分辨率把 1/100 英寸换算成像素100 单位 96 像素和设计器画布比例一致。放大缩小只需改 zoom 参数重建 BitmapPictureBox 设成 Zoom 模式直接展示。这个通道还有个额外好处Bitmap 可以Save成 PNG 存证。客户改版式来回扯皮时把渲染图直接发过去省去反复打样的成本。验证方法也很简单打印一张 A4拿尺子量边框误差 1 毫米以内就算过。资源里把设计器工程、模板文件格式说明和几个现成的发货单、标签模板都打包好了照着第 2 章的控件模型改就能接进自己的项目。从那以后我每次动打印模块都强制走一遍“Bitmap 预览 → 存 PNG 目测 → 真机试打”的流程模板出问题的概率几乎归零这套“一套坐标、两处输出”的设计思路也成了我做打印功能的默认模板。希望帮到你。本文还有配套的精品资源点击获取