C#手写二维码条形码生成打印软件:源码详解与实战排错 扫码枪“嘀”一声货就入账了贴标机“啪”一贴包裹就上路了。二维码和条形码早就渗透到仓库、超市、医院、行政办公的每一个角落但真到自己动手做一套生成打印工具时你会发现市面上的在线生成器要么满屏广告和水印要么图片分辨率低得离谱打到不干胶上扫码枪根本不认。所以我用 C# 手写了一套二维码条形码生成打印软件界面干净、离线可用、分辨率可控、支持批量打印源码完整分享出来适合要用 C# 做上位机、做资产标签、做仓储打码工具的朋友直接改着用。这套源码并不复杂核心只有三件事二维码生成、条形码生成、调用系统打印。但它把我在实际生产环境里踩过的坑——打印模糊、扫码不识别、中文字符乱码、批量打印串页——几乎都填平了。这篇文章就把它的设计思路、关键代码和排错经验一次讲透看完你完全可以自己拉一个项目复现出来。1. 先盘需求为什么放着现成的在线工具不用偏要自己写一套1.1 在线工具看着方便真到了批量打印环节全是坑先说结论临时生成一两个二维码在线工具完全够用一旦涉及批量打印、格式统一、数据敏感这三个场景在线工具就是灾难。我之前在某工厂做仓储系统配套每天要打上千张物料标签。在线工具的问题不是“不好用”而是根本没考虑过生产环境第一生成的二维码图片默认只有两三百像素在屏幕上放大看很清晰但打印到 50mm×50mm 的标签上条码边缘全是锯齿扫码枪扫几次才能识别一次。第二免费版普遍带水印去掉水印要付费而且生成的数据要经过别人服务器产品批次号、序列号这些信息就这么走了一遍第三方接口合规性上就说不过去。第三批量生成几百张标签时在线工具要么限次数要么要手动一张张下载效率低到没法用。自己写一套就完全不一样。数据不出本地分辨率想设多高就多高生成逻辑可以嵌进上位机程序里PLC 或者扫码枪读到工单号后直接调用生成接口整个过程完全离线、完全自动化。这套源码本质上不是“一个工具”而是一个可以被集成的组件层。1.2 技术选型WinForms ZXing.Net PrintDocument为什么是这三样市面上生成二维码的 C# 库不少我也不是没对比过最终留下的组合是 WinForms 做界面、ZXing.Net 做编解码、PrintDocument 做打印。三个选型理由都很实际方案支持的码制维护状态适用场景ZXing.NetQR、Code 128、EAN、DataMatrix 等几十种活跃持续更新二维码条形码通吃推荐QRCoder仅二维码维护一般只做二维码且要求极简BarcodeLib一维码为主很久不更新老项目维护ZXing.Net 最打动我的一点是它把二维码和一维码统一在了同一套 API 下面BarcodeWriter只管写BarcodeFormat指定码制后面接哪种码都顺手。QRCoder 虽然二维码生成也很方便但条形码还得再找一个库对接格式和参数风格都不一样徒增维护成本。界面用 WinForms 而不是 WPF原因更直白这类小工具没什么复杂动画和自定义模板WinForms 启动快、内存占用小、部署时只需要 .NET Framework 或 .NET 6/8 运行时产线上的老工控机配置再差也跑得动。打印模块直接用 .NET 自带的PrintDocument配合PrintPreviewDialog可以零成本做预览不需要引入任何第三方打印组件。提示如果你以后要把这套生成逻辑嵌到 ASP.NET Core Web API 里也是可以的把生成代码抽成 Service 类即可和界面无关。2. 二维码生成参数控得好扫码枪才认账2.1 二维码不是一张“图片”而是一段二进制信息的可视化很多人以为二维码就是把文字转成图片其实它的本质是把一段二进制信息按规则铺在矩阵里再加上定位、纠错、掩码等附加信息。QR 码最基础的尺寸是 21×21 的模块矩阵这叫版本 1每往上升一个版本长宽各增加 4 个模块最多到版本 40 的 177×177。模块越多能塞进去的内容就越多但每个模块在物理尺寸不变的前提下会更小扫码枪识别难度也随之上升。这就是第一个要理解的关键点二维码的容量和可识别性是一对矛盾。生产环境中标签尺寸往往是固定的比如 50mm×50mm如果你想塞几百个字符进去二维码版本只能往上升每个模块连 0.3mm 都不到打印稍微糊一点、标签稍微脏一点整张码就废了。所以做标签的通用法则是“能存编号就不存网址能存短码就不存长串”。我在源码注释里特意写了这条设计原则二维码内容不是给人看的是给扫码设备看的短是美德。二维码的纠错能力来自 Reed-Solomon 纠错算法分为 L、M、Q、H 四档分别能恢复约 7%、15%、25%、30% 的码字。标签打印场景最怕的就是“被遮挡、被油污、打印浓度不够”这些都会直接破坏模块信息所以我一般默认用 Q重要物料标签直接上 H。纠错级别越高同一个内容需要的模块越多二维码尺寸会相应增大所以也不是无脑选最高。2.2 生成二维码的核心代码ZXing.Net 的完整用法先装依赖NuGet 里搜ZXing.Net或直接在包管理器控制台执行Install-Package ZXing.Net然后看生成二维码的核心方法using ZXing; using ZXing.QrCode; using ZXing.QrCode.Internal; public Bitmap CreateQrCode(string content, string charset UTF-8) { var writer new BarcodeWriter { Format BarcodeFormat.QR_CODE, Options new QrCodeEncodingOptions { CharacterSet charset, Width 600, // 生成位图的像素宽度 Height 600, // 生成位图的像素高度 Margin 4, // 静区宽度按模块数计算 ErrorCorrection ErrorCorrectionLevel.Q, PureBarcode false } }; return writer.Write(content); }这段代码看起来简单但每个参数都值得说道。Width和Height是生成位图的像素尺寸不是物理尺寸实际打印多大由打印阶段的 DPI 换算决定这部分在后面展开讲。CharacterSet设成UTF-8是中文内容的刚需不设的话用微信扫出来经常是乱码这是最容易踩的坑之一。ErrorCorrection我上面说了按场景选 Q 或 H。Margin在 ZXing 里是按模块数算的QR 码标准要求四周至少留 4 个模块宽的静区低于这个值扫码设备很容易找不到定位图案。PureBarcode设成false表示生成带人眼可读信息的完整二维码图片一般在二维码场景下没有文字信息设不设差别不大。但代码里保留这个属性是为了和条形码生成保持一致的 API 风格方便后续扩展。2.3 静区不足是“扫不出来”的第一大元凶写这段的时候我特意把静区单独拿出来说因为这个坑我一个人踩了不算后来帮朋友排查问题十个扫码失败的案例里至少有四个是静区不够。ZXing 的Margin虽然能设置静区但不同版本的库对它的解释略有差异有人设成 1 也能扫有人设成 4 还是不行实在不建议在这个参数上赌运气。我的做法是生成完二维码后再手动往外扩一圈白边把静区彻底锁死public static Bitmap AddQuietZone(Bitmap src, int quietZonePx) { var bmp new Bitmap( src.Width 2 * quietZonePx, src.Height 2 * quietZonePx, System.Drawing.Imaging.PixelFormat.Format32bppArgb); using (var g Graphics.FromImage(bmp)) { g.Clear(Color.White); g.DrawImage(src, quietZonePx, quietZonePx); } return bmp; }调用的时候quietZonePx取原图宽度的 10%~15% 即可我实测下来按原图宽度 1/8 左右扩白扫码率最稳定。这里补充一个直觉QR 码标准规定的静区是 4 个模块宽模块是二维码里最小的黑白方块。如果你生成的二维码是 600×600 像素、包含约 30 个模块那么 4 个模块大约就是 600/30×480 像素扩这么多白边就很稳。注意PrintPage 里绘制二维码时如果直接按图片原始大小去打印静区也会被缩放所以静区是在生成阶段加够不要在打印阶段指望它自动留白。3. 条形码生成Code 128、EAN-13 到底怎么选校验码怎么算3.1 条形码码制不是“随便挑一个”用错会被渠道拒收二维码和条形码虽然都是“码”但应用逻辑差异很大。二维码是矩阵码可以横竖两个方向存信息容量大条形码是一维的只能沿水平方向排列黑白条信息量小但打印和识别成本极低至今仍是零售和物流的主流。C# 做条码生成首先要面对的是一堆码制名词Code 128、Code 39、EAN-13、UPC-A、ITF-14。码制支持的字符长度典型场景Code 128全 ASCII含数字字母符号可变长密度高仓储、物流、内部标签Code 39数字大写字母部分符号可变长汽车/军工/资产管理EAN-1312 位数字校验位固定 13 位全球流通商品零售UPC-A11 位数字校验位固定 12 位北美零售商品ITF-1414 位数字固定外箱条码、物流包装选型逻辑其实很清晰商品条码走国际标准在中国零售渠道基本就是 EAN-13这个没有商量余地企业内部用的物料标签、资产标签、流转单据选 Code 128因为它的字符集最全、密度最高、同样信息量下条码最短如果客户的扫描系统很老只认 Code 39那就老老实实用 Code 39虽然它密度低但兼容性出奇地好。3.2 Code 128 的生成代码和校验逻辑ZXing 生成 Code 128 的代码模式和二维码类似只是Options换成了EncodingOptionsusing ZXing; using ZXing.Common; public Bitmap CreateCode128(string content) { var writer new BarcodeWriter { Format BarcodeFormat.CODE_128, Options new EncodingOptions { Width 600, Height 200, Margin 10, PureBarcode false } }; return writer.Write(content); }这里的Height不能设太低因为条码识别靠的是扫描光线穿过“条”和“空”的宽度变化扫描线是水平打的条码越矮能命中扫描线的概率越低。业内经验是打印出来的条码高度不低于 15mm我一般给到 20~25mm。Width也不是随便拍的过宽会让条码稀疏、浪费标签面积过窄会让条挤在一起难以识别。先在屏幕上预览让条码整体占标签宽度的 70% 左右是通用做法。Code 128 本身有 A、B、C 三个字符集A 是数字大写字母控制字符B 是大写小写C 是纯数字且密度最高。ZXing 会自动选择最优字符集这个不用担心。但校验码的计算原理还是建议了解一下因为排查“打出来的条码内容对不上”这类问题时要用Code 128 的校验码是把每个字符的值乘以其位置权重从 1 开始累加后对 103 取模。举一个简单例子如果内容前两个字符的值分别是 20 和 30那么校验值就是 (20×1 30×2) mod 103 80 mod 103 80。ZXing.Net 内部会自动计算并追加校验位所以源码里不需要自己处理。知道这个逻辑的意义在于当你在做数据校验时能判断收到的一维码内容是否被中间环节篡改过。3.3 EAN-13 校验位算法12 位和 13 位的坑EAN-13 是固定 13 位数字但你在输入时往往只需要给前 12 位第 13 位是校验位由算法自动算出。ZXing 会帮你完成这一步但如果你自己写死传了 13 位就会报“内容格式错误”的异常。校验位算法是标准的 EAN 加权算法从右往左奇数位权重 3偶数位权重 1所有加权和取 10 的补数。假设商品码前 12 位是690123456789从右往左逐位加权求和倒数第 1 位是 9权重 3倒数第 2 位是 8权重 1……依次类推最后算出的校验和假如是 40因为 40 已经是 10 的整数倍补数为 0校验位就是 0。这个公式我经常写成一个小函数放工具类里导出 Excel 时提前校验避免把错误编码发给印刷厂。PureBarcode这个参数在一维码场景下就很有用了。设成true生成的图片只有黑白条纹没有下面那排数字适合印刷在包装上的装饰场景设成falseZXing 会在条码下方自动绘制一行可读字符仓库作业人员人工核对时非常有用。文字字体如果要自定义别用宋体数字会被拉得很丑用 Arial 或 Consolas识别率和人工可读性都好很多。4. 打印模块实现从屏幕像素到打印机 DPI换算对了才清晰4.1 PrintDocument 三件套预览、打印设置、输出WinForms 打印的标配是三个类PrintDocument负责真正的输出PrintPreviewDialog负责预览PrintDialog负责让用户选打印机和份数。完整流程是先创建PrintDocument设置纸张大小订阅PrintPage事件然后预览或直接打印。我习惯把打印逻辑独立出来界面只负责传数据和触发using System.Drawing.Printing; PrintDocument pd new PrintDocument(); // 自定义标签尺寸单位是 1/100 英寸 // 70mm x 50mm 换算70/25.4*100 ≈ 27650/25.4*100 ≈ 197 pd.DefaultPageSettings.PaperSize new PaperSize(70x50, 276, 197); pd.DefaultPageSettings.Margins new Margins(0, 0, 0, 0); pd.PrintPage Pd_PrintPage; // 预览 PrintPreviewDialog preview new PrintPreviewDialog(); preview.Document pd; preview.ShowDialog(); // 直接打印 pd.Print();PrintPage事件里做的事只有一个把 Bitmap 画到打印 Graphics 上。private void Pd_PrintPage(object sender, PrintPageEventArgs e) { // 直接把绘图单位切换成毫米后面所有坐标都用毫米算 e.Graphics.PageUnit GraphicsUnit.Millimeter; Bitmap bmp _currentLabelBitmap; // 当前要打印的标签位图 float x 0, y 0, w 52, h 44; e.Graphics.DrawImage(bmp, new RectangleF(x, y, w, h)); }注意PaperSize的单位是百分之一英寸而PrintPage里设成GraphicsUnit.Millimeter后绘图的坐标和尺寸都可以直接用毫米。这两个单位不一样千万别混。Margins默认不一定为 0如果你打自定义标签不主动归零打印内容会整体偏移这个我在 6.2 节还会讲。4.2 DPI 换算为什么打印出来模糊又怎么彻底解决这是整套源码里最值钱的干货建议你仔细看。很多人打印二维码模糊第一反应是“图片不够大”然后疯狂把生成位图的像素从 300 调到 600、900打出来还是糊。其实问题不在图片像素多少而在 DPI 对不上。屏幕是 96 DPI打印机的物理分辨率通常是 300 DPI 或 600 DPI。你生成一张 300×300 像素的二维码图片在屏幕上显示约 79mm300/96×25.4看起来很合适但拿到 300 DPI 打印机上如果软件按图片原始尺寸输出实际打印宽度只有 25.4mm300/300×25.4被缩小到原来的 1/3。如果你拉伸到 52mm 打印GDI 会把 300 像素强行拉伸到 614 像素宽再输出中间再插值放大边缘自然就虚了。解决方向不是盲目的“加大像素”而是“按目标物理尺寸和打印机 DPI 反推像素数”。公式很简单// 目标物理宽度 52mm打印机 300dpi float targetWidthMm 52; int targetDpi 300; int targetPx (int)Math.Round(targetWidthMm / 25.4 * targetDpi); // 结果614 像素也就是说52mm 标签在 300 DPI 下需要约 614 像素宽的原图这时打印才是 1:1 像素对应不经过任何缩放黑白块的边缘才锐利。源码里我用了一个LabelSpec参数类把标签长宽毫米、打印机 DPI、静区像素这些参数统一管理生成和打印都从这里读改标签尺寸时只动一处不怕忘了两边同步。另外打印 Graphics 上的插值模式也会影响质量。默认InterpolationMode是Bilinear放大位图时边缘发虚改成HighQualityBicubic会好一点但最关键的还是上面说的像素匹配这个我实测下来是根治方案。还有两个小项值得顺手设置e.Graphics.InterpolationMode System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; e.Graphics.SmoothingMode System.Drawing.Drawing2D.SmoothingMode.HighQuality; e.Graphics.PixelOffsetMode System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;这三行放在PrintPage开头对二维码这种“纯硬边”图形的输出质量提升是肉眼可见的。4.3 批量打印HasMorePages 的正确用法批量打印的场景在仓库最常遇到一筐物料每筐一个批次号一次打 50 张标签。用PrintDocument做批量靠的是HasMorePages这个属性。private int _printIndex 0; private ListBitmap _printBitmaps new ListBitmap(); private void BeginBatchPrint(ListBitmap bitmaps, string printerName) { _printBitmaps bitmaps; _printIndex 0; PrintDocument pd new PrintDocument(); pd.PrinterSettings.PrinterName printerName; pd.PrintPage BatchPrintPage; pd.Print(); } private void BatchPrintPage(object sender, PrintPageEventArgs e) { e.Graphics.PageUnit GraphicsUnit.Millimeter; e.Graphics.DrawImage(_printBitmaps[_printIndex], 1, 1, 52, 44); _printIndex; e.HasMorePages _printIndex _printBitmaps.Count; }这里有个非常经典的 bugHasMorePages必须在PrintPage里赋值且每次Print()前必须把_printIndex重置为 0否则会出现上一次打印的索引残留——要么少打一页要么打出来全是最后一张图。我第一次写完测试时50 张标签打了 53 张其中重复了 3 张排查了半天才发现是索引没重置。批量打印的另一个细节是进度反馈。打印任务提交给 Windows 打印队列后程序端是拿不到“是否打完”的回调的所以不要用“打印完成”这种提示骗用户。我的做法是在EndPrint事件里弹一个“已提交 X 张到打印队列”的提示配合打印机的任务管理去查进度这样用户体验更诚实。5. 源码如何组织一个小工具也要考虑扩展5.1 项目分层界面和业务逻辑别揉在一坨很多初学者写这种小工具一个 Form 里从生成图片到打印全写完两三百行代码全塞在按钮点击事件里功能是实现了但后面想加一个“从 Excel 导入”就只能在 Form 里继续堆越堆越没法维护。这套源码里我做了简单但清晰的拆分QrBarPrint/ ├── Models/ │ └── LabelItem.cs // 标签数据模型内容码制尺寸 ├── Services/ │ ├── QrCodeService.cs // 二维码生成 │ ├── BarcodeService.cs // 一维码生成 │ ├── PrintService.cs // 打印与预览 │ └── QuietZoneHelper.cs // 静区补白工具 ├── MainForm.cs // 界面 └── Program.cs分层的好处是生成图片的逻辑完全不依赖 WinForms 控件将来如果要把生成接口接入 Web API 或者上位机服务直接把QrCodeService拿过去用就行不用复制粘贴代码。LabelItem是一个很轻的模型类包含文本内容、选择的码制、横向/纵向尺寸等作为生成和打印之间的数据契约。QrCodeService和BarcodeService我特意实现了同一个接口ICodeGeneratorpublic interface ICodeGenerator { Bitmap Generate(LabelItem item); BarcodeFormat Format { get; } }这样主界面上只需要根据 ComboBox 选中的码制去一个注册表里取对应的实现新增一种码制时不用改界面代码只要加一个实现类再注册进去即可。这种“面向接口”的习惯在小工具里看似小题大做等项目长大后就明白当初的收益了。5.2 可以怎么扩展CSV 批量导入、模板化、与上位机联动源码不是只能干瞪眼扩展方向我梳理了三个最实用的。第一个是 CSV 批量导入。仓库里的批次数据经常在 Excel 里维护导出成 CSV 后用OpenFileDialog读进来逐行创建LabelItem再调PrintService批量打印这一个功能就能撑起一个小型仓库的标签需求。解析 CSV 时注意处理逗号和引号转义别用简单的string.Split(,)我吃过这亏。第二个是模板化。把标签布局拆成“顶部 Logo 区、中间二维码区、底部文字区”三个区域用LabelTemplate类存每个区域的坐标和内容来源这样同一个二维码生成引擎可以适配不同客户的标签样式改起来只是调整几个矩形坐标不用动生成逻辑。第三个是跟上位机联动。很多做产线的朋友在用 C# 写上位机比如通过 OPC 或 CAN 总线从 PLC 拿到序列号、工单号这正好是二维码/条形码生成的天然上游。把本项目里的QrCodeService和PrintService编译成类库上位机收到数据后直接调用实现“读到数据即出码”不需要人工介入这在追溯系统里几乎是刚需。另外如果印刷厂要求矢量图可以考虑将来用 PdfSharp 或 SkiaSharp 把二维码画成矢量路径而不是输出位图。ZXing 本身也提供了BarcodeWriterGeneric可以直接拿到矩阵数据自己逐模块绘制成矢量矩形这个方向适合有精力的朋友深入研究。6. 实战中踩过的坑与排查清单6.1 扫码扫不出来先别怪打印机查这四个点扫码失败的原因五花八门但绝大多数逃不出下面这个清单。我建议排查时按顺序检查不要一上来就怀疑打印机坏。排查点检查内容解决方向静区二维码四周白边是否足够用AddQuietZone补白至少留 4 模块纠错等级标签可能被污染或打印偏淡升级到 Q 或 H打印尺寸物理尺寸是否过小一般不小于 15mm×15mm对比度碳粉/热敏纸是否发灰调高打印浓度换高对比度标签纸其中“打印尺寸过小”特别容易出现在换标签纸的时候。原来用 60mm×40mm 的标签做得好好的换成 40mm×30mm 后二维码跟着变小一圈扫码枪就开始挑角度了。这种问题用 App 里的扫码测试功能看模块数就能定位模块密集到一坨黑时直接缩小内容长度或升级纠错等级都救不回来唯一可靠的办法是给二维码分配更多物理面积。6.2 打印标签的经典翻车现场纸张尺寸、偏移、裁切打印模块最常见的翻车是“预览正常实际打印偏移”。原因九成是DefaultPageSettings.Margins没归零。WinForms 的PrintDocument默认边距不一定为 0有些驱动甚至会给 PaperSize 再加内边距。自定义标签打印时我建议把Margins显式设成 0然后再按标签上的实际绘制区域去排布内容而不是依赖系统默认边距。还有一类问题出在“打印机驱动的工作模式”上。像 TSC、Zebra 这类工业条码打印机驱动可能同时提供 Windows 驱动模式和指令模式如 TSPL、ZPL。PrintDocument只能走 Windows 驱动模式如果你在驱动设置里选成了纯指令模式打印任务会提交成功但打印机毫无反应。遇到“任务进队列但机器不动”先检查驱动模式选的是不是 Windows 打印机驱动。纸张被裁切也很常见。自定义 PaperSize 是 276×197百分之一英寸但打印机驱动的默认纸张列表里没有这个尺寸Windows 会把内容按最近的纸张类型输出右边和下边直接切掉。解决方式是在打印前主动调用pd.PrinterSettings检查是否支持自定义纸型不支持就在驱动里添加一个同名自定义纸张。6.3 几个典型异常和处理口诀写这套源码过程中我整理了几个高频异常都是“一看报错就觉得难实际原因特别简单”的货。异常信息根因处理方式“参数无效”Bitmap 被提前 Dispose或 Graphics 在 PrintPage 之后还在用不要用 using 包住要传进打印事件的 Bitmap“文件正由另一进程使用因此该进程无法访问此文件”保存 PNG 时 Bitmap 还占着文件句柄用 MemoryStream 中转或先 Dispose 再保存“值不在预期的范围内”EAN-13 传了 13 位Code 128 内容含中文EAN-13 只传 12 位一维码内容保持 ASCIIPreview 显示空白生成代码抛了异常但没有 UI 提示在生成按钮事件外层加 try-catch先看具体异常文本“参数无效”这个问题我印象最深。当初我把生成好的 Bitmap 放在using块里函数返回后 Bitmap 已经被释放PrintPage 里DrawImage一执行就崩。很多人遇到这个异常会以为是打印参数不对其实只是生命周期问题。记住打印事件的 Graphics 是事件参数提供的页面渲染完由系统回收但你的源 Bitmap 要活得比 PrintPage 长。最后再补充一个压箱底的小技巧正式批量打印前先打一张校准页——页面上画一个 50mm 见方的黑色定位框和一把毫米尺用尺子量一下实际打印尺寸和设置尺寸是否一致。这一步只要 30 秒但能帮你避免整卷标签纸全部打废的惨剧。我后来把这个校准页也放进了源码的工具菜单里每次更换标签纸规格时都会跑一遍。这套工具从最初的“临时用一下”到后来成了仓库和产线上离不了的小部件前前后后改了很多轮。二维码也好条形码也好本质都是数据和现实物品之间的桥梁把这座桥修得可靠比折腾再花哨的功能都更有价值。