资讯动态

C# WPF + ZXing:二维码生成与识别实践指南

发布时间:2026/10/18 21:51:33 来源:尧图企业网站定制
简介这是一份C# WPF环境下二维码生成与识别的示例项目面向需要在桌面应用中集成条码生成与读取功能的开发者。项目围绕zxing.dll与EPFMediaKit.dll两大核心库演示了通过BarcodeWriter创建二维码、配置QrCodeEncodingOptions尺寸参数以及结合多媒体库处理摄像头或图片数据以完成二维码识别同时提供界面按钮与调用思路。压缩包为RAR格式共148个文件整体约22.48MB其中35个dll为运行依赖12个cs为源码2个xaml为窗口布局另有31个pdb调试符号与29个xml注释文档方便断点调试和查看API同时包含sln、csproj等工程文件解压后可直接用Visual Studio打开运行。目前已有1002人学习/下载适合有WPF基础、希望快速搭建二维码功能原型的开发者。通过学习该demo可掌握二维码生成参数、图像解码流程以及库集成中的关键步骤是一份可复用的入门参考无论是学习ZXing.Net用法还是作为功能模板都能节省不少摸索时间。1. 为什么是 C# WPF ZXing 做二维码一个能直接落地的组合在做桌面端工具时需求往往不是跑通个算法而是扫码、出码、不崩、能交付。C# WPF 技术栈里做二维码生成与识别最省心的路径就是用 ZXing.Net 这个库二维码的编解码、容错、矩阵生成它全包了WPF 这边只负责把像素数据交给 UI 显示或者从图像源取数据回喂给解码器。这个 demo 虽然名字带 demo实际把生成二维码、识别二维码两条链路都走通了能直接作为工具模块嵌进项目里。适合正在做 WPF 桌面工具、需要快速集成扫码或出码能力的人也适合想搞清 ZXing 在 WPF 里数据怎么流转的开发者。接下来我把整个实现拆开讲参数、边界、翻车点都会说到。2. ZXing 在 WPF 里的核心机制从像素数据到 BitmapSource 的转换逻辑2.1 ZXing 三个关键类先分清谁干什么ZXing.Net 虽然是 Java ZXing 的移植版但 WPF 场景下你真正打交道的类就三个BarcodeWriter、BarcodeReader、BitMatrix。BarcodeWriter管生成它有多种泛型重载或者说有多个Write重载有的返回Bitmap有的返回BitMatrix有的返回PixelData。WPF 里最值得用的就是返回PixelData的那个因为PixelData直接暴露了像素数组Pixels你可以绕开 GDI直接交给 WPF 的BitmapSource.Create去构造ImageSource这才是 WPF 友好路径。BarcodeReader管识别它的核心方法是Decode接收的参数不是图片文件路径而是LuminanceSource灰度数据源。ZXing 内部会把图像的彩色像素转成灰度亮度图再做定位、解码头、纠错解码。这个LuminanceSource的获取方式决定了你识别的是文件路径、内存流还是摄像头帧。BitMatrix是二维码的真身它是一个布尔矩阵每个格子代表一个模块。生成端的逻辑是先把内容字符串编码成BitMatrix再把BitMatrix转成可视化的像素图。理解这个顺序后面调试生成的码扫不出来时会省很多时间。2.2 生成端的像素流转为什么不能直接塞 Bitmap在 WPF 里如果你用BarcodeWriter.Write()拿到一个System.Drawing.Bitmap再转BitmapImage显示中间要经过MemoryStream编码绕路不说还容易踩 Stream 未关闭的坑。更干净的路径是拿PixelData。我来写一个 WPF 下标准的二维码生成方法using ZXing; using ZXing.Common; using ZXing.QrCode.Internal; using System.Windows.Media; using System.Windows.Media.Imaging; public static BitmapSource GenerateQrCode(string content, int size 300, int margin 1) { var writer new BarcodeWriterPixelData { Format BarcodeFormat.QR_CODE, Options new QrCodeEncodingOptions { Width size, Height size, Margin margin, ErrorCorrection ZXing.QrCode.Internal.ErrorCorrectionLevel.M, CharacterSet UTF-8 } }; // 返回的是 PixelData内含 Width、Height、PixelsBGRA 格式 var pixelData writer.Write(content); // 注意 PixelFormats.Bgr32 要和 PixelData 的字节顺序对应 var bitmap BitmapSource.Create( pixelData.Width, pixelData.Height, 96, 96, PixelFormats.Bgr32, null, pixelData.Pixels, pixelData.Width * 4); bitmap.Freeze(); // 冻结后可跨线程使用 return bitmap; }这段代码的逻辑很直接BarcodeWriterPixelData是把BitMatrix渲染成像素数组的专用类Write(content)执行编码和渲染两步。拿到的PixelData.Pixels是 BGRA 字节数组每像素 4 字节。BitmapSource.Create的stride参数是每行字节数这里就是Width * 4因为没做任何缩放、没有任何填充位。注意两个细节DPI 参数写 96 是 WPF 的惯例让二维码图片的物理尺寸和像素尺寸一致bitmap.Freeze()是为了避免后续绑定到 UI 时出现跨线程访问问题。如果没有 Freeze你在后台线程生成完扔给 UI 线程使用在某些绑定场景会抛异常。2.3 识别端的 LuminanceSource文件、内存图、摄像头怎么统一识别路径比生成路径绕一点。ZXing 的BarcodeReaderGeneric.Decode()不认Bitmap只认LuminanceSource。这时候你要做一层适配如果有文件路径用BitmapLuminanceSource直接从图片读。如果二维码图已经在内存里比如刚从网络下载、或从剪贴板拿到用RGBLuminanceSource喂字节数组。如果是摄像头帧WPF 里通常拿到的是WriteableBitmap要从它的BackBuffer复制像素字节再转成RGBLuminanceSource。using ZXing; using ZXing.Common; using ZXing.Rendering; public static string DecodeQrCode(byte[] bgraPixels, int width, int height) { var reader new BarcodeReaderGeneric { AutoRotate true, Options new DecodingOptions { TryHarder true, PossibleFormats new ListBarcodeFormat { BarcodeFormat.QR_CODE } } }; // RGBLuminanceSource 要求 RGB 顺序而 WPF 的 BitmapSource 默认是 BGRA var source new RGBLuminanceSource(bgraPixels, width, height); var result reader.Decode(source); return result?.Text; }这段代码里AutoRotate true是给手机拍歪的图兜底的ZXing 在解码失败时会尝试旋转 90/180/270 度重新定位。TryHarder true会启用更耗时的扫描策略适合图上带干扰的情况。PossibleFormats限定只识别 QR_CODE误识别率会低很多如果同时识别多种码制会明显变慢。这里有一个容易踩的坑RGBLuminanceSource内部是按字节顺序组装灰度图的WPF 的PixelFormats.Bgr32字节序是 B-G-R-A直接塞进去会让图像偏色而 ZXing 的灰度化逻辑对通道顺序敏感偏色后对比度下降弱光二维码会识别失败。所以第一步要把 BGRA 转换成 RGB或者用BitmapLuminanceSource让它走内部转换。这个我会在第 5 章展开细说。3. 二维码生成实战参数配置与画面上屏3.1 生成端完整代码事件链现在把生成端做成一个能用的控件逻辑。假设窗口上有一个Image控件叫QrImage一个文本框叫ContentBox。点击按钮后生成并显示private void GenerateBtn_Click(object sender, RoutedEventArgs e) { string content ContentBox.Text.Trim(); if (string.IsNullOrEmpty(content)) { MessageBox.Show(内容不能为空); return; } try { var bitmap GenerateQrCode(content, 280, 2); QrImage.Source bitmap; SaveQrPng(bitmap, output.png); } catch (WriterException ex) { // 内容太长或参数非法时会抛 WriterException MessageBox.Show($生成失败: {ex.Message}); } }保存 PNG 的方法同样建议从 BitmapSource 走不要绕回 GDIprivate static void SaveQrPng(BitmapSource source, string path) { var encoder new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(source)); using (var fs File.Create(path)) { encoder.Save(fs); fs.Flush(); } }这里using必须要写。很多人保存完图片立刻在资源管理器里打开发现文件损坏十有八九是FileStream还没写完就释放了。PngBitmapEncoder.Save是同步写的但释放流时如果不 Flush缓冲数据可能没落盘。Flush之后文件才是完整可用的。3.2 关键参数Size、Margin、ErrorCorrection 背后的选择逻辑QrCodeEncodingOptions里几个参数不是随便填的。Width/Height二维码矩阵被缩放到的目标像素尺寸。如果你的二维码内容是 21x21 模块Version 1Width210意味着每个模块 10 像素。设置太小会让模块糊在一起扫描设备容易误判。Margin白边宽度单位是模块数而不是像素。这个参数特别容易被忽略。ZXing 默认是 1但很多扫码枪要求白边至少 4 个模块宽。你用微信扫码时如果码贴在深色背景上Margin1 基本扫不出。我一般生成工具类默认设 2特殊场景设 4。ErrorCorrection纠错级别分 L约 7%、M约 15%、Q约 25%、H约 30%。纠错级别越高二维码能容忍的遮挡、破损面积越大但码的模块密度也会变大。纯文本内容建议 M 起步如果二维码要被印在包装袋、瓶盖上直接上 H。CharacterSet UTF-8指定编码。这个参数影响中文内容的兼容性。如果你不指定ZXing 默认按 ISO-8859-1 处理中文内容会直接抛异常或者编出一个谁也扫不出来的码。PureBarcode这个参数在BarcodeWriter场景下别开。PureBarcodetrue时ZXing 输出的矩阵不包含定位图案周围的白边处理逻辑生成出来的是一个最小单元的裸矩阵用普通扫码枪反而容易识别失败。这个参数是留给测试解码器用的不是给生产环境用的。// 内容长度会倒逼 Version 升级纠错级别据此动态调整的例子 public static QrCodeEncodingOptions BuildOptions(int contentLength, int size) { var ecLevel ErrorCorrectionLevel.M; if (contentLength 100) { // 长内容时如果还要保持高纠错Version 会变大模块更密 ecLevel ErrorCorrectionLevel.Q; } return new QrCodeEncodingOptions { Width size, Height size, Margin 2, ErrorCorrection ecLevel, CharacterSet UTF-8 }; }注意当内容变长时单纯调大Width不会让码变清晰Width只是渲染时的放大倍数。真正决定密度的是内容长度和纠错级别共同决定的 Version。内容 100 个字符和 1000 个字符生成的二维码矩阵尺寸完全不同前者可能是 29x29后者可能是 57x57。所以拿到的PixelData.Width不等于你设置的Options.Width后者是渲染目标尺寸前者是内容编码后的实际矩阵尺寸被渲染时按整数倍放大。这个差异很多人调了半天才发现。3.3 常见误用把 Bitmap 当万能中转有多坑在 WPF 项目里见过不少同事这么写var bmp writer.Write(content); // 返回 System.Drawing.Bitmap MemoryStream ms new MemoryStream(); bmp.Save(ms, ImageFormat.Png); QrImage.Source BitmapFrame.Create(ms, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);这段代码能跑但它在Bitmap和MemoryStream之间多绕了一次 PNG 编码内存开销大而且BitmapCacheOption.OnLoad必须加否则MemoryStream被释放后图片就变空白了。我在实际项目里还见过忘了ms.Position 0导致 BitmapFrame 解析失败的。直接用BarcodeWriterPixelData可以完全跳过这一步代码更短还没有 Stream 生命周期管理的负担。另外有个隐藏问题System.Drawing.Bitmap是 GDI 资源WPF 和 WinForms 混用时如果项目目标平台是 .NET Core 3.0在某些 Windows 服务环境下System.Drawing.Common会抛平台不支持异常。用PixelData方案就完全没有这个依赖跨平台属性更好。所以我的结论是WPF 项目里生成二维码不要再碰System.Drawing路径越短坑越少。4. 二维码识别实战文件、剪贴板与摄像头输入三路处理4.1 从文件路径识别最基础的场景文件识别是三个场景里最简单的用BitmapLuminanceSource直接走文件读取。要注意的是 ZXing 的BitmapLuminanceSource构造函数在 .NET 里对图片格式有要求如果Bitmap的 PixelFormat 是 Indexed 或 1bpp内部转换逻辑有时会异常。稳妥做法是先把图片转成 24bppRgb 再交给 ZXingpublic static string DecodeFromFile(string filePath) { using (var bmp new System.Drawing.Bitmap(filePath)) { // 转成 24 位 RGB避免索引像素格式在 ZXing 内部转换出错 using (var rgbBmp bmp.Clone(new Rectangle(0, 0, bmp.Width, bmp.Height), System.Drawing.Imaging.PixelFormat.Format24bppRgb)) { var source new BitmapLuminanceSource(rgbBmp); var reader new BarcodeReaderGeneric { AutoRotate true, Options new DecodingOptions { TryHarder true, PossibleFormats new ListBarcodeFormat { BarcodeFormat.QR_CODE } } }; var result reader.Decode(source); return result?.Text; } } }这里bmp.Clone转格式是有代价的大图会多占一份内存。但二维码识别场景中图片通常就几百 KB转一下无感。如果你处理的图特别大比如 4000x3000 的手机照片建议先缩放到 800 宽再识别ZXing 识别速度会快很多而且不会因缩放丢失二维码信息。另一个细节Decode方法返回null时不抛异常所以result?.Text这个空值传播写法很安全。不要写成result.Text直接取识别不到时就是NullReferenceException。4.2 从剪贴板截图识别处理 WPF 的流生命周期工具类项目里最常见的场景是用户截屏或复制图片然后粘贴到程序里识别。WPF 里从剪贴板拿图片有两种形态BitmapSource或者System.Drawing.Bitmap取决于复制图片时数据源。统一转成BitmapSource处理最省事public static string DecodeFromClipboardImage(BitmapSource source) { // 转成 BGRA 字节数组 var stride source.PixelWidth * 4; var pixels new byte[stride * source.PixelHeight]; source.CopyPixels(pixels, stride, 0); // 注意这里需要把 BGRA 重排成 RGB var rgbPixels BgraToRgb(pixels); var luminance new RGBLuminanceSource(rgbPixels, source.PixelWidth, source.PixelHeight); var reader new BarcodeReaderGeneric { AutoRotate true, Options new DecodingOptions { TryHarder true } }; return reader.Decode(luminance)?.Text; } private static byte[] BgraToRgb(byte[] bgra) { var rgb new byte[bgra.Length / 4 * 3]; for (int i 0, j 0; i bgra.Length; i 4, j 3) { rgb[j] bgra[i 2]; // R rgb[j 1] bgra[i 1]; // G rgb[j 2] bgra[i]; // B } return rgb; }这里面的CopyPixels很关键。BitmapSource是 WPF 的只读 ImageSource你不能直接拿它的Pixels必须通过CopyPixels(byte[], stride, offset)把像素拷到托管数组里。数组大小是stride * height而stride等于像素宽 * 4注意满 32 位对齐在某些格式下 stride 会大于像素宽乘以通道数所以别手算直接按source.PixelWidth * 4算同时把PixelWidth而不是Width传出去因为Width是 DIP 单位PixelWidth才是真实像素数。剪贴板识别的性能问题如果复制的是一张 1920x1080 全屏截图直接把整个图给 ZXing 解码耗时可能几百毫秒。这里有两个优化方向一是缩小图像再解码二是先用低分辨率快速解码一次失败再试原图。ZXing 对二维码的定位依赖模块的清晰边界过度缩小反而会失败。实测截图场景缩放到 50% 通常还能识别再小就悬了。所以别一刀切可以写两遍解码策略。4.3 摄像头实时识别从 WriteableBitmap 取帧摄像头场景是最能体现demo 变工具价值的但 WPF 没有官方摄像头控件一般用CaptureElement或走MediaCapture把帧打到WriteableBitmap上。取帧识别时核心是把WriteableBitmap的像素同步拷出来public static string DecodeFromWriteableBitmap(WriteableBitmap wb) { var stride wb.PixelWidth * 4; var buffer new byte[stride * wb.PixelHeight]; // 加锁防止摄像头线程在 CopyPixels 期间写像素 wb.Lock(); try { Marshal.Copy(wb.BackBuffer, buffer, 0, buffer.Length); } finally { wb.Unlock(); } var rgb BgraToRgb(buffer); return new BarcodeReaderGeneric { AutoRotate true, Options new DecodingOptions { TryHarder false } // 实时识别要控制单帧耗时 }.Decode(new RGBLuminanceSource(rgb, wb.PixelWidth, wb.PixelHeight))?.Text; }wb.Lock()和wb.Unlock()这对锁是很多人忽略的。摄像头帧率如果是 30fps后台线程可能同时在往BackBuffer写数据你CopyPixels读到一半像素被覆盖轻则花屏重则越界。Lock不是拿互斥锁那种重量级操作它是短暂禁止前端渲染访问BackBuffer开销很小。实时识别里有个常见的性能误区TryHarder true是给静态图用的视频流里每一帧都 TryHarder 会让算法在上百种扫描路径里撞运气CPU 占用直接拉满。实时场景把TryHarder关掉配合上一帧失败时跳过下一帧的节流逻辑体验反而稳定。5. 排查与避坑ZXing 在 WPF 场景的五个翻车点5.1 生成的 PNG 文件损坏或打不开现象程序跑完保存的 PNG 文件双击提示无法打开图片。原因PngBitmapEncoder.Save之后没有 Flush或者FileStream在写入中途被 GC 回收。更隐蔽的原因是BitmapSource未 Freeze跨线程使用时渲染引擎延迟写。解决保存方法里用using包住FileStream保存后显式调Flush()。我一般还会在Save后检查File.Exists和文件长度长度小于 500 字节基本就是写失败了using (var fs File.Create(path)) { encoder.Save(fs); fs.Flush(true); // Flush 到磁盘 }5.2 中文内容生成的码扫出来是乱码现象内容里含中文扫码结果出现好之类的乱码。原因编码不一致。Windows 上默认编码可能是 GBKZXing 输入端字符串的编码和它内部写的字符集不对齐。解决QrCodeEncodingOptions里强制CharacterSet UTF-8并且确保传入字符串不是从byte[]用默认编码转出来的。如果内容是从文件读的用File.ReadAllText(path, Encoding.UTF8)而不是不带编码参数的重载。5.3 相机实时识别时接缝处崩异常现象WriteableBitmap.BackBuffer的stride和像素宽不匹配CopyPixels抛ArgumentException。原因WriteableBitmap创建时指定了非 32 位对齐的格式比如PixelFormats.Bgra32在某些分享屏幕场景下 stride 会带填充位。你拿PixelWidth * 4算 stride实际系统要求(PixelWidth * 4 3) / 4 * 4。解决不要手算直接用wb.BackBufferStridevar stride wb.BackBufferStride; var buffer new byte[stride * wb.PixelHeight];这行代码以后写 Dead code 都不用担心。5.4 BGRA 字节序直接塞给 ZXing 导致识别率骤降现象文件识别一切正常剪贴板和摄像头识别时二维码就在画面正中央但Decode返回 null。原因RGBLuminanceSource需要 RGB 字节序WPF 的Bgra32是 BGRA 字节序直接塞进去颜色通道错位灰度化后二维码图形明暗关系被破坏。被破坏的程度不一定肉眼可见但 ZXing 的二值化阈值就找不准了。解决拷贝像素后做通道重排。我上面写的BgraToRgb是朴素版本还可以用unsafe指针优化但 90% 场景下朴素版本足够。真正要优化的不是通道重排本身而是减少不必要的byte[]拷贝次数。5.5 长内容或特殊字符生成时抛 WriterException现象内容超过 1000 字符时Write()抛WriterException提示Data too big。原因二维码 Version 最多到 40能容纳的字节数有限而且纠错级别越高容量越小。你没指定 Version 时 ZXing 会按最小 Version 编码内容塞不下就抛异常。解决捕获WriterException提示用户或者自动降级纠错级别重试try { return writer.Write(content); } catch (WriterException ex) when (ex.Message.Contains(too big)) { writer.Options.ErrorCorrection ErrorCorrectionLevel.L; return writer.Write(content); }降级到 L 之后如果还抛异常说明内容长度超出了二维码的理论上限这时候应该提示用户内容太长而不是继续尝试。6. 从 demo 到工具的最后一步生成后回读校验demo 能跑通和工具能用之间差一个自检环节——生成二维码后立即回读解析确认内容一致。这是我做工具类项目的一个习惯出码不是终点码能被人扫出来才是。public static BitmapSource GenerateQrCodeWithVerify(string content) { var bitmap GenerateQrCode(content, 300, 2); // 回读校验把生成的图再解码一次 var pixels CopyPixelsFromBitmapSource(bitmap); var decoded DecodeQrCode(pixels, bitmap.PixelWidth, bitmap.PixelHeight); if (decoded ! content) { throw new InvalidOperationException($二维码自检失败输入和输出不一致。输入长度 {content.Length}解码长度 {decoded?.Length}); } return bitmap; }这个校验有两个价值。第一它能抓到编码问题——比如你写了CharacterSet UTF-8但传进去的字符串其实被转码器破坏了生成时看不出问题回读解码时内容对不上。第二它能间接验证尺寸和纠错级别的搭配——如果生成端把二维码渲染得过于紧凑TryHarder true的模式下仍然能解出来但你用微信扫可能就费劲了。自检注意控制粒度不是每次出码都必须做如果工具是批量生成几百个二维码每次都回读一次解码整体耗时可能翻倍。一般我会在开发调试阶段强制开启正式批量生成时留下一个调试开关。还有一个经验是回读校验时不要用TryHarder用默认参数扫一遍就行因为默认参数更接近真实扫码枪的行为。从那以后我每次把二维码功能交付出去之前都会强制走一遍生成 → 回读 → 对比内容这个流程连带着把CharacterSet、Margin、ErrorCorrection这几个参数也顺手检查一遍。很多莫名奇妙的别人扫不出来问题其实在这一步就能提前拦下来。希望这套拆解对你也有帮助。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑