简介:这份资源是面向C#与WPF开发者的二维码生成与识别示例工程,围绕Zxing.Net(zxing.dll)实现QR Code的编码与解码,并涉及EPFMediaKit.dll在图像、摄像头等多媒体场景下的辅助处理,适合希望快速上手桌面端二维码交互的中级开发者参考。压缩包共148个文件,约22.48MB,以35个dll运行库、31个pdb调试符号、29个xml文档说明及12个cs源码为主,另含xaml界面文件、csproj与sln工程配置、exe可执行程序等,构成可直接编译运行的完整解决方案。目前已有1002人学习下载。工程内含BarcodeWriter生成二维码并绑定Image控件的示例代码,以及识别流程的接口调用思路,读者可据此理解编码参数设置、位图输出与UI集成方式,并在此基础上扩展摄像头扫描、错误处理与性能优化等功能。
1. 从一张贴纸说起:这个 WPF 二维码 Demo 到底能干什么
上周帮朋友处理一个仓库盘点的小工具,需求很朴素:把货位编号批量生成二维码贴到货架上,再用摄像头扫一下回填到表格里。他一开始想用网页方案,结果现场那台工控机连浏览器都卡,最后还是回到桌面端。这类“生成 + 识别”两头都要的活儿,C# 配 WPF 其实是最省事的路子,而这个C#_WPF二维码的生成和识别demo正好把两件事都做了:一边用 zxing 把文本编码成 QRcode 图片,一边把图片或摄像头画面解码回字符串。
它适合谁?手上有个 Windows 桌面项目、需要离线跑二维码逻辑的开发者;想学 WPF 里怎么把第三方库接进 UI 线程的新手;以及想拿一个能跑的最小骨架去改造成资产标签、票据核验、登录凭证之类功能的人。整个 Demo 不追求花哨,核心就是 zxing 的编码器和解码器在 WPF 界面里怎么落地,这一点恰恰是很多人第一次接会翻车的地方。
2. 技术选型与工程结构:zxing 怎么塞进 WPF 里
2.1 为什么是 zxing 而不是自己撸编码
二维码看着简单,真要从零写编码器,光是 Reed-Solomon 纠错、掩码评估、版本选择这几步就够喝一壶。zxing(Zebra Crossing)是这套算法里最成熟的实现之一,C# 侧常用的是ZXing.Net这个移植包,它把MultiFormatWriter、BarcodeReader这些类都暴露出来,生成和识别共用一套核心,省得你生成用一个库、识别又换一个库,参数对不上。
在 WPF 项目里接它,常见做法是通过 NuGet 装ZXing.Net,再按需加一个渲染包。生成二维码时,MultiFormatWriter输出的是BitMatrix,这是个纯位矩阵,不是图片。WPF 的Image控件吃的是ImageSource,中间得转一道。很多人第一次卡住就是这里:以为 writer 直接吐 Bitmap,结果发现根本没有。转换的活儿一般交给WriteableBitmap或者BarcodeWriter的 WPF 渲染器来做。
2.2 目录结构长什么样
这类 Demo 的工程结构通常很扁平,我按实际拆过的类似项目给你还原一下,方便你对照自己下载的包:
QrCodeDemo/ ├── QrCodeDemo.sln ├── QrCodeDemo/ │ ├── App.xaml │ ├── App.xaml.cs │ ├── MainWindow.xaml # 主界面:生成区 + 识别区 │ ├── MainWindow.xaml.cs # 事件绑定、调用编解码 │ ├── Services/ │ │ ├── QrGenerator.cs # 封装生成逻辑 │ │ └── QrDecoder.cs # 封装识别逻辑 │ ├── Models/ │ │ └── QrResult.cs # 识别结果载体 │ └── Assets/ │ └── sample.png # 测试用图 └── packages/ 或 obj/ # NuGet 还原产物Services这一层是关键。如果生成和识别逻辑全堆在MainWindow.xaml.cs里,后面想复用到别的窗口就得复制粘贴,血泪经验是尽早抽出来。QrGenerator只负责“字符串进、位图出”,QrDecoder只负责“位图或像素进、字符串出”,UI 层只管拿结果刷新控件,职责一分开,改起来心里有底。
2.3 依赖与目标框架
WPF 项目一般锁在.NET Framework 4.7.2或.NET 6/8的net6.0-windows/net8.0-windows上。选哪个直接影响你装哪个版本的ZXing.Net:老框架配老版本包,新框架配新包,混用会出现System.Drawing引用冲突或者Bitmap类型找不到的编译错误。这一点在还原依赖时就要定死,别中途换。
提示:如果你的项目是
net8.0-windows,注意ZXing.Net某些渲染扩展包对System.Drawing.Common有依赖,而后者在新版 .NET 里对非 Windows 平台有限制。桌面端跑没问题,但别把这个服务层直接搬到跨平台项目里。
3. 生成模块拆解:从字符串到一张能扫的 QRcode
3.1 编码参数怎么定
生成二维码不是“给个字符串就完事”,几个参数直接决定扫不扫得出来。MultiFormatWriter.encode的核心入参是内容、格式、宽高和一个Hint字典。宽高给太小,模块挤在一起,手机镜头对焦都费劲;纠错级别给太低,贴纸磨花一点就废。我一般这么配:
using ZXing; using ZXing.Common; using ZXing.QrCode.Internal; public BitMatrix GenerateMatrix(string content, int width = 300, int height = 300) { var writer = new MultiFormatWriter(); var hints = new Dictionary<EncodeHintType, object> { // 纠错级别:H 最高,适合有磨损风险的贴纸场景 { EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H }, // 字符集,中文内容必须显式指定,否则可能乱码 { EncodeHintType.CHARACTER_SET, "UTF-8" }, // 静默区,二维码四周留白,太小会导致识别率下降 { EncodeHintType.MARGIN, 2 } }; // 返回的是位矩阵,不是图片,后续还要转 ImageSource return writer.encode(content, BarcodeFormat.QR_CODE, width, height, hints); }逐项说下:ERROR_CORRECTION设成H意味着约 30% 的面积可以损坏仍能解码,代价是同样内容需要更多模块,图会“密”一些,所以宽高别抠。CHARACTER_SET不写的话,默认走 ISO-8859-1,中文进去出来就是问号,这是新手最常踩的坑之一。MARGIN是静默区宽度,按模块数算,给 2 到 4 比较稳,给 0 的话有些扫码器直接不认。
3.2 BitMatrix 转 WPF 的 ImageSource
拿到BitMatrix后,得把它画成像素。最直接的办法是逐点写WriteableBitmap:
using System.Windows.Media; using System.Windows.Media.Imaging; using ZXing.Common; public BitmapSource ToBitmapSource(BitMatrix matrix) { int w = matrix.Width; int h = matrix.Height; // Bgra32:每像素 4 字节,WPF 原生支持,避免格式转换 var bitmap = new WriteableBitmap(w, h, 96, 96, PixelFormats.Bgra32, null); var pixels = new byte[w * h * 4]; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { // matrix[x, y] 为 true 表示黑模块 byte color = matrix[x, y] ? (byte)0 : (byte)255; int idx = (y * w + x) * 4; pixels[idx] = color; // B pixels[idx + 1] = color; // G pixels[idx + 2] = color; // R pixels[idx + 3] = 255; // A,不透明 } } bitmap.WritePixels(new Int32Rect(0, 0, w, h), pixels, w * 4, 0); return bitmap; }这里有两个细节值得说。第一,WriteableBitmap的dpi我给了 96,和屏幕一致,避免显示时被缩放糊掉。第二,WritePixels的stride参数是w * 4,不是w,写错了图会斜着撕裂,这个 bug 我第一次接的时候盯了半小时才反应过来。像素顺序是 BGRA,别按 RGBA 填,否则黑白图看不出来,但一旦你改成彩色前景就会翻车。
3.3 把图落到界面和文件
生成完BitmapSource,赋给Image.Source就能显示。要保存成文件,用PngBitmapEncoder:
public void SaveToFile(BitmapSource source, string path) { var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(source)); using var fs = File.Create(path); encoder.Save(fs); }选 PNG 不选 JPG,是因为二维码是硬边黑白块,JPG 的有损压缩会在边缘产生振铃,模块边界糊掉,识别率下降。这个选择在贴纸打印场景尤其重要,打印分辨率本来就有限,再叠一层压缩损失就是雪上加霜。
4. 识别模块拆解:图片和摄像头两条路
4.1 从静态图片解码
识别侧的核心是BarcodeReader。给它一个Bitmap或像素数组,它返回Result,里面带Text和ResultPoints。WPF 里图片通常是BitmapSource,得先转成 zxing 能吃的格式:
using ZXing; using System.Drawing; using System.Windows.Media.Imaging; public string DecodeFromBitmapSource(BitmapSource source) { // 先编码成 PNG 流,再交给 System.Drawing 读,绕开类型不兼容 var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(source)); using var ms = new MemoryStream(); encoder.Save(ms); ms.Position = 0; using var bmp = new Bitmap(ms); var reader = new BarcodeReader { Options = new ZXing.Common.DecodingOptions { // 允许识别多种格式,纯二维码项目可收窄为 QR_CODE PossibleFormats = new[] { BarcodeFormat.QR_CODE }, TryHarder = true // 图像质量一般时开启,代价是慢一点 } }; var result = reader.Decode(bmp); return result?.Text; // 识别失败返回 null,调用方要判空 }TryHarder这个开关值得单独讲:它会让解码器尝试更多旋转和采样策略,对付拍歪的、光照不均的图很有效,但耗时上升。批量处理几百张图时我一般先关掉跑一遍,失败的再开着重试,比全程开着快不少。PossibleFormats收窄到QR_CODE也能提速,因为不用挨个试一维码格式。
4.2 摄像头实时识别的基本链路
实时识别比静态图多一层:取帧。WPF 里常见做法是用MediaCapture(新框架)或AForge.Video/OpenCvSharp(老框架)拿摄像头帧,转成Bitmap后丢给BarcodeReader。链路是“取帧 → 转位图 → 解码 → 命中就停”。这里有个节奏问题:如果每帧都解码,UI 线程会被拖死,正确做法是放到后台线程,用Task.Run或独立线程跑解码循环,命中后通过Dispatcher.Invoke回主线程更新界面。
private async Task ScanLoopAsync(CancellationToken token) { while (!token.IsCancellationRequested) { // GrabFrame 是伪代码,代表从采集源取一帧 using var frame = GrabFrame(); if (frame == null) { await Task.Delay(50); continue; } // 解码放后台,避免阻塞 UI string text = await Task.Run(() => DecodeFromBitmap(frame)); if (!string.IsNullOrEmpty(text)) { // 回主线程刷新结果框 Dispatcher.Invoke(() => ResultBox.Text = text); break; // 命中即停,避免重复刷同一个码 } await Task.Delay(100); // 控制帧率,别把 CPU 吃满 } }Task.Delay(100)这个节流不是可有可无的。不节流的话,解码循环会把一个核跑满,风扇狂转,用户体验极差。100ms 大约 10 帧每秒,对扫码足够,对 CPU 友好。
4.3 识别结果的数据结构
Result对象里除了Text,还有BarcodeFormat、ResultPoints(码在画面里的四个角点坐标)。如果你要做“框出二维码位置”的效果,ResultPoints就是画矩形框的依据。Demo 里通常只取Text,但二次开发做视觉反馈时这几个点很有用,别浪费。
5. 避坑与排查:那些让我返工的细节
5.1 中文内容扫出来是乱码
现象:生成时输入中文,扫出来一堆问号或方块。原因:EncodeHintType.CHARACTER_SET没设,默认按 ISO-8859-1 编码。解决:生成侧显式写"UTF-8",识别侧如果拿到的是字节流也要按 UTF-8 解。两边字符集必须一致,只改一边等于没改。
5.2 生成的图能显示但扫不出来
现象:界面上二维码清清楚楚,手机就是扫不动。原因多半是静默区(margin)给太小,或者宽高不是模块数的整数倍导致模块宽度不均。解决:MARGIN给 2 以上,宽高尽量让每个模块落在整数像素上;如果内容很长,适当加大尺寸,别硬塞进 200×200。
5.3 识别时报 Bitmap 类型冲突
现象:编译或运行时提示System.Drawing.Bitmap与System.Windows.Media.Imaging.BitmapSource不兼容。原因:WPF 用后者,zxing 的BarcodeReader吃前者,两个命名空间混了。解决:按 4.1 的做法走 PNG 流中转,或者用ZXing.Net提供的 WPF 专用渲染/读取扩展,别在两个类型之间硬转。
5.4 摄像头识别卡顿甚至假死
现象:打开摄像头后界面无响应。原因:解码跑在 UI 线程,或者每帧都全量解码没有节流。解决:解码挪到后台线程,加Task.Delay节流,命中后及时break,别让循环空转。
5.5 保存的 JPG 图识别率骤降
现象:PNG 能扫,另存为 JPG 后扫不出。原因:JPG 有损压缩破坏模块边缘。解决:二维码图一律存 PNG 或无损 BMP,打印和传输都别走 JPG。
6. 二次开发:把它改成你自己的扫码工具
拿到这个 Demo,多数人不会停在“生成一张图、识别一张图”上,而是想接进自己的业务流程。我一般从三个方向改,按投入产出排个序。
第一个方向是批量生成。把单条内容换成List<string>,循环调GenerateMatrix,文件名用内容哈希或序号,输出到一个目录。这里要注意的是别在主线程里循环几百次,UI 会假死,用Task.Run包起来,进度用IProgress<int>回传。批量场景下纠错级别可以降到M,图小一点、打印省墨,反正贴纸是新的不容易磨损。
第二个方向是识别结果结构化。二维码里塞的不该是裸字符串,而是 JSON,比如{"type":"asset","id":"A-1024","loc":"B3"}。解码后JsonSerializer.Deserialize一把,按type分发到不同处理逻辑。这样同一个扫码入口能服务多种业务,不用为每种码写一套判断。代价是内容变长、图变密,所以生成时尺寸要相应放大。
第三个方向是摄像头选型与预览。Demo 里如果只做了静态图识别,加摄像头是最大的一块增量。老框架用AForge.Video.DirectShow枚举设备、拿VideoCaptureDevice.NewFrame事件取帧;新框架用MediaCapture的GetPreviewFrameAsync。两条路的共同点是:帧到手先转Bitmap,再走 4.1 的解码函数,复用现成逻辑,别重写。
改造时有个验证习惯我一直保持:每改完一个模块,先用一张已知内容的测试图跑一遍“生成 → 保存 → 重新读取 → 解码”,比对原文。这个闭环能挡住 80% 的参数错误,尤其是字符集和纠错级别这类改了不报错、但结果悄悄变坏的地方。有一次我把CHARACTER_SET从 UTF-8 改成默认值想“简化代码”,结果英文测试全过、中文全挂,就是靠这个闭环当场抓出来的。从那以后我每次动编码参数,都强制走一遍生成再解码的往返验证,不省这一步。希望这个习惯也能帮到你,少在“看起来没问题”的地方返工。
本文还有配套的精品资源,点击获取