
简介这是一套面向C#/.NET开发者打造的可视化打印模板设计解决方案适用于需快速定制发票、报告、证书等单据类或标签类打印场景的中高级开发人员。资源提供完整的模板编辑器、图形设计工具与布局管理器支持拖拽控件、所见即所得预览并可仅凭Excel数据驱动打印自动处理单头/明细结构及跨页逻辑无需第三方依赖纯原生.NET实现。压缩包共639个文件含188个运行时DLL、182个配置与序列化XML、70个临时或元数据文件、43个说明文本及41个调试符号PDB另有CS源码16个、EXE可执行示例3个、Sln/Csproj工程文件各1个整体89.25MB结构完整、开箱即用。已有1581人学习下载读者可直接复用核心设计器模块、集成打印引擎至自有项目或基于Demo源码快速掌握模板绑定、数据映射与分页渲染全流程。1. C# 可视化打印模板设计为什么“拖控件所见即所得”在产线报表、单据定制场景里不是炫技而是刚需某制造企业产线每天要生成 200 种工单、质检卡、装箱单每种单据字段位置、字体大小、条码区域、公司 Logo 落点都不同业务部门提需求时只甩来一张手绘草图“这里加个二维码那边日期右对齐红色框线要加粗”。传统做法是让开发改 WinForms 打印逻辑、硬编码坐标、反复编译调试——一次变更平均耗时 3 小时出错重打浪费纸张不说还常因 DPI 缩放或打印机驱动差异导致“屏幕上看着对实际打出来偏移 2mm”。而本方案用纯 .NET 实现的可视化模板编辑器让非技术人员直接拖拽 Label、TextBox、PictureBox、Barcode自绘、Line 等控件实时预览打印效果保存为 XML 模板文件运行时动态加载渲染——不依赖任何第三方商业控件如 DevExpress、Telerik不调用 GDI 外部封装库所有绘制逻辑基于 System.Drawing.Common 和 PrintDocument 原生 API 完成。它适合两类人一是中小项目组缺预算买控件、又不愿被商业授权卡脖子的 .NET 开发者二是需要快速交付可配置单据系统的集成商。核心价值不在“能拖”而在“拖完就能打、打出来和屏幕一模一样”。2. 从零搭建可视化模板编辑器控件容器、拖拽逻辑与实时渲染三件套2.1 设计主窗体与可编辑画布用 Panel 模拟“纸张”用双缓冲防闪烁核心思路是不直接在 Form 上拖控件而是创建一个继承自Panel的自定义控件TemplateCanvas它作为所有可拖拽控件的父容器并承载整个页面坐标系单位毫米1mm 3.7795px 96 DPI。关键在于启用双缓冲 重写OnPaint避免拖拽时频繁重绘导致撕裂。public class TemplateCanvas : Panel { public TemplateCanvas() { this.DoubleBuffered true; // 启用双缓冲 this.ResizeRedraw true; this.AutoScroll true; this.BorderStyle BorderStyle.FixedSingle; this.Size new Size(827, 1169); // A4 纸默认尺寸像素96 DPI 下 this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); // 先绘制背景网格可选 DrawGrid(e.Graphics); // 再绘制所有已添加的模板控件 foreach (var ctrl in this.Controls.OfTypeTemplateControlBase()) { ctrl.Draw(e.Graphics, this.ClientRectangle); } } private void DrawGrid(Graphics g) { using (var pen new Pen(Color.LightGray, 0.5f)) { for (int x 0; x this.Width; x 20) // 每20px一条竖线 g.DrawLine(pen, x, 0, x, this.Height); for (int y 0; y this.Height; y 20) // 每20px一条横线 g.DrawLine(pen, 0, y, this.Width, y); } } }说明TemplateCanvas不是普通 Panel它是整个模板的“画布根节点”。DoubleBuffered true是防闪烁第一道防线SetStyle中显式开启OptimizedDoubleBuffer是 .NET Framework 下更彻底的双缓冲控制Size初始化为 A4 尺寸827×1169 px是为后续 DPI 适配打基础——所有控件位置/大小均按此物理尺寸映射而非屏幕像素。DrawGrid仅用于辅助对齐生产环境可关闭。2.2 实现可拖拽控件基类捕获鼠标、计算偏移、限制边界所有可放入模板的控件Label、TextBox、Barcode 等必须继承自TemplateControlBase它封装了通用拖拽逻辑按下时记录初始鼠标位置与控件左上角移动时计算 delta 并更新Location松开时校验是否越界不能拖出画布可视区。public abstract class TemplateControlBase : Control { private Point _dragStartPoint; private Point _controlStartPoint; private bool _isDragging; protected TemplateControlBase() { this.MouseDown OnMouseDown; this.MouseMove OnMouseMove; this.MouseUp OnMouseUp; this.Resize OnResize; } private void OnMouseDown(object sender, MouseEventArgs e) { if (e.Button MouseButtons.Left) { _isDragging true; _dragStartPoint e.Location; _controlStartPoint this.Location; } } private void OnMouseMove(object sender, MouseEventArgs e) { if (_isDragging this.Parent is TemplateCanvas canvas) { var deltaX e.X - _dragStartPoint.X; var deltaY e.Y - _dragStartPoint.Y; var newX _controlStartPoint.X deltaX; var newY _controlStartPoint.Y deltaY; // 限制在画布内留 5px 边距 newX Math.Max(5, Math.Min(newX, canvas.ClientSize.Width - this.Width - 5)); newY Math.Max(5, Math.Min(newY, canvas.ClientSize.Height - this.Height - 5)); this.Location new Point(newX, newY); canvas.Invalidate(); // 主动触发重绘 } } private void OnMouseUp(object sender, MouseEventArgs e) { _isDragging false; } private void OnResize(object sender, EventArgs e) { if (this.Parent is TemplateCanvas canvas) { canvas.Invalidate(); } } // 抽象方法由子类实现具体绘制逻辑 public abstract void Draw(Graphics g, Rectangle canvasBounds); }参数说明_dragStartPoint是鼠标按下时的相对坐标_controlStartPoint是控件原始位置两者差值即为拖拽位移。canvas.ClientSize是画布当前可视区域含滚动条Invalidate()强制刷新画布确保拖拽过程实时可见。注意此处未使用DoDragDrop因为那是 Windows Forms 的“跨控件拖放”而我们需要的是“画布内自由拖拽”必须自己管理鼠标事件链。2.3 构建模板控件集合Label、TextBox、Barcode 的差异化绘制逻辑TemplateControlBase是骨架真正决定“所见即所得”的是各子类的Draw方法。它们必须将自身属性Text、Font、ForeColor、Bounds转换为Graphics绘制指令并严格遵循画布 DPI 设置。// 示例文本标签控件 public class TemplateLabel : TemplateControlBase { public string Text { get; set; } Label; public Font Font { get; set; } new Font(微软雅黑, 10); public Color ForeColor { get; set; } Color.Black; public override void Draw(Graphics g, Rectangle canvasBounds) { if (string.IsNullOrEmpty(Text)) return; // 关键使用画布 DPI 缩放字体保证打印尺寸准确 var dpiScale GetDpiScale(g); var scaledFont new Font(Font.FontFamily, Font.Size * dpiScale, Font.Style); var textRect new Rectangle(this.Location, this.Size); var sf new StringFormat { Alignment StringAlignment.Near, LineAlignment StringAlignment.Center }; using (var brush new SolidBrush(ForeColor)) { g.DrawString(Text, scaledFont, brush, textRect, sf); } scaledFont.Dispose(); } private float GetDpiScale(Graphics g) { // 获取当前 Graphics 的 DPI 缩放因子用于高 DPI 显示适配 return g.DpiX / 96f; // 以 96 DPI 为基准 } } // 示例条码控件EAN-13纯 GDI 绘制无第三方依赖 public class TemplateBarcode : TemplateControlBase { public string BarcodeValue { get; set; } 6901234567890; public int BarHeight { get; set; } 50; public int BarWidth { get; set; } 2; // 条宽像素 public override void Draw(Graphics g, Rectangle canvasBounds) { if (string.IsNullOrEmpty(BarcodeValue) || BarcodeValue.Length ! 13) return; // EAN-13 编码逻辑简化版真实项目需完整校验编码表 var bars EncodeEan13(BarcodeValue); var startX this.Location.X; var startY this.Location.Y; using (var pen new Pen(Color.Black, BarWidth)) { for (int i 0; i bars.Length; i) { if (bars[i] 1) // 黑条 { g.DrawLine(pen, startX i * BarWidth, startY, startX i * BarWidth, startY BarHeight); } } } } private string EncodeEan13(string value) { /* 实际编码逻辑此处省略 */ return ; } }关键点GetDpiScale是“所见即所得”的命脉——它让屏幕显示字体大小与打印输出物理尺寸严格对应。例如在 125% 缩放的显示器上g.DpiX可能是 120dpiScale 120/96 1.25字体自动放大 25%但打印时PrintDocument使用真实 96 DPI最终输出仍是 10pt 字体。EncodeEan13是纯算法实现不调用ZXing或BarcodeLib完全自主可控。所有控件的Draw方法都只做一件事把this.Location/Size和属性通过Graphics绘制到指定矩形内绝不修改this.Controls或this.Parent——因为它们只是“数据载体”不是真实 WinForms 控件。3. 模板持久化与运行时加载XML 序列化 动态实例化控件树3.1 定义模板数据模型用可序列化的类结构描述页面与控件模板不是保存窗体状态而是保存“页面元数据 控件属性快照”。我们设计两个核心类TemplateDocument描述整页纸张尺寸、边距、缩放TemplateControlItem描述每个控件类型、位置、大小、文本等。所有属性必须为 public 且可被XmlSerializer序列化。[Serializable] public class TemplateDocument { public string Name { get; set; } New Template; public float PageWidthMm { get; set; } 210; // A4 宽 210mm public float PageHeightMm { get; set; } 297; // A4 高 297mm public float LeftMarginMm { get; set; } 10; public float TopMarginMm { get; set; } 10; public float RightMarginMm { get; set; } 10; public float BottomMarginMm { get; set; } 10; public ListTemplateControlItem Items { get; set; } new ListTemplateControlItem(); } [Serializable] public class TemplateControlItem { public string Type { get; set; } Label; // Label, TextBox, Barcode public string Text { get; set; } ; public string FontName { get; set; } 微软雅黑; public float FontSize { get; set; } 10; public FontStyle FontStyle { get; set; } FontStyle.Regular; public string ForeColor { get; set; } #FF000000; // ARGB 十六进制 public int X { get; set; } 0; // 相对于页面左上角的毫米坐标 public int Y { get; set; } 0; public int Width { get; set; } 100; // 毫米 public int Height { get; set; } 30; public string BarcodeValue { get; set; } ; public int BarHeight { get; set; } 50; public int BarWidth { get; set; } 2; }说明所有坐标/尺寸单位统一为毫米mm这是工业级打印的通用单位。ForeColor存为#AARRGGBB字符串方便跨平台解析FontStyle是枚举XmlSerializer可直接处理。Items列表顺序即为绘制顺序后添加的在上层无需 ZIndex 属性。3.2 保存模板将画布上所有控件转为 TemplateControlItem 并序列化为 XML保存操作发生在用户点击“保存模板”时。遍历TemplateCanvas.Controls对每个TemplateControlBase子类反射提取其公共属性填充到TemplateControlItem实例中并转换坐标——关键是从像素坐标转为毫米坐标依据画布当前 DPI。private void SaveTemplate(string filePath) { var doc new TemplateDocument { Name Invoice_Template, PageWidthMm 210, PageHeightMm 297, LeftMarginMm 10, TopMarginMm 10, RightMarginMm 10, BottomMarginMm 10 }; foreach (TemplateControlBase ctrl in templateCanvas.Controls.OfTypeTemplateControlBase()) { var item new TemplateControlItem { Type ctrl.GetType().Name.Replace(Template, ), X PixelToMillimeter(ctrl.Left, templateCanvas), Y PixelToMillimeter(ctrl.Top, templateCanvas), Width PixelToMillimeter(ctrl.Width, templateCanvas), Height PixelToMillimeter(ctrl.Height, templateCanvas) }; // 反射获取控件属性并赋值 var props ctrl.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (var prop in props) { if (prop.Name Text prop.PropertyType typeof(string)) item.Text (string)prop.GetValue(ctrl); else if (prop.Name Font prop.PropertyType typeof(Font)) { var font (Font)prop.GetValue(ctrl); item.FontName font.FontFamily.Name; item.FontSize font.Size; item.FontStyle font.Style; } else if (prop.Name ForeColor prop.PropertyType typeof(Color)) { var color (Color)prop.GetValue(ctrl); item.ForeColor $#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}; } else if (prop.Name BarcodeValue prop.PropertyType typeof(string)) item.BarcodeValue (string)prop.GetValue(ctrl); else if (prop.Name BarHeight prop.PropertyType typeof(int)) item.BarHeight (int)prop.GetValue(ctrl); else if (prop.Name BarWidth prop.PropertyType typeof(int)) item.BarWidth (int)prop.GetValue(ctrl); } doc.Items.Add(item); } var serializer new XmlSerializer(typeof(TemplateDocument)); using (var writer new StreamWriter(filePath, false, Encoding.UTF8)) { serializer.Serialize(writer, doc); } } private int PixelToMillimeter(int pixel, Control canvas) { // 1mm 3.7795px 96 DPI → pixel / 3.7795 mm return (int)Math.Round(pixel / 3.7795); }参数说明PixelToMillimeter是核心转换函数它把画布上像素坐标ctrl.Left/Top转为物理毫米值确保保存的模板与设备无关。BindingFlags.Public | BindingFlags.Instance限定只取公有实例属性避免序列化Site、Parent等 WinForms 内部字段。Encoding.UTF8保证中文字段如“客户名称”不乱码。3.3 加载模板反序列化 XML动态创建控件并添加到画布加载是保存的逆过程。读取 XML 得到TemplateDocument遍历Items根据Type字符串反射创建对应控件类型设置属性再Add到TemplateCanvas。private void LoadTemplate(string filePath) { var serializer new XmlSerializer(typeof(TemplateDocument)); using (var reader new StreamReader(filePath, Encoding.UTF8)) { var doc (TemplateDocument)serializer.Deserialize(reader); // 清空画布 templateCanvas.Controls.Clear(); foreach (var item in doc.Items) { TemplateControlBase ctrl null; switch (item.Type) { case Label: ctrl new TemplateLabel(); break; case TextBox: ctrl new TemplateTextBox(); break; case Barcode: ctrl new TemplateBarcode(); break; default: continue; } if (ctrl ! null) { // 设置位置和大小毫米→像素 ctrl.Location new Point( MillimeterToPixel(item.X, templateCanvas), MillimeterToPixel(item.Y, templateCanvas) ); ctrl.Size new Size( MillimeterToPixel(item.Width, templateCanvas), MillimeterToPixel(item.Height, templateCanvas) ); // 反射设置属性 var props ctrl.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (var prop in props) { if (prop.Name Text item.Text ! null) prop.SetValue(ctrl, item.Text); else if (prop.Name Font !string.IsNullOrEmpty(item.FontName)) { var font new Font(item.FontName, item.FontSize, item.FontStyle); prop.SetValue(ctrl, font); } else if (prop.Name ForeColor !string.IsNullOrEmpty(item.ForeColor)) { var color ColorTranslator.FromHtml(item.ForeColor); prop.SetValue(ctrl, color); } else if (prop.Name BarcodeValue !string.IsNullOrEmpty(item.BarcodeValue)) prop.SetValue(ctrl, item.BarcodeValue); else if (prop.Name BarHeight) prop.SetValue(ctrl, item.BarHeight); else if (prop.Name BarWidth) prop.SetValue(ctrl, item.BarWidth); } templateCanvas.Controls.Add(ctrl); } } } } private int MillimeterToPixel(int mm, Control canvas) { return (int)Math.Round(mm * 3.7795); }关键点MillimeterToPixel必须与PixelToMillimeter互为逆运算否则加载后位置偏移。ColorTranslator.FromHtml是 .NET 原生方法安全解析#AARRGGBB。注意此处未使用Activator.CreateInstance因为已知类型列表有限switch更高效且可控若未来扩展控件类型只需在此处增加case分支。4. 所见即所得打印PrintDocument 与 Graphics DPI 对齐的终极校准4.1 创建打印文档绑定 Canvas 内容设置页面尺寸与边距PrintDocument是 .NET 打印的核心。它的PrintPage事件中e.Graphics提供的DpiX/DpiY就是目标打印机的真实 DPI如激光打印机常用 600 DPI而e.MarginBounds是扣除边距后的可用区域。我们必须让画布渲染逻辑与之完全对齐。private void PrintTemplate() { var printDoc new PrintDocument(); printDoc.PrintPage (sender, e) { // 关键用 e.Graphics 的 DPI 计算缩放因子而非屏幕 DPI var dpiScale e.Graphics.DpiX / 96f; // 计算打印区域毫米→像素 var pageWidthPx (int)(210 * 3.7795 * dpiScale); // A4 宽 210mm → 像素 var pageHeightPx (int)(297 * 3.7795 * dpiScale); // 创建与打印区域等大的 Bitmap用于离屏渲染 using (var bmp new Bitmap(pageWidthPx, pageHeightPx)) { using (var g Graphics.FromImage(bmp)) { // 设置高质量渲染 g.SmoothingMode SmoothingMode.AntiAlias; g.TextRenderingHint TextRenderingHint.ClearTypeGridFit; g.InterpolationMode InterpolationMode.HighQualityBicubic; // 绘制背景白色 g.Clear(Color.White); // 遍历所有控件按 DPI 缩放后绘制到 Bitmap foreach (TemplateControlBase ctrl in templateCanvas.Controls.OfTypeTemplateControlBase()) { // 将控件位置/大小按 DPI 缩放 var scaledX (int)(ctrl.Left * dpiScale); var scaledY (int)(ctrl.Top * dpiScale); var scaledWidth (int)(ctrl.Width * dpiScale); var scaledHeight (int)(ctrl.Height * dpiScale); // 创建临时控件副本设置缩放后的位置大小 var tempCtrl CreateScaledControl(ctrl, scaledX, scaledY, scaledWidth, scaledHeight, dpiScale); tempCtrl.Draw(g, new Rectangle(0, 0, pageWidthPx, pageHeightPx)); } } // 将 Bitmap 绘制到打印 Graphics居中 var destRect new Rectangle( (e.PageBounds.Width - bmp.Width) / 2, (e.PageBounds.Height - bmp.Height) / 2, bmp.Width, bmp.Height ); e.Graphics.DrawImage(bmp, destRect); } }; printDoc.Print(); }说明e.Graphics.DpiX是打印机真实 DPIdpiScale e.Graphics.DpiX / 96f是缩放倍数。我们不直接在e.Graphics上绘制控件易受打印机驱动影响而是先创建高 DPIBitmap在上面用Graphics.FromImage离屏渲染最后DrawImage到打印输出——这是最稳定、最可控的“所见即所得”方案。CreateScaledControl是辅助方法返回一个位置/大小已缩放的新控件实例不修改原控件。4.2 实现缩放控件副本避免污染原画布精准匹配打印 DPICreateScaledControl必须为每种控件类型创建新实例并复制所有属性同时按dpiScale缩放字体、条码宽度等。private TemplateControlBase CreateScaledControl(TemplateControlBase src, int x, int y, int w, int h, float dpiScale) { TemplateControlBase clone null; switch (src.GetType().Name) { case TemplateLabel: var label new TemplateLabel { Text ((TemplateLabel)src).Text, ForeColor ((TemplateLabel)src).ForeColor, Location new Point(x, y), Size new Size(w, h) }; // 缩放字体 var origFont ((TemplateLabel)src).Font; label.Font new Font(origFont.FontFamily, origFont.Size * dpiScale, origFont.Style); clone label; break; case TemplateBarcode: var barcode new TemplateBarcode { BarcodeValue ((TemplateBarcode)src).BarcodeValue, BarHeight (int)(((TemplateBarcode)src).BarHeight * dpiScale), BarWidth (int)(((TemplateBarcode)src).BarWidth * dpiScale), Location new Point(x, y), Size new Size(w, h) }; clone barcode; break; // 其他类型类似... } return clone; }参数说明dpiScale直接作用于Font.Size、BarHeight、BarWidth确保打印时条码宽度、文字大小与物理尺寸严格对应。Location/Size已在调用前计算好此处直接赋值。注意CreateScaledControl返回的是临时对象用完即弃绝不Add到任何控件树避免内存泄漏。4.3 预览打印效果用 PrintPreviewDialog 实现真·所见即所得用户需要确认打印效果再实际出纸。PrintPreviewDialog会自动调用PrintDocument.PrintPage因此只要PrintPage逻辑正确预览图就和实际打印一模一样。private void ShowPrintPreview() { var printDoc new PrintDocument(); printDoc.PrintPage (sender, e) { /* 同 4.1 中的绘制逻辑 */ }; var preview new PrintPreviewDialog { Document printDoc, WindowState FormWindowState.Maximized }; preview.ShowDialog(); }提示PrintPreviewDialog是 Windows Forms 原生控件无需额外引用。它显示的缩略图、翻页、缩放功能全部由系统提供开发者只需保证PrintPage事件中绘制逻辑正确。这是“所见即所得”最直观的验证方式——预览里看到什么打印机就打什么。5. 避坑指南那些让“所见即所得”变成“所见非所得”的血泪经验5.1 现象屏幕上控件位置精准打印出来整体向右下偏移 5mm原因PrintDocument的e.MarginBounds是扣除打印机硬件边距后的区域而e.PageBounds是整页物理区域。若直接用e.PageBounds作为绘图基准未考虑e.MarginBounds的起始坐标会导致内容被“挤”到右下角。解决在PrintPage事件中所有绘制坐标必须相对于e.MarginBounds.Location。例如若想让控件从页面左上角含边距开始绘制应设destRect.X e.MarginBounds.X而非0。修正代码// 错误以 (0,0) 为起点 e.Graphics.DrawImage(bmp, 0, 0); // 正确以页边距左上角为起点 e.Graphics.DrawImage(bmp, e.MarginBounds.X, e.MarginBounds.Y);5.2 现象高 DPI 显示器如 200% 缩放下画布网格线变粗、控件拖拽跳变原因Panel默认不支持 DPI 感知ClientSize返回的是逻辑像素而Graphics绘制时使用物理像素导致比例错乱。解决在TemplateCanvas构造函数中强制启用 DPI 感知并重写OnHandleCreatedprotected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); if (Environment.OSVersion.Version.Major 6) // Windows Vista { SetProcessDPIAware(); } } [DllImport(user32.dll)] private static extern bool SetProcessDPIAware();同时在OnPaint中用g.Transform统一缩放protected override void OnPaint(PaintEventArgs e) { var scale this.CreateGraphics().DpiX / 96f; e.Graphics.ResetTransform(); e.Graphics.ScaleTransform(scale, scale); // ... 后续绘制逻辑 }5.3 现象条码打印后扫描枪无法识别但屏幕预览正常原因条码宽度BarWidth是像素值未随 DPI 缩放。在 600 DPI 打印机上2px 宽度可能只有 0.085mm低于扫描枪最小识别宽度通常 ≥0.15mm。解决条码宽度必须按物理毫米定义而非像素。修改TemplateBarcode类将BarWidth属性改为float BarWidthMm默认值设为0.3f0.3mm绘制时再转为像素public float BarWidthMm { get; set; } 0.3f; // 物理宽度单位毫米 public override void Draw(Graphics g, Rectangle canvasBounds) { var barWidthPx (int)Math.Round(BarWidthMm * 3.7795 * GetDpiScale(g)); // ... 后续绘制使用 barWidthPx }5.4 现象加载 XML 模板后中文文本显示为方块或乱码原因XmlSerializer默认使用 UTF-8 编码但若 XML 文件保存时用了 ANSI 或 GB2312读取时会解码失败。解决强制指定StreamReader编码为 UTF-8并在保存时写 BOM 头// 保存时 using (var writer new StreamWriter(filePath, false, new UTF8Encoding(true))) // true 表示写 BOM { serializer.Serialize(writer, doc); } // 加载时 using (var reader new StreamReader(filePath, Encoding.UTF8)) // 显式指定 UTF-8 { var doc (TemplateDocument)serializer.Deserialize(reader); }5.5 现象拖拽控件时画布滚动条自动跳到顶部无法拖到页面底部原因TemplateCanvas启用了AutoScroll true但未设置AutoScrollMinSize导致滚动范围不足。解决在TemplateCanvas的OnSizeChanged中动态设置最小滚动尺寸protected override void OnSizeChanged(EventArgs e) { base.OnSizeChanged(e); // 最小滚动尺寸 画布内容尺寸A4 边距 this.AutoScrollMinSize new Size( (int)(210 * 3.7795) 20, // A4宽20px边距 (int)(297 * 3.7795) 20 // A4高20px边距 ); }6. 进阶技巧用模板变量实现动态数据绑定与条件显示6.1 定义模板变量语法在文本控件中嵌入{FieldName}占位符真正的业务单据不是静态的而是要填入数据库查询结果。我们在TemplateLabel和TemplateTextBox中支持变量替换当Text属性包含{OrderNo}、{CustomerName}等格式时在打印时自动替换为实际值。// 在 TemplateLabel.Draw 方法中 public override void Draw(Graphics g, Rectangle canvasBounds) { var displayText Text; if (displayText.Contains({) displayText.Contains(})) { displayText ReplaceVariables(displayText, DataContext); } // ... 后续用 displayText 绘制 } private string ReplaceVariables(string text, object dataContext) { var result text; var matches Regex.Matches(text, \{(\w)\}); foreach (Match match in matches) { var fieldName match.Groups[1].Value; var prop dataContext.GetType().GetProperty(fieldName); if (prop ! null) { var value prop.GetValue(dataContext)?.ToString() ?? ; result result.Replace(match.Value, value); } } return result; }说明DataContext是一个object类型的公共属性由使用者在打印前赋值例如label.DataContext order;其中order是一个包含OrderNo、CustomerName等属性的 POCO 类。正则\{(\w)\}精确匹配{FieldName}格式避免误替换。6.2 支持条件显示用{if:Condition}Content{endif}控制控件可见性某些字段只在特定条件下显示如“折扣金额”仅当Discount 0时出现。我们扩展变量语法支持简单条件判断。// 在 TemplateControlBase 中添加 public string VisibilityExpression { get; set; } // 例如 Discount 0 // 修改 Draw 方法开头 public override void Draw(Graphics g, Rectangle canvasBounds) { if (!IsVisible()) return; // 先判断是否显示 // ... 后续绘制 } private bool IsVisible() { if (string.IsNullOrEmpty(VisibilityExpression)) return true; try { // 使用 DataTable.Compute 简单计算仅支持基础表达式 var table new DataTable(); var row table.NewRow(); foreach (var prop in DataContext.GetType().GetProperties()) { table.Columns.Add(prop.Name, prop.PropertyType); row[prop.Name] prop.GetValue(DataContext); } table.Rows.Add(row); var result table.Compute(VisibilityExpression, ); return Convert.ToBoolean(result); } catch { return false; // 表达式错误则隐藏 } }参数说明DataTable.Compute是 .NET 内置的轻量表达式计算器支持,,,,||等无需引入NCalc或Jint。VisibilityExpression存为字符串如TotalAmount 1000 IsVip true在Draw时动态求值。注意此方案适用于简单条件复杂逻辑建议在数据层预处理。6.3 打印时传入数据上下文一行代码完成数据绑定最终打印调用变得极其简洁。用户只需准备一个数据对象设置DataContext调用PrintTemplate即可// 准备数据 var invoice new Invoice { OrderNo INV-2023-00 p a hrefhttps://download.csdn.net/download/guo9long/89689575 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p