WPF 中使用 ZXing.Net 实现二维码生成与识别实战指南

发布时间:2026/10/18 21:05:14
WPF 中使用 ZXing.Net 实现二维码生成与识别实战指南 简介面向C# WPF开发者的二维码生成与识别示例工程基于zxing.netzxing.dll完成QR码编码并引入EPFMediaKit.dll辅助摄像头/图像处理解决桌面应用快速集成二维码交互的需求。压缩包体积22.48MB共148个文件以dll依赖库、cs源码、xaml界面布局、sln工程文件为主附带pdb调试符号、xml注释、config配置与packages.config依赖声明目录结构完整可直接在Visual Studio中打开运行。已有1002人学习下载。示例不仅演示了BarcodeWriter创建二维码并绑定Image控件还给出了从图像或摄像头读取识别码的参考思路通过阅读MainWindow相关代码和xaml布局可快速掌握WPF下的UI绑定、按钮交互与二维码模块集成方式并借鉴项目中的错误处理与工程组织方法便于迁移到自己的应用中。1. 这个 demo 解决什么问题C# WPF 桌面端接入二维码功能最大的障碍往往不是业务逻辑而是 ZXing 与 WPF 图像体系之间的适配。ZXing 的原始 API 面向 Java 和 WinForms 设计到了 WPF 里输出要对齐 WriteableBitmap输入要做 BitmapSource 到 System.Drawing.Bitmap 的像素拷贝每一步都有版本兼容的坑。这个 demo 把二维码的生成和识别两条链路完整跑通生成侧配置 ZXing.Net 的 BarcodeWriter输出 WriteableBitmap 直接绑定界面上的 Image识别侧支持本地图片和剪贴板两个入口解码后返回文本内容与定位点坐标。适合做桌面工具、需要给业务单据加二维码标签或者做扫码录入的开发者直接照抄关键方法就能落地不用再花一个下午去翻 ZXing 的英文文档。2. 技术选型与项目结构ZXing.Net 在 WPF 中的落地姿势2.1 为什么选 ZXing.Net 而不是其他库.NET 生态里二维码库的现状是这样的QRCoder 简洁但只管生成、不支持识别做“扫码录入”这类功能还得另找解码方案ThoughtWorks.QRCode 是前些年的老库到今天已经没有维护在 .NET 6/8 上编译横竖都是问题。ZXing.Net 是 ZXing 的 .NET 移植版NuGet 包名很直白搜 ZXing.Net 就能拿到。它从生成到识别都有统一的抽象不只支持 QRCode还覆盖 Code128、EAN-13、DataMatrix 等几十种条码格式同一个 BarcodeFormat 枚举在生成和解码两侧共用。选型的时候我一般会先确认一件事这个库在 WPF 下有没有现成的渲染器。ZXing.Net 官方带了一个 WriteableBitmapRenderer所以 BarcodeWriter 可以直接泛型实例化为 BarcodeWriter 输出就是 WPF 原生位图类型不需要自己再去把 Bitmap 转一遍 BitmapSource。这个 demo 虽然只用了 QR_CODE 一种格式但枚举的扩展点留在那里后续想做商品条码扫描工具也不用换库。2.2 项目目录结构与分层拆开 demo 源码后合理的组织方式是这样的界面事件只做编排把 ZXing 相关的初始化、参数配置全部收进 Service 层避免 MainWindow.xaml.cs 里堆一两百行库代码。目录结构如下QrCodeDemo/ └─ QrCodeDemo/ ├─ App.xaml 应用入口统一资源与窗口配置 ├─ MainWindow.xaml 主窗体左侧生成区 右侧识别区 ├─ MainWindow.xaml.cs 事件处理只负责调用 Service、刷新界面 ├─ Services/ │ ├─ QrCodeEncoder.cs 生成封装BarcodeWriter 与二维码参数集中管理 │ └─ QrCodeDecoder.cs 识别封装统一文件/剪贴板解码入口 ├─ Extensions/ │ └─ BitmapConverter.cs BitmapSource 与 System.Drawing.Bitmap 互转 └─ Models/ └─ QrDecodeResult.cs 识别结果模型含文本、格式、定位点这个分层的价值在你二次开发的时候才会体现出来。比如你要加一个“批量生成工单二维码”的功能只需要在某个 ViewModel 里循环调用 QrCodeEncoder.Generate不需要再碰任何 ZXing 代码要换解码策略也只需要改 QrCodeDecoder 一个类。demo 规模小不强上 MVVM 框架但至少做到“界面不直接依赖第三方库”这是我拿到源码后第一个会检查的点。2.3 主窗体布局与两条调用链路主窗体是标准的左右分栏左侧输入内容生成二维码右侧打开图片或读剪贴板做识别。XAML 里关键的几个控件如下Grid Margin12 Grid.ColumnDefinitions ColumnDefinition Width*/ ColumnDefinition Width*/ /Grid.ColumnDefinitions GroupBox Header二维码生成 Padding8 StackPanel TextBox x:NametxtContent Height80 TextWrappingWrap AcceptsReturnTrue VerticalScrollBarVisibilityAuto/ Button x:NamebtnGenerate Content生成 ClickBtnGenerate_Click HorizontalAlignmentLeft Padding16,6 Margin0,8,0,0/ Image x:NameimgQrCode Width240 Height240 StretchUniform RenderOptions.BitmapScalingModeNearestNeighbor Margin0,12,0,0/ Button x:NamebtnSave Content保存 PNG ClickBtnSave_Click HorizontalAlignmentLeft Padding16,6 Margin0,8,0,0/ /StackPanel /GroupBox GroupBox Header二维码识别 Grid.Column1 Padding8 StackPanel Button x:NamebtnOpenImage Content打开图片识别 ClickBtnOpenImage_Click HorizontalAlignmentLeft Padding16,6/ Button x:NamebtnDecodeClipboard Content从剪贴板识别 ClickBtnDecodeClipboard_Click HorizontalAlignmentLeft Padding16,6 Margin0,8,0,0/ TextBox x:NametxtResult Height80 TextWrappingWrap VerticalScrollBarVisibilityAuto IsReadOnlyTrue/ /StackPanel /GroupBox /Grid生成链路是点击生成按钮 → QrCodeEncoder.Generate 返回 WriteableBitmap → 赋值给 imgQrCode.Source识别链路是打开图片文件 → BitmapImage 加载 → 转成 System.Drawing.Bitmap → BarcodeReader.Decode → 结果回填到 txtResult。两条链路都不经过磁盘中间文件数据全在内存里流动这也是这个 demo 比网上那些“生成图片保存到临时目录再读回来识别”的示例干净的地方。3. 生成模块拆解从内容字符串到可保存的二维码图片3.1 QrCodeEncoder 的封装与关键参数生成侧的入口是 Services/QrCodeEncoder.cs核心代码不长但每一行参数都有自己的讲究// Services/QrCodeEncoder.cs using ZXing; using ZXing.QrCode; using System.Windows.Media.Imaging; public class QrCodeEncoder { private readonly BarcodeWriterWriteableBitmap _writer; public QrCodeEncoder() { _writer new BarcodeWriterWriteableBitmap { Format BarcodeFormat.QR_CODE, Options new QrCodeEncodingOptions { Width 360, Height 360, Margin 1, CharacterSet UTF-8, ErrorCorrection ZXing.QrCode.Internal.ErrorCorrectionLevel.M, QrVersion 0 } }; } public WriteableBitmap Generate(string content) { if (string.IsNullOrWhiteSpace(content)) throw new ArgumentException(二维码内容不能为空, nameof(content)); return _writer.Write(content); } }这里最关键的是泛型参数 WriteableBitmap它决定了 ZXing 输出哪种位图。BarcodeWriter 会根据泛型类型自动选择对应的渲染器在 WPF 项目里用它输出 WriteableBitmap可以直接赋给 Image.Source不需要额外的类型转换。参数层面的几个取值我整理成一张表方便你对照调整参数作用demo 取值什么时候需要改Width / Height输出位图的像素尺寸360需要打印大标签时按比例加大比如 600Margin静区宽度单位是“模块”而不是像素1要贴在包装上时建议加到 2防止裁切CharacterSet内容编码方式UTF-8内容只含纯 ASCII 时无所谓含中文必须为 UTF-8ErrorCorrection容错等级 L/M/Q/HM打印在易磨损介质上时改成 HQrVersion指定二维码版本0 表示自动选择0一般不动交给库自动算ErrorCorrection 是容错等级从低到高是 L、M、Q、H对应约 7%、15%、25%、30% 的损毁容忍度。等级越高二维码图案越密但抗污抗折能力越强。demo 里取 M 是日常屏幕显示和普通打印的平衡点如果扫码场景是包装箱、标签纸这种容易脏的环境直接改 H。3.2 生成按钮的调用链与界面绑定按钮事件里的逻辑很薄就是把文本交给 Encoder把返回的 WriteableBitmap 丢给 Imageprivate void BtnGenerate_Click(object sender, RoutedEventArgs e) { try { var qr _encoder.Generate(txtContent.Text); imgQrCode.Source qr; btnSave.IsEnabled true; } catch (ArgumentException ex) { MessageBox.Show(ex.Message, 输入检查); } catch (Exception ex) { MessageBox.Show($生成失败{ex.Message}, 提示); } }WriteableBitmap 继承自 BitmapSource所以它本身就是 WPF 图像链路上的一等公民赋给 Image.Source 不需要再转成 BitmapImage。有一点值得注意WriteableBitmap 默认是“可写”状态所以除非你要跨线程共享这个位图否则不要调用 Freeze()一旦冻结后续想再改像素就会抛 InvalidOperationException。这里我把异常分开了两层处理ArgumentException 是业务校验其他异常归为系统异常。实际项目里不要直接吞掉异常至少要先记日志再弹窗方便用户把错误信息带给你。3.3 保存为 PNG 文件的实现生成二维码之后下一步通常是落盘WPF 自带的 PngBitmapEncoder 就够了不用引入 System.Drawing 的 Save 方法private void BtnSave_Click(object sender, RoutedEventArgs e) { var dlg new SaveFileDialog { Filter PNG 图片|*.png, FileName $QR_{DateTime.Now:yyyyMMddHHmmss}.png }; if (dlg.ShowDialog() ! true) return; var encoder new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create((BitmapSource)imgQrCode.Source)); using (var fs new FileStream(dlg.FileName, FileMode.Create)) { encoder.Save(fs); } }这里选择 PNG 而不是 JPEG是因为二维码是二值图像PNG 的无损压缩能完整保留模块边缘JPEG 的压缩反而会在黑白边界上产生振铃效应影响扫码枪识别。从 Image.Source 拿位图而不是自己再存一份 WriteableBitmap是为了保证保存的图和用户看到的是同一份数据避免“预览正常、存下来花掉”这种诡异问题。4. 识别模块拆解从图片文件到识别结果的完整链路4.1 绕不开的类型转换BitmapSource 到 System.Drawing.Bitmap识别侧第一道坎是类型转换。ZXing.Net 的 BarcodeReader.Decode 方法接受的是 System.Drawing.Bitmap、RGBLuminanceSource 或原始像素数组它不认识 WPF 的 BitmapSource。所以在 WPF 项目里必须先做一个像素拷贝把 BitmapSource 的像素拷进一个 System.Drawing.Bitmap 的缓冲区。这里封装了一个扩展方法// Extensions/BitmapConverter.cs using System.Drawing; using System.Drawing.Imaging; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; public static class BitmapConverter { public static Bitmap ToGdiBitmap(this BitmapSource source) { // 统一转成 Bgra32保证后面按 4 字节/像素计算 stride 不会出错 var converted new FormatConvertedBitmap(source, PixelFormats.Bgra32, null, 0); int width converted.PixelWidth; int height converted.PixelHeight; int stride width * 4; var bmp new Bitmap(width, height, PixelFormat.Format32bppArgb); var data bmp.LockBits( new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); converted.CopyPixels( new Int32Rect(0, 0, width, height), data.Scan0, height * stride, stride); bmp.UnlockBits(data); return bmp; } }逻辑拆开讲三步先通过 FormatConvertedBitmap 把任意来源的 BitmapSource 统一转成 Bgra32这一步解决了 InteropBitmap、灰度图、不同 DPI 带来的像素格式差异然后 new 一个 Format32bppArgb 的 Bitmap它的内部像素布局和 Bgra32 完全对应最后用 LockBits 锁定内存区把 data.Scan0 这个首地址交给 CopyPixelsWPF 的像素缓冲就被直接写进 Bitmap 的内存里。UnlockBits 必须在 CopyPixels 之后调用否则数据没有真正落地。这段代码里最值得留意的是 stride 的计算。stride 是 bitmap 每行像素占用的字节数Bgra32 每像素固定 4 字节所以 width 乘 4 就是 stride。如果你在网上抄到“height 乘 data.Stride”的写法那要求源图像的 stride 和目标完全一致一旦源是灰度图就会踩坑。统一先转 Bgra32 是最稳的路径。4.2 文件识别的完整流程文件识别流程要处理两个问题一是图片加载不能锁文件二是解码参数要配置到适合二维码识别的状态。完整实现如下// Services/QrCodeDecoder.cs using ZXing; using ZXing.Common; public class QrCodeDecoder { public QrDecodeResult DecodeFile(string filePath) { var bitmapImage new BitmapImage(); using (var fs new FileStream(filePath, FileMode.Open, FileAccess.Read)) { bitmapImage.BeginInit(); bitmapImage.CacheOption BitmapCacheOption.OnLoad; bitmapImage.StreamSource fs; bitmapImage.EndInit(); } // 走到这里文件流已经释放图片数据完整驻留内存 using (var bmp bitmapImage.ToGdiBitmap()) { return DecodeCore(bmp); } } private QrDecodeResult DecodeCore(Bitmap bmp) { var reader new BarcodeReader { Options new DecodingOptions { TryHarder true, PossibleFormats new ListBarcodeFormat { BarcodeFormat.QR_CODE } } }; var result reader.Decode(bmp); if (result null) return null; return new QrDecodeResult { Text result.Text, FormatName result.BarcodeFormat.ToString(), Points result.ResultPoints }; } }BitmapImage 加载这段有两个细节值得展开。CacheOption 设置为 OnLoad意思是 EndInit 时立刻把图像完全解码进内存而不是等 Image 控件渲染时才读取。加上 StreamSource 直接接 FileStreamEndInit 之后 using 块退出文件流被释放文件句柄也就释放了。如果你用 UriSource 加载Windows 下会持有文件锁识别完想删图或改名就会报“文件被占用”。DecodingOptions 里的 TryHarder 是一个很有用的选项。它的作用是让解码器在第一次扫描失败后继续尝试更多亮度阶梯和旋转角度对倾斜、偏暗、反光的图片友好很多。代价是耗时大约翻倍单张图几十毫秒到一两百毫秒不等桌面交互场景完全可以接受。PossibleFormats 限制成 QR_CODE 后解码器不需要去匹配条形码的形态误识别率会低不少速度也更快。4.3 剪贴板图片识别剪贴板识别是桌面工具里被用得很多的功能用户截图后直接点按钮就能读出二维码不用先存文件再打开。实现上要处理剪贴板返回的 InteropBitmappublic QrDecodeResult DecodeFromClipboard() { if (!Clipboard.ContainsImage()) return null; try { var source Clipboard.GetImage(); // InteropBitmap 的像素格式不稳定统一走 Bgra32 再解码 var converted new FormatConvertedBitmap(source, PixelFormats.Bgra32, null, 0); using (var bmp converted.ToGdiBitmap()) { return DecodeCore(bmp); } } catch (Exception ex) { Debug.WriteLine($剪贴板取图失败{ex}); return null; } }Clipboard.GetImage() 返回的是一个特殊类型它在内部持有剪贴板数据的引用像素格式和 DPI 都取决于截图来源不稳定。直接拿它去做像素拷贝轻则 stride 不匹配导致图像错位重则 CopyPixels 抛“参数无效”。所以在进 ToGdiBitmap 之前先 FormatConvertedBitmap 统一转一遍后面所有逻辑就和文件识别路径完全一致了。另外注意一点如果剪贴板里的图是一张 4K 截图转换成 Bitmap 再解码的耗时可能接近一秒。这个环节不该放 UI 线程上裸跑。demo 里为了保持简单直接同步调用你如果要做成产品至少包一层 Task.Run解码完成再通过 Dispatcher 回到 UI 线程更新文本框。5. 常见问题排查WPF 集成 ZXing 时最容易踩的坑5.1 Decode 参数类型不匹配编译直接报错现象把 WPF 的 BitmapImage 直接传给 reader.Decode(bitmapImage)编辑器直接标红提示没有匹配的重载。原因ZXing.Net 的 BarcodeReader 只吃 System.Drawing.Bitmap、LuminanceSource 或原始像素数组不认 BitmapSource 家族。这是两条图像技术栈一个是 WPF 的 Freezable 体系一个是 GDI 的非托管包装二者没有隐式转换。解决写一个 BitmapConverter 扩展方法做像素拷贝如 4.1 节所示。注意 System.Drawing.Common 在 .NET 5 下要显式添加 NuGet 包引用WPF Windows 桌面项目可以正常用但不要忘记在 csproj 里加 PackageReference。5.2 识别一次后图片文件被占用现象程序打开某张图片识别成功之后在资源管理器里删除或重命名该文件弹出“文件正在被另一个进程使用”。原因BitmapImage 的默认 CacheOption 是 OnDemand图像数据延迟加载底层 FileStream 一直被 BitmapImage 持有不释放。如果用的是 Bitmap 构造函数直接读文件也会因为流没释放导致句柄卡住。解决用 FileStream 显式打开文件设置 CacheOption BitmapCacheOption.OnLoadEndInit 后再退出 using 块。OnLoad 的语义就是图片数据立即解码进内存流用完就可以扔。5.3 中文内容生成后扫出来乱码现象二维码内容里带中文比如“工单A001”用手机扫出来是“G-CON…”或者直接空缺一块。原因ZXing 编码字符串时默认走的不是 UTF-8而是按系统区域的默认编码。解码端手机普遍按 UTF-8 解读字节流两边不一致就出现乱码或空字符。解决生成时在 QrCodeEncodingOptions 里显式设置 CharacterSet UTF-8保证写入二维码的字节流用的是 UTF-8。这里不用自己去 UTF8.GetBytesZXing 内部会按 CharacterSet 处理字符串手动转字节反而会破坏输入的字符串类型约束。5.4 打印后手机扫不出来现象屏幕显示识别正常打印到标签纸上一扫就失败用微信和扫码枪都试过。原因最常见的两类一是静区被裁掉Margin 设为 0 时二维码模块贴边裁切线或折痕直接吃掉定位角二是容错等级太低打印墨迹扩散导致相邻模块粘连识别器无法区分黑白边界。解决Margin 至少设 1推荐 2ErrorCorrection 从 M 改成 H。注意 Margin 单位是模块360 像素的图 Margin2 大约在四边各留约 25 像素空白。改完重新生成、重新打印识别率提升非常明显。5.5 小尺寸二维码放大显示边缘发虚现象生成 120×120 的二维码界面 Image 控件按 240 显示边缘全是灰阶过渡扫码偶尔失败。原因WPF 默认用线性插值对位图做缩放二维码是纯黑白二值图线性插值会在黑白边界产生灰色像素破坏了二维码的锐利边缘。解决要么直接把 Width / Height 设成最终显示尺寸从源头避免放大要么给 Image 设置 RenderOptions.BitmapScalingModeNearestNeighbor让 WPF 用最近邻采样保持像素边缘的清晰度。打印场景下建议用第一种生成多大就是多大不做任何缩放。6. 二次开发思路把 demo 改造成批量识别工具6.1 批量识别文件夹中的图片demo 的识别入口是单张图片改成批量识别只需要在 QrCodeDecoder 外面套一层循环。核心代码可以这样组织public async Task BatchDecodeAsync(IEnumerablestring files, IProgressBatchProgress progress) { await Task.Run(() { var decoder new QrCodeDecoder(); foreach (var file in files) { var result decoder.DecodeFile(file); progress.Report(new BatchProgress { FilePath file, Text result?.Text, Succeeded result ! null }); } }); }批量场景最大的坑是 UI 卡死。解码单张图几十毫秒一百张图就是好几秒放在 UI 线程上会直接失去响应。用 Task.Run 把解码放到线程池再通过 IProgress 回传进度界面用 DataGrid 或者 ListBox 绑定结果列表每处理完一张刷新一行。如果一次要处理上万张图还需要考虑内存每张图用完后立即释放DecodeFile 里的 using 已经保证了这一点。拿到这份 demo 源码后第一件事不是打开工程而是先找到 src 目录下 csproj 里的 PackageReference确认 ZXing.Net 的版本再去看 QrCodeEncoder 和 QrCodeDecoder 两个类把 3.1 和 4.2 的参数表打印出来贴显示器旁边后面改参数随时对照。6.2 三个值得继续扩展的方向第一个方向是摄像头实时识别。用 MediaCapture 或者 AForge.NET 抓帧每一帧转成 BitmapSource再走 ToGdiBitmap DecodeCore 这条链路。注意控制帧率ZXing 单帧解码耗时 30 到 200 毫秒实时识别建议用独立线程解码一帧的同时丢弃后续帧避免队列堆积。第二个方向是带 Logo 的二维码。生成后把 BitmapSource 转成 System.Drawing.Bitmap用 Graphics 把 Logo 画到中心区域。Logo 面积不要超过整个二维码的四分之一容错等级必须调到 H不然遮挡的模块会超过纠错能力扫码直接失败。第三个方向是失败重试机制。Decode 返回 null 时把图像旋转 90 度和 180 度再各试一次手机上拍摄的二维码经常带角度靠 TryHarder 不一定能拉回来。这是一个很有效的补丁批量识别的成功率能提升不少。以前我在某个桌面项目里为了能删掉识别过的图片把缓存目录整体挪到了临时文件夹治标不治本后来把 BitmapCacheOption 的原理搞明白才发现文件句柄问题从源头就该用 OnLoad 解决。从那以后我每次在 WPF 里碰图像 I/O都强制走一遍“FileStream OnLoad → 用完即释放 → 识别完再解绑”这个流程省下的调试时间远比多敲的几行代码多。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询