WinForm批量二维码工具:参数、异步、命名与打印避坑
批量生成二维码这件事,最容易被低估的地方不是“怎么调用二维码库”,而是“怎么让一个 WinForm 小工具在真实数据、真实打印机、真实操作员手里稳定跑完几千条”。我这些年做过不少内部资产标签、设备铭牌、物料卡、巡检点标识的小工具,需求听起来都很像:导入一张表,按某一列批量生成二维码,保存成 PNG,最好还能直接打印。真正落地时,坑集中在参数、文件命名、异常数据、UI 卡死、打印模糊和重复覆盖上。这篇就围绕“winform关于批量生二维码的做法”展开,把我自己反复踩过的坑和现在比较稳定的实现方式拆开讲。适合手里有 C# 基础、需要在 WinForm 里做离线批量二维码工具的开发者,也适合不写代码但需要评估方案的实施人员。
1. 先把需求拆开:批量二维码不是循环调用生成函数
1.1 三条典型业务线决定工具形态
我习惯先问一句:这批二维码最终贴在哪里、给谁扫、扫出来要看到什么。答案不同,工具形态完全不一样。资产标签通常是“编号 + 名称 + 部门”,二维码内容短,重点是文件名能和资产编号对应,打印出来耐磨损;设备铭牌往往内容更短,但可能要求加 Logo、加边框、贴金属表面,纠错级别要偏高;物料卡和巡检点则更强调批量导入、批量命名、出错可追溯,因为一次可能几千条,不能生成完才发现中间有几十条是空值。把这三类场景混在一起做,界面会越来越臃肿,参数也会互相打架。
我的做法是先定“最小可用输入”。如果二维码内容只来自一列文本,那就支持 TXT 和 CSV 就够了;如果客户坚持用 Excel,再接 NPOI 或 ClosedXML 读单元格,但不要一上来就做复杂模板。输出侧也一样,先保证批量 PNG 能稳定落盘,再考虑 PDF 拼版、标签打印、自动发邮件。很多工具失败不是功能少,而是一开始就想把所有场景吃下,结果参数面板十几个选项,操作员根本不知道选哪个。
还有一个容易忽略的点:二维码内容不等于文件名。内容可能是ASSET-2024-0001|打印机|三楼,文件名却应该短、可排序、可检索,比如00001_ASSET-2024-0001_ab12cd34.png。内容负责被扫码读取,文件名负责被人和系统管理,这两件事要分开设计。后面讲文件命名时我会给一个可直接用的规则。
1.2 为什么在 WinForm 里做,而不是临时脚本
临时脚本当然快,几十行 Python 或 C# 控制台就能跑完。但批量二维码工具的使用者往往不是开发者,他们需要选文件、选输出目录、看进度、中途取消、生成完打开文件夹,最好还能直接打印。WinForm 的优势就在这里:离线、双击即用、文件对话框和打印 API 都是现成的,部署到内网电脑也不需要额外开浏览器或装服务。对于“一个月跑一次、一次几千张”的内部工具,WinForm 的投入产出比很合适。
但 WinForm 也有天然限制。生成二维码是 CPU 和磁盘 IO 混合操作,如果你在按钮事件里直接for循环,界面必然假死,操作员会以为程序崩了,然后连点几次按钮,最后多个任务同时写同一个目录。另一个限制是绘图资源,Bitmap、Graphics、Image这些对象不及时释放,跑几千张后 GDI 对象涨上去,轻则保存失败,重则整个进程异常。所以 WinForm 方案的重点不是“能不能生成”,而是“异步、可取消、资源可控、错误可追踪”。
技术栈上,我一般优先选 .NET 6 或 .NET 8 的 WinForm,代码更现代,File.ReadAllLinesAsync、Parallel.ForEachAsync都顺手;如果甲方环境锁死在 .NET Framework 4.8,也能做,只是异步文件 API 和并行写法要换一下。选型时不要只看自己电脑,要看目标机器有没有对应运行时,是否允许安装 NuGet 依赖,是否有杀毒软件拦截写文件。这些问题提前问清楚,比写完再改省事得多。
1.3 生成库选型:QRCoder、ZXing.Net 怎么选
纯生成二维码,我通常选 QRCoder。它是纯 C# 实现,MIT 协议,生成 PNG、SVG 都方便,API 简单,依赖少,特别适合 WinForm 内部工具。批量场景里我更愿意用它的PngByteQRCode直接拿byte[]写文件,绕开Bitmap和Graphics,GDI 泄漏风险小很多。需要中文内容时,创建二维码时把forceUtf8打开,避免不同扫码器对编码理解不一致。
如果项目还需要“生成后自动扫一遍验证”,或者要做二维码识别、解码、图片检测,那 ZXing.Net 更合适。它生成和解码都能做,但 API 相对重一些,包也更大。我的常见组合是:生产环境用 QRCoder 批量生成,测试或质检环节用 ZXing.Net 抽检解码。这样生成端轻,验证端独立,出问题容易定位。
| 方案 | 强项 | 弱项 | 适用场景 |
|---|---|---|---|
| QRCoder | 纯 C#、依赖少、PNG/SVG 方便、批量生成轻 | 不带解码识别 | WinForm 批量生成、标签导出 |
| ZXing.Net | 生成 + 解码、生态成熟、可做扫码验证 | API 偏多、包体相对大 | 需要自动质检、识别已有二维码 |
| 手写绘图 | 可控性看起来高 | 版本选择、纠错、掩码都要自己处理,极易出错 | 学习和实验,不建议生产 |
我不建议自己用Graphics画黑白格子来实现二维码。二维码的版本选择、纠错码、掩码、格式信息都有规范,手写很容易生成“看起来像二维码但扫不出来”的图。用成熟库把底层规范处理好,把精力放在批量流程、参数计算和异常处理上,才是划算的做法。
2. 二维码参数不是随便填:尺寸、纠错、内容长度互相牵制
2.1 先定内容,再定纠错级别
二维码的纠错级别常见有 L、M、Q、H,恢复能力大约对应 7%、15%、25%、30%。很多人一上来就选 H,觉得越高越好,但 H 会增加冗余,同样内容需要更多模块,二维码更密。标签尺寸固定时,模块变小,扫码反而更难。我的经验是:普通打印标签、内容较短,选 M 就够;贴在易磨损、油污、金属表面,或者中间要放小 Logo,选 Q 或 H;内容很长、标签又小,优先缩短内容,而不是硬上高纠错。
内容长度也要控制。二维码不是数据库,别把整段 JSON、长 URL、几十个字段都塞进去。能放编号就放编号,扫码后再用系统查详情。内容越长,版本越高,模块越多,打印出来越密。比如一个资产标签,内容用ASSET-2024-0001就够了,名称和部门可以印在二维码旁边给人看,不必全塞进码里。这样既好扫,也方便后期改内容规则。
| 纠错级别 | 大致恢复能力 | 模块密度 | 建议 |
|---|---|---|---|
| L | 约 7% | 最低 | 内容长、屏幕展示、环境干净 |
| M | 约 15% | 中等 | 默认推荐,普通标签够用 |
| Q | 约 25% | 较高 | 有轻微磨损、油污、需要更稳 |
| H | 约 30% | 最高 | 金属铭牌、加 Logo、强磨损环境 |
2.2 每模块像素与最终物理尺寸的计算
批量生成时最容易犯的错是:先生成一张 200×200 的图,再拉伸到标签大小。拉伸会插值,模块边缘变糊,扫码率直线下降。正确做法是先确定最终物理尺寸和打印分辨率,再反推每模块像素。计算公式不复杂:目标像素宽 ≈ 模块数 × 每模块像素。模块数可以从QRCodeData.ModuleMatrix.Count拿到,但不同库对静区处理略有差异,所以代码里最好以实际生成后的Width为准,或者先试算再微调。
举个实际例子。一个 40mm×30mm 的标签,用 203dpi 的标签打印机,40mm 对应约40 / 25.4 × 203 ≈ 320像素。假设二维码模块数是 25,那么每模块 12 像素就是 300 像素,放得下;每模块 13 像素就是 325 像素,可能超边。打印时模块物理边长建议不低于 0.4mm,0.5mm 更稳。203dpi 下 0.5mm 约等于 4 个打印点,所以每模块至少 4 像素是底线,实际标签上我通常留到 8 到 12 像素,扫描容错更好。
| 标签宽度 | 打印分辨率 | 可用像素宽 | 模块数示例 | 每模块像素 | 实际像素宽 |
|---|---|---|---|---|---|
| 30mm | 203dpi | 约 240 | 25 | 8 | 200 |
| 40mm | 203dpi | 约 320 | 25 | 12 | 300 |
| 50mm | 300dpi | 约 590 | 29 | 18 | 522 |
| 60mm | 300dpi | 约 708 | 33 | 20 | 660 |
注意:二维码生成后不要再做双线性缩放。需要固定画布时,用白色背景加边距,不要拉伸二维码本体。打印时如果必须缩放,至少用最近邻插值,并且先在小批量上实测扫码。
2.3 中文、换行、特殊字符和内容模板
中文内容不是不能编,关键是编码一致。QRCoder 创建二维码时可以设置forceUtf8: true,让内容按 UTF-8 处理,减少扫码端乱码。CSV 导入时也要注意编码,Excel 另存为“CSV UTF-8”通常最稳;如果直接读 ANSI CSV,中文可能变成问号。TXT 导入我一般要求 UTF-8,程序里用Encoding.UTF8读,遇到 BOM 也能兼容。
内容模板要提前定好分隔符。比如用|分隔字段,ASSET-2024-0001|打印机|三楼,扫码端按|拆分。不要在内容里随意加换行、制表符、不可见控制字符,不同扫码器处理不一致。如果内容本身包含分隔符,要么转义,要么换一个不常见的分隔符。URL 里有中文参数时,先做 URL 编码再放进二维码,别直接把原始中文拼进去。
特殊字符方面,文件名和二维码内容是两套规则。二维码内容可以包含大多数可打印字符,但文件名不能包含\ / : * ? " < > |这些字符,也不能太长。我的做法是内容照原样编码,文件名单独清洗。这样即使内容里有冒号,也不会导致保存失败。
2.4 文件命名、目录和重复数据
批量生成最怕两种事故:文件重名互相覆盖,以及生成到一半分不清哪批是哪批。我现在固定用“批次目录 + 序号 + 清洗后的内容 + 短哈希”的命名方式。批次目录用时间戳,比如20250518_143000,每次生成独立目录,不覆盖历史结果。文件名用00001_ASSET-2024-0001_ab12cd34.png,序号保证排序,清洗后的内容方便人看,短哈希避免不同内容清洗后重名。
重复数据要提前问清楚:同样的二维码内容出现两次,是生成两张图,还是只生成一张?资产标签通常每行都要一张,哪怕内容重复;物料卡可能要求去重。我的默认做法是不去重,按行生成,但在日志里标记重复内容,生成结束后给一个重复统计。这样既不丢行,又能让操作员决定要不要清理源数据。
| 命名要素 | 示例 | 作用 |
|---|---|---|
| 批次时间 | 20250518_143000 | 隔离每次任务,防止覆盖 |
| 行号 | 00001 | 稳定排序,方便对照源表 |
| 清洗内容 | ASSET-2024-0001 | 人眼快速识别 |
| 短哈希 | ab12cd34 | 防止清洗后重名 |
| 扩展名 | .png | 通用图片格式,打印和预览都方便 |
3. WinForm界面与批量流程:把“能跑”做成“好用”
3.1 界面控件最小集合
WinForm 界面不需要花哨,但状态要清楚。我通常放这些控件:输入文件选择框、输出目录选择框、每模块像素数值框、纠错级别下拉框、生成按钮、取消按钮、进度条、状态标签、日志文本框,再加一个“生成后打开目录”的复选框。控件不多,但每个都对应一个真实动作。输入路径和输出路径用只读文本框加“浏览”按钮,避免操作员手输错路径。
纠错级别下拉框可以直接绑定枚举:cmbEcc.DataSource = Enum.GetValues(typeof(QRCodeGenerator.ECCLevel));。每模块像素用NumericUpDown,范围设 4 到 30,默认 8 或 10。进度条最大值在任务开始时设置,不要固定 100 再换算百分比,批量行数就是最直观的进度。日志框用多行只读TextBox,不要用ListBox一行行刷,几千条会卡。
状态管理是关键。生成开始后,生成按钮禁用,取消按钮启用,输入控件禁用;任务结束后反过来。不要允许同时开两个生成任务写同一个目录。如果确实需要并发,也要在程序层面加锁或给每次任务独立目录。这个小细节能避免大量“文件被占用”“覆盖了一半”的售后问题。
3.2 导入数据:TXT、CSV、Excel的容错
TXT 最简单,一行一条内容,空行跳过。CSV 要多一步:让用户选内容列,或者约定第一列就是内容列。读 CSV 时不要用Split(',')硬拆,内容里可能有逗号、引号、换行。用成熟的 CSV 解析库,或者至少按引号规则处理。编码优先 UTF-8,带 BOM 也能读;如果用户给的是 GBK,给一个编码下拉框,别让程序猜。
Excel 导入我一般放在第二阶段。用 NPOI 或 ClosedXML 读工作表,把单元格统一转成字符串。这里有个坑:Excel 会把000123显示成123,把长数字变成科学计数法,把日期变成数字序列。所以导入前要提示用户把编号列设为文本,程序里也要按字符串读,不要按数值读。合并单元格、空行、隐藏列都会让行号对不上,日志里要记录“源文件行号”,方便回去改表。
数据校验至少做三件事:空内容跳过并记录、超长内容截断或报错、重复内容统计。二维码容量有限,内容太长会直接抛异常。与其让任务跑到一半失败,不如导入后先做一次预检,把空行、超长行、非法字符列出来,让用户确认后再生成。这个预检界面不需要复杂,一个只读列表加“继续生成”按钮就够。
3.3 异步与进度:UI线程不能碰生成循环
WinForm 的按钮事件运行在 UI 线程,生成几千张二维码如果直接循环,界面一定卡死。正确写法是async void事件里await Task.Run(...),把批量生成放到后台线程。进度通信用Progress<T>,它在构造时捕获 UI 同步上下文,回调会自动回到 UI 线程更新进度条和标签,不需要自己写Invoke。
进度刷新不要太频繁。每生成一张就刷一次,几千张会让 UI 消息队列堆积,反而拖慢速度。我一般每 10 张或每 50 张刷一次,最后一张强制刷一次。日志也不要每张都追加,成功的不写,失败的写,最后给汇总。这样界面流畅,日志也有用。
取消机制要真正生效。用CancellationTokenSource,在循环开头ThrowIfCancellationRequested(),在文件写入时把 token 传进去。取消后不要立刻删半成品目录,保留已经生成的文件和错误日志,让操作员知道跑到哪了。等确认不需要后再手动清理。这个细节在真实交付时很重要,直接删目录会让人怀疑程序把数据弄丢了。
3.4 结果校验和日志
生成结束必须给三个数:总数、成功数、失败数,再加输出目录。失败明细写到_errors.txt或_errors.csv,包含源行号、原始内容、异常信息。这样操作员能直接拿源表对照,不需要开发人员远程猜。成功文件也可以写一个_manifest.csv,记录行号、内容、文件名,后续打印或入库都能用。
如果项目对质量要求高,可以加一个抽检:生成完后随机抽 5 到 10 张,用 ZXing.Net 解码,和原始内容比对。不要全量解码,太慢;抽检能发现参数配置错、编码错、图片损坏这类系统性问题。抽检不通过就提示“本批次建议不要打印”,比打印完几千张再发现扫不出来强得多。
4. 关键代码落地:从一行文本到一张PNG
4.1 NuGet与基础封装
先在项目里安装 QRCoder。包管理控制台执行Install-Package QRCoder,或者用 NuGet 界面搜索安装。下面代码基于 .NET 6 及以上,.NET Framework 4.8 也能用,只是异步文件 API 要换成同步版本或自己封装。先定义批量参数和进度对象,把输入文件、输出根目录、每模块像素、纠错级别、是否强制 UTF-8 放进去。
using QRCoder; public sealed class QrBatchOptions { public string InputFile { get; set; } = ""; public string OutputRoot { get; set; } = ""; public int PixelsPerModule { get; set; } = 8; public QRCodeGenerator.ECCLevel EccLevel { get; set; } = QRCodeGenerator.ECCLevel.M; public bool ForceUtf8 { get; set; } = true; } public sealed class QrProgress { public int Done { get; set; } public int Total { get; set; } public string Message { get; set; } = ""; } public sealed class QrBatchResult { public int Total { get; set; } public int Success { get; set; } public int Failed { get; set; } public string OutputDir { get; set; } = ""; public List<string> Errors { get; } = new List<string>(); }这里把参数封装成对象,而不是在方法里传七八个参数,后面改配置、加字段都方便。PixelsPerModule默认给 8,实际项目里根据标签尺寸调整。ForceUtf8默认打开,中文内容更稳。错误列表放在结果对象里,UI 层只负责展示,不掺和生成逻辑。
4.2 批量生成主流程:PngByteQRCode避免GDI泄漏
批量生成我优先用PngByteQRCode,直接拿 PNG 字节数组写文件,不经过Bitmap和Graphics。这样即使跑一万张,GDI 对象也不会堆积。下面的写法是串行版本,稳定、好排查,适合先跑通再优化。每张二维码创建QRCodeData后用using释放,生成器对象在循环外复用。
using System.Text; using System.Text.RegularExpressions; using System.Security.Cryptography; using QRCoder; public sealed class QrBatchService { public async Task<QrBatchResult> GenerateAsync( QrBatchOptions opt, IProgress<QrProgress> progress, CancellationToken token) { var rawLines = await File.ReadAllLinesAsync(opt.InputFile, Encoding.UTF8, token); var lines = rawLines.Select(x => x.Trim()) .Where(x => x.Length > 0) .ToList(); var batchDir = Path.Combine( opt.OutputRoot, DateTime.Now.ToString("yyyyMMdd_HHmmss")); Directory.CreateDirectory(batchDir); var result = new QrBatchResult { Total = lines.Count, OutputDir = batchDir }; var generator = new QRCodeGenerator(); int done = 0; foreach (var text in lines) { token.ThrowIfCancellationRequested(); try { using var qrData = generator.CreateQrCode( text, opt.EccLevel, forceUtf8: opt.ForceUtf8); var pngBytes = new PngByteQRCode(qrData) .GetGraphic(opt.PixelsPerModule); var fileName = BuildSafeFileName(text, done + 1) + ".png"; var fullPath = Path.Combine(batchDir, fileName); await File.WriteAllBytesAsync(fullPath, pngBytes, token); result.Success++; } catch (OperationCanceledException) { throw; } catch (Exception ex) { result.Failed++; result.Errors.Add($"{done + 1}\t{text}\t{ex.Message}"); } finally { done++; if (done % 10 == 0 || done == lines.Count) { progress?.Report(new QrProgress { Done = done, Total = lines.Count, Message = $"已处理 {done}/{lines.Count}" }); } } } await File.WriteAllLinesAsync( Path.Combine(batchDir, "_errors.txt"), result.Errors, Encoding.UTF8, token); return result; } private static string BuildSafeFileName(string text, int index) { var invalid = Path.GetInvalidFileNameChars(); var safe = new string(text .Select(c => invalid.Contains(c) ? '_' : c) .ToArray()); safe = Regex.Replace(safe, @"\s+", "_"); if (safe.Length > 24) safe = safe.Substring(0, 24); if (safe.Length == 0) safe = "qr"; using var sha = SHA256.Create(); var hashBytes = sha.ComputeHash(Encoding.UTF8.GetBytes(text)); var hash = BitConverter.ToString(hashBytes) .Replace("-", "") .Substring(0, 8) .ToLowerInvariant(); return $"{index:D5}_{safe}_{hash}"; } }这段代码有几个刻意的设计。批次目录用时间戳,避免重复运行覆盖旧文件;文件名带行号和短哈希,既方便排序又避免清洗后重名;错误列表最后统一写文件,不在循环里频繁开关文件;进度每 10 条报一次,减少 UI 压力。QRCodeData用完即释放,PngByteQRCode不产生 GDI 对象,批量稳定性比直接操作Bitmap好很多。
4.3 WinForm按钮事件与取消
UI 层只做参数收集、任务启动、进度展示和取消。下面是一个可直接套用的按钮事件。注意_cts为空判断,防止重复点击;Task.Run把批量任务放到后台线程;Progress<QrProgress>自动回到 UI 线程更新控件。取消按钮只负责Cancel(),不要在里面直接关程序或删文件。
private CancellationTokenSource? _cts; private async void btnGenerate_Click(object sender, EventArgs e) { if (_cts != null) return; var opt = new QrBatchOptions { InputFile = txtInputPath.Text.Trim(), OutputRoot = txtOutputDir.Text.Trim(), PixelsPerModule = (int)numModulePixels.Value, EccLevel = (QRCodeGenerator.ECCLevel)cmbEcc.SelectedItem }; _cts = new CancellationTokenSource(); btnGenerate.Enabled = false; btnCancel.Enabled = true; progressBar1.Value = 0; var progress = new Progress<QrProgress>(p => { progressBar1.Maximum = p.Total; progressBar1.Value = Math.Min(p.Done, p.Total); lblStatus.Text = p.Message; }); try { var service = new QrBatchService(); var result = await Task.Run( () => service.GenerateAsync(opt, progress, _cts.Token)); txtLog.AppendText( $"完成:成功 {result.Success},失败 {result.Failed},目录 {result.OutputDir}{Environment.NewLine}"); } catch (OperationCanceledException) { txtLog.AppendText("任务已取消" + Environment.NewLine); } catch (Exception ex) { MessageBox.Show(ex.Message, "生成失败"); } finally { _cts.Dispose(); _cts = null; btnGenerate.Enabled = true; btnCancel.Enabled = false; } } private void btnCancel_Click(object sender, EventArgs e) { _cts?.Cancel(); }实际项目里还要在启动前校验输入文件存在、输出目录可写、纠错级别已选。不要等任务跑起来再报“路径为空”。如果输出目录在 U 盘或网络盘,写文件可能很慢,进度会卡在最后几张,这时候可以在日志里提示“正在写入磁盘”。不是代码必须,但对操作员体验很有帮助。
4.4 打印标签时的尺寸换算
生成 PNG 只是第一步,很多项目最终要打印。WinForm 用PrintDocument可以打,但尺寸单位是 1/100 英寸,不是毫米。40mm 宽换算成打印单位是40 / 25.4 * 100 ≈ 157,30mm 高约118。纸张大小要在开始打印前设置,不要等到PrintPage里再改,部分打印驱动不认。二维码绘制时用最近邻插值,避免模糊。
using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Printing; private void PrintOneLabel(string imagePath) { int labelWidth = (int)(40 / 25.4 * 100); int labelHeight = (int)(30 / 25.4 * 100); var doc = new PrintDocument(); doc.DefaultPageSettings.PaperSize = new PaperSize("Label40x30", labelWidth, labelHeight); doc.DefaultPageSettings.Margins = new Margins(0, 0, 0, 0); doc.PrintPage += (s, e) => { e.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor; e.Graphics.PixelOffsetMode = PixelOffsetMode.Half; using var img = Image.FromFile(imagePath); e.Graphics.DrawImage( img, new Rectangle(0, 0, labelWidth, labelHeight)); e.HasMorePages = false; }; doc.Print(); }这段只是单张打印示例。批量打印时不要每张新建PrintDocument,而是维护一个待打印列表,在PrintPage里取下一张,设置HasMorePages。真正上标签打印机前,先用普通 A4 打一张看比例,再换标签机调 Gap、黑标、浓度。不同打印机驱动对自定义纸张支持不一样,必要时让操作员在驱动里建好 40×30 的标签尺寸,程序里按驱动名称选纸,而不是硬写尺寸。
5. 排错与优化:批量任务最容易翻车的地方
5.1 常见问题速查表
批量二维码的故障大多有规律,下面这张表是我自己排错时最常翻的。遇到问题先对照现象,不要一上来改代码。尤其是“生成成功但扫不出”,九成是尺寸、对比度、缩放或内容太长,不是库的问题。
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| 中文扫码乱码 | 编码不一致 | 创建时forceUtf8: true,CSV 用 UTF-8 |
| 生成成功但扫不出 | 模块太小、缩放模糊、对比度低 | 增大每模块像素,禁止双线性缩放,用纯黑白 |
| 文件被覆盖 | 文件名重复或输出同一目录 | 批次时间戳目录 + 行号 + 短哈希 |
| 界面卡死 | 在 UI 线程跑循环 | Task.Run+Progress<T> |
| 跑几千张后报错 | GDI 对象或文件句柄泄漏 | 用PngByteQRCode,using释放资源 |
| 路径过长 | 文件名太长或目录太深 | 文件名截断到 24 字符内,输出根目录别太深 |
| Excel 编号变数字 | 单元格被识别为数值 | 编号列设为文本,程序按字符串读 |
| 打印模糊 | 生成图被拉伸或打印机分辨率低 | 按打印 DPI 生成,最近邻插值 |
| 取消没反应 | 循环没检查 token | 循环开头和写文件时传 token |
| 网络盘写入慢 | IO 瓶颈 | 先写本地临时目录,完成后再复制 |
| 权限拒绝 | 输出目录不可写 | 换目录或提升权限,启动时预检 |
| 重复内容处理不清 | 业务规则未定 | 默认按行生成,日志统计重复 |
5.2 内存与GDI对象泄漏排查
WinForm 里最容易忽略的是 GDI 对象。用Bitmap生成二维码时,Bitmap、Graphics、Image都要及时释放。更隐蔽的是Image.FromFile,它会在图片文件上保持句柄,直到图片对象被回收,导致文件删不掉、覆盖失败。批量场景我干脆不用Bitmap生成,直接用PngByteQRCode写字节数组,从源头减少 GDI 对象。需要预览时再单独加载一张小图,预览完立即释放。
如果已经用了Bitmap,排查方式是看任务管理器里的 GDI 对象数,跑一百张看是否持续上涨。上涨就找没using的Graphics、Pen、Brush、Bitmap。另一个办法是生成一批后手动删输出目录,如果提示“文件被占用”,基本就是图片或文件流没释放。File.WriteAllBytesAsync内部会自己关闭文件,比手写FileStream省心,但前提是你没有别的地方还开着同一个文件。
5.3 大批量生成的速度优化
串行生成一千张通常够用,一万张就要考虑优化。瓶颈一般在磁盘 IO,不在二维码计算。优化顺序我一般是:先用PngByteQRCode去掉 GDI 开销;再把进度刷新降到每 50 张一次;然后按 CPU 核数开并行,但不要开满,Environment.ProcessorCount / 2起步,观察磁盘和 CPU 再调。并行时每个线程用自己的QRCodeGenerator,不要共享可变对象,避免线程安全问题。
using System.Collections.Concurrent; using System