条形码与二维码
大约 9 分钟约 2623 字
条形码与二维码
简介
条形码和二维码是工业生产、物流仓储中的常见需求。WPF 应用通过 ZXing.Net 或 QRCoder 库可以生成和识别各种条形码(Code128、EAN-13)和二维码。结合工业相机或扫码枪,可以实现产品追溯、物料管理、质量追溯等功能。
特点
二维码生成
QRCoder 生成
/// <summary>
/// QRCoder 二维码生成
/// </summary>
using QRCoder;
public static class QrCodeHelper
{
// 生成 BitmapSource(WPF 使用)
public static BitmapSource GenerateQrCode(string content, int pixelsPerModule = 10)
{
using var generator = new QRCodeGenerator();
var qrCodeData = generator.CreateQrCode(content, QRCodeGenerator.ECCLevel.M);
using var qrCode = new PngByteQRCode(qrCodeData);
var qrCodeImage = qrCode.GetGraphic(pixelsPerModule);
using var stream = new MemoryStream(qrCodeImage);
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = stream;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();
return bitmap;
}
// 带颜色的二维码
public static BitmapSource GenerateColoredQrCode(string content,
Color darkColor, Color lightColor, int size = 200)
{
using var generator = new QRCodeGenerator();
var qrCodeData = generator.CreateQrCode(content, QRCodeGenerator.ECCLevel.H);
using var qrCode = new QRCode(qrCodeData);
using var bitmap = qrCode.GetGraphic(
pixelsPerModule: 5,
darkColor: System.Drawing.Color.FromArgb(darkColor.A, darkColor.R, darkColor.G, darkColor.B),
lightColor: System.Drawing.Color.FromArgb(lightColor.A, lightColor.R, lightColor.G, lightColor.B),
drawQuietZones: true);
return ConvertToBitmapSource(bitmap);
}
// 带Logo 的二维码
public static BitmapSource GenerateWithLogo(string content, string logoPath, int size = 300)
{
using var generator = new QRCodeGenerator();
var qrCodeData = generator.CreateQrCode(content, QRCodeGenerator.ECCLevel.H);
using var logo = new System.Drawing.Bitmap(logoPath);
using var qrCode = new QRCode(qrCodeData);
using var bitmap = qrCode.GetGraphic(
pixelsPerModule: 8,
icon: logo,
iconSizePercent: 15,
iconBorderWidth: 2);
return ConvertToBitmapSource(bitmap);
}
private static BitmapSource ConvertToBitmapSource(System.Drawing.Bitmap bitmap)
{
var hBitmap = bitmap.GetHbitmap();
try
{
return Imaging.CreateBitmapSourceFromHBitmap(
hBitmap, IntPtr.Zero, Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
}
finally
{
DeleteObject(hBitmap);
}
}
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
private static extern bool DeleteObject(IntPtr hObject);
}条形码生成
ZXing.Net 条码生成
/// <summary>
/// ZXing.Net 条形码生成
/// </summary>
using ZXing;
using ZXing.Common;
public static class BarcodeHelper
{
// 生成 Code128 条形码
public static BitmapSource GenerateCode128(string content, int width = 300, int height = 100)
{
var writer = new BarcodeWriter
{
Format = BarcodeFormat.CODE_128,
Options = new EncodingOptions
{
Width = width,
Height = height,
Margin = 10,
PureBarcode = false
},
Renderer = new BitmapRenderer()
};
// 添加文字
var hints = new Dictionary<EncodeHintType, object>
{
{ EncodeHintType.PURE_BARCODE, false }
};
using var bitmap = writer.Write(content);
return ConvertToBitmapSource(bitmap);
}
// 生成 EAN-13
public static BitmapSource GenerateEAN13(string content)
{
var writer = new BarcodeWriter
{
Format = BarcodeFormat.EAN_13,
Options = new EncodingOptions
{
Width = 250,
Height = 100,
Margin = 10
}
};
using var bitmap = writer.Write(content);
return ConvertToBitmapSource(bitmap);
}
// 生成 DataMatrix
public static BitmapSource GenerateDataMatrix(string content)
{
var writer = new BarcodeWriter
{
Format = BarcodeFormat.DATA_MATRIX,
Options = new EncodingOptions
{
Width = 200,
Height = 200,
Margin = 5
}
};
using var bitmap = writer.Write(content);
return ConvertToBitmapSource(bitmap);
}
}条码识别
工业相机连续扫码
在产线场景中,需要通过工业相机连续采集图像并实时识别条码。以下是基于 MVVM 模式的完整实现。
/// <summary>
/// 产线扫码 ViewModel
/// </summary>
public class ScannerViewModel : ObservableObject, IDisposable
{
private readonly BarcodeScanner _scanner;
private CancellationTokenSource? _cts;
private BitmapSource? _currentFrame;
public BitmapSource? CurrentFrame
{
get => _currentFrame;
set => SetProperty(ref _currentFrame, value);
}
private string _lastResult = "";
public string LastResult
{
get => _lastResult;
set => SetProperty(ref _lastResult, value);
}
private int _scanCount;
public int ScanCount
{
get => _scanCount;
set => SetProperty(ref _scanCount, value);
}
private bool _isScanning;
public bool IsScanning
{
get => _isScanning;
set => SetProperty(ref _isScanning, value);
}
public ICommand StartScanCommand { get; }
public ICommand StopScanCommand { get; }
public ScannerViewModel()
{
_scanner = new BarcodeScanner();
StartScanCommand = new RelayCommand(StartScan, () => !IsScanning);
StopScanCommand = new RelayCommand(StopScan, () => IsScanning);
}
private void StartScan()
{
IsScanning = true;
_cts = new CancellationTokenSource();
var token = _cts.Token;
Task.Run(async () =>
{
while (!token.IsCancellationRequested)
{
try
{
// 从相机获取帧(伪代码,实际对接相机 SDK)
var frameData = CaptureFrame();
if (frameData == null) continue;
// 解码
var result = _scanner.DecodeFromCameraFrame(
frameData.Data, frameData.Width, frameData.Height);
if (result != null)
{
// 切回 UI 线程更新
Application.Current.Dispatcher.Invoke(() =>
{
LastResult = result;
ScanCount++;
OnCodeScanned?.Invoke(result);
});
}
await Task.Delay(50, token); // 控制扫描频率
}
catch (OperationCanceledException) { break; }
catch (Exception ex)
{
// 日志记录,不中断扫描
System.Diagnostics.Debug.WriteLine($"扫码异常: {ex.Message}");
}
}
}, token);
}
private void StopScan()
{
_cts?.Cancel();
IsScanning = false;
}
public event Action<string>? OnCodeScanned;
public void Dispose()
{
StopScan();
_cts?.Dispose();
}
}扫码去重与防抖
产线扫码时,同一个码可能被多次扫描到,需要去重和防抖处理。
/// <summary>
/// 扫码去重器
/// </summary>
public class ScanDeduplicator
{
private readonly TimeSpan _cooldownPeriod;
private readonly Dictionary<string, DateTime> _lastScanTime = new();
private readonly object _lock = new();
public ScanDeduplicator(TimeSpan? cooldownPeriod = null)
{
_cooldownPeriod = cooldownPeriod ?? TimeSpan.FromSeconds(3);
}
/// <summary>
/// 检查是否为重复扫码,非重复时返回 true
/// </summary>
public bool TryAccept(string code)
{
lock (_lock)
{
var now = DateTime.UtcNow;
// 清理过期记录
var expired = _lastScanTime
.Where(kvp => now - kvp.Value > _cooldownPeriod)
.Select(kvp => kvp.Key)
.ToList();
foreach (var key in expired)
_lastScanTime.Remove(key);
// 检查是否在冷却期内
if (_lastScanTime.TryGetValue(code, out var lastTime))
{
if (now - lastTime < _cooldownPeriod)
return false; // 冷却期内,拒绝
}
_lastScanTime[code] = now;
return true;
}
}
/// <summary>
/// 清空记录
/// </summary>
public void Reset()
{
lock (_lock)
_lastScanTime.Clear();
}
}条码内容编码规范
/// <summary>
/// 条码内容编码工具 — 定义统一的数据格式
/// </summary>
public static class BarcodeContentEncoder
{
// 格式规范:字段间用 | 分隔,字段名:值
// 示例:SN:202401001|TYPE:PRODUCT|BATCH:B001|DATE:2024-01-15
/// <summary>
/// 编码产品追溯信息
/// </summary>
public static string EncodeProductTrace(string serialNo, string productType,
string batchNo, DateTime productionDate)
{
return $"SN:{serialNo}|TYPE:{productType}|BATCH:{batchNo}|DATE:{productionDate:yyyy-MM-dd}";
}
/// <summary>
/// 解码条码内容为字典
/// </summary>
public static Dictionary<string, string> Decode(string barcodeContent)
{
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var fields = barcodeContent.Split('|');
foreach (var field in fields)
{
var parts = field.Split(new[] { ':' }, 2);
if (parts.Length == 2)
result[parts[0].Trim()] = parts[1].Trim();
}
return result;
}
/// <summary>
/// 验证条码内容是否为有效的产品追溯码
/// </summary>
public static bool ValidateProductTrace(string barcodeContent, out string error)
{
error = "";
var data = Decode(barcodeContent);
if (!data.ContainsKey("SN"))
{ error = "缺少序列号(SN)"; return false; }
if (!data.ContainsKey("TYPE"))
{ error = "缺少产品类型(TYPE)"; return false; }
if (!data.ContainsKey("BATCH"))
{ error = "缺少批次号(BATCH)"; return false; }
if (data["SN"].Length < 3 || data["SN"].Length > 50)
{ error = "序列号长度无效"; return false; }
return true;
}
}
// 使用示例
var content = BarcodeContentEncoder.EncodeProductTrace(
"202401001", "SENSOR_A", "B001", DateTime.Now);
var qrImage = QrCodeHelper.GenerateQrCode(content);
// 扫码后解析
var decoded = BarcodeContentEncoder.Decode(scannedContent);
var serialNo = decoded["SN"]; // "202401001"
var isValid = BarcodeContentEncoder.ValidateProductTrace(scannedContent, out var err);解码识别
/// <summary>
/// ZXing.Net 条码识别
/// </summary>
public class BarcodeScanner
{
private readonly BarcodeReader<Bitmap> _reader;
public BarcodeScanner()
{
_reader = new BarcodeReader<Bitmap>
{
AutoRotate = true,
Options = new DecodingOptions
{
PossibleFormats = new List<BarcodeFormat>
{
BarcodeFormat.CODE_128,
BarcodeFormat.CODE_39,
BarcodeFormat.EAN_13,
BarcodeFormat.QR_CODE,
BarcodeFormat.DATA_MATRIX
},
TryHarder = true,
TryInverted = true
}
};
}
// 从图片文件识别
public string? DecodeFromFile(string imagePath)
{
using var bitmap = new Bitmap(imagePath);
var result = _reader.Decode(bitmap);
return result?.Text;
}
// 从 BitmapSource 识别
public string? DecodeFromBitmapSource(BitmapSource bitmapSource)
{
using var bitmap = ConvertToBitmap(bitmapSource);
var result = _reader.Decode(bitmap);
return result?.Text;
}
// 从摄像头帧识别
public string? DecodeFromCameraFrame(byte[] frameData, int width, int height)
{
var reader = new BarcodeReader
{
AutoRotate = true,
Options = new DecodingOptions { TryHarder = true }
};
var luminanceSource = new RGBLuminanceSource(frameData, width, height);
var binarizer = new HybridBinarizer(luminanceSource);
var binaryBitmap = new BinaryBitmap(binarizer);
var result = reader.Decode(binaryBitmap);
return result?.Text;
}
private Bitmap ConvertToBitmap(BitmapSource source)
{
using var stream = new MemoryStream();
BitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(source));
encoder.Save(stream);
return new Bitmap(stream);
}
}批量生成与打印
标签批量打印
/// <summary>
/// 批量生成二维码标签
/// </summary>
public class LabelGenerator
{
// 生成标签数据
public List<LabelItem> GenerateLabels(List<Product> products)
{
var labels = new List<LabelItem>();
foreach (var product in products)
{
labels.Add(new LabelItem
{
ProductName = product.Name,
SerialNumber = product.SerialNo,
QrCode = QrCodeHelper.GenerateQrCode($"SN:{product.SerialNo}|NAME:{product.Name}"),
Barcode = BarcodeHelper.GenerateCode128(product.SerialNo)
});
}
return labels;
}
// 打印标签(使用 PrintDialog)
public void PrintLabels(List<LabelItem> labels)
{
var printDialog = new PrintDialog();
if (printDialog.ShowDialog() != true) return;
var document = new FixedDocument();
document.DocumentPaginator.PageSize = new Size(
printDialog.PrintableAreaWidth, printDialog.PrintableAreaHeight);
// 每页 6 个标签(2列 × 3行)
var labelsPerPage = 6;
for (int i = 0; i < labels.Count; i += labelsPerPage)
{
var page = new FixedPage();
var grid = CreateLabelGrid(labels.Skip(i).Take(labelsPerPage));
page.Children.Add(grid);
var pageContent = new PageContent { Child = page };
document.Pages.Add(pageContent);
}
printDialog.PrintDocument(document.DocumentPaginator, "标签打印");
}
private Grid CreateLabelGrid(IEnumerable<LabelItem> labels)
{
var grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition());
grid.ColumnDefinitions.Add(new ColumnDefinition());
int row = 0, col = 0;
foreach (var label in labels)
{
var border = new Border
{
BorderBrush = Brushes.Black,
BorderThickness = new Thickness(1),
Margin = new Thickness(5),
Child = new StackPanel
{
Children =
{
new Image { Source = label.QrCode, Width = 80, Height = 80 },
new TextBlock { Text = label.ProductName, FontSize = 10 },
new Image { Source = label.Barcode, Width = 100, Height = 30 }
}
}
};
Grid.SetRow(border, row);
Grid.SetColumn(border, col);
grid.Children.Add(border);
col++;
if (col >= 2) { col = 0; row++; }
}
return grid;
}
}优点
缺点
总结
条码/二维码推荐 QRCoder(二维码专用)和 ZXing.Net(通用条码)。生成核心:QRCodeGenerator → CreateQrCode → GetGraphic。识别核心:BarcodeReader → Decode。批量标签建议 FixedDocument + PrintDialog 实现精确打印。生产环境建议使用工业扫码枪直接获取解码结果,减少图像识别的不确定性。
关键知识点
- 先分清主题属于界面层、ViewModel 层、线程模型还是设备接入层。
- WPF 文章真正的价值在于把 UI、数据、命令、线程和资源关系讲清楚。
- 上位机场景里,稳定性和异常恢复常常比界面花哨更重要。
- WPF 主题往往要同时理解依赖属性、绑定、可视树和命令系统。
项目落地视角
- 优先明确 DataContext、绑定路径、命令源和线程切换位置。
- 涉及设备时,补齐超时、重连、日志、告警和资源释放策略。
- 复杂控件最好提供最小可运行页面,便于后续复用和排障。
- 优先确认 DataContext、绑定路径、命令触发点和资源引用来源。
常见误区
- 把大量逻辑堆进 code-behind,导致页面难测难维护。
- 在后台线程直接操作 UI,或忽略 Dispatcher 切换。
- 只验证正常路径,不验证设备掉线、权限缺失和资源泄漏。
- 在 code-behind 塞太多状态与业务逻辑。
进阶路线
- 继续向 MVVM 工具链、控件可复用性、性能优化和视觉树诊断深入。
- 把主题放回真实设备流程,思考启动、连接、采集、显示、告警和恢复。
- 沉淀成控件库、通信层和诊断工具,提高整套客户端的复用度。
- 继续补齐控件库、主题系统、诊断工具和启动性能优化。
适用场景
- 当你准备把《条形码与二维码》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合桌面业务系统、监控看板、工控上位机和设备交互界面。
- 当系统需要同时处理复杂 UI、后台任务和硬件通信时,这类主题尤为关键。
落地建议
- 优先明确 View、ViewModel、服务层和设备层边界,避免代码隐藏过重。
- 涉及实时刷新时,提前设计 UI 线程切换、节流和资源释放。
- 对硬件通信、日志、告警和异常恢复建立标准流程。
排错清单
- 先检查 DataContext、绑定路径、INotifyPropertyChanged 和命令状态是否正常。
- 排查 Dispatcher 调用、死锁、后台线程直接操作 UI 的问题。
- 出现内存增长时,优先检查事件订阅、图像资源和窗口生命周期。
复盘问题
- 如果把《条形码与二维码》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《条形码与二维码》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《条形码与二维码》最大的收益和代价分别是什么?
