
简介面向C# WPF开发者的二维码生成与识别示例项目基于Zxing.Net二维码库与EPFMediaKit多媒体库实现适合需要在桌面应用中集成条码或二维码功能的初中级开发人员也适合准备实现扫码登录、电子票券、物料管理等场景的开发者作为参考不论用于项目改造还是功能验证都有实际帮助。项目是一个完整的WPF客户端工程演示了从生成到识别的完整流程通过BarcodeWriter配置二维码格式与尺寸后生成位图并在界面中显示同时演示了借助多媒体库从本地图片或摄像头画面读取二维码的扩展方式界面交互上给出按钮触发、图像展示与结果反馈等设计参考。整个压缩包共148个文件整体约22.48MB文件类型以动态链接库、调试符号、XML文档、C#源码与XAML界面文件为主其中动态链接库是运行时依赖调试符号和XML文档便于问题排查与接口查阅C#源码和XAML界面文件则是主要学习内容此外还附带配置文件、打包资源与解决方案文件目录组织清晰方便直接编译、阅读和二次开发。目前已有1001人学习。通过该示例可以掌握Zxing.Net在WPF中的实际接入方式学会调整二维码生成参数、将二维码位图绑定到图片控件并了解摄像头或图片输入模式下二维码识别的实现思路还可进一步熟悉BarcodeReader的调用逻辑、异常处理与图像预处理等细节为后续增加批量识别、自定义样式、扫码交互等高级功能打好基础。 我一直觉得WPF做二维码工具是个特别典型的“看着简单做起来一堆事”的需求。网上搜“C# 二维码”出来的Demo十个里有八个是WinForms的或者直接就是一个控件拖上去就完事真要用到WPF里涉及图像格式转换、UI线程、批量解码、摄像头取流这些环节代码一写就各种小毛病。这篇就把我做的一个生成加识别Demo完整拆开讲包含选型理由、核心代码、踩过的坑给后面做上位机、MES追溯、设备标签这类需求的朋友一个可以直接抄的底子。1. 项目选型为什么锁定QRCoder ZXing.Net这套组合先说明一下我这个Demo针对的是Windows桌面端框架是.NET Framework 4.7.2的WPF项目后来也顺手在.NET 6的WPF工程里验证了一遍逻辑基本一致差异点在下面会单独讲。二维码生成和识别其实是两个方向最好分开选库。生成端我用的QRCoder识别端用的是ZXing.Net。为什么不是用一个库全搞定因为ZXing虽然也能生成二维码但生成API的手感和参数控制明显不如QRCoder直观反过来QRCoder没有识别能力。拆成两个专业库各自的坑还更少依赖也更干净。我当时对比过几个方案组件用途优点缺点QRCoder生成API简单支持Logo版本持续更新不能识别ZXing.Net识别支持格式多有Windows兼容绑定配置项多版本间命名空间变化大ThoughtWorks.QRCode生成老牌多年不更新对中文支持一般OpenCV实现识别识别可控性强开发量大不值得结论很直接QRCoder负责把字符串变成BitmapZXing.Net负责把图像变成字符串两个库各管一头组合起来最省心。NuGet安装也顺手提一下Install-Package QRCoder Install-Package ZXing.Net Install-Package ZXing.Net.Bindings.Windows.Compatibility第三个包是关键新版ZXing.Net的Windows专用解码器被拆到了这个绑定包里不装的话BarcodeReader用不了很多人在这卡住。2. 生成模块实操从字符串到WPF窗口中的高清二维码2.1 QRCoder的核心代码与参数理解QRCoder生成二维码的逻辑链条是先通过QRCodeGenerator把内容编译成QRCodeData再用QRCode把数据渲染成图片。数据类型不只是字符串还可以直接生成WiFi配置、网址、联系人名片等结构化内容但最常用的还是普通文本。using QRCoder; QRCodeGenerator generator new QRCodeGenerator(); QRCodeData data generator.CreateQrCode( SN:20240115-A23, QRCodeGenerator.ECCLevel.Q // 纠错级别 ); QRCode qrCode new QRCode(data); Bitmap qrImage qrCode.GetGraphic(20); // 每模块像素数这里面两个参数要理解清楚ECCLevel纠错级别L、M、Q、H四个档Q是25%H是30%。级别越高二维码被遮挡、残缺后越容易识别但码会变得更密。实际做设备标签或产品追溯码我建议直接用Q折中效果最好如果码里内容是长链接或大量中文再考虑L。GetGraphic(int pixelsPerModule)这个参数决定每个二维码模块渲染成几个像素。值越大图片分辨率越高比如20就比10清晰得多。但要注意图片分辨率高不等于识别更稳识别器对“清晰可见的模块边缘”更敏感过大的图反而会让某些算法出现边缘模糊的问题。2.2 从Bitmap到BitmapSourceWPF显示的关键一步QRCoder输出的是WinForms的System.Drawing.BitmapWPF的Image控件不认识它必须转成BitmapSource。这一步是WPF版Demo最容易出问题的地方网上很多老代码直接new BitmapImage()塞过去那是给文件路径用的内存流搞不好就报“无法访问已关闭的流”。using System.Windows.Interop; using System.Windows.Media.Imaging; BitmapSource ConvertBitmapToBitmapSource(Bitmap bitmap) { IntPtr hBitmap bitmap.GetHbitmap(); try { return Imaging.CreateBitmapSourceFromHBitmap( hBitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); } finally { DeleteObject(hBitmap); // 要释放否则GDI句柄泄漏 } } [DllImport(gdi32.dll)] static extern bool DeleteObject(IntPtr hObject);GetHbitmap()拿到的句柄一定要手动释放不释放的话跑几次界面就卡了任务管理器会看到GDI对象数一路涨。finally里删除是底线。2.3 带Logo、白边和前景色的细节控制Demo做到后面客户必然提需求二维码中间要加公司Logo。QRCoder实现这个其实很简单using QRCoder; // 使用专用渲染器 QRCodeGenerator generator new QRCodeGenerator(); QRCodeData data generator.CreateQrCode(text, QRCodeGenerator.ECCLevel.H); var renderer new QRCodeRenderer(data); Bitmap qrImage renderer.GetGraphic( pixelsPerModule: 20, darkColor: Color.Black, lightColor: Color.White, icon: new Bitmap(logoPath), // 中间Logo图片 iconSizePercent: 15, // Logo占整体比例 drawQuietZones: true); // 是否绘制静区四周白边这里有两个经验加了Logo必须把纠错级别调到H不然中间被Logo盖住后算法补不回来扫出来就是废码。drawQuietZones建议保持true二维码四周需要至少4个模块宽度的空白打印时如果没留白边很多扫码枪会识别不稳定不是码本身的问题是静区没了。渲染时还可以用darkColor和lightColor定制颜色比如黑色码放在深色产品上可以把前景改成白色。但要注意改成彩色或浅色前景要慎重扫码设备的红光对某些颜色不敏感打印上去可能识别率骤降。3. 识别模块实操单张、批量与拖拽场景下的解码逻辑3.1 一张图片怎么解出内容ZXing.Net在Windows下的使用流程是拿到图像数据 - 转成LuminanceSource- 交给BarcodeReader解码。WPF里最顺手的路径是把BitmapSource转成RenderTargetBitmap再通过CopyPixels取出像素字节数组。using ZXing; using ZXing.Windows.Compatibility; string DecodeImage(BitmapSource source) { // 转成ZXing需要的像素格式 var luminance new BitmapLuminanceSource(source); var reader new BarcodeReader(); reader.Options.TryHarder true; reader.Options.PossibleFormats new ListBarcodeFormat { BarcodeFormat.QR_CODE }; reader.Options.CharacterSet UTF-8; Result result reader.Decode(luminance); return result?.Text; }新版ZXing里BitmapLuminanceSource位于ZXing.Windows.Compatibility命名空间它可以直接接收BitmapSource不用自己手动做像素拷贝非常省事。老版本有个RGBLuminanceSource只能从byte[]构建得先CopyPixels区别就在这里。Options.PossibleFormats限制为QR_CODE有双重意义一是性能不用去匹配其他条码类型二是准确率防止图片里恰好有别的图案干扰。如果场景是混合扫码既有二维码又有条码这里就不要限制让ZXing自己跑。3.2 拖拽文件和批量识别的交互实现Demo里我加了拖拽识别用户体验一下子就上来了。WPF里实现拖拽只需要三件事窗口或目标区域设AllowDropTrue。监听DragOver检查e.Data.GetDataPresent(DataFormats.FileDrop)。在Drop事件里取文件路径解码更新UI。private void Grid_Drop(object sender, DragEventArgs e) { if (e.Data.GetData(DataFormats.FileDrop) is string[] files) { foreach (string file in files) { BitmapImage img new BitmapImage(new Uri(file)); string decoded DecodeImage(img); if (!string.IsNullOrEmpty(decoded)) { ResultList.Add(new QrResult { FileName file, Content decoded }); } else { ResultList.Add(new QrResult { FileName file, Content 未能识别 }); } } } }批量识别在后台线程跑会更好但要注意BitmapImage不能在非UI线程创建并绑定到UI。我的做法是先把文件路径收集好在线程池里用FileStream加载图片解码后把结果通过Dispatcher.BeginInvoke抛回UI线程更新ListBox或DataGrid。批量识别界面建议直接绑定一个ObservableCollectionQrResult用DataGrid展示文件名和解码内容再提供一个导出CSV的按钮。客户拿这个去做批量盘点比一张一张扫码枪扫效率高太多。3.3 模糊、倾斜、反色的图形能不能救实际使用中图片质量参差不齐有的是手机拍的光线差有的二维码是印在曲面包装上的形变严重。ZXing的TryHarder选项就是为这个设计的它会用更多算法轮次去尝试各种旋转和畸变补偿。反色二维码白码黑底比较特殊。默认情况下ZXing按黑码白底解码反转后识别不了。有一种情况是打印时颜色配置失误造成但客户就是要求识别它。可以这样处理reader.Options.TryInverted true;新版ZXing支持直接开TryInverted会自动尝试反色方案。如果版本比较老不支持就先把图像像素反转生成副本再解码。这个功能建议默认开启开销不大但能救回一批劣质图。需要注意倾斜超过45度的二维码ZXing也经常无能为力。真实场景里遇到这种我的经验是配合OpenCV做透视矫正但这是另一个大工程Demo阶段可以先告知客户“拍摄时尽量正对二维码”不需要过度设计。4. 进阶摄像头实时扫码与扫码枪接入的思路4.1 摄像头取流方案怎么选在WPF里做摄像头实时扫码方案主要分三派AForge.NET老牌稳定但在.NET Core/.NET 6下要额外处理兼容问题。OpenCvSharp跨平台好取帧方便但要把Mat转成BitmapSource代码多一点。Windows.Media.CaptureUWP风格API在纯WPF里用需要搞WinRT互操作坑比较多。我的选择是OpenCvSharp它的VideoCapture在后台线程循环读帧解码结果回传UI线程整个链路很顺。using OpenCvSharp; _capture new VideoCapture(0); _frame new Mat(); _backgroundTask Task.Run(() { while (_capturing) { if (_capture.Read(_frame)) { BitmapSource bs MatToBitmapSource(_frame); string content DecodeImage(bs); if (!string.IsNullOrEmpty(content)) { Dispatcher.BeginInvoke(() { CodeTextBox.Text content; }); } } } });4.2 每帧都解码的开销和节流策略逐帧解码是性能瓶颈一帧1280x720的图ZXing解码一次可能要几十毫秒配上摄像头30帧/秒CPU会被吃满。实际不需要每帧都解加个简单节流Stopwatch watch Stopwatch.StartNew(); while (_capturing) { if (watch.ElapsedMilliseconds 100) continue; // 100ms解一次约10FPS watch.Restart(); // 取帧解码 }实测下来100ms到150ms的间隔很舒服既能保证连续扫码的流畅感又不会把CPU占用顶到100%。对多数扫码场景来说每秒能识别10次已经绰绰有余了。4.3 扫码枪接入把它当键盘事件处理搞上位机的朋友经常遇到扫码枪接入的问题。市面上的USB扫码枪大多模拟键盘输入扫一下等于快速敲一串字符再敲一个回车。WPF里处理这个比串口方案简单得多private void Window_PreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key Key.Enter) { string barcode _barcodeBuffer.Trim(); if (!string.IsNullOrEmpty(barcode)) { // 这里拿到了扫码枪传输的内容 MessageBox.Show($扫码结果{barcode}); } _barcodeBuffer.Clear(); e.Handled true; return; } if (Keyboard.FocusedElement is TextBox ((TextBox)Keyboard.FocusedElement).IsReadOnly) { // 从KeyEventArgs提取输入字符 string input ExtractKeyInput(e); if (!string.IsNullOrEmpty(input)) { _barcodeBuffer input; e.Handled true; } } }核心思路是维护一个字符串缓冲遇到回车就认为一次扫码结束。这里有个细节必须判断当前焦点控件是不是只读TextBox否则扫码枪触发事件时会把你正在输入的内容也吞进缓冲或者干扰正常的键盘输入。很多扫码枪Demo没处理这个结果用户一边打字一边扫码内容全串了。焦点处理方面还可以用一个专门的TextBox接收扫码输入失焦时自动重新获得焦点做成扫码枪模式。这个在产线用很顺手。5. 实践中踩过的坑像素句柄、解码参数与性能细节5.1 Bitmap转BitmapSource的句柄泄漏这个前面已经说了一次但我愿意再强调一次GetHbitmap()返回的句柄绝对要释放。我在做批量生成时没注意一次循环生成500张二维码界面直接卡死。排查后发现问题不是生成慢而是每次循环都在创建GDI句柄但没释放系统的GDI对象上限是10000个跑完就崩。加上DeleteObject后500张轻轻松松。顺带一提如果你用的是.NET Core 3.1以上的WPFBitmap.GetHbitmap()和Imaging.CreateBitmapSourceFromHBitmap依然可用但编译器会提示平台兼容性警告功能上没有问题。5.2 中文内容和特殊字符的编码问题QRCoder写入中文时默认编码是UTF-8ZXing解码时如果Options.CharacterSet没设置或者设成GB2312解码出来就是乱码。我的建议是统一在识别端指定UTF-8reader.Options.CharacterSet UTF-8;但要注意如果你是用别的工具生成的二维码比如微信生成的二维码内容可能是GBK编码这时指定UTF-8反而解不出中文。所以更稳妥的做法是不设置CharacterSet让ZXing自动检测或者解码失败后尝试另一种编码再解一次。// 兜底默认失败后用GBK再试 try { result reader.Decode(luminance); if (result null) return null; if (result.Text.Contains(\uFFFD)) // 含替换字符说明编码不对 { reader.Options.CharacterSet GBK; result reader.Decode(luminance); } }5.3 同一张图时好时坏问题往往出在图像预处理有一种情况排查了很久同一张二维码图片扔进Demo里有时候能解出来有时候提示识别失败。后来发现是因为我用的BitmapLuminanceSource直接吃BitmapSource而BitmapSource的Dpi不对时CopyPixels拿到的数据没问题但ZXing内部对亮度信息的计算会受到像素格式影响。比如灰度图可以正常解PNG的32位带Alpha通道图有时就会异常。解决办法有两种统一转成Bgr32或Pbgra32格式再交给ZXing避免格式差异导致的问题。先缩放图片到合理大小再解码。很多手机拍出来的照片有4000x3000ZXing处理这种大图要么慢要么直接失败。我做了个预处理如果宽度超过1000像素先等比缩放解码速度和成功率反而更好。public static BitmapSource ResizeForDecode(BitmapSource source, int maxWidth 1000) { if (source.PixelWidth maxWidth) return source; double scale (double)maxWidth / source.PixelWidth; int newHeight (int)(source.PixelHeight * scale); var scaled new TransformedBitmap(source, new ScaleTransform(scale, scale)); return scaled; }这个“缩小反而更容易识别”的反直觉结论是我在这个Demo里最大的收获之一。原因在于二维码的模块是离散的黑白块过度放大会让边缘产生大量灰度过渡像素干扰解码算法定位模块边界。5.4 WPF绑定模式下的图像更新WPF的Image控件绑定BitmapSource时很多人踩过这个坑识别完一张图界面上不刷新。原因通常是BitmapSource被赋值但INotifyPropertyChanged没触发或者图片在后台线程创建导致UI线程无法访问。我的习惯是resultImage.Dispatcher.BeginInvoke(new Action(() { resultImage.Source bitmapSource; resultText.Text decodedContent; }));所有图像和文本的更新都通过Dispatcher切回UI线程确保线程安全也避免“调用线程无法访问此对象”的经典报错。最后再分享一个使用技巧这个Demo做完后我又加了一个小功能把识别结果自动汇总到一个CSV文件每次扫码完成就追加一行带上时间戳和来源文件名。这个改动成本很低但实用性翻倍客户做追溯时只需把这个CSV丢进Excel就能筛选汇总。做工具的兄弟可以借鉴这个思路很多时候用户真正需要的不是“能扫码”而是“扫码之后数据怎么进系统”。如果后续你想把这个Demo往生产环境推我建议Priority是先把二维码识别改成后台任务队列避免大批量识别时卡界面再做摄像头扫码的自动对焦和连续识别状态机最后考虑用PrintDialog和FlowDocument把生成的二维码直接送到标签打印机。一步步来这个小工具就能从Demo变成真正能扛业务的模块。本文还有配套的精品资源点击获取