简介:本资源是一套基于Windows Forms的C#自定义打印设计工具完整实现方案,面向.NET桌面应用开发者,解决报表生成、文档动态排版与二维码嵌入等实际打印需求。资源包含563个文件,主体为42个核心C#源码文件(含PrintDocument事件处理、图形绘制逻辑及控件拖拽设计模块)、190个运行依赖DLL、99个XML配置与文档说明,以及预编译的EXE可执行示例和Sln工程结构,整体压缩包达56.26MB。已有3624人学习下载,表明其在企业级Winform项目中具备较高实践参考价值。读者可直接复用设计界面拖放逻辑、Graphics动态绘图代码、PrintPage事件驱动的打印流程,以及集成QRCoder生成二维码的完整链路;同时获得打印预览窗体实现、PageSettings纸张适配策略和元素状态(可见/选中)控制机制等关键细节,显著降低定制化打印功能开发门槛。
1. 项目概述:从零构建一个WinForm自定义打印设计工具
在桌面应用开发,尤其是基于WinForm的各类业务系统(如ERP、MES、WMS)中,打印功能几乎是刚需。然而,开发者和最终用户常常陷入一种困境:开发者疲于应对层出不穷、频繁变更的打印模板需求,每次调整都要重新编码、编译、发布;而用户则受限于固定的打印格式,无法根据现场单据、标签或报表的实际情况进行灵活排版。市面上的报表工具虽然强大,但往往过于臃肿,集成复杂,或者无法满足高度定制化的动态数据填充需求。
这个项目就是为了解决这个痛点:打造一个内嵌于WinForm应用中的、轻量级但功能完备的自定义打印设计工具。它不是一个独立的软件,而是一个可复用的组件库(DLL)。核心目标有两个:第一,提供一个可视化的设计器,让用户(或实施人员)能够像搭积木一样,通过拖拽文本框、线条、条码、图片等元素,自由设计打印模板;第二,提供简洁的API,让开发者能在代码中动态加载这些设计好的模板,绑定业务数据,并驱动打印机(或虚拟打印机)完成打印作业。这相当于把“设计权”部分移交给了用户,将开发者从繁琐的模板维护工作中解放出来,同时极大地提升了软件的适应性和用户体验。
2. 核心需求与架构设计解析
2.1 需求深度拆解
要构建这样一个工具,我们必须先厘清核心需求,这决定了我们的架构方向。
2.1.1 可视化设计器需求这是工具的门面,用户体验的关键。用户需要一个类似简易版Visio或报表设计器的界面。核心功能包括:
- 画布与基础操作:一个可缩放、带网格辅助线的设计画布。支持元素的选择、拖拽移动、缩放、旋转(针对某些元素)、对齐(左对齐、居中等)、层级调整(置顶、置底)。
- 元素工具箱:提供丰富的可拖放元素,如静态文本、数据绑定文本框、线条、矩形、图片、一维/二维条码(如Code 128, QR Code)、表格等。
- 属性面板:当选中某个元素时,能动态显示并编辑其所有属性,如位置(X, Y)、尺寸(Width, Height)、字体、颜色、边框、数据绑定表达式(例如
{OrderID}或{Customer.Name})。 - 模板管理:支持新建、打开、保存、另存为打印模板文件。模板文件需要一种结构化的格式来存储所有元素及其属性。
2.1.2 动态打印引擎需求这是工具的心脏,负责将设计好的模板与实时数据结合,并输出到打印设备。
- 模板加载与解析:能够读取并解析由设计器生成的模板文件,在内存中重建元素树。
- 数据绑定机制:支持将业务对象(如一个
Order实例)或数据集(如DataTable)绑定到模板。元素的数据绑定表达式需要在运行时被求值,替换为实际数据。例如,文本框的Text属性绑定为{TotalAmount},运行时就需要从数据源中获取TotalAmount属性的值。 - 打印逻辑控制:控制打印的份数、页码范围、打印机选择。更重要的是处理数据分页,比如一个订单有10个明细行,而模板中只定义了5行的空间,引擎需要能自动创建新页来容纳所有数据。
- 预览功能:在真正发送到打印机前,提供一个“所见即所得”的打印预览窗口,允许用户检查最终效果,这是专业性的体现。
2.1.3 开发者集成需求工具必须易于被其他WinForm项目集成和调用。
- 清晰的API接口:暴露少数几个核心类和方法,如
PrintDesignerForm(设计器窗体)、PrintEngine(打印引擎)。调用应该像printEngine.Print(templatePath, dataSource)一样简单。 - 松耦合设计:将设计器UI、打印逻辑、数据绑定引擎分层,确保核心逻辑不依赖于特定的UI框架,未来甚至可能移植到WPF或.NET Core/5+。
2.2 技术选型与架构设计
基于以上需求,我们选择C#和.NET Framework/WinForms作为实现技术栈,这是最自然的选择。架构上,我们采用典型的三层模式,但更侧重于组件内部的分离。
2.2.1 项目结构规划建议创建三个核心类库项目:
- Printing.Core:包含所有领域模型和核心逻辑。定义
PrintElement(所有打印元素的基类)、TextElement、LineElement、BarcodeElement等。包含模板的序列化/反序列化逻辑(如使用JSON或XML)。定义数据绑定引擎接口IDataBinder。 - Printing.Designer:WinForm项目,实现可视化设计器。它引用
Printing.Core,并包含DesignCanvas(自定义控件)、ToolboxControl、PropertiesPanel等UI组件。负责将用户操作转化为对PrintElement对象树的修改,并调用Core层的序列化功能保存模板。 - Printing.Engine:实现具体的打印和预览逻辑。它引用
Printing.Core。核心类是PrintEngine,它利用.NET Framework自带的PrintDocument类进行底层打印,但需要在其PrintPage事件中,根据模板元素树和绑定后的数据,进行复杂的GDI+绘图操作。同时,它也会实现一个PrintPreviewForm。
2.2.2 关键技术点决策
- 模板存储格式:推荐使用JSON(Newtonsoft.Json库)。相比XML,JSON更轻量,可读性好,序列化/反序列化方便。一个模板本质上就是一个复杂对象(包含元素列表)的序列化字符串。
- 图形渲染:打印和预览的核心是GDI+。我们需要在
PrintDocument.PrintPage事件的Graphics对象上,根据每个PrintElement的类型和属性,调用相应的DrawString、DrawLine、DrawImage等方法。这是性能关键路径,需要考虑绘图优化,比如缓存画笔、字体对象。 - 数据绑定:实现一个简单的表达式求值器。对于
{PropertyName}这样的表达式,我们可以通过反射(Reflection)从数据源对象中获取值。对于更复杂的表达式(如{Price*Quantity}),可以引入轻量级的动态编译(如System.Linq.Dynamic.Core库)或解释器,但这会增加复杂性。初期支持反射和点分路径(如{Customer.Address.City})即可。 - 条码生成:选择成熟的第三方库,如ZXing.Net,它支持生成多种条码的位图,我们只需在
BarcodeElement的渲染逻辑中调用它。
注意:关于性能与复杂度。在画布上实时渲染大量元素时,直接操作GDI+可能导致闪烁。我们需要为
DesignCanvas启用双缓冲(SetStyle(ControlStyles.OptimizedDoubleBuffer, true))。对于极其复杂的模板,可以考虑在打印引擎中使用Metafile进行中间渲染,但初期不必过度优化。
3. 核心模块实现详解
3.1 领域模型构建(Printing.Core)
这是整个系统的基石,定义了什么是一个“打印模板”。
3.1.1 打印元素基类设计所有可打印的元素都应继承自一个共同的基类PrintElement。这个类定义了元素的通用属性和行为。
using System.Drawing; using Newtonsoft.Json; namespace Printing.Core { public abstract class PrintElement { public string Id { get; set; } = Guid.NewGuid().ToString(); // 唯一标识 public string Name { get; set; } = "未命名元素"; public float X { get; set; } // 相对于画布或容器的位置(毫米或像素,建议用毫米存储) public float Y { get; set; } public float Width { get; set; } public float Height { get; set; } public bool Visible { get; set; } = true; // 数据绑定表达式,如 "{OrderID}" public string DataBinding { get; set; } = string.Empty; // 抽象方法,用于在指定的Graphics上绘制此元素 public abstract void Draw(Graphics g, PointF offset, object dataContext); // 判断点是否在元素区域内(用于设计器中的点选) public virtual bool HitTest(PointF point) { return point.X >= X && point.X <= X + Width && point.Y >= Y && point.Y <= Y + Height; } } }3.1.2 具体元素实现以TextElement为例,展示如何扩展基类。
namespace Printing.Core { public class TextElement : PrintElement { public string Text { get; set; } = "文本"; public string FontFamily { get; set; } = "微软雅黑"; public float FontSize { get; set; } = 10; // 单位:磅 public FontStyle FontStyle { get; set; } = FontStyle.Regular; public Color Color { get; set; } = Color.Black; public StringAlignment HorizontalAlignment { get; set; } = StringAlignment.Near; public StringAlignment VerticalAlignment { get; set; } = StringAlignment.Near; public override void Draw(Graphics g, PointF offset, object dataContext) { if (!Visible) return; string finalText = Text; // 如果设置了数据绑定,则尝试从数据源获取值 if (!string.IsNullOrWhiteSpace(DataBinding) && dataContext != null) { finalText = DataBinder.Evaluate(dataContext, DataBinding)?.ToString() ?? Text; } using (Font font = new Font(FontFamily, FontSize, FontStyle, GraphicsUnit.Point)) using (Brush brush = new SolidBrush(Color)) { RectangleF rect = new RectangleF(X + offset.X, Y + offset.Y, Width, Height); // 使用StringFormat实现对齐 StringFormat format = new StringFormat(); format.Alignment = HorizontalAlignment; format.LineAlignment = VerticalAlignment; // 注意:Graphics的坐标单位需要与元素存储单位一致。这里假设都已转换为Graphics的PageUnit(如毫米)。 g.DrawString(finalText, font, brush, rect, format); } } } }3.1.3 模板类与序列化PrintTemplate类代表一个完整的模板,它包含页面设置(如纸张大小、边距)和一个元素列表。
namespace Printing.Core { public class PrintTemplate { public string TemplateName { get; set; } public SizeF PageSize { get; set; } = new SizeF(210, 297); // A4 毫米 public Margins Margins { get; set; } = new Margins(20, 20, 20, 20); // 页边距 public List<PrintElement> Elements { get; set; } = new List<PrintElement>(); public string ToJson() { // 使用JsonConvert序列化时,需要注意处理Color、Font等非原生类型。 // 可以定义自定义的JsonConverter,或者将其转换为可序列化的格式(如ARGB值)。 var settings = new JsonSerializerSettings { Formatting = Formatting.Indented, TypeNameHandling = TypeNameHandling.Auto // 保留类型信息,反序列化时能创建正确的子类 }; return JsonConvert.SerializeObject(this, settings); } public static PrintTemplate FromJson(string json) { var settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto }; return JsonConvert.DeserializeObject<PrintTemplate>(json, settings); } } }实操心得:序列化的坑。直接序列化
Color和Font会出问题。我们的做法是为Color存储其ToArgb()的整数值,反序列化时用Color.FromArgb()还原。对于Font,我们只存储其家族名、大小和样式,在Draw方法中临时创建Font对象。虽然创建Font有一定开销,但保证了序列化的简洁和跨平台兼容性。
3.2 可视化设计器实现(Printing.Designer)
设计器是一个复杂的WinForm,核心是自定义的DesignCanvas控件。
3.2.1 DesignCanvas:画布控件这个控件负责显示和交互。它内部维护一个PrintTemplate实例的引用。
public partial class DesignCanvas : UserControl { private PrintTemplate _currentTemplate; private PrintElement _selectedElement; private Point _lastMousePos; private bool _isDragging = false; private float _zoomFactor = 1.0f; public DesignCanvas() { InitializeComponent(); this.DoubleBuffered = true; // 关键!启用双缓冲消除闪烁 this.SetStyle(ControlStyles.ResizeRedraw | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true); } public void LoadTemplate(PrintTemplate template) { _currentTemplate = template; this.Invalidate(); // 触发重绘 } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); if (_currentTemplate == null) return; e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; // 抗锯齿 e.Graphics.PageUnit = GraphicsUnit.Millimeter; // 设置单位为毫米 e.Graphics.TranslateTransform(Padding.Left, Padding.Top); // 偏移 e.Graphics.ScaleTransform(_zoomFactor, _zoomFactor); // 缩放 // 1. 绘制背景和网格 DrawGrid(e.Graphics); // 2. 绘制所有元素 foreach (var element in _currentTemplate.Elements) { element.Draw(e.Graphics, PointF.Empty, null); // 设计模式下无数据源 } // 3. 如果选中了元素,绘制选择框(手柄) if (_selectedElement != null) { DrawSelectionBorder(e.Graphics, _selectedElement); } } private void DrawGrid(Graphics g) { // 绘制浅灰色网格线,辅助对齐 using (Pen gridPen = new Pen(Color.LightGray, 0.1f)) { float step = 5.0f; // 5毫米一格 for (float x = 0; x < _currentTemplate.PageSize.Width; x += step) { g.DrawLine(gridPen, x, 0, x, _currentTemplate.PageSize.Height); } for (float y = 0; y < _currentTemplate.PageSize.Height; y += step) { g.DrawLine(gridPen, 0, y, _currentTemplate.PageSize.Width, y); } } // 绘制页面边框 using (Pen borderPen = new Pen(Color.Black, 0.2f)) { g.DrawRectangle(borderPen, 0, 0, _currentTemplate.PageSize.Width, _currentTemplate.PageSize.Height); } } // 鼠标事件处理:选择、拖拽 protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); PointF clickPoint = this.PointToCanvas(e.Location); // 转换坐标到画布坐标系 _selectedElement = _currentTemplate?.Elements.LastOrDefault(el => el.HitTest(clickPoint)); // 选中最上层的元素 if (_selectedElement != null) { _isDragging = true; _lastMousePos = e.Location; // 通知属性面板更新 OnElementSelected?.Invoke(this, _selectedElement); } this.Invalidate(); } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (_isDragging && _selectedElement != null) { PointF delta = this.PointToCanvasDelta(new Point(e.X - _lastMousePos.X, e.Y - _lastMousePos.Y)); _selectedElement.X += delta.X; _selectedElement.Y += delta.Y; _lastMousePos = e.Location; this.Invalidate(); // 拖拽时实时更新显示 } } protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); _isDragging = false; } }3.2.2 工具箱与属性面板工具箱(ToolboxControl)可以是一个FlowLayoutPanel,里面放一堆代表不同元素的按钮(如Button,设置其Tag为元素类型)。点击按钮时,在画布中央创建一个新的元素实例,并添加到_currentTemplate.Elements中。
属性面板(PropertiesPanel)通常使用WinForm自带的PropertyGrid控件。将_selectedElement赋值给PropertyGrid.SelectedObject,它就能自动显示其所有公共属性。但是,PropertyGrid对自定义类型的属性编辑支持有限(比如我们的Color属性显示为Color [A=255, R=0, G=0, B=0],编辑不便)。我们可以通过为属性添加[TypeConverter]和[Editor]特性,或者创建自定义的UITypeEditor来提供更好的编辑体验,例如一个颜色选择下拉框。
踩坑记录:PropertyGrid的刷新。当在画布上拖拽元素改变其
X,Y属性时,PropertyGrid并不会自动刷新。我们需要在DesignCanvas的OnMouseUp事件中,或者在元素属性变更时,手动触发PropertyGrid.Refresh()。更优雅的方式是实现INotifyPropertyChanged接口,让元素属性变更时发出通知。
3.3 动态打印引擎实现(Printing.Engine)
打印引擎负责将模板和数据合成为最终的打印输出。
3.3.1 PrintEngine 核心类
namespace Printing.Engine { public class PrintEngine { private PrintTemplate _template; private object _dataSource; private int _currentPageIndex = 0; private List<object> _pagedData; // 用于处理分页的数据 public void Print(string templatePath, object dataSource, string printerName = null) { LoadTemplate(templatePath); _dataSource = dataSource; PrepareDataForPaging(); // 准备分页数据 using (PrintDocument pd = new PrintDocument()) { if (!string.IsNullOrEmpty(printerName)) { pd.PrinterSettings.PrinterName = printerName; } pd.DefaultPageSettings.PaperSize = new PaperSize("Custom", (int)(_template.PageSize.Width * 100 / 25.4f), // 转换毫米到百分之一英寸 (int)(_template.PageSize.Height * 100 / 25.4f)); pd.DefaultPageSettings.Margins = _template.Margins; pd.PrintPage += Pd_PrintPage; pd.QueryPageSettings += Pd_QueryPageSettings; // 用于多页不同设置(如横向纵向) pd.Print(); } } public void ShowPreview(string templatePath, object dataSource) { LoadTemplate(templatePath); _dataSource = dataSource; PrepareDataForPaging(); using (PrintDocument pd = new PrintDocument()) { // ... 页面设置同上 ... pd.PrintPage += Pd_PrintPage; PrintPreviewDialog previewDlg = new PrintPreviewDialog(); previewDlg.Document = pd; previewDlg.ShowDialog(); } } private void Pd_PrintPage(object sender, PrintPageEventArgs e) { Graphics g = e.Graphics; g.PageUnit = GraphicsUnit.Millimeter; // 与模板单位一致 g.TranslateTransform(e.MarginBounds.Left, e.MarginBounds.Top); // 偏移到页边距内 // 获取当前页的数据上下文 object pageDataContext = _pagedData?[_currentPageIndex] ?? _dataSource; // 绘制所有可见元素 foreach (var element in _template.Elements.Where(el => el.Visible)) { element.Draw(g, PointF.Empty, pageDataContext); } // 判断是否还有下一页 _currentPageIndex++; e.HasMorePages = (_pagedData != null && _currentPageIndex < _pagedData.Count); if (!e.HasMorePages) { _currentPageIndex = 0; // 重置,供下次打印使用 } } private void PrepareDataForPaging() { // 分页逻辑是难点。这里假设数据源是订单列表,模板中有一个“明细行”区域。 // 我们需要根据明细区域的高度和每页能容纳的行数,将数据拆分到多个“虚拟页”。 // 这通常需要模板中有特殊的“重复区域”标记,实现较为复杂。 // 简化版:如果数据源是集合,且模板未定义分页,则每项数据打一页。 if (_dataSource is System.Collections.IEnumerable enumerable && !(_dataSource is string)) { _pagedData = new List<object>(); foreach (var item in enumerable) { _pagedData.Add(item); } } else { _pagedData = null; // 单页数据 } } } }3.3.2 数据绑定引擎我们之前提到的DataBinder.Evaluate方法需要实现。这里展示一个基于反射的简单版本。
namespace Printing.Core { public static class DataBinder { public static object Evaluate(object dataContext, string bindingExpression) { if (dataContext == null || string.IsNullOrWhiteSpace(bindingExpression)) return null; string expression = bindingExpression.Trim('{', '}', ' '); if (string.IsNullOrEmpty(expression)) return bindingExpression; // 简单属性路径,如 "OrderID" 或 "Customer.Name" string[] parts = expression.Split('.'); object currentObj = dataContext; foreach (string part in parts) { if (currentObj == null) return null; PropertyInfo prop = currentObj.GetType().GetProperty(part); if (prop != null && prop.CanRead) { currentObj = prop.GetValue(currentObj); } else { // 如果不是属性,尝试作为字段或字典键查找(此处省略) return null; } } return currentObj; } } }注意事项:复杂数据绑定与性能。上述反射方法在每次绘制每个绑定元素时都会调用,在打印大量数据时可能成为性能瓶颈。一个优化方案是,在打印开始前,为每个绑定表达式预编译一个获取值的委托(
Func<object, object>)。可以使用System.Linq.Expressions动态创建表达式树并编译,这能大幅提升性能。对于固定模板的批量打印,这个优化是值得的。
4. 集成、调用与高级功能拓展
4.1 在宿主应用程序中集成与调用
假设我们有一个WinForm订单管理系统,现在要集成我们的打印组件。
4.1.1 引用与初始化首先,在宿主项目中添加对Printing.Designer、Printing.Engine和Printing.Core(如果直接引用)的引用。由于设计器是一个Form,通常我们会在需要的地方(比如“设计打印模板”菜单点击事件)实例化并显示它。
// 在设计模板的按钮点击事件中 private void btnDesignPrintTemplate_Click(object sender, EventArgs e) { using (var designerForm = new PrintDesignerForm()) // PrintDesignerForm是Printing.Designer项目的主窗体 { // 可以加载一个已存在的模板 // designerForm.LoadTemplate(@"C:\Templates\OrderTemplate.json"); if (designerForm.ShowDialog() == DialogResult.OK) { // 保存设计好的模板 string savedPath = designerForm.SaveTemplate(); MessageBox.Show($"模板已保存至:{savedPath}"); } } }4.1.2 动态调用打印在需要打印订单的地方,调用打印引擎。
private void btnPrintOrder_Click(object sender, EventArgs e) { Order currentOrder = GetCurrentOrder(); // 获取业务数据 string templatePath = @"C:\Templates\OrderTemplate.json"; PrintEngine engine = new PrintEngine(); try { // 直接打印 // engine.Print(templatePath, currentOrder); // 或先预览 engine.ShowPreview(templatePath, currentOrder); } catch (Exception ex) { MessageBox.Show($"打印失败:{ex.Message}"); } }4.2 高级功能与优化方向
一个基础版本实现后,可以考虑以下增强功能,这会让你的组件更具竞争力。
4.2.1 表格与重复区域这是实现清单、明细打印的关键。需要引入新的元素类型TableElement或RepeaterElement。它在设计时定义表头和一行数据的模板,在运行时根据数据源的行数进行垂直复制和填充。这需要打印引擎有更强大的布局计算能力。
4.2.2 脚本支持允许在元素属性或事件(如“格式化文本”)中嵌入简单的脚本(如JavaScript或C#脚本),实现复杂的逻辑,例如:
Text = {UnitPrice} * {Quantity}(计算小计)Visible = {Quantity} > 0(条件显示) 可以通过集成Roslyn脚本引擎或Jint (JavaScript解释器)来实现。
4.2.3 条码与二维码的增强除了生成位图,可以支持直接输出矢量指令到支持PCL/PS的打印机,提高打印质量和速度。同时,增加对更多条码制式的支持。
4.2.4 模板版本管理与云端同步将模板文件存储在数据库或云存储中,实现模板的集中管理和版本控制。宿主程序启动时从服务器拉取最新模板,实现打印格式的静默更新。
4.2.5 打印到PDF除了物理打印机,集成如PDFSharp或iTextSharp库,将打印输出直接生成PDF文件,方便电子存档和邮件发送。
4.3 常见问题与调试技巧
在实际开发和使用中,你肯定会遇到各种问题。这里记录几个典型的:
4.3.1 打印位置偏差
- 现象:设计时预览正常,但实际打印出来位置偏移。
- 排查:
- 单位一致性:确保设计器画布、
PrintElement属性、PrintDocument的Graphics对象使用的单位一致。全程使用毫米(Millimeter)是最稳妥的。 - 打印机驱动:不同打印机对页边距的处理可能不同。在
PrintPageEventArgs中,使用e.MarginBounds作为绘图区域的原点,而不是(0,0)。 - DPI差异:屏幕DPI和打印机DPI不同。我们的毫米单位是物理单位,能较好规避此问题。避免使用像素(Pixel)单位。
- 单位一致性:确保设计器画布、
4.3.2 数据绑定失败
- 现象:绑定的字段显示为空或表达式本身。
- 排查:
- 检查
DataBinding字符串格式是否正确,如{OrderID}。 - 在
DataBinder.Evaluate方法内部添加日志,输出当前正在解析的表达式和数据源类型。 - 确认数据源对象的属性是否是公共的(Public)且有
get访问器。 - 对于复杂路径(如
{Customer.Address.City}),检查中间对象是否为null。
- 检查
4.3.3 设计器性能卡顿
- 现象:当模板元素过多(如超过100个)时,拖拽或缩放操作不流畅。
- 解决:
- 确保
DesignCanvas已启用DoubleBuffered。 - 在
OnPaint中,只重绘失效区域(e.ClipRectangle),而不是整个画布。但这需要更精细的脏矩形计算。 - 对于非常复杂的模板,可以考虑在非UI线程上预渲染静态部分到位图缓存。
- 确保
4.3.4 “操作无法完成,错误 0x00000bbb”
- 现象:调用
Print()时抛出异常。 - 排查:这个Windows错误码通常与打印机状态有关。
- 检查指定的打印机名称是否存在、是否在线。
- 检查打印队列是否被暂停或塞满。
- 尝试使用
PrintDialog让用户选择打印机,而不是硬编码名称。 - 以管理员身份运行程序,看是否权限问题。
开发这样一个工具是一次对WinForm图形编程、对象模型设计、数据绑定和打印系统的深度之旅。从简单的文本框绘制到支持动态数据的复杂表格,每一步都需要仔细权衡易用性、性能和扩展性。我个人在实现过程中的最大体会是,前期花在领域模型设计上的时间越多,后期添加新功能就越轻松。例如,一个设计良好的PrintElement基类,能让后续添加圆形、图表等新元素类型变得非常容易。另外,一定要尽早引入真实打印机的测试,虚拟打印机和物理打印机的行为差异可能会让你大吃一惊。最后,将这个组件封装成NuGet包,将会极大方便在其他项目中的复用和分发。
本文还有配套的精品资源,点击获取