简介:面向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负责把字符串变成Bitmap,ZXing.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到BitmapSource:WPF显示的关键一步
QRCoder输出的是WinForms的System.Drawing.Bitmap,WPF的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 List<BarcodeFormat> { 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里实现拖拽只需要三件事:
- 窗口或目标区域设
AllowDrop="True"。 - 监听
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。
批量识别界面建议直接绑定一个ObservableCollection<QrResult>,用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.Capture:UWP风格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以上的WPF,Bitmap.GetHbitmap()和Imaging.CreateBitmapSourceFromHBitmap依然可用,但编译器会提示平台兼容性警告,功能上没有问题。
5.2 中文内容和特殊字符的编码问题
QRCoder写入中文时,默认编码是UTF-8,ZXing解码时如果Options.CharacterSet没设置或者设成GB2312,解码出来就是乱码。我的建议是统一在识别端指定UTF-8:
reader.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,避免格式差异导致的问题。 - 先缩放图片到合理大小再解码。很多手机拍出来的照片有4000x3000,ZXing处理这种大图要么慢要么直接失败。我做了个预处理:如果宽度超过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变成真正能扛业务的模块。
本文还有配套的精品资源,点击获取