C# WPF 二维码生成与识别实战:ZXing 集成与避坑指南
简介:这份资源是面向C#与WPF开发者的二维码生成与识别实战示例,适合已掌握WPF基础、希望快速集成扫码功能的中级开发者参考。项目以Zxing.Net为核心,演示如何通过BarcodeWriter设置QR_CODE格式与尺寸参数生成二维码并绑定到Image控件显示,同时结合多媒体库从摄像头或图片中读取二维码,覆盖生成与识别两条主线。压缩包共148个文件、约22.48MB,包含35个dll依赖库、12个cs源码、2个xaml界面文件及sln、csproj工程配置,另有pdb调试符号、xml文档说明与nupkg包等,工程结构完整可直接编译运行。资源已有1002人学习下载,读者可从中获取二维码编解码的接口调用方式、WPF界面与业务逻辑的组织思路,以及依赖引用配置的参考,便于在自己的应用中快速复现并扩展扫码交互功能。
1. 从一张贴纸说起:这个 WPF 二维码 Demo 到底能干什么
上周同事拿着一台工控机过来,说产线要加个「扫码录入」的环节,问我能不能两天内搞个能生成二维码、又能用摄像头识别的桌面小工具。我第一反应不是去翻 NuGet,而是想起手里这个 C# WPF 二维码生成与识别 Demo——它把 ZXing 的编码、解码能力包进了一个 WPF 界面里,左边输入文本点一下生成,右边开摄像头或选图片就能把码读回来。这类需求在仓储、设备巡检、资产贴标里反复出现,用 WPF 做壳、用 ZXing 做核心,是相当稳的组合。这个 Demo 适合两类人:一类是想快速跑通「生成 + 识别」闭环的新手,另一类是准备把它嵌进自己桌面工具里做二次开发的熟手。下面我按自己拆项目的顺序,把它从结构到代码再到改造点讲透。
2. 项目结构与技术选型:WPF 壳 + ZXing 内核怎么搭
2.1 目录与文件职责划分
这个 Demo 的体量不大,但结构是标准的 WPF 单项目布局,拆开看每一层都有明确分工。我一般拿到源码先看目录,因为它比任何文档都诚实。
QrCodeDemo/ ├── QrCodeDemo.sln ├── QrCodeDemo/ │ ├── App.xaml │ ├── App.xaml.cs │ ├── MainWindow.xaml # 主界面:生成区 + 识别区 │ ├── MainWindow.xaml.cs # 事件绑定、调用编解码 │ ├── Services/ │ │ ├── QrGenerator.cs # 封装 ZXing 编码 │ │ └── QrDecoder.cs # 封装 ZXing 解码 │ ├── Helpers/ │ │ └── BitmapConverter.cs # WPF BitmapSource 与 System.Drawing.Bitmap 互转 │ └── Assets/ │ └── sample.png └── packages/ 或 NuGet 引用Services目录是核心,把 ZXing 的调用从界面代码里剥出来,这样以后换 UI 框架或者加命令行入口都不用动业务逻辑。Helpers里的转换器是 WPF 项目绕不开的一环——ZXing 的BarcodeWriter输出的是System.Drawing.Bitmap,而 WPF 的Image控件吃的是BitmapSource,两者之间必须转一道。很多新手卡在「生成成功但界面不显示」,八成就是漏了这一步。
2.2 技术栈与依赖说明
技术栈很克制:.NET(常见是 .NET Framework 4.7.2 或 .NET 6/8 的net6.0-windows)、WPF 做界面、ZXing.Net 做二维码编解码。ZXing.Net 是 ZXing 的 .NET 移植版,NuGet 包名是ZXing.Net,编码用BarcodeWriter,解码用BarcodeReader。摄像头采集部分常见做法是引入OpenCvSharp4或AForge.Video.DirectShow,前者跨平台、API 现代,后者在纯 Windows 桌面项目里更轻。这个 Demo 如果只做「选图片识别」,那连摄像头依赖都可以省掉,先跑通静态图识别再上摄像头,是我推荐的推进顺序。
| 依赖 | 作用 | 备注 |
|---|---|---|
| ZXing.Net | 二维码编码/解码 | 核心,必装 |
| System.Drawing.Common | 位图对象 | .NET 6+ 需显式引用 |
| OpenCvSharp4 | 摄像头帧采集 | 做实时识别时引入 |
| WPF (PresentationFramework) | 界面 | 项目模板自带 |
提示:.NET 6 及以上在非 Windows 平台使用
System.Drawing.Common会受限,但这个项目本身就是 WPF,天然只在 Windows 上跑,所以不构成问题。
2.3 界面与事件流
MainWindow.xaml大致分两块:上半部分是生成区,一个TextBox收内容、一个Button触发、一个Image显示结果;下半部分是识别区,一个Button选图或开摄像头、一个TextBlock显示解码文本。事件流很直白:点生成 → 读TextBox.Text→ 调QrGenerator.Generate→ 转BitmapSource→ 赋给Image.Source;点识别 → 打开文件对话框或启动采集 → 调QrDecoder.Decode→ 把结果写回TextBlock。把这条链路记住,后面看代码就不会迷路。
3. 核心模块拆解:生成与识别两条链路逐段看
3.1 二维码生成:BarcodeWriter 的参数怎么调
生成这块的代码量很小,但参数选择直接决定扫不扫得出来。下面是我从这个 Demo 里抽出来的生成逻辑,加了注释:
using ZXing; using ZXing.Common; using ZXing.QrCode; using ZXing.QrCode.Internal; public static Bitmap Generate(string content, int width = 300, int height = 300) { // 编码选项:字符集、容错级别、边距 var options = new QrCodeEncodingOptions { DisableECI = true, // 关闭 ECI,兼容老扫码器 CharacterSet = "UTF-8", // 中文内容必须显式指定 Width = width, Height = height, Margin = 1, // 静区,太小会扫不出 ErrorCorrection = ErrorCorrectionLevel.M // 容错级别 L/M/Q/H }; var writer = new BarcodeWriter { Format = BarcodeFormat.QR_CODE, Options = options }; // 返回 System.Drawing.Bitmap return writer.Write(content); }逐项说清楚:CharacterSet不设成 UTF-8,中文内容在部分扫码器上会变乱码,这是血泪经验;Margin是二维码四周的静区,设成 0 时某些识别库会因为缺少留白而失败,1 是比较保险的值;ErrorCorrection从 L 到 H 容错能力递增,但码面也会更密,内容短的时候用 M 足够,要贴在有磨损风险的设备上就上 H。Width/Height不是越大越好,300 到 500 像素在屏幕上显示和打印都够用,太大反而拖慢渲染。
3.2 Bitmap 转 BitmapSource:WPF 显示的关键一步
生成出来的Bitmap不能直接塞给 WPF 的Image,必须转换。Demo 里的转换器长这样:
using System.Drawing; using System.IO; using System.Windows.Media.Imaging; public static BitmapSource ToBitmapSource(Bitmap bitmap) { using var memory = new MemoryStream(); bitmap.Save(memory, System.Drawing.Imaging.ImageFormat.Png); memory.Position = 0; var imageSource = new BitmapImage(); imageSource.BeginInit(); imageSource.CacheOption = BitmapCacheOption.OnLoad; // 关键:立即加载,避免流关闭后失效 imageSource.StreamSource = memory; imageSource.EndInit(); imageSource.Freeze(); // 跨线程使用时必须 Freeze return imageSource; }这里有两个容易翻车的点。一是CacheOption必须设成OnLoad,否则BitmapImage是延迟加载的,using块结束、流被释放后再渲染就会抛异常;二是如果生成放在后台线程、显示在主线程,Freeze()不能省,冻结后的BitmapSource才能跨线程安全使用。这两行注释看着不起眼,实际是新手报错的高发区。
3.3 静态图识别:BarcodeReader 的配置与结果处理
识别静态图片是最容易验证的入口,先把它跑通再谈摄像头。核心代码如下:
using ZXing; using ZXing.Common; public static string Decode(Bitmap bitmap) { var reader = new BarcodeReader { AutoRotate = true, // 允许旋转识别 TryInverted = true, // 尝试反色(白码黑底) Options = new DecodingOptions { TryHarder = true, // 提高识别率,代价是慢一点 PossibleFormats = new[] { BarcodeFormat.QR_CODE }, CharacterSet = "UTF-8" } }; var result = reader.Decode(bitmap); return result?.Text ?? string.Empty; // 识别失败返回空串,由调用方提示 }TryHarder打开后会做更多采样尝试,模糊、倾斜的图识别率明显提升,但单张耗时上升,做批量识别时要权衡。TryInverted针对反色码,有些打印材质是白码黑底,不开这个就识别不出来。PossibleFormats限定只找二维码,能减少误判、加快速度。返回值用空串而不是抛异常,是为了让界面层统一处理「没识别到」的提示,调用方判断string.IsNullOrEmpty即可。
3.4 摄像头实时识别:采集与解码的节奏控制
实时识别不是「每帧都解码」那么简单,帧率太高会把 CPU 吃满。常见做法是定时器抽帧,比如每 200 毫秒取一帧送去解码:
private void OnFrameReady(object sender, FrameEventArgs e) { // 抽帧:避免每帧都解码 if (Environment.TickCount - _lastDecodeTick < 200) return; _lastDecodeTick = Environment.TickCount; using var frame = e.Frame.Clone(); // 复制一份,避免采集线程复用缓冲 var text = QrDecoder.Decode(frame); if (!string.IsNullOrEmpty(text)) { Dispatcher.Invoke(() => ResultText.Text = text); // 回主线程更新 UI } }Clone()这一步不能省,采集库往往会复用同一块缓冲,不复制的话解码时数据可能已经被下一帧覆盖。Dispatcher.Invoke是因为 UI 控件只能在主线程更新。抽帧间隔 200 毫秒是经验值,既能保证「对准就出结果」的体感,又不会让解码线程排队堆积。如果现场光线差、码面小,可以适当放宽到 300 毫秒并配合TryHarder。
4. 跑起来:环境准备与首次运行排查
4.1 环境与依赖安装
先确认机器上有 .NET SDK,命令行敲dotnet --list-sdks能看到版本即可。如果是 .NET Framework 项目,用 Visual Studio 打开.sln更省事。依赖安装两种方式:Visual Studio 里右键项目「管理 NuGet 程序包」搜ZXing.Net安装;或者命令行进项目目录执行:
dotnet add package ZXing.Net dotnet add package System.Drawing.Common装完检查.csproj里是否有对应的PackageReference,有就说明引用成功。摄像头方案如果选了 OpenCvSharp,再补一个OpenCvSharp4.Windows,它自带原生库,省去手动配 DLL 的麻烦。
4.2 首次运行与最小验证
第一次跑不要急着开摄像头,按这个顺序验证:先编译,确认无报错;再在生成区输入一段英文,点生成,看Image是否出图;然后输入一段中文,再生成一次,确认中文不乱码;最后把生成的图保存下来,用手机扫一下,能读出原文就说明编码链路通了。识别侧先选一张本地二维码图片,确认能读出文本,再考虑接摄像头。这个顺序能把问题隔离在单点上,比一上来就调摄像头高效得多。
4.3 常见报错与定位思路
System.Drawing.Common找不到类型,多半是没装包或目标框架不匹配;BitmapImage显示空白,回去看CacheOption和流是否提前释放;中文乱码,检查CharacterSet是否设为 UTF-8;识别率低,先调TryHarder和Margin,再考虑图片分辨率。定位时优先看异常堆栈的第一行,WPF 的绑定错误则去「输出」窗口看绑定跟踪信息,比瞎猜快。
5. 避坑与排查:五个真实踩过的坑
5.1 生成成功但界面一片空白
现象:点生成按钮没报错,Image控件就是不显示。原因:BitmapImage用了默认的延迟加载,MemoryStream在using块结束时被释放,渲染时流已失效。解决:CacheOption设为OnLoad,并在EndInit后调用Freeze()。这个坑我见过不止一次,属于 WPF 图片处理的经典陷阱。
5.2 中文内容扫出来是乱码
现象:界面显示正常,手机扫出来一堆问号。原因:QrCodeEncodingOptions.CharacterSet没设或设成了默认值,编码时按单字节处理了中文。解决:显式设CharacterSet = "UTF-8",解码侧DecodingOptions也同步设 UTF-8,两端一致才不会错位。
5.3 摄像头识别卡顿甚至假死
现象:开摄像头后界面卡住,点按钮没反应。原因:解码在主线程同步执行,或者每帧都解码导致队列堆积。解决:采集和解码放到后台线程,用抽帧控制频率,UI 更新统一走Dispatcher.Invoke;解码对象不要跨线程共享,每次新建或用锁保护。
5.4 二维码贴到设备上扫不出来
现象:屏幕上能扫,打印贴到金属外壳上就扫不出。原因:静区Margin太小、对比度不足、表面反光。解决:Margin至少留 1 到 2 个模块宽度,打印时保证黑白对比,必要时把容错级别提到 H,并在码外加一圈白底。这是现场部署最容易忽略的细节。
5.5 NuGet 还原失败或版本冲突
现象:dotnet restore报找不到包或版本不兼容。原因:目标框架与包的最低框架要求不匹配,或本地缓存损坏。解决:确认项目目标框架(如net6.0-windows)满足包要求,清理obj、bin后重新还原,必要时指定明确版本号而不是浮动版本。
6. 二次开发:把它改成能落地的工具
6.1 从 Demo 到工具的改造清单
这个 Demo 的骨架够用,但要真放进产线或办公场景,我一般按下面几处动手。第一,把Services里的生成和识别抽成独立类库,界面只做调用,方便以后加命令行或服务端入口。第二,生成侧加「批量生成」:读一个 CSV,每行一个编号,循环生成并按编号命名保存,配合ErrorCorrectionLevel.H和固定尺寸,直接能打印贴标。第三,识别侧加「结果去重」:实时识别时同一张码会连续读出多次,用一个HashSet记录最近结果并加时间窗,避免重复录入。第四,加日志,把每次识别的原文、耗时、失败原因写文件,现场排查时这就是黑匣子。
6.2 一个可直接套用的批量生成片段
// 读 CSV 批量生成,每行格式:编号,内容 foreach (var line in File.ReadLines("items.csv")) { var parts = line.Split(',', 2); if (parts.Length < 2) continue; var code = parts[0].Trim(); var content = parts[1].Trim(); using var bmp = QrGenerator.Generate(content, 400, 400); var path = Path.Combine("output", $"{code}.png"); bmp.Save(path, System.Drawing.Imaging.ImageFormat.Png); // 直接落盘,不走界面 }这段不依赖 WPF,放在控制台项目里也能跑,适合做离线批处理。参数上把尺寸提到 400 是为了打印清晰,ErrorCorrectionLevel在Generate内部统一设成 H,牺牲一点码面密度换容错。跑完检查output目录的文件数和 CSV 行数是否一致,是最快的验证方式。
6.3 验证改造是否生效的习惯
改完不要只看「能跑」,要按场景验证:批量生成后随机抽三张用手机扫,确认内容与 CSV 一致;实时识别连续扫同一张码十次,确认去重逻辑只录入一次;把生成的图缩小到 50% 再识别,看容错是否还扛得住。我吃过「本地测试全过、现场全崩」的亏,从那以后每次改完识别链路,都强制走一遍「缩小 + 倾斜 + 反色」三组异常样本,确认TryHarder和TryInverted的组合真的起作用,而不是碰巧。这套习惯看着笨,但能省下大量现场返工的时间。希望帮到你。
本文还有配套的精品资源,点击获取