简介:面向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.3)
- ZXing.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 Width="2*"/> <ColumnDefinition Width="5"/> <ColumnDefinition Width="3*"/> </Grid.ColumnDefinitions> <!-- 左:生成区域 --> <StackPanel Grid.Column="0" Margin="10"> <TextBlock Text="输入内容"/> <TextBox x:Name="TxtContent" Height="80" TextWrapping="Wrap" AcceptsReturn="True" VerticalScrollBarVisibility="Auto"/> <ComboBox x:Name="CmbLevel" Margin="0,10,0,0" SelectedIndex="2"/> <Button x:Name="BtnGenerate" Content="生成二维码" Height="36" Margin="0,10,0,0" Click="BtnGenerate_Click"/> <Image x:Name="ImgQrCode" Margin="0,10,0,0" Stretch="Uniform" MaxHeight="240"/> </StackPanel> <!-- 分割条 --> <GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" Background="#DDD"/> <!-- 右:识别区域 --> <StackPanel Grid.Column="2" Margin="10"> <Button x:Name="BtnLoadImage" Content="加载图片识别" Height="36" Click="BtnLoadImage_Click"/> <Image x:Name="ImgSource" Margin="0,10,0,0" Stretch="Uniform" MaxHeight="240"/> <TextBlock Text="识别结果:"/> <TextBox x:Name="TxtResult" Height="60" IsReadOnly="True" TextWrapping="Wrap"/> </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级,就是综合了数据容量和抗损坏能力的平衡点。
要不要嵌Logo?QRCoder支持在二维码中心嵌入图片,调用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模式下,可以在识别前把待处理的图片临时保存到本地,那一帧到底长什么样、有没有反光、有没有失焦,肉眼一看就清楚了。很多时候排查识别率问题,靠的就是这一张临时导出的现场截图,比对着代码猜效率高得多。
本文还有配套的精品资源,点击获取