十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C#入库标签打印系统实战:从WinForms到Dapper的完整实现

C#入库标签打印系统实战:从WinForms到Dapper的完整实现 接到这个需求时我第一反应是入库标签打印不就是打印个标签嘛真做起来才知道这里面的坑一点不比写一套业务管理系统少。你要处理的不是一个“打印功能”而是一整条和入库流程绑定的数据链路——物料信息从哪来、批次号怎么生成、二维码里放什么、打印机为什么偏了、序列号怎么保证不重复这些环节拆开来看每一个都值得单独琢磨。我这两年用C#做过几套类似的入库标签打印系统从单机工具到对接WMS的客户端都碰过。这篇文章不打算讲那种“Hello World”级别的入门而是把从需求分析、技术选型、数据库设计、打印实现到上线维护的完整过程摊开来说。如果你正在做仓储、生产制造相关的MES/WMS系统或者你的项目里恰好需要和条码打印机打交道这篇文章可以直接当参考用。1. 需求先理清入库标签到底要干什么很多人上来就问“标签打印用哪个库”“条码怎么生成”其实这些问题都要往后放。入库标签打印系统最容易被低估的就是需求本身。1.1 入库流程里的标签位置先看一个典型的入库流程采购到货 - 质检 - 库管录入入库单 - 系统生成标签 - 打印标签 - 粘贴到物料/托盘 - 扫码上架标签在这条链路里不是最后一步而是“实物”和“信息系统”之间的连接点。物料到货之后如果没贴标签后面上架、出库、盘点全都靠人眼识别稍微一乱就失控。系统要做的也不只是打印一张纸出来而是确保标签上的数据和数据库里的入库单、批次、数量完全一致并且后续扫码能反查到这个实物对应的所有业务数据。所以我在设计系统边界时通常会画两条线入库单管理是上游库存管理是下游标签打印系统只负责“根据入库单生成符合规则的实物标识”。这条边界一确定很多需求就好谈了——你要不要管入库单状态要不要管库存数量都不用你只负责把标签这块做精。1.2 一张入库标签上该放哪些内容标签内容不是越多越好要考虑标签纸尺寸和可读性。通常一张标准入库标签会包含这些字段字段作用是否需要条码/二维码物料编码唯一标识物料强烈建议物料名称人工识别否入库单号关联入库单据建议批次号质量追溯关键强烈建议生产日期/到货日期效期管理否入库数量校验与上架否供应商来源追溯否二维码内容扫码快速解析全部关键信息是这里我想多提醒一句二维码内容不要随意拼。我见过有人直接把“物料编码批次号数量”用逗号拼在一起扔进二维码当时能用后来换了扫码系统或者要对接别的系统解析规则就乱了。我常用的做法是定一个简单的键值对格式比如MMAT001|B20250115|D2025-01-10|Q500这种格式即使将来系统换了用通用的解析逻辑也能拆出来不会绑定在某一家厂商的编码方案上。1.3 打印设备和标签纸的选型标签最终是打在实物上的所以硬件选型很关键。市面上的条码打印机里工业场景用Zebra和TSC比较多。打印方式有热敏和热转印两种热敏不用碳带成本低但热敏纸遇热、遇光、遇油容易变色适合短期周转的物料。热转印用碳带把蜡基/树脂基墨转印到标签上耐刮擦、耐溶剂适合长期保存和恶劣环境。入库标签如果要跟着物料放很久建议热转印。在软件方案上无非三条路用BarTender这类标签设计软件做好模板C#调用它的接口或命令行去打印。系统里用GDI绘制标签再通过Windows打印机驱动打印。直接用打印机支持的指令语言比如ZPL、TSPL向打印机发送文本指令。我的建议是追求排版简单、维护交给业务人员选BarTender追求快速集成、动态内容多选GDI驱动打印如果打印机网络环境很稳定、效率要求极高选ZPL。后面第三章我会重点讲GDI的做法因为它在多数Windows环境下兼容性最好也不容易遇到字体缺失的问题。2. 技术选型这些C#技术点组合起来刚刚好既然标题是C#入库标签打印系统那技术栈就围绕C#来。这里我不会用很重的框架够用、稳定、好维护是第一原则。2.1 WinForms还是WPF这个问题被问过太多次了。如果这是一个运行在工厂/仓库老电脑上的工具型系统我一般选WinForms。原因很实在开发快、部署简单、对低配机器和旧系统兼容性好我甚至遇到过Windows 7只装了.NET Framework 4.0的工控机WinForms在这种环境里几乎没有兼容性压力。如果项目是全新的界面交互有较高要求比如需要数据绑定、MVVM、异步UI更新选WPF会更舒服。热词里也经常看到“wpf c#”说明现在很多上位机和新版桌面应用都在转向WPF。但我的核心观点是界面框架不要成为标签打印模块的限制。我会把打印、条码生成、数据库操作拆成独立的类库这样不管外面包的是WinForms还是WPF核心逻辑都能直接复用。2.2 数据访问层用Dapper而不是EF Core入库标签系统本身数据量不大但打印时对数据库的读写频率可能很高尤其是多台电脑同时打印的时候。EF Core虽然功能全但在这种工具型系统里有点重而且有时候生成的SQL不一定可控。我习惯用Dapper。它是个轻量级ORM性能好SQL写出来自己心里有底。你只需要写原生SQLDapper帮你做映射写起来也很接近ADO.NET排查问题特别方便。热词里不是有一条“c# dapper超全详细使用教程”嘛确实Dapper在中小型系统里非常好用。如果企业已经有中心数据库那你可以直接用SQL Server或者MySQL如果只是单机跑我经常用SQLite一个文件搞定部署时不用装数据库服务。Dapper对这两种数据库都支持得很好。2.3 条码与二维码生成条码/二维码生成这块我用得最多的是ZXing.Net。它是Java版ZXing的C#移植NuGet上直接装使用简单支持Code128、QR Code等常用格式。有几个细节要注意条码内容不建议放中文尤其是Code128对字符集有限制中文会导致编码复杂且扫码容易出错。物料编码、批次号这些尽量用字母数字。二维码内容可以放中文但生成时务必统一用UTF-8否则扫码出来是乱码。条码/二维码本质上只是“数据的图形化展示”真正要追溯的数据必须入库。很多人以为标签打印出来就完事了结果扫码查不到数据就是因为数据只存在于图片里没落库。2.4 打印实现路线用C#实现打印最稳妥的是System.Drawing.Printing.PrintDocument配合GDI。你先把标签画成一张图然后再交给打印机驱动输出。这样做的好处是不依赖特定打印机的指令集普通Windows驱动的打印机都能用而且标签布局用代码就能控制得很精确。如果对打印速度有极端要求比如一秒钟要打三五张可以考虑走ZPL指令直接发给Zebra打印机。ZPL生成文本很小传输快但调试起来不如GDI直观而且打印机换了型号指令集可能不兼容。我在实际项目里通常这样取舍优先用GDI驱动打印如果客户明确说“用的Zebra又不换”再考虑ZPL。这也是很多上位机项目的通用做法。3. 从数据库到标签完整实现过程这一章我会把一套最小可用的入库标签打印系统实现过程拆开讲包括数据库表、序列号生成、标签绘制、打印调用。代码不会很长但每个关键点我都会给出来。3.1 数据库设计设计三个表入库单、入库单明细、标签打印记录。入库单关联供应商和日期明细表存物料和数量标签表存每一次打印的标签号一个被反复打印的标签也会在这里留下完整历史。CREATE TABLE InboundOrder ( Id INTEGER PRIMARY KEY AUTOINCREMENT, OrderNo TEXT NOT NULL, SupplierName TEXT, InboundDate TEXT NOT NULL, Status INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE InboundOrderItem ( Id INTEGER PRIMARY KEY AUTOINCREMENT, OrderId INTEGER NOT NULL, MaterialCode TEXT NOT NULL, MaterialName TEXT NOT NULL, Quantity INTEGER NOT NULL ); CREATE TABLE StockLabel ( Id INTEGER PRIMARY KEY AUTOINCREMENT, LabelNo TEXT NOT NULL UNIQUE, OrderItemId INTEGER NOT NULL, MaterialCode TEXT NOT NULL, BatchNo TEXT NOT NULL, Quantity INTEGER NOT NULL, PrintCount INTEGER NOT NULL DEFAULT 1, FirstPrintTime TEXT NOT NULL, LastPrintTime TEXT NOT NULL );注意StockLabel.LabelNo必须加唯一索引这是防止重复标签的最后一道防线。哪怕代码里有并发漏洞数据库层也能拦住。3.2 序列号生成不重复是底线入库单号和标签号都需要连续编号。这个“连续”并不是说不允许有空洞而是不能重复。很多人喜欢在代码里用DateTime.Now.ToString(yyyyMMddHHmmss)做编号在一秒内多次打印就可能撞上而且生成出来的编号还很长。我常用的方案是独立序列号表加事务。拿SQLite为例可以这样设计CREATE TABLE Sequence ( SeqName TEXT PRIMARY KEY, CurrentValue INTEGER NOT NULL );插入一个入库单号时在事务里取号并递增using var conn new SqliteConnection(Data Sourcelabel.db); conn.Open(); using var tx conn.BeginTransaction(); var nextNo conn.QuerySingleint( UPDATE Sequence SET CurrentValue CurrentValue 1 WHERE SeqName InboundNo RETURNING CurrentValue, transaction: tx); var orderNo IN DateTime.Now.ToString(yyyyMMdd) nextNo.ToString(D4); // 插入入库单业务表 conn.Execute( INSERT INTO InboundOrder(OrderNo, SupplierName, InboundDate, Status) VALUES(OrderNo, SupplierName, InboundDate, 0), new { OrderNo orderNo, SupplierName XX供应商, InboundDate DateTime.Now.ToString(yyyy-MM-dd) }, transaction: tx); tx.Commit();SQL Server的写法略有不同用OUTPUT INSERTED.CurrentValue代替RETURNING。多线程并发时数据库的行锁会保证取号不重复所以这种方案在生产环境实测下来是很稳的。3.3 用C#绘制标签并打印核心是PrintDocument。首先在窗体或服务里配置好打印机、纸张大小和边距然后在PrintPage事件里用GDI画内容。下面是一段简化的绘制示例生成了包含物料编码、名称、批次、数量、二维码的标签private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e) { var g e.Graphics; g.TextRenderingHint System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; // 假设标签宽度约 60mm高度约 40mm工位常用 float mm2px 100f / 25.4f; // 图形单位是 1/100 英寸 float width 60f * mm2px; float height 40f * mm2px; // 背景 g.FillRectangle(Brushes.White, 0, 0, width, height); // 字体 using var fontTitle new Font(Microsoft YaHei, 9, FontStyle.Bold); using var fontText new Font(Microsoft YaHei, 8, FontStyle.Regular); // 公司标题 g.DrawString(XX制造有限公司, fontTitle, Brushes.Black, 2, 2); // 物料信息 g.DrawString(物料编码 materialCode, fontText, Brushes.Black, 2, 18); g.DrawString(物料名称 materialName, fontText, Brushes.Black, 2, 32); g.DrawString(批次号 batchNo, fontText, Brushes.Black, 2, 46); g.DrawString(数量 quantity, fontText, Brushes.Black, 2, 60); // 生成二维码 var barcodeWriter new BarcodeWriterPixelData { Format BarcodeFormat.QR_CODE, Options new QrCodeEncodingOptions { Height 120, Width 120, Margin 0, CharacterSet UTF-8 } }; var pixelData barcodeWriter.Write(M materialCode |B batchNo |Q quantity); using var qrBitmap new Bitmap( pixelData.Width, pixelData.Height, PixelFormat.Format32bppRgb); // 将像素数据写入Bitmap var rect new Rectangle(0, 0, qrBitmap.Width, qrBitmap.Height); var bmpData qrBitmap.LockBits(rect, ImageLockMode.WriteOnly, qrBitmap.PixelFormat); Marshal.Copy(pixelData.Pixels, 0, bmpData.Scan0, pixelData.Pixels.Length); qrBitmap.UnlockBits(bmpData); g.DrawImage(qrBitmap, width - 125, 15, 120, 120); // 边框 g.DrawRectangle(Pens.Black, 0, 0, width - 2, height - 2); }这里我要重点提醒几个关键字如果标签内容需要精确到毫米用像素计算很痛苦所以最好统一用Graphics的PageUnit也就是把绘图单位设置成毫米或百分之一英寸。不然换一台DPI不同的打印机位置就可能整体偏移。g.PageUnit GraphicsUnit.Millimeter;设置成毫米之后前面的mm2px换算就可以去掉画图直接用毫米坐标直观多了。3.4 配置管理与多模板扩展标签打印系统最容易被业务方提出“改模板”的需求。如果把标签大小、边距、字体大小全写死在代码里每次调整都要改代码重新编译非常被动。我会建议在程序里增加一个简单的配置系统可以用appsettings.json存基础配置用一张LabelTemplate表存模板字段。基础配置大致是这样{ PrinterName: Zebra ZD421, PaperWidth: 60, PaperHeight: 40, MarginLeft: 2, MarginTop: 2, CompanyName: XX制造有限公司, LogoPath: C:\\label\\logo.png }模板表里存字段名、在标签上的X坐标、Y坐标、字体大小、是否加粗、是否换行。打印时按模板动态遍历字段这样业务人员改位置时只需要在维护界面里拖一拖或者改数字不需要改代码。这个设计在项目后期会给你省下大量时间。4. 实际踩过的坑排查问题的方法再完美的代码上了车间总会遇到各种问题。这里我把这些年常见的坑整理成速查表省得大家再走一遍弯路。4.1 打印位置偏移和内容被切这个现象太常见了。标签内容画得好好的打印出来整体往左偏或者内容被纸的边缘切掉。大部分原因是纸张尺寸设置和驱动里的不一致。Windows打印系统会把PrintDocument里设置的纸张尺寸和打印机驱动里的默认纸张尺寸做匹配如果不匹配就按驱动里的来于是你的内容就落在错误的位置上。我的解决步骤是打开打印机属性先把纸张尺寸设成和实际标签纸完全一致。在代码里设置e.PageSettings.PaperSize new PaperSize(Custom, 600, 400);这里的单位是1/100英寸。一定要把页边距清掉e.PageSettings.Margins new Margins(0, 0, 0, 0);。标签纸有间隙传感器打印机第一次使用要手动确认传感器识别正常。建议先打印一张纯色底纹的测试页看看内容是不是正好覆盖整张标签再逐步微调坐标。4.2 中文乱码如果你选择的是GDI驱动打印中文不会乱码因为Windows字体渲染已经处理好了。乱码通常发生在使用ZPL指令时。打印机的内置字库往往不支持中文或者只支持单一编码直接发UTF-8中文过去就会变成“口口口”或者乱码。有两种解决办法在ZPL里指定支持中文的字体但这依赖打印机型号和字库。干脆把中文内容先用GDI画成图片再用ZPL把图片传给打印机打印。所以我前面说能用GDI驱动打印就先用它中文乱码问题会消失大半。4.3 条码扫描枪读不出来或经常错读标签贴到实物上之后扫码枪扫不出或者偶尔扫错会直接影响上架效率。常见原因条码长度太长Code128清OKCode39就吃力了。标签内容过于密集条码没有足够的静区条码左右两侧的空白区域。打印头脏了或者碳带质量不好导致黑条和白色间隙对比度不足。二维码纠错等级太低稍微有一点污损就扫不出来。我的经验是条码内容尽量不要超过40个字符二维码纠错等级选M或H打印头每隔一段时间用清洁笔清理。如果发现扫描率下降先怀疑硬件而不是改代码。4.4 序列号重复和跳号序列号重复在单机程序里不容易发生但一旦做成多客户端同时打印就很容易踩雷。两个线程同时读到同一个当前值各自加1最后生成两个一样的编号或者业务操作时一条失败了但序列号已经消费掉导致后面出现空洞。我不建议完全消除跳号因为数据库事务回滚时自增ID本来就会留下空洞。只要“不重复”就是合格的。具体做法序列号表加事务保证取号过程原子化。标签表加唯一索引重复插入直接报错。打印重试时同一个标签应该沿用原来的标签号把PrintCount加1而不是重新生成一个新号否则客户会质疑“为什么两个标签不一样”。4.5 打印机突然不响应打印机USB松了、网线断了、驱动假死后代码里看PrintDocument还“打印成功”了但实际上什么也没打出来。这就是为什么我建议在客户端里主动检测打印队列状态。using System.Printing; var queue new LocalPrintServer().GetPrintQueue(printerName); if (!queue.IsOnline) { MessageBox.Show(打印机不在线请检查连接状态。); return; }如果打印机数量多可以做一个独立的“打印机状态监控”界面轮询所有打印机的状态和队列长度这对产线维护人员特别有用。4.6 用HttpClient对接WMS/ERP入库标签打印系统不是孤岛通常打完标签要把数据上报给WMS或ERP。如果公司有接口我一般写一个异步的上报服务using var httpClient new HttpClient(); httpClient.Timeout TimeSpan.FromSeconds(5); var payload new StringContent( JsonSerializer.Serialize(new { labelNo, materialCode, batchNo, quantity }), Encoding.UTF8, application/json); var response await httpClient.PostAsync(http://wms-server/api/label/confirm, payload); if (!response.IsSuccessStatusCode) { // 记录失败日志放入重试队列 logger.Error(标签上报失败标签号:{LabelNo}, labelNo); }注意超时和失败重试别因为上报失败就把打印给卡住。生产环境里网络抖动是常态宁可先打印成功再异步补偿上报也不要让打印流程被外网接口拖死。5. 进阶从单机标签工具到产线系统如果上面的功能都做完了你会发现它已经能应付大部分入库场景。但真正到了产线级别还有很多可以扩展的地方。5.1 接入扫码枪实现自动打印在仓库里库管人员往往希望用扫码枪扫一下入库单上的条码系统自动查询明细并打印出这一单的全部标签不需要手动敲键盘。我经常在WinForms的输入框里监听KeyDown事件当用户扫入一个带回车结尾的条码时自动触发打印private void txtScanner_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode Keys.Enter) { var orderNo txtScanner.Text.Trim(); LoadOrderAndPrint(orderNo); txtScanner.Clear(); e.SuppressKeyPress true; } }扫码枪本质上就是一个键盘设备这种方式实现最快也最不容易出问题。5.2 多客户端打印和打印服务分离如果同时有几台电脑在打标签最简单的方案是共用同一个数据库每台电脑本地各装一套客户端。这种方式部署方便但打印机配置和客户端更新会比较分散。更稳妥的做法是把“打印服务”抽成一个独立的Windows服务或者做一个打印网关。业务端只提交打印任务打印服务统一从队列取数据发送到指定的打印机。这样打印队列集中管理不同客户端的数据不会互相争抢同一台打印机后续也好扩展成“打印审批”“打印日志追溯”这些功能。当然这个看预算和复杂度。小的工厂两三个人操作没必要搞分布式。5.3 从标签到追溯数据才是核心资产做了入库标签之后你会发现后面的事情都顺了。扫码枪扫一下标签上的二维码就能查到这张物料的入库单号、供应商、批次、到货日期、打印次数。进一步还能把出库、领料、生产工单关联起来实现全流程追溯。这也是我一直在强调的一点标签打印系统的真正价值不在“打印”这两个字而在于它把每一件实物和一段完整的数据记录绑定在了一起。代码写得好不好最终要看这套绑定关系能不能经得起扫码枪和仓库现场的各种考验。最后再分享一个小技巧在调试标签排版时别一上来就用成品标签纸先用普通A4纸按相同尺寸打印拿尺子量一下边距确认没问题后再换标签纸。我因为图省事跳过这一步浪费过一整卷标签纸从那以后再也没有跳过。开发这种系统细节上多耐心一点上线之后你会少很多麻烦。
返回列表