C#实战:如何用PrintDocument直接控制针式打印机(附完整代码)

📅 发布时间:2026/7/6 15:42:27 👁️ 浏览次数:
C#实战:如何用PrintDocument直接控制针式打印机(附完整代码)
C#实战告别低效绕路用PrintDocument精准驾驭针式打印机在票据、标签、单据等需要精确控制的打印场景里很多C#开发者都曾走过一段弯路为了生成一份格式固定的打印件不得不先调用Office或WPS组件生成一个Word或Excel文件再通过系统命令“打印”这个文件。这个过程不仅引入了对第三方软件的强依赖导致部署环境复杂更关键的是你几乎失去了对打印过程的直接控制权——字体位置差一个像素行间距无法精确调整对不起你得和Word的排版引擎去“商量”。这种隔靴搔痒的方式效率低下且难以满足工业级、高并发的定制化打印需求。真正的解决方案其实一直静静地躺在.NET Framework的System.Drawing.Printing命名空间里。PrintDocument类这个看似古老的API恰恰是连接C#应用程序与物理打印机的直接桥梁。它允许你以编程方式像在画布上作画一样精确控制每一个字符、每一条线的输出位置完全绕开任何中间软件。这对于使用针式打印机进行多联票据打印、连续纸标签打印的开发者而言意味着可以摆脱环境束缚实现稳定、高效、可预测的打印输出。本文将带你深入PrintDocument的核心从原理到实战构建一套可直接用于生产环境的针式打印机控制方案。1. 理解PrintDocument绕过驱动直达打印核心在深入代码之前我们需要厘清一个关键概念PrintDocument是如何工作的它并非直接与打印机硬件对话而是与Windows的图形设备接口GDI紧密协作。当你调用PrintDocument进行打印时本质上是在发起一个GDI绘图指令序列。这个指令序列会被Windows的打印后台处理程序接收并最终由对应的打印机驱动程序翻译成打印机能够理解的指令如ESC/P、PCL等。对于针式打印机这套流程尤其重要因为许多针打指令如切纸、走纸到定位点都需要通过精确的坐标控制来模拟或直接发送ESC/POS命令。那么为什么直接使用PrintDocument比通过WPS等中间件更优我们可以从几个维度来对比对比维度直接使用PrintDocument通过WPS/Office中间件打印控制精度像素级精确可直接指定(X, Y)坐标。受限于Word/Excel的排版引擎调整困难。环境依赖仅需.NET Framework和打印机驱动依赖极简。必须安装完整的WPS或Office部署复杂。性能开销内存占用小直接生成打印指令速度极快。需要启动外部进程、生成临时文件开销巨大。稳定性进程内直接控制异常易捕获稳定性高。依赖外部进程通信容易因软件冲突或版本问题失败。功能灵活性可自由绘制图形、条形码、自定义字体功能无限。受限于中间件功能实现复杂格式难度大。提示对于针式打印机PrintDocument的Graphics对象绘图最终会被转换为点阵图像或ESC/POS指令。这意味着即使你绘制的是平滑曲线在针式打印机上输出的也是由点阵构成的图案。理解这一点有助于在设计打印内容时做出更合适的选择。因此选择PrintDocument就是选择了一条直接、高效、可控的技术路径。它尤其适合以下场景票据打印系统如餐饮小票、零售收据、银行凭条。物流标签打印在连续纸上打印格式固定的运单标签。工业报表打印需要精确分页和定位的车间生产报表。证书/证件套打在预制好的纸质表单上精确填充信息。2. 构建你的第一个PrintDocument打印程序让我们从零开始搭建一个最小化的、可运行的打印示例。这个例子将演示如何设置打印机、定义纸张大小并在PrintPage事件中绘制简单的文本。首先在Visual Studio中创建一个新的Windows Forms App (.NET Framework)项目。在窗体上放置一个按钮命名为btnPrint。我们将在这个按钮的点击事件中编写核心打印逻辑。using System.Drawing; using System.Drawing.Printing; using System.Windows.Forms; public partial class MainForm : Form { private PrintDocument pd; public MainForm() { InitializeComponent(); pd new PrintDocument(); pd.PrintPage new PrintPageEventHandler(this.PrintPageHandler); } private void btnPrint_Click(object sender, EventArgs e) { // 1. 指定打印机名称请替换为你的实际打印机名称 pd.PrinterSettings.PrinterName EPSON LQ-630K; // 示例针式打印机 // 2. 自定义纸张大小宽度x高度单位百分之一英寸 PaperSize customPaper new PaperSize(Custom, 800, 550); // 宽8英寸高5.5英寸 pd.DefaultPageSettings.PaperSize customPaper; // 3. 设置打印控制器可选禁用打印进度对话框 pd.PrintController new StandardPrintController(); try { // 4. 开始打印作业 pd.Print(); } catch (InvalidPrinterException ex) { MessageBox.Show($打印机设置错误{ex.Message}); } catch (Exception ex) { MessageBox.Show($打印过程中发生错误{ex.Message}); } } private void PrintPageHandler(object sender, PrintPageEventArgs e) { // 获取绘图表面 Graphics g e.Graphics; // 定义字体和画刷 Font titleFont new Font(黑体, 16, FontStyle.Bold); Font contentFont new Font(宋体, 10); SolidBrush blackBrush new SolidBrush(Color.Black); // 在指定坐标绘制文本 g.DrawString(销售单, titleFont, blackBrush, 300, 50); g.DrawString(订单号: 20231027001, contentFont, blackBrush, 100, 120); g.DrawString(商品名称: 笔记本电脑, contentFont, blackBrush, 100, 150); g.DrawString(数量: 1, contentFont, blackBrush, 100, 180); g.DrawString(单价: 6, 999.00, contentFont, blackBrush, 100, 210); // 绘制一条分隔线 Pen blackPen new Pen(Color.Black, 1); g.DrawLine(blackPen, 80, 250, 520, 250); // 设置HasMorePages为false表示只有一页 e.HasMorePages false; // 重要释放非托管资源在真实项目中应考虑使用using语句 titleFont.Dispose(); contentFont.Dispose(); blackBrush.Dispose(); blackPen.Dispose(); } }这段代码勾勒出了PrintDocument打印的基本骨架。有几个关键点需要特别注意坐标系统DrawString和DrawLine等方法使用的坐标原点(0,0)位于打印页面的可打印区域的左上角而非物理纸张的左上角。这个可打印区域由打印机驱动决定通常四周会留有白边。单位默认情况下Graphics对象的度量单位是百分之一英寸即1个单位0.01英寸。这是针式打印机编程中最常用的单位因为它与打印机的物理步进电机移动距离有较好的对应关系。资源管理Font,Pen,Brush等GDI对象是非托管资源必须及时释放。在上面的示例中我们在方法末尾进行了释放。但在更复杂的打印任务中建议使用using语句块来确保资源被正确释放即使发生异常。// 更安全的资源管理方式 using (Font titleFont new Font(黑体, 16, FontStyle.Bold)) using (SolidBrush brush new SolidBrush(Color.Black)) { g.DrawString(标题, titleFont, brush, 100, 100); }3. 高级技巧应对针式打印机的特殊需求针式打印机与常见的激光或喷墨打印机在工作原理上有本质区别。它通过撞针击打色带在纸张上形成点阵常用于打印多联压感纸。因此在编程时需要特别关注以下几点。3.1 精确控制纸张与走纸针式打印机通常使用连续纸或单页穿孔纸。PrintDocument的PageSettings提供了强大的纸张控制能力。private void ConfigurePageSettings(PrintDocument pd) { PageSettings ps pd.DefaultPageSettings; // 关键设置自定义纸张大小。这是精确控制的基础。 // PaperSize构造参数纸张名称宽度高度单位百分之一英寸 // 例如常见的80mm3.15英寸宽卷纸高度设为固定值或根据内容动态计算 PaperSize customSize new PaperSize(My 80mm Paper, 315, 0); // 高度为0表示不限制不需要具体值。 // 实际上对于连续纸我们常设置一个足够大的高度然后通过内容控制实际打印范围。 customSize new PaperSize(Continuous Paper, 315, 2000); // 宽3.15英寸高20英寸 ps.PaperSize customSize; ps.Landscape false; // 纵向打印 // 调整页边距。对于票据打印通常需要非常小的边距甚至为0。 // 注意Margins的单位也是百分之一英寸。 ps.Margins new Margins(20, 20, 10, 10); // 左、右、上、下边距 }注意并非所有打印机驱动都支持任意自定义纸张尺寸。如果设置后打印异常可能需要先在Windows的“设备和打印机”中手动创建并安装一个自定义纸张规格然后在代码中通过名称PaperSize.PaperName来引用它。3.2 打印多页内容与分页控制打印长内容时需要在PrintPage事件中手动处理分页。PrintPageEventArgs参数的HasMorePages属性是控制分页的关键。private Liststring printItems new Liststring(); // 假设这是要打印的长列表 private int currentItemIndex 0; private const int ItemsPerPage 20; private void PrintMultiPageHandler(object sender, PrintPageEventArgs e) { Graphics g e.Graphics; Font itemFont new Font(宋体, 9); float yPos e.MarginBounds.Top; // 从页面上边距开始 float lineHeight itemFont.GetHeight(g); int itemsPrinted 0; while (currentItemIndex printItems.Count itemsPrinted ItemsPerPage) { string itemText ${currentItemIndex 1}. {printItems[currentItemIndex]}; g.DrawString(itemText, itemFont, Brushes.Black, e.MarginBounds.Left, yPos); yPos lineHeight; currentItemIndex; itemsPrinted; } // 判断是否还有更多内容需要打印 if (currentItemIndex printItems.Count) { e.HasMorePages true; // 告诉PrintDocument还有下一页 } else { e.HasMorePages false; // 打印结束 // 重置索引以便下次打印 currentItemIndex 0; } }在开始打印作业前需要将printItems列表填充好数据并将PrintPage事件指向PrintMultiPageHandler。PrintDocument会反复触发PrintPage事件直到HasMorePages被设置为false。3.3 绘制条形码与简单图形针式打印机虽然分辨率较低但打印一维条形码如Code 39, Code 128是完全可以的。我们可以使用开源库如BarcodeLib生成条形码图像然后将其绘制到打印页面上。首先通过NuGet安装BarcodeLib包。然后在打印事件中集成条形码生成。using BarcodeLib; // 引入BarcodeLib命名空间 private void PrintWithBarcodeHandler(object sender, PrintPageEventArgs e) { Graphics g e.Graphics; // 1. 生成条形码图像 Barcode barcode new Barcode(); barcode.IncludeLabel true; // 是否在条形码下方显示编码文本 barcode.Alignment AlignmentPositions.CENTER; Image barcodeImage barcode.Encode(TYPE.CODE128, ABC123456, Color.Black, Color.White, 300, 100); // 宽度300高度100 // 2. 将条形码图像绘制到打印页面指定位置 g.DrawImage(barcodeImage, 100, 200); // 3. 释放图像资源 barcodeImage.Dispose(); // ... 绘制其他文本内容 g.DrawString(产品编码: ABC123456, new Font(宋体, 10), Brushes.Black, 100, 320); }对于简单的线条和矩形可以直接使用Graphics的DrawLine和DrawRectangle方法这对于绘制表格边框非常有用。4. 实战构建一个健壮的票据打印模块现在我们将前面所有的知识点整合起来设计一个用于打印销售票据的、相对健壮的类。这个类需要考虑错误处理、打印状态反馈以及配置的灵活性。using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Printing; using System.Text; public class ReceiptPrinter { private PrintDocument _printDoc; private ReceiptData _currentReceipt; private int _currentDetailIndex; private const float DETAIL_LINE_HEIGHT 25f; public class ReceiptData { public string ShopName { get; set; } public string ReceiptNo { get; set; } public DateTime DateTime { get; set; } public ListSaleItem Items { get; set; } public decimal TotalAmount { get; set; } public string Cashier { get; set; } } public class SaleItem { public string Name { get; set; } public int Quantity { get; set; } public decimal UnitPrice { get; set; } public decimal SubTotal Quantity * UnitPrice; } public ReceiptPrinter(string printerName) { _printDoc new PrintDocument(); _printDoc.PrinterSettings.PrinterName printerName; _printDoc.PrintPage OnPrintPage; // 配置纸张假设使用80mm宽度的票据纸 PaperSize ps new PaperSize(Receipt 80mm, 315, 0); // 高度通常由内容决定 _printDoc.DefaultPageSettings.PaperSize ps; _printDoc.DefaultPageSettings.Margins new Margins(15, 15, 20, 20); } public bool PrintReceipt(ReceiptData receipt) { if (!_printDoc.PrinterSettings.IsValid) { OnPrintError?.Invoke($指定的打印机 {_printDoc.PrinterSettings.PrinterName} 无效或不可用。); return false; } _currentReceipt receipt; _currentDetailIndex 0; try { _printDoc.Print(); return true; } catch (InvalidPrinterException ex) { OnPrintError?.Invoke($打印机异常: {ex.Message}); return false; } catch (Exception ex) { OnPrintError?.Invoke($打印过程出错: {ex.Message}); return false; } } public event Actionstring OnPrintError; public event Actionstring OnPrintStatus; private void OnPrintPage(object sender, PrintPageEventArgs e) { Graphics g e.Graphics; float currentY e.MarginBounds.Top; using (Font headerFont new Font(黑体, 14, FontStyle.Bold)) using (Font normalFont new Font(宋体, 10)) using (Font smallFont new Font(宋体, 9)) using (SolidBrush brush new SolidBrush(Color.Black)) using (Pen dashPen new Pen(Color.Black, 0.5f) { DashStyle System.Drawing.Drawing2D.DashStyle.Dash }) { // 1. 打印抬头 g.DrawString(_currentReceipt.ShopName, headerFont, brush, e.MarginBounds.Left, currentY); currentY headerFont.GetHeight(g) 10; // 2. 打印单据号和日期 string headerInfo $单据号: {_currentReceipt.ReceiptNo} 时间: {_currentReceipt.DateTime:yyyy-MM-dd HH:mm}; g.DrawString(headerInfo, normalFont, brush, e.MarginBounds.Left, currentY); currentY normalFont.GetHeight(g) 15; // 3. 绘制表头分隔线 g.DrawLine(Pens.Black, e.MarginBounds.Left, currentY, e.MarginBounds.Right, currentY); currentY 10; // 4. 打印商品明细表头 string detailHeader string.Format({0,-20}{1,8}{2,12}{3,12}, 商品名称, 数量, 单价, 小计); g.DrawString(detailHeader, normalFont, brush, e.MarginBounds.Left, currentY); currentY normalFont.GetHeight(g) 5; g.DrawLine(dashPen, e.MarginBounds.Left, currentY, e.MarginBounds.Right, currentY); currentY 10; // 5. 循环打印商品明细处理分页 int itemsPrintedThisPage 0; const int maxItemsPerPage 8; while (_currentDetailIndex _currentReceipt.Items.Count itemsPrintedThisPage maxItemsPerPage) { SaleItem item _currentReceipt.Items[_currentDetailIndex]; string detailLine string.Format({0,-20}{1,8}{2,12:C}{3,12:C}, TruncateString(item.Name, 20), item.Quantity, item.UnitPrice, item.SubTotal); g.DrawString(detailLine, smallFont, brush, e.MarginBounds.Left, currentY); currentY DETAIL_LINE_HEIGHT; _currentDetailIndex; itemsPrintedThisPage; } // 6. 判断是否还有更多明细需要打印分页逻辑 if (_currentDetailIndex _currentReceipt.Items.Count) { e.HasMorePages true; return; // 退出当前页处理下一页会再次触发此事件 } // 7. 打印总计和收银员信息只在最后一页打印 currentY 10; g.DrawLine(Pens.Black, e.MarginBounds.Left, currentY, e.MarginBounds.Right, currentY); currentY 15; string totalLine $合计金额: {_currentReceipt.TotalAmount:C}; g.DrawString(totalLine, new Font(宋体, 11, FontStyle.Bold), brush, e.MarginBounds.Right - 150, currentY); currentY 25; g.DrawString($收银员: {_currentReceipt.Cashier}, normalFont, brush, e.MarginBounds.Left, currentY); currentY 20; // 8. 打印结束语 g.DrawString(谢谢惠顾欢迎再次光临, normalFont, brush, e.MarginBounds.Left 50, currentY); e.HasMorePages false; // 所有内容打印完毕 } } private string TruncateString(string input, int maxLength) { if (string.IsNullOrEmpty(input) || input.Length maxLength) return input; return input.Substring(0, maxLength - 2) ..; } }这个ReceiptPrinter类封装了完整的票据打印逻辑。使用时只需构造数据并调用PrintReceipt方法// 使用示例 var printer new ReceiptPrinter(EPSON LQ-630K); printer.OnPrintError (msg) Console.WriteLine($[错误] {msg}); printer.OnPrintStatus (msg) Console.WriteLine($[状态] {msg}); var receiptData new ReceiptPrinter.ReceiptData { ShopName XX科技旗舰店, ReceiptNo XS20231027001, DateTime DateTime.Now, Cashier 张三, TotalAmount 6999.00m, Items new ListReceiptPrinter.SaleItem { new ReceiptPrinter.SaleItem { Name 高性能笔记本电脑, Quantity 1, UnitPrice 6999.00m }, new ReceiptPrinter.SaleItem { Name 无线鼠标, Quantity 1, UnitPrice 89.00m }, // ... 更多商品 } }; bool success printer.PrintReceipt(receiptData);通过这样的封装我们将复杂的打印逻辑、分页处理、错误处理都内聚在一个类中业务代码只需关注数据的准备实现了清晰的责任分离。在实际项目中你还可以进一步扩展这个类例如添加打印预览功能、支持不同的票据模板、或者集成更复杂的条形码和二维码生成库。掌握PrintDocument直接控制针式打印机的技术就像从手动挡汽车换到了自动挡你获得了对打印流程的完全掌控力。从简单的文本定位到复杂的分页报表从静态内容到动态生成的条形码这套方案都能游刃有余。更重要的是它让你的应用程序摆脱了对特定办公软件的依赖部署和运行变得更加清爽和稳定。下次当你面对那些需要精确对齐的票据或标签时不妨直接拿起PrintDocument这把利器在代码的画布上亲手绘制出每一份完美的打印件。