C# WPF二维码生成与识别实战:基于QRCoder和ZXing.Net的完整方案

发布时间:2026/9/7 9:53:36
C# WPF二维码生成与识别实战:基于QRCoder和ZXing.Net的完整方案 简介面向C# WPF开发者的二维码生成与识别示例项目基于开源库Zxing.Net生成QR Code同时引入EPFMediaKit.dll处理图像采集与识别覆盖从界面输入到扫码识别的完整链路。资源共148个文件压缩包约22.48MB内容以35个dll运行库、31个pdb调试符号、29个xml文档/配置、12个cs源码文件为主体配合xaml界面、config配置、sln解决方案、可执行exe及编译缓存工程结构完整可直接打开对照学习或二次修改。已有1001人学习下载。通过本包可掌握BarcodeWriter对象创建、QrCodeEncodingOptions参数设置、Bitmap与WPF Image控件绑定等关键写法也能参考从摄像头或图片中提取并识别二维码的整合思路完整源码、依赖库、符号文件与界面编译产物均包含在内便于排查引用问题并理解WPF编译流程适合有WPF基础、希望快速在业务工具或演示项目中加入二维码交互的开发者。1. 项目概述与核心思路1.1 需求拆解一个二维码Demo为什么值得认真做有朋友问我能不能搞一个二维码生成和识别的Demo我说这玩意儿看着不起眼做起来坑是真不少。尤其是放在C# WPF这种桌面技术栈里很多人以为调个库就完事了结果一上手就遇到图像格式转换、识别率不稳定、扫码枪联动这些幺蛾子。这个Demo能做什么简单说就是两个功能一是把文本、链接、设备编码等信息生成二维码图片并保存二是从本地图片或摄像头画面中识别二维码内容并解析出来。放在实际场景里这几乎是C#上位机开发的高频需求——生产线的产品追溯码识别、仓库的物料标签扫描、设备参数扫码录入全都能落到这两个核心能力上。适合谁来参考如果你正在做WPF上位机软件、需要对接扫码枪做数据录入、或者想在桌面端做一个轻量级二维码工具箱这篇内容可以直接给你一条完整的实现路径。我自己在做的过程中踩了不少坑比如WPF的BitmapSource和传统Bitmap互转、ZXing解码时像素格式的选择、还有扫码枪当键盘输入时的焦点问题这些都会一一说到。1.2 技术选型QRCoder和ZXing.Net这对组合怎么选二维码生成和识别的生态里C#可用的库不少但我最终选了QRCoder做生成、ZXing.Net做识别这个组合在当前社区里算是非常成熟且顺手的方案。先看QRCoder。这是一个纯C#实现的二维码生成库不需要依赖原生组件NuGet直接拉取就能用。它支持生成标准的QR Code可以设置像素大小、容错级别还支持嵌入Logo图标。最关键的是它输出的是Bitmap对象在WPF里稍微转一下就能绑定到Image控件上非常方便。ZXing.Net则是Java世界ZXing库的C#移植版功能覆盖条码和二维码的解码支持从Bitmap、BitmapSource、byte数组等不同数据源解码。它内部封装了灰度化、二值化、定位、纠错这些算法我们需要做的只是把图像数据喂给它然后接收解码结果。有人可能会问为什么不直接用同一个库搞定生成和识别理论上ZXing.Net也能生成二维码但QRCoder在自定义能力上更灵活比如设置Logo位置、前景色背景色而ZXing.Net的生成能力相对基础。反过来QRCoder不擅长识别所以让它俩各司其职生成用QRCoder、识别用ZXing.Net是在项目实践中比较合理的搭配。1.3 整体架构设计三块逻辑各管各的整个Demo的架构不需要搞得太复杂但也不能乱写一通。我按职责划分成三个模块界面交互层、二维码服务层、图像数据处理层。界面层就是WPF的XAML和Code-Behind负责按钮点击、文本框输入、图片展示这些事。二维码服务层封装生成和识别的核心逻辑对外暴露两个方法GenerateQrCode(string content)和DecodeQrCode(Bitmap image)。图像数据处理层专门处理WPF和传统图像API之间的格式转换问题这是整个项目里最容易翻车的地方单独抽出来方便统一排查。这个分层的好处很明显以后想在接口里复用二维码能力或者把界面换成其他框架服务层和数据层可以直接搬走不用动业务逻辑。2. 环境准备与项目搭建2.1 开发环境和依赖包安装开发这个Demo我用的环境是**.NET 6 Visual Studio 2022**。为什么选.NET 6而不是.NET Framework 4.8因为WPF在.NET Core时代已经全面支持了而且.NET 6是LTS版本后续维护升级都方便。当然如果你手头项目还在用.NET Framework 4.7.2下面这套代码同样适用因为QRCoder和ZXing.Net两个库都支持多目标框架。创建项目的时候选WPF应用程序模板目标框架选.NET 6.0然后打开NuGet包管理器安装两个依赖QRCoder最新稳定版当前是1.4.3ZXing.Net当前推荐0.16.9安装完成后在项目文件里能看到引用项。这里有个细节需要注意ZXing.Net的包名是ZXing.Net不是ZXing别装错了。另外还有个大神封装过的ZXing.Net.Bindings.*系列专门对接不同图像库但基础版已经够用暂时不用管。2.2 界面布局设计一个窗口两个功能区界面我设计成左右分栏左边是生成区右边是识别区中间用GridSplitter分割方便拖拽调整宽度。生成区放一个TextBox输入内容、一个ComboBox选容错级别、一个按钮触发生成、一个Image控件展示生成的二维码。识别区放一个Button加载本地图片、一个TextBox显示识别结果、一个Image控件显示待识别的图片。XAML布局粗略是这样的Grid Grid.ColumnDefinitions ColumnDefinition Width2*/ ColumnDefinition Width5/ ColumnDefinition Width3*/ /Grid.ColumnDefinitions !-- 左生成区域 -- StackPanel Grid.Column0 Margin10 TextBlock Text输入内容/ TextBox x:NameTxtContent Height80 TextWrappingWrap AcceptsReturnTrue VerticalScrollBarVisibilityAuto/ ComboBox x:NameCmbLevel Margin0,10,0,0 SelectedIndex2/ Button x:NameBtnGenerate Content生成二维码 Height36 Margin0,10,0,0 ClickBtnGenerate_Click/ Image x:NameImgQrCode Margin0,10,0,0 StretchUniform MaxHeight240/ /StackPanel !-- 分割条 -- GridSplitter Grid.Column1 Width5 HorizontalAlignmentStretch Background#DDD/ !-- 右识别区域 -- StackPanel Grid.Column2 Margin10 Button x:NameBtnLoadImage Content加载图片识别 Height36 ClickBtnLoadImage_Click/ Image x:NameImgSource Margin0,10,0,0 StretchUniform MaxHeight240/ TextBlock Text识别结果/ TextBox x:NameTxtResult Height60 IsReadOnlyTrue TextWrappingWrap/ /StackPanel /Grid容错级别ComboBox的选项直接用枚举绑定在窗口构造函数里塞进去就行CmbLevel.ItemsSource Enum.GetValues(typeof(QRCodeGenerator.ECCLevel)); CmbLevel.SelectedIndex 2; // 默认Q级这样界面部分就搭完了跑起来能看到一个干净的操作面板功能按钮都在该在的位置后面的核心逻辑往事件里填就行。3. 二维码生成模块的实现3.1 生成核心代码与参数含义生成二维码的核心逻辑不复杂QRCoder把底层编码、掩码、纠错全给封装了我们只需要关心传什么参数、要什么输出。直接看代码using QRCoder; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Windows.Media.Imaging; private void BtnGenerate_Click(object sender, RoutedEventArgs e) { string content TxtContent.Text.Trim(); if (string.IsNullOrEmpty(content)) { MessageBox.Show(请输入要生成的内容); return; } using (var generator new QRCodeGenerator()) { var eccLevel (QRCodeGenerator.ECCLevel)CmbLevel.SelectedIndex; var data generator.CreateQrCode(content, eccLevel); using (var qrCode new QRCode(data)) { using (Bitmap bitmap qrCode.GetGraphic(20)) { ImgQrCode.Source BitmapToBitmapSource(bitmap); } } } }细说一下每个参数的含义。CreateQrCode的第一个参数是待编码的内容可以是普通文本、URL、Wi-Fi配置字符串、vCard名片信息等QRCoder不限制内容的格式它只负责把字符串变成二维码矩阵。第二个参数是容错级别枚举值有L、M、Q、H四个等级从低到高分别能容忍约7%、15%、25%、30%的码面损坏。GetGraphic(20)里的20是每个模块的像素大小。二维码本身是一个由若干个黑白小方块组成的矩阵比如一个存20个字符的二维码可能是33×33的矩阵乘以像素大小20生成的图片就是660×660。这个值不是越大越好大了图片体积大、小型化困难小了扫不出来——扫码设备通常要求二维码在屏幕上占据足够大的物理尺寸。如果是打印标签建议20到30之间如果只是屏幕上展示15到20就够。3.2 生成效果的几个优化细节容错级别怎么选很多人无脑选H级其实不对。容错级别越高二维码能承载的数据量越小。同样一段长文本H级可能比L级多好几行模块生成出来的码面更密反而不好扫。我的经验是普通场景用M级或Q级如果二维码要印在包装上、可能被遮挡或磨损再考虑H级。Demo里默认选了Q级就是综合了数据容量和抗损坏能力的平衡点。要不要嵌LogoQRCoder支持在二维码中心嵌入图片调用GetGraphic时传入一个Bitmap作为Logo即可。但这里有个坑嵌入Logo会遮挡中心区域的数据模块如果容错级别不够高扫码设备可能直接报废。嵌Logo时建议至少用Q级容错且Logo大小控制在二维码总边长的五分之一以内。这个经验是从实际项目里折腾出来的——最开始嵌了一个三厘米的Logo到五厘米的码里结果自家扫码枪都认不出来。颜色和反白效果怎么实现GetGraphic方法支持传入前景色和背景色参数比如生成蓝底白字的二维码。可以设置背景色为Color.White或透明。但务必保证前景色和背景色对比度足够高否则识别率会断崖式下跌。工业扫码枪基本只认黑白高对比花里胡哨的配色在演示时好看到了产线上容易坑人。3.3 生成结果导出与保存用户生成二维码之后往往需要保存成图片文件。在Demo里我加了一个右键保存的功能核心逻辑是先用SaveFileDialog选定路径再把Bitmap保存为PNG格式private void SaveQrCodeToFile(Bitmap bitmap) { var saveDialog new SaveFileDialog { Filter PNG图片|*.png|JPG图片|*.jpg|BMP图片|*.bmp, FileName $QRCode_{DateTime.Now:yyyyMMdd_HHmmss}.png }; if (saveDialog.ShowDialog() true) { ImageFormat format saveDialog.FilterIndex switch { 1 ImageFormat.Png, 2 ImageFormat.Jpeg, _ ImageFormat.Bmp }; bitmap.Save(saveDialog.FileName, format); MessageBox.Show(保存成功); } }这里注意一个细节PNG是无损压缩适合保存带精细边缘的二维码推荐优先选PNG。JPG是有损压缩高压缩比下二维码边缘会产生伪影可能导致识别失败。如果一定要用JPG保存时把质量参数调到90以上。4. 二维码识别模块的实现4.1 从本地图片识别二维码识别功能的入口是加载图片然后调用ZXing.Net解码。核心代码如下using ZXing; using ZXing.Common; private void BtnLoadImage_Click(object sender, RoutedEventArgs e) { var openDialog new OpenFileDialog { Filter 图片文件|*.png;*.jpg;*.jpeg;*.bmp;*.gif }; if (openDialog.ShowDialog() true) { BitmapImage source new BitmapImage(); source.BeginInit(); source.UriSource new Uri(openDialog.FileName); source.CacheOption BitmapCacheOption.OnLoad; source.EndInit(); ImgSource.Source source; DecodeQrCode(openDialog.FileName); } } private void DecodeQrCode(string imagePath) { using (Bitmap bitmap new Bitmap(imagePath)) { var reader new BarcodeReader(); var result reader.Decode(bitmap); TxtResult.Text result?.Text ?? 未识别到二维码; } }BarcodeReader是ZXing.Net的入口类Decode方法接收Bitmap或BitmapSource内部会自动进行图像预处理然后尝试定位和解码每一个二维码区域。Result对象里面有Text解码内容、ResultPoints定位点坐标、BarcodeFormat条码类型等信息。这里有个有意思点ZXing.Net的识别不要求整张图都是二维码它会自动扫描整幅图像去找可能的条码区域。所以哪怕二维码只占图片左上角一小块理论上也能识别出来。但如果二维码太小或旋转角度太大识别率会降低。4.2 识别模块最大的坑BitmapSource和Bitmap互转写WPF的朋友应该已经意识到了上面代码里我用了System.Drawing.Bitmap但WPF的Image控件需要的是BitmapSource这俩在类型体系里是两套东西。更麻烦的是从摄像头或其他数据源拿到的往往是BitmapSource如果不想保存成临时文件再读取就得学会内存中互转。我在Demo里写了一个转换函数生成和识别两个方向都用它public static BitmapSource BitmapToBitmapSource(Bitmap bitmap) { var hBitmap bitmap.GetHbitmap(); try { return Imaging.CreateBitmapSourceFromHBitmap( hBitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); } finally { DeleteObject(hBitmap); } } public static Bitmap BitmapSourceToBitmap(BitmapSource source) { using (var stream new MemoryStream()) { var encoder new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(source)); encoder.Save(stream); return new Bitmap(stream); } } [DllImport(gdi32.dll)] private static extern bool DeleteObject(IntPtr hObject);BitmapToBitmapSource用GetHbitmap拿到GDI句柄再通过Imaging.CreateBitmapSourceFromHBitmap转成WPF可以用的BitmapSource。务必记得用DeleteObject释放句柄否则会造成GDI资源泄漏长时间运行的程序会越用越卡这是老WPF开发都懂的坑。BitmapSourceToBitmap就简单了用PngBitmapEncoder把BitmapSource编成内存流再new一个Bitmap读取。这个方案是万金油什么格式的BitmapSource都能转代价是多了几毫秒的编解码时间对桌面交互程序来说完全感知不到。4.3 摄像头实时识别的思路扩展Demo里做的是静态图片识别但实际产线上更多是摄像头实时扫码。我给Demo留了一个扩展思路用OpenCV或Windows Media Foundation抓取摄像头帧每帧转成BitmapSource再调用ZXing解码。为了性能不用每帧都解码可以设置一个定时器每200毫秒取一帧做识别避免CPU满载。这里直接上简化后的定时器逻辑private DispatcherTimer _timer new DispatcherTimer(); private void StartCameraScan() { _timer.Interval TimeSpan.FromMilliseconds(200); _timer.Tick (s, e) { // 从摄像头拿到当前帧 BitmapSource frame using (Bitmap bitmap BitmapSourceToBitmap(frame)) { var result new BarcodeReader().Decode(bitmap); if (result ! null) { TxtResult.Text result.Text; _timer.Stop(); } } }; _timer.Start(); }这个方案的问题在于摄像头厂商的SDK各不相同Demo里没法覆盖所有型号。但逻辑是通用的拿帧、转格式、丢给ZXing解。真正落地时你只需要替换从摄像头拿到当前帧这一行代码改成你们的工业相机SDK接口就行。4.4 扫码枪触发事件联动热词里频繁出现扫码枪触发事件这里展开讲一下。市面上大多数USB扫码枪默认是HID键盘模式说白了就是它在电脑眼里是一个键盘扫码结果相当于是以极快速度录入的一串按键。集成方式有两种。第一种最省事把扫码枪焦点定位到某个TextBox上扫码枪扫完自动输入一串字符并回车监听TextBox的KeyDown或PreviewTextInput事件拿数据。代码上这样处理private void TxtScanner_KeyDown(object sender, KeyEventArgs e) { if (e.Key Key.Enter) { string barcode TxtScanner.Text.Trim(); if (!string.IsNullOrEmpty(barcode)) { TxtResult.Text barcode; TxtScanner.Clear(); } e.Handled true; } }第二种是全局捕获模式扫码枪在WPF窗体外也能触发走全局键盘钩子或者使用Raw Input API。全局钩子相对复杂牵扯到Window消息循环和Hook生命周期管理但好处是不需要用户手动点击任何输入框焦点在哪儿都能扫。我实际做过一个MES工位软件扫码枪是Honeywell的软件里放了五个扫入点用户希望不管当前界面焦点落在哪扫码都能自动进入当前激活的文本框。最后就是用全局键盘钩子实现的。不过这种方案在Demo阶段没必要上先把文本框触发模式跑通够用且省事。5. 常见问题与排查技巧实录5.1 问题速查表实操过程中遇到过不少问题整理成一个速查表遇到类似情况可以直接对号入座现象可能原因解决方案生成时报错“内容过长”二维码容量超出单码上限改用压缩编码如缩短URL、拆分成多个二维码、降低容错级别识别结果乱码编码格式不匹配确认生成时是UTF-8识别时也走UTF-8解析二维码生成后扫不出模块像素设置太小或对比度不足增大GetGraphic参数检查前景色背景色对比度摄像头识别频繁漏扫帧率太高或图像模糊降低识别频率、增加自动对焦、提高分辨率裁剪区域BitmapSource转Bitmap后为null位图已Dispose转换时不要提前释放源对象程序长时间运行GDI句柄暴涨没有释放GetHbitmap的句柄用DeleteObject释放5.2 识别率不稳定的几个隐蔽原因识别率不稳定是最让人头疼的。我在调试过程中发现几个平时很难察觉的坑图像分辨率太高反而是负担。从专业相机或高像素手机拍的图片动辄4000×3000像素ZXing解码时预处理时间长得离谱有时还会因为边缘锯齿化反而解不出来。解决办法是在解码前先缩放图像控制在1000像素以内很多情况下识别率和速度都会提升。ZXing的TryHarder模式要按场景开。BarcodeReader有个TryHarder属性设为true时会花更多时间在兜底扫描策略上对模糊、低质量的图片有效果但对规则清晰的图片反而会拖慢速度。我的经验是静态图片识别可以开TryHarder摄像头实时识别场景关掉帧率差别很明显。二维码边缘留白太窄。QR Code规范要求二维码四周必须保留至少4个模块宽度的静区白色区域但在界面展示时很多人喜欢把二维码撑满控件静区被裁剪掉了导致识别困难。生成图片时我在GetGraphic后再加一圈白边问题立刻解决private Bitmap AddQuietZone(Bitmap src, int marginInPixels) { var newBitmap new Bitmap(src.Width marginInPixels * 2, src.Height marginInPixels * 2); using (var g Graphics.FromImage(newBitmap)) { g.Clear(Color.White); g.DrawImage(src, marginInPixels, marginInPixels); } return newBitmap; }这个AddQuietZone函数是我自己加的原版QRCoder生成的图片默认有静区但如果你用GetGraphic的某些重载方法自定义过边距就要小心静区被调没了。5.3 老项目升级WPF版本时的兼容性热词里面有“.NET 8.0调用WinForm .NET Framework 4.6库”这种问题我顺便提一嘴。WPF项目可以引用经典.NET Framework类库但不一定能直接引用取决于目标框架的兼容性。一般而言.NET 6以上项目引用.NET Framework库时如果目标库只用了核心API可以正常编译运行一旦用了WPF/WinForms专属且没被兼容的API就会编译失败。这类兼容性问题的排查思路很简单先看编译错误信息确认是缺少引用还是API不存在。然后优先找NuGet上的替代包比如把System.Drawing.Common换成NuGet版、把老的硬件SDK封装成独立进程通过进程间通迅调用这些都是在实际项目里验证过的方案。6. 扩展方向与经验心得6.1 从Demo到产品化要补哪些能力Demo跑通只是第一步。如果要做成真正能上产线的工具还需要补齐这些东西批量识别能力。同一个画面里有多个二维码BarcodeReader默认只返回第一个识别到的结果。如果想识别全部可以用DecodeMultiple方法返回数组。这在流水线分拣场景下特别有用。自定义解码区域。在摄像头画面里划定一个扫码区域ROI只对这块区域做解码能大幅提升识别速度和准确率。ZXing.Net支持的Rect裁剪方案可以提前把Bitmap裁剪好再送进解码器。连续扫码防重逻辑。产线上同一张条码可能被摄像头扫到很多帧如果每次都触发事件系统会被刷屏。一般要做一个短时间去重的逻辑比如3秒内识别到相同内容只触发一次。日志和调试信息。产品化一定要记录识别日志每张图的识别耗时、识别结果、失败原因。遇到识别率低的批次有日志才能定位是图像问题、光照问题还是解码参数问题。6.2 个人实操中的一些体会做这个Demo的过程中我最大的感触是二维码相关的库已经把复杂算法都解决了真正的难点反而在图像处理和工程整合上尤其是WPF和传统图像API之间的类型转换、资源和句柄的生命周期管理。从实际使用来说QRCoder加ZXing.Net这套组合在稳定性和社区活跃度上都对得起我最初的预期。后来我在两三个上位机项目里持续使用这套方案配合海康等工业相机、基恩士扫码枪使用识别率都能稳定在99%以上说明这条路走得通。最后再分享一个小技巧Debug模式下可以在识别前把待处理的图片临时保存到本地那一帧到底长什么样、有没有反光、有没有失焦肉眼一看就清楚了。很多时候排查识别率问题靠的就是这一张临时导出的现场截图比对着代码猜效率高得多。本文还有配套的精品资源点击获取