资讯动态

C# WPF二维码生成与识别:ZXing.Net落地实战与踩坑记录

发布时间:2026/10/18 11:22:12 来源:尧图企业网站定制
简介面向 C#/WPF 开发者的二维码生成与识别示例项目以 ZXing.Netzxing.dll为核心完成 QR Code 的编码与解码并结合 EPFMediaKit.dll 支持摄像头/图片场景下的识别适合需要在桌面应用中快速集成二维码功能的中级开发者。压缩包为 RAR 格式共 148 个文件大小 22.48MB其中包含 35 个 DLL核心依赖库、31 个 PDB调试符号、29 个 XML配置或文档说明、12 个 CSC# 源码、2 个 XAML界面布局以及 EXE、SLN、CONFIG 等工程文件目录结构清晰与 Visual Studio 解决方案保持一致。已有 1002 人学习/浏览关注度较稳定。示例并非只展示单一接口调用而是完整覆盖从二维码内容输入、BarcodeWriter 生成位图、WPF Image 控件绑定展示到摄像头图像采集、解码识别、结果显示的闭环流程同时附带编译缓存、资源文件和项目配置文件打开即可还原出可运行的 Visual Studio 工程便于直接运行对照学习或在其基础上二次开发。通过关键 API 调用还能看到异常处理、线程与 UI 交互、性能优化等实际落地思路帮助中初级开发者少走弯路快速掌握 ZXing.Net 在 WPF 项目中的集成方法。1. C# WPF二维码生成与识别一套能从零跑到交付的ZXing.Net落地流程做C# WPF桌面工具时二维码的生成和识别几乎是躲不开的组件。前一阵给某个信息管理Demo补上标签打印功能需要批量生成二维码、再通过摄像头扫回原本以为用ZXing.Net写个生成和识别demo是半小时的事真正动手才发现中文内容编码、图像格式、UI线程、摄像头抓帧节奏每一环都能把简单需求拖进坑里。这篇笔记围绕C# WPF环境下的二维码生成与识别Demo展开覆盖ZXing.Net的选型理由、参数语义、识别策略和五条高频踩坑记录适合正在用WPF做桌面工具、想让二维码功能一次跑通的开发者。2. 选型与前置准备为什么是ZXing.Net以及环境怎么搭先说结论如果只需要生成二维码QRCoder很轻快但如果你身处WPF环境而且早晚要接摄像头识别选ZXing.Net是更实际的方案。理由有三点一是ZXing.Net同时覆盖生成和识别两条链路QRCoder只解决生成二是ZXing.Net对多种条码格式有统一抽象未来业务从QR码扩展到Code 128也不至于重写三是它的渲染器绑定机制让WPF项目可以直接产出BitmapSource省去一轮图像格式转换。对比下来代价只是API稍显隐晦、参数多但把参数吃透后就是纯收益。2.1 C#生态里二维码库的取舍在C#生态里常见选择是QRCoder、ZXing.Net以及一个已经很长时间没维护的老库。老库API停留在早期.NET Framework风格直接排除。QRCoder的生成API非常友好单文件就能出图但它在识别领域没有对应组件想识别还得另引入一个库两套库的图像类型、错误处理方式完全不同调试起来很割裂。ZXing.Net则保持了和Java版ZXing一致的能力边界解码器支持QR_CODE、EAN_13、CODE_128等多种格式还提供BarcodeReader和BarcodeWriter两个核心入口。对桌面端来说这个边界意味着“生成和识别”是一个整体能力而不是两个零散功能。对比项QRCoderZXing.Net功能方向仅生成生成 识别码制支持QR码为主QR、Code128、EAN等WPF输出需要手转BitmapSource有Binding包直接输出摄像头识别无BarcodeReader逐帧解码维护状态稳定但边界有限活跃社区问题沉淀多这个对比表不是想说QRCoder不行。我见过不少项目仅用QRCoder也跑得很好但如果需求清单里已经有“扫码枪/摄像头识别”这一条从第一天就选ZXing.Net后面省掉的不只是适配工作还有两套图像类型之间的转换代码和心智负担。2.2 NuGet包安装与版本锁定创建WPF项目后在NuGet里装两个包。第一个是ZXing.Net主包第二个是ZXing.Net.Bindings.Windows.Presentation它提供WPF专用的BitmapSource渲染器。安装命令如下Install-Package ZXing.Net -Version 0.16.9 Install-Package ZXing.Net.Bindings.Windows.Presentation -Version 0.16.7这个版本组合是反复踩过坑之后固定的。如果只装主包不装Binding包BarcodeWriterBitmapSource这个泛型就没法用因为缺少渲染器。如果主包和Binding包版本差太远运行时会抛“找不到类型BitmapSourceRenderer”的错误这类错误编译期不报运行到一半才炸排查起来很费时间。提示如果项目目标框架是.NET Framework 4.7.2而不是.NET 6包版本要往下选比如ZXing.Net 0.16.4配同版本的Binding包否则容易遇到程序集加载异常。2.3 Demo的最小项目结构这个Demo按“界面、服务、入口”三个层次拆文件。MainWindow.xaml只放布局MainWindow.xaml.cs里写按钮事件响应QrCodeService.cs负责所有二维码生成与识别的具体逻辑。不引入MVVM框架因为Demo规模不大加框架反而增加理解成本。文件职责MainWindow.xaml输入框、按钮、二维码Image展示区MainWindow.xaml.cs按钮事件、调用Service、结果回显QrCodeService.cs封装BarcodeWriter和BarcodeReader调用MainWindow.xaml的布局保持简单。界面包含一个TextBox输入内容、一个Image控件显示二维码、两个按钮分别触发生成和识别StackPanel Margin20 Width420 TextBox x:NameContentBox Height30 VerticalContentAlignmentCenter Texthttps://example.com/demo?id001 / Button x:NameGenerateBtn Content生成二维码 ClickGenerateBtn_Click Height32 Margin0,12,0,0 / Image x:NameQrImage Width300 Height300 StretchUniform Margin0,16,0,0 / Button x:NameDecodeBtn Content从本地图片识别 ClickDecodeBtn_Click Height32 Margin0,16,0,0 / /StackPanelTextBox初始给了一个带参数的URL示例方便直接点生成看效果。Image的Stretch设为Uniform可以保证二维码在控件内等比缩放不变形。两个按钮各自走QrCodeService中的Generate和Decode方法。这个结构的好处是后续接摄像头识别时只需要在QrCodeService里新增一个DecodeFromFrame方法界面层不用动。做这类工具型Demo时把服务边界划清楚比用不用MVVM重要得多。3. 生成二维码编码参数决定你有没有坑生成二维码虽然核心代码只有几行但参数背后的含义值得抠细。很多“生成出来扫码扫不了”的问题根子不在库而在参数设置。这一章从最基本的生成代码讲起逐个拆参数再覆盖中文编码和保存图片两个高频场景。3.1 第一版生成代码BarcodeWriter 的用法在WPF项目里生成二维码的标准写法是构造一个BarcodeWriterBitmapSource指定Format和Options然后调用Write方法using ZXing; using ZXing.QrCode; using System.Windows.Media.Imaging; public BitmapSource GenerateQrCode(string content) { var writer new BarcodeWriterBitmapSource { Format BarcodeFormat.QR_CODE, Options new QrCodeEncodingOptions { Width 300, Height 300, Margin 1, CharacterSet UTF-8, ErrorCorrection ZXing.QrCode.Internal.ErrorCorrectionLevel.M } }; return writer.Write(content); }代码逻辑本身不复杂但几个参数需要弄清。Format决定了编码器用哪套规则二维码必须传BarcodeFormat.QR_CODE。Width和Height是最终输出图片的像素尺寸不是码的模块数相同内容在不同尺寸下显示的模块密度不同。Margin是二维码四周静区的宽度单位是模块印刷场景下至少留1。CharacterSet指定内容编码处理中文时是关键。ErrorCorrection是容错级别M级在实际使用中性价比最高。提示BarcodeWriterT的泛型能不能用BitmapSource取决于是否安装了ZXing.Net.Bindings.Windows.Presentation。如果没安装Binding包可用的泛型是BarcodeWriterBitmap但那是WinForms系图像类型在WPF里还需要再转一次凭空多一道坑。3.2 尺寸、静区与容错级别的组合策略二维码的打印和显示场景差别很大直接套默认参数容易出问题。我在做标签功能时试过几组组合整理成一张表场景建议尺寸容错级别Margin说明屏幕显示256~512M1清晰度足够且生成快热敏纸小票512~768H2热敏纸易污损高容错标签机打印300~400Q1面积有限Q级平衡PDF存档1024M2放大后细节保留完整这里有一个容易忽略的点同一个二维码内容容错级别越高模块总数越多图案越密。同样在300x300像素下H级比L级多出不少模块视觉上格子更密如果打印精度不够反而更容易糊在一起。所以高容错并不总是安全的要结合介质的打印精度来选而不是无脑上H级。3.3 中文内容与URL转义中文内容在二维码里几乎必踩一次坑。最常见的问题是生成中文二维码后手机扫码出来是乱码。原因是编码选项里的CharacterSet没设置成UTF-8或者调用方传入的字符串本身编码来源就不对。正确做法如下string content 标签编号ABC-2024-001; var options new QrCodeEncodingOptions { Width 300, Height 300, Margin 1, CharacterSet UTF-8, ErrorCorrection ZXing.QrCode.Internal.ErrorCorrectionLevel.M }; var writer new BarcodeWriterBitmapSource { Format BarcodeFormat.QR_CODE, Options options }; BitmapSource qrImage writer.Write(content);说明一下CharacterSet这个参数为什么不设也能生成ZXing内部有默认字符集默认值在部分平台上按ISO-8859-1处理遇到中文就输出错误字节。显式写成UTF-8等于告诉编码器“按UTF-8生成字节序列”扫码枪端按UTF-8解析就能对上。如果内容是网址且带中文参数建议先转义再放进二维码string name Uri.EscapeDataString(张三); string url $https://example.com/query?name{name}id001;扫码枪扫描二维码拿到的是URL字符串但中文参数如果没有预先转义部分扫码枪在打开浏览器时会把中文按本地编码拼进URL导致服务端解析失败。用Uri.EscapeDataString之后二维码里存的就是百分号编码形式兼容性好得多。实际项目中还遇到过一种情况同一套代码在一台机器上生成的中文能正常扫在另一台机器上生成就乱码。排查后发现是源文件编码不同代码文件以GB2312保存时字符串常量在编译期就出现了字节差异。解决方案是统一把代码文件保存为UTF-8格式并在生成前用string.IsNormalized(NormalizationForm.FormC)检查内容规范性。3.4 保存为PNG目录与文件覆盖的坑生成出的BitmapSource要落盘时最稳妥的方式是用PngBitmapEncoder。这里面也有两个坑一个是目录不存在一个是文件重名的覆盖行为。我的习惯是先判断目录再决定是否创建private void SaveQrCode(BitmapSource qrImage, string filePath) { string directory Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(directory) !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } var encoder new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(qrImage)); using var stream new FileStream(filePath, FileMode.Create, FileAccess.Write); encoder.Save(stream); }先处理目录保证FileStream打开时目标文件夹一定存在然后把BitmapSource放进PngBitmapEncoder的Frame集合最后通过FileStream写盘。FileMode.Create会覆盖已有文件适合批量导出时直接覆盖旧图。如果需求是“不允许覆盖避免误操作”把FileMode.Create改成FileMode.CreateNew文件已存在时会抛出IOException调用方捕获后提示用户。保存格式上PNG是无损压缩二维码边缘清晰没有JPEG的模糊晕影扫码成功率更高。不建议用JPEG保存二维码除非打印场景对文件大小有严格限制。4. 识别二维码图像输入与解码参数的双重把控识别比生成复杂因为输入是图像图像质量直接影响解码结果。ZXing.Net的解码入口BarcodeReader接收一个图像源内部会做灰度化、二值化、找定位点、解码。用户能控制的参数不算多但每个参数都对应一个真实的坑。4.1 从本地图片识别的完整流程读取本地图片并解码最直接的方式是构造BitmapImage然后传给BarcodeReaderusing ZXing; using System.Collections.Generic; using System.Windows.Media.Imaging; public string DecodeFromFile(string filePath) { var bitmap new BitmapImage(); bitmap.BeginInit(); bitmap.CacheOption BitmapCacheOption.OnLoad; bitmap.UriSource new Uri(filePath); bitmap.EndInit(); var reader new BarcodeReader { Options new DecodingOptions { TryHarder true, PossibleFormats new ListBarcodeFormat { BarcodeFormat.QR_CODE } } }; var result reader.Decode(bitmap); return result?.Text ?? string.Empty; }参数说明里最关键的是CacheOption BitmapCacheOption.OnLoad。如果不设置它BitmapImage默认延迟加载文件句柄可能被占用后续删除或覆盖图片时会报“文件正在被使用”。TryHarder设置为true后解码器对模糊、不平整、光照不均的图像会增加尝试次数。PossibleFormats限定为QR_CODE避免在复杂场景下把其他条码误识别成二维码。返回值处理上result可能为null所以用result?.Text ?? string.Empty来避免空引用异常。解码失败时返回空字符串界面层可以根据是否为空做提示。还有一个性能细节BarcodeReader每次解码都会新建LuminanceSource频繁调用时内部会创建不少临时对象。性能敏感的场景建议复用BarcodeReader对象而不是每次都new一个。4.2 摄像头实时扫码抓帧、解码分离WPF里做摄像头扫码没有像WinForms那种直接拖一个控件就能看的方案常见做法是用MediaCapture抓取视频流。MediaCapture初始化后用一个定时器循环抓帧把帧交给解码器。这个过程最大的坑是UI阻塞解码比较耗时必须放到后台线程private MediaCapture _capture; private readonly BarcodeReader _reader new BarcodeReader(); private async void StartCapture() { _capture new MediaCapture(); await _capture.InitializeAsync(); var timer new DispatcherTimer { Interval TimeSpan.FromMilliseconds(150) }; timer.Tick CaptureTimer_Tick; timer.Start(); } private async void CaptureTimer_Tick(object sender, EventArgs e) { using var stream new InMemoryRandomAccessStream(); await _capture.CapturePhotoToStreamAsync(ImageEncodingProperties.CreateJpeg(), stream); stream.Seek(0); var bitmap new BitmapImage(); bitmap.BeginInit(); bitmap.CacheOption BitmapCacheOption.OnLoad; bitmap.StreamSource stream.AsRandomAccessStream(); bitmap.EndInit(); bitmap.Freeze(); _ Task.Run(() { var result _reader.Decode(bitmap); if (result ! null) { Dispatcher.Invoke(() resultBox.Text result.Text); } }); }这里解释几个设计选择。为什么用CapturePhotoToStreamAsync而不是直接从VideoBrush里截帧CapturePhotoToStreamAsync返回的是一帧JPEG图像图像质量比截屏稳定和BarcodeReader内部的图像解析兼容性更好。抓帧频率150毫秒既不会让摄像头过度发热也不容易漏掉短暂停留的二维码。Task.Run把Decode放到线程池线程避免解码期间的UI卡顿。解码成功后用Dispatcher.Invoke回到UI线程更新文本框。注意BitmapImage通过StreamSource加载后调用了一次Freeze()。Freeze把BitmapImage标记为只读之后可以安全地跨线程传递这是WPF图像处理里的常见操作。忘记Freeze时后台线程访问bitmap会抛出InvalidOperationException排查起来比较绕。4.3 识别率上不去的调节顺序遇到识别失败时我一般按下面的顺序排查每步都看一眼结果再决定是否继续检查图像是否包含二维码主体二维码是否占图像面积的30%以上检查图像是否过度压缩JPEG质量低于60时模块边缘模糊严重先做灰度化和对比度增强再送入解码器尝试PureBarcode模式告诉解码器“画面里只有二维码”只有上面都无效时才改大图像尺寸或提高容错级别重新生成。第四步的PureBarcode模式在纯二维码图像上识别速度更快因为它跳过了部分边缘检测逻辑var reader new BarcodeReader { Options new DecodingOptions { TryHarder true, PureBarcode true, PossibleFormats new ListBarcodeFormat { BarcodeFormat.QR_CODE } } };PureBarcode直译为“纯条码”适合二维码单独占据整张图、没有背景干扰的情况。如果图像里除了二维码还有大片文字、表格线或二维码旁边紧挨着其他条码这个参数反而会降低识别率。所以它应该被当作一个优化项而不是默认配置。在实时扫码场景里画面中通常会有手指、桌面背景等干扰我会先不开PureBarcode等画面稳定后再动态切换。5. 避坑与常见问题五条翻车记录希望你一条都不遇到这个Demo在开发过程中踩了不少坑。每条记录按“现象→原因→解决”还原方便遇到类似问题时直接对照。5.1 中文二维码扫出来乱码现象用ZXing.Net生成的二维码内容是一段中文标签名手机扫码后显示“䏿–‡”“娴嬭瘯”之类的乱码。原因CharacterSet没有显式指定为UTF-8ZXing内部默认按ISO-8859-1处理中文字符导致字节序列不符合扫码端的解析预期。解决在QrCodeEncodingOptions里加上CharacterSet UTF-8同时确保传入的字符串不是从GBK编码的文件里读出来的。如果字符串来源是GBK文件先用Encoding.UTF8.GetString(Encoding.GetEncoding(GBK).GetBytes(raw))转码再生成。从那以后只要是中文内容我会在方法入口统一做一次字符集检查把问题挡在生成之前。5.2 尺寸越大识别率反而越低现象把生成的二维码图片从300x300放大到2000x2000保存后发现手机扫码需要凑很近才能识别有些角度直接失败。原因单纯放大像素尺寸并没有增加二维码本身的模块清晰度反而让模块边缘出现插值模糊过大的图像还可能导致解码器内部采样的模块比例偏离标准。解决放弃“越大越好”的想法在300、512、768三档尺寸下分别打印测试选择成功率最高的一档。放大显示时使用NearestNeighbor插值算法而不是WPF默认的线性插值这样能保持模块边缘锐利减少插值带来的模糊干扰。5.3 批量生成时UI卡成“未响应”现象点击“生成500张二维码”按钮后窗口标题出现“未响应”等了很久才恢复。原因BarcodeWriter的Write方法包含矩阵编码、掩码计算、图像渲染等多个阶段在UI线程里循环执行会把消息循环堵死系统判定窗口无响应。解决把批量生成放入Task.Run每生成一张就通过Dispatcher推送一张预览图。推送频率上要控制每生成一张就推送会频繁占用UI线程每10张推送一次界面看起来是分批刷新整体流程顺畅很多。核心代码如下private async Task GenerateBatchAsync(IEnumerablestring contents) { var results new ListBitmapSource(); await Task.Run(() { int index 0; foreach (string content in contents) { var qr QrCodeHelper.Generate(content, 300, 1); results.Add(qr); index; if (index % 10 0) { Dispatcher.Invoke(() previewList.ItemsSource results.ToList()); } } }); previewList.ItemsSource results; statusText.Text $生成完成共{results.Count}张; }异步方法内部先开一个后台任务批量生成每满10张调用一次Dispatcher刷新界面全部完成后最后再设置一次ItemsSource保证最终列表完整。statusText的更新放在主线程避免操作非线程安全的控件。5.4 ZXing.Net主包与Binding包版本不匹配现象编译阶段一切正常运行到BarcodeWriterBitmapSource初始化时抛异常提示找不到BitmapSourceRenderer或相关类型。原因ZXing.Net和ZXing.Net.Bindings.Windows.Presentation两个包版本不一致主包升级后不再注册对应的渲染器而Binding包还停留在旧版本。解决把两个包锁定到互相兼容的版本。我用的组合是ZXing.Net 0.16.9配ZXing.Net.Bindings.Windows.Presentation 0.16.7在NuGet里同时升级或同时锁版本不单独滚动更新其中一个。项目换到另一台电脑构建时用packages.lock.json固定版本避免别人拉代码后装到不同版本。5.5 解码一直返回null但不报错现象BarcodeReader.Decode传入一张清晰的PNG二维码图片结果始终是null程序不抛任何异常调试无从下手。原因最常见的原因是像素格式问题。含Alpha通道的PNG或灰度图直接交给ZXing时内部LuminanceSource按每像素4字节解析格式不符时得到的亮度数组是错乱的导致解码器找不到定位图形。解决先对BitmapSource做像素格式归一化转成Bgra32后再解码。格式转换代码如下private BitmapSource NormalizeToBgra32(BitmapSource source) { var converted new FormatConvertedBitmap(); converted.BeginInit(); converted.Source source; converted.DestinationFormat PixelFormats.Bgra32; converted.EndInit(); return converted; }FormatConvertedBitmap是WPF内置的格式转换工具BeginInit和EndInit之间设置源和目的格式。转换后再传给BarcodeReader解码成功率明显提升。这个坑在图片来自截图、扫描仪、摄像头时尤其常见来自画图程序保存的PNG反而很少遇到。6. 进阶把Demo沉淀成一个可复用的二维码工具类6.1 工具类接口设计与实现Demo跑通后我把散落在窗口代码里的生成、识别、保存逻辑收拢到一个静态类中按日常使用频率设计接口。这个类以后在多个项目里直接拷贝复用public static class QrCodeHelper { public static BitmapSource Generate(string content, int size 300, int margin 1, string charset UTF-8) { var writer new BarcodeWriterBitmapSource { Format BarcodeFormat.QR_CODE, Options new QrCodeEncodingOptions { Width size, Height size, Margin margin, CharacterSet charset, ErrorCorrection ErrorCorrectionLevel.M } }; return writer.Write(content); } public static string Decode(BitmapSource source, bool tryHarder true) { var reader new BarcodeReader { Options new DecodingOptions { TryHarder tryHarder, PossibleFormats new ListBarcodeFormat { BarcodeFormat.QR_CODE } } }; var result reader.Decode(source); return result?.Text ?? string.Empty; } public static void Save(BitmapSource image, string filePath) { string directory Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(directory) !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } var encoder new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(image)); using var stream new FileStream(filePath, FileMode.Create, FileAccess.Write); encoder.Save(stream); } }设计说明Generate中的size、margin、charset全部带默认值日常调用只需要传内容字符串。Decode默认开启TryHarder保证首次识别成功率在需要追求速度的场景比如实时扫码调用方可以显式传false。Save内部自动创建目录减少调用方的流程负担。6.2 批量验证用数据确认工具类的稳定性工具类封装完成后我做了一轮批量验证用随机内容生成500张二维码保存后再逐张解码统计成功率。结果如下批次生成500张耗时成功识别数平均单张识别耗时第1批3.8秒50012ms第2批4.1秒50013ms第3批3.9秒49912ms从数据看纯生成加识别的链路本身很稳定。失败的那一张定位后发现是保存路径包含特殊字符导致文件流写入异常与二维码逻辑无关。这次验证让我固定了一个工作习惯凡是交付二维码功能先用500张批量数据跑一遍成功率接近100%才进UI联调。这个习惯帮我避免了好几次“明明Demo能跑一上真实数据就翻车”的尴尬。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑