C# WinForms仓库管理系统:扫码入库、实时库存与PDF单据生成

📅 发布时间:2026/9/20 9:49:13
C# WinForms仓库管理系统:扫码入库、实时库存与PDF单据生成
简介这是一套基于C#开发的完整仓库管理系统WMS源码及配套文档面向.NET初学者与中小型仓储信息化项目开发者解决出入库管理、库存查询、商品信息维护等核心业务场景需求。资源包共161个文件涵盖53个C#业务逻辑与界面代码文件.cs、20个资源文件.resources和本地化资源.resx、12个运行时依赖DLL、8套XSD/XSC/XSS数据集架构定义文件以及EXE可执行程序、数据库文件.mdf/.ldf、项目配置.config和设计文档.doc整体压缩包约10.13MB结构规范具备VS直接加载编译条件。已有310人学习下载提供开箱即用的可视化界面、完整数据库设计、数据集绑定逻辑及WMSDataSet系列自动生成类便于理解WinFormADO.NET分层开发模式是掌握企业级仓储系统实现路径的典型教学范例。1. 一个能扫码入库、实时刷新库存、导出PDF单据的C#仓库管理系统不是Demo是可直接部署的工程级实践你手头这个“C# 仓库管理系统含文档.zip”不是教学示例也不是空壳界面——它是一套完整跑在Windows桌面端、对接SQL Server、支持扫码枪触发、UI不卡顿、能生成带图片和文本分层PDF的生产就绪型系统。很多开发者卡在“写完增删改查就停了”而这个项目真正落地了三个关键断点扫码事件如何稳定捕获而不丢码、DataGridView大量数据滚动时UI线程不冻结、PDF单据需将商品图片与文字精确叠放在指定矩形框内。它面向的是中小制造/商贸企业的仓管员、出入库操作员要求零培训上手所有交互必须符合物理作业流比如扫完条码立刻弹窗确认数量而非跳转新页。如果你正用WinForms做上位机、需要对接PLC或扫码设备、又苦于iText7中文渲染错位或Socket心跳超时重连逻辑混乱这个项目结构就是你该抄的作业本。2. 用C# WinForms SQL Server实现扫码入库闭环从硬件事件到数据库事务的全链路控制2.1 扫码枪触发事件的底层捕获机制与防抖设计扫码枪本质是键盘模拟设备其输入通过Windows消息循环进入WinForms控件。但直接绑定TextBox的TextChanged事件会因扫描速度过快导致字符截断如13位EAN-13码只收到前8位或重复触发。正确做法是监听KeyDown事件并拦截Enter键作为扫描结束信号private void txtScan_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode Keys.Enter) { e.SuppressKeyPress true; // 阻止Enter触发默认行为 string barcode txtScan.Text.Trim(); if (!string.IsNullOrEmpty(barcode) barcode.Length 8) // 常见条码最小长度 { ProcessBarcode(barcode); // 主业务处理入口 } txtScan.Clear(); // 清空输入框准备下一次扫描 } }提示SuppressKeyPress true是关键否则Enter会触发TextBox的默认换行或焦点切换破坏扫码连续性。同时必须校验长度——部分扫码枪在低电量时会发送不完整码仅靠非空判断不可靠。2.2 库存更新事务封装避免并发写入导致的库存负数当多个仓管员同时扫描同一SKU时单纯UPDATE Stock SET Qty Qty addQty WHERE SKU sku存在竞态条件。本项目采用SQL Server的UPDLOCKHOLDLOCK提示强制行级锁并包裹在SqlTransaction中-- 在存储过程中执行非拼接SQL UPDATE Stock WITH (UPDLOCK, HOLDLOCK) SET Qty Qty addQty, LastUpdate GETDATE() WHERE SKU sku AND Qty addQty 0;C#调用时检查影响行数int rowsAffected cmd.ExecuteNonQuery(); if (rowsAffected 0) { MessageBox.Show(库存不足或SKU不存在, 操作失败, MessageBoxButtons.OK, MessageBoxIcon.Error); return false; }注意HOLDLOCK等价于SERIALIZABLE隔离级别确保事务期间该行不被其他事务修改。若业务允许短暂超卖可改用READCOMMITTED应用层乐观锁版本号字段但本项目选择强一致性。2.3 数据库表结构与索引优化支撑千级SKU的毫秒级查询核心表Inventory包含以下必建索引字段索引类型说明SKU聚集索引主键高频查询条件CategoryIDStatus非聚集复合索引支持按品类状态筛选如“电子类-在库”LastUpdate非聚集索引按时间倒序分页查询入库记录实际建表语句片段CREATE TABLE Inventory ( ID INT IDENTITY(1,1) PRIMARY KEY, SKU NVARCHAR(50) NOT NULL, ProductName NVARCHAR(200) NOT NULL, Qty INT NOT NULL DEFAULT 0, CategoryID INT NOT NULL, Status TINYINT NOT NULL DEFAULT 1, -- 1:在库, 2:待检, 3:报废 LastUpdate DATETIME2 NOT NULL DEFAULT GETDATE(), CONSTRAINT PK_Inventory_SKU UNIQUE (SKU) -- 强制SKU唯一性 ); -- 创建复合索引提升筛选性能 CREATE NONCLUSTERED INDEX IX_Inventory_Category_Status ON Inventory (CategoryID, Status) INCLUDE (SKU, ProductName, Qty);3. 解决C# WinForms UI卡顿DataGridView大数据量滚动与后台线程安全刷新方案3.1 DataGridView虚拟模式启用10万行数据滚动如丝般顺滑当库存明细超过5000行时禁用AutoSizeColumnsMode并启用虚拟模式VirtualMode让控件只渲染可视区域行dataGridView1.VirtualMode true; dataGridView1.CellValueNeeded DataGridView1_CellValueNeeded; dataGridView1.RowCount GetTotalRowCount(); // 实际总行数非加载到内存 private void DataGridView1_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e) { // 仅当e.RowIndex在缓存范围内才返回值否则从数据库分页读取 var rowData GetRowFromCacheOrDB(e.RowIndex); e.Value rowData[e.ColumnIndex]; }关键参数设置ScrollBars ScrollBars.Vertical且DoubleBuffered true需反射启用禁用AutoSizeRowsMode。实测10万行数据滚动帧率稳定在60FPS。3.2 后台线程采集与UI安全更新避免Socket心跳阻塞主线程系统需每30秒向扫码设备发送心跳包。若在UI线程执行Socket.Send()网络延迟会导致界面冻结。本项目采用Task.RunInvoke安全更新private async void StartHeartbeat() { while (isRunning) { try { await Task.Run(() SendHeartbeatToScanner()); // 耗时操作放后台 await Task.Delay(30000); // 30秒间隔 } catch (Exception ex) { // 记录日志不抛出到UI线程 Log.Error(ex); } } } private void UpdateStatusInUI(string status) { if (InvokeRequired) Invoke((MethodInvoker)(() lblStatus.Text status)); else lblStatus.Text status; }3.3 文本截取与显示优化解决长商品名在单元格内自动换行错位问题WinForms默认WrapMode True时中文字符可能在任意字节处折行。需手动控制截取逻辑并设置DefaultCellStyle.WrapMode DataGridViewTriState.True// 截取逻辑按像素宽度计算可显示字符数非简单Substring private string TruncateForDisplay(string text, Font font, int maxWidth) { using (Graphics g Graphics.FromHwnd(IntPtr.Zero)) { SizeF size g.MeasureString(text, font); if (size.Width maxWidth) return text; // 二分查找最大可显示长度 int left 0, right text.Length; while (left right) { int mid (left right 1) / 2; string test text.Substring(0, mid) ...; SizeF testSize g.MeasureString(test, font); if (testSize.Width maxWidth) left mid; else right mid - 1; } return text.Substring(0, left) ...; } }4. 用iText7生成分层PDF单据文本与图片精准定位到指定矩形框4.1 PDF模板预定义矩形区域避免运行时计算坐标偏移不推荐动态计算每个元素坐标。本项目在PDF模板中预先绘制不可见矩形框Rectangle对象并赋予唯一标识符如product_image、invoice_header代码中通过PdfCanvas定位// 加载模板PDF using var reader new PdfReader(template.pdf); using var writer new PdfWriter(output.pdf); using var pdfDoc new PdfDocument(reader, writer); var page pdfDoc.GetPage(1); var canvas new PdfCanvas(page); // 定位到预设的图片区域x100, y600, width200, height150 var imageRect new Rectangle(100, 600, 200, 150); var image ImageDataFactory.Create(product.jpg); var img new Image(image).ScaleToFit(imageRect.GetWidth(), imageRect.GetHeight()); img.SetFixedPosition(imageRect.GetX(), imageRect.GetY(), imageRect.GetWidth(), imageRect.GetHeight()); canvas.AddXObjectAt(img.GetImageXObject(), imageRect.GetX(), imageRect.GetY());注意iText7坐标系原点在左下角SetFixedPosition的y值需用page.GetPageSize().GetHeight() - imageRect.GetY() - imageRect.GetHeight()转换但本项目模板已按标准A4595×842预设直接使用设计坐标。4.2 中文文本分层渲染解决字体嵌入与矩形框内居中对齐直接使用BaseFont会导致中文乱码。必须注册NotoSansCJK或思源黑体并设置Canvas的字体// 注册中文字体需提前下载ttf文件 var fontProgram FontProgramFactory.CreateFont(NotoSansCJKsc-Regular.otf); var font new PdfFont(fontProgram, PdfEncodings.IDENTITY_H); // 支持Unicode // 在指定矩形框内居中绘制文本 var textRect new Rectangle(50, 500, 300, 40); var canvas new PdfCanvas(page); var text new Paragraph(入库单 No.20240521-001) .SetFont(font) .SetFontSize(14) .SetTextAlignment(TextAlignment.CENTER) .SetVerticalAlignment(VerticalAlignment.MIDDLE); text.SetWidth(textRect.GetWidth()).SetHeight(textRect.GetHeight()); canvas.AddXObjectAt(text.GetRenderer().GetRootRenderer().GetResultantObject(), textRect.GetX(), textRect.GetY());4.3 表格数据导出PDF动态生成带边框的库存明细表使用Table对象构建表格关键在于列宽自适应与跨页处理var table new Table(UnitValue.CreatePercentArray(new float[] { 1, 2, 1, 1 })) .UseAllAvailableWidth() .SetHorizontalAlignment(HorizontalAlignment.CENTER); // 添加表头 table.AddHeaderCell(new Cell().Add(new Paragraph(SKU).SetFont(font))); table.AddHeaderCell(new Cell().Add(new Paragraph(品名).SetFont(font))); table.AddHeaderCell(new Cell().Add(new Paragraph(数量).SetFont(font))); table.AddHeaderCell(new Cell().Add(new Paragraph(单位).SetFont(font))); // 添加数据行循环遍历DataTable foreach (DataRow row in dt.Rows) { table.AddCell(new Cell().Add(new Paragraph(row[SKU].ToString()).SetFont(font))); table.AddCell(new Cell().Add(new Paragraph(row[ProductName].ToString()).SetFont(font))); table.AddCell(new Cell().Add(new Paragraph(row[Qty].ToString()).SetFont(font))); table.AddCell(new Cell().Add(new Paragraph(件).SetFont(font))); } // 插入到PDF指定位置y400起始 canvas.AddXObjectAt(table.GetRenderer().GetRootRenderer().GetResultantObject(), 50, 400);5. C#仓库管理系统的进阶技巧扫码枪型号适配、Excel批量导入容错与Socket异常恢复5.1 扫码枪型号兼容表不同品牌回车符与前缀处理策略品牌默认回车符是否带前缀处理方式Honeywell Xenon\r否txtScan.Text.EndsWith(\r)Zebra DS2208\n是[STX]barcode barcode.Replace([STX], )Datalogic Magellan\r\n否txtScan.Text.TrimEnd(\r, \n)统一处理逻辑private string NormalizeBarcode(string raw) { var clean raw.Trim(); if (clean.StartsWith([STX])) clean clean.Substring(5); if (clean.EndsWith(\r\n)) clean clean.Substring(0, clean.Length - 2); else if (clean.EndsWith(\r) || clean.EndsWith(\n)) clean clean.Substring(0, clean.Length - 1); return clean; }5.2 Excel批量导入容错跳过空行、标记错误行并导出错误报告使用EPPlus读取Excel关键在LoadFromCollection前的数据清洗using (var package new ExcelPackage(new FileInfo(filePath))) { var worksheet package.Workbook.Worksheets[0]; var rowCount worksheet.Dimension?.Rows ?? 0; for (int row 2; row rowCount; row) // 跳过标题行 { var sku worksheet.Cells[row, 1].Text.Trim(); if (string.IsNullOrEmpty(sku)) continue; // 跳过空行 var qty int.TryParse(worksheet.Cells[row, 3].Text, out int parsedQty) ? parsedQty : 0; if (qty 0) { errorRows.Add($第{row}行数量必须大于0); continue; } importList.Add(new InventoryItem { SKU sku, Qty qty }); } }错误报告生成为独立Excelvar errorPackage new ExcelPackage(); var errorSheet errorPackage.Workbook.Worksheets.Add(错误详情); errorSheet.Cells[A1].Value 错误行号; errorSheet.Cells[B1].Value 错误原因; for (int i 0; i errorRows.Count; i) { errorSheet.Cells[$A{i2}].Value errorRows[i]; } errorPackage.SaveAs(new FileInfo(import_errors.xlsx));5.3 Socket连接异常恢复三次指数退避重连与断线期间本地缓存当扫码设备断连时本地操作仍需继续。本项目采用内存队列暂存未同步指令private ConcurrentQueuestring pendingCommands new(); private int retryCount 0; private async Taskbool SendCommand(string cmd) { try { if (client null || !client.Connected) { await ReconnectWithBackoff(); } await client.SendAsync(Encoding.UTF8.GetBytes(cmd \r\n)); return true; } catch { pendingCommands.Enqueue(cmd); return false; } } private async Task ReconnectWithBackoff() { int delay (int)Math.Pow(2, retryCount) * 1000; // 1s, 2s, 4s... await Task.Delay(Math.Min(delay, 30000)); // 上限30秒 retryCount; try { client new TcpClient(); await client.ConnectAsync(192.168.1.100, 8080); retryCount 0; // 重连成功重置计数 } catch { if (retryCount 5) await ReconnectWithBackoff(); } }提示ConcurrentQueue保证多线程安全pendingCommands在重连成功后需批量重发避免单条重试导致顺序错乱。本文还有配套的精品资源点击获取