尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

C# WinForm实战:扫码枪出入库与仓储管理系统开发全解析

C# WinForm实战:扫码枪出入库与仓储管理系统开发全解析 简介在仓储管理、门店收银、固定资产盘点等场景中条码扫描是提升录入效率的关键手段。扫码枪通过HID键盘模式或串口模式将条码数据快速传入系统配合WinForm桌面应用可实现出入库的自动记录与库存实时更新。这类系统不依赖外网部署轻量尤其适合预算有限、要求稳定的中小型仓库环境。理解扫码枪的数据接入原理、条码格式校验、库存流水设计以及订单与库存的联动逻辑是构建可靠业务闭环的核心。C# WinForm凭借其丰富的控件和高效的开发效率配合SQLite及Entity Framework能快速搭建出可落地的仓储管理工具。本文从硬件选型到软件架构详细讲解扫码入库、出库校验、订单联动和DataGridView优化等关键技术并结合真实项目中的常见问题给出工程实践方案。 去年帮朋友做了一套仓库管理的小系统需求很直接扫码枪扫一下货物条码出入库记录自动生成订单信息自动关联不需要人工敲键盘录单。整套东西用C# WinForm落地配合霍尼韦尔1900扫码枪从硬件选型到软件逻辑走完了一遍踩了不少坑也沉淀出一些比较实用的做法。今天把这套系统的完整思路、核心代码、常见坑都整理出来给正准备做同类项目的朋友一个参考。这套系统适合谁如果你正在做仓储管理、门店收银、图书借阅、固定资产盘点这类需要“扫一下条码就完成录入”的场景而且预算有限、不想被Web前端绑住那WinForm这套方案非常合适。它不依赖外网内网单机就能跑硬件成本就是一把扫码枪加一台电脑部署起来也简单。1. 项目整体设计与思路拆解1.1 为什么选WinForm而不是Web或WPF先说选型。现在很多人一提到管理系统就想到Web前后端分离、Vue加Spring Boot但对于一个仓库内部的出入库工具这套技术栈反而有点重。仓库环境一般没有公网IP电脑配置也不高工人操作时就是一个窗口开在那里扫码、显示、保存。WinForm在这种场景下有几个硬优势。第一开发效率极高。DataGridView、TextBox、ComboBox这些控件拖一拖就能用做表单、列表、明细编辑都是现成的。做一个Web项目你要配路由、配接口、处理跨域WinForm里一个按钮事件就能搞定所有逻辑。第二设备对接方便。扫码枪通过USB或串口连上之后系统可以直接监听键盘输入事件或者串口数据。就算走TCP/IP的工业扫码枪WinForm里写Socket客户端也一样顺手。Web浏览器反而限制多尤其是串口和HID设备访问除了Chrome的WebUSB之外基本没什么好办法。第三部署和维护简单。编译出一个exe拷贝到仓库电脑上就能跑不需要装IIS、不需要装数据库服务端用SQLite或LocalDB就够了最多装一个.NET Framework运行时而Win10自带了.NET Framework 4.6以上版本基本做到零依赖。1.2 系统架构与模块划分这套系统整体采用传统三层结构界面层WinForm窗体、业务层BLL、数据层DAL。考虑到项目规模不大我没有引入过于复杂的框架Entity Framework写了数据访问业务逻辑直接写在窗体后面的类里。核心模块划分如下基础数据商品档案管理、条码管理、供应商/客户信息入库管理采购入库、退货入库、扫码逐件入库出库管理销售出库、扫码逐件出库订单管理创建订单、扫条码添加商品、订单审核后自动扣减库存库存管理实时库存查询、库存流水、库存预警系统设置扫码枪参数、串口参数、界面主题其中出入库和订单管理是整个系统的核心。订单模块和出入库模块打通订单审核完成后自动生成出库单出库单的状态反过来驱动订单状态的更新这样的联动逻辑让业务流转很顺畅。1.3 扫码枪选型与接入方式分析说到扫码枪得仔细讲讲。市面上常见的品牌有霍尼韦尔Honeywell、斑马Zebra、新大陆常见的型号包括霍尼韦尔1900、1470等。扫码枪和电脑的连接方式主要有三种USB键盘口模式HID、USB串口模式虚拟COM口、蓝牙模式。USB键盘口模式是默认模式。扫码枪把扫描到的条码当成键盘敲进去光标在哪里条码就输入到哪里。优点是完全免驱即插即用任何系统都支持。缺点也很明显如果用户不小心点击了其他窗口条码就会扫到不该去的地方造成数据错乱。USB串口模式需要安装驱动识别为一个COM口通过串口协议接收数据。这种方式下扫码枪的数据流只给你的程序不会跑到别的窗口去稳定性更高。我最终选择的是USB键盘口模式原因很实际仓库电脑是老机器装驱动很费劲管理员的权限也受限。后来在代码层面做了“焦点锁定”的处理把扫码输入限制在主窗体内这个问题就解决了。如果你要做工业级部署且扫码枪和系统软件都有技术人员维护那我建议用串口模式后面我会详细讲两种模式的编程实现。2. 扫码枪核心接入与条码数据处理2.1 键盘模式下的事件监听与防串扰USB键盘口模式的扫码枪本质上是一个“极速键盘输入设备”。它会以极快的速度把条码字符逐个发送到当前焦点控件。最直接的实现方式是放一个TextBox让扫码枪输入但这样做很快会发现一个致命问题如果用户点了别的窗口或者TextBox失去了焦点条码就丢了。哪怕焦点一直在TextBox里系统日志、快捷键等操作也可能导致焦点闪掉数据不完整。我的处理方案是在主窗体上设置KeyPreview true在窗体的KeyPress事件里统一接收扫码枪数据。先说KeyPreview的原理当窗体属性KeyPreview设为true时键盘事件会先经过窗体自身的KeyDown/KeyPress/KeyUp然后再传递到焦点控件。这样就能在窗体验收数据而不是让控件直接吃掉。我在主窗体的KeyPress事件里写了一个全局条码缓冲区代码如下private StringBuilder barcodeBuffer new StringBuilder(); private DateTime lastKeyTime DateTime.MinValue; protected override void OnKeyPress(KeyPressEventArgs e) { base.OnKeyPress(e); // 判断是否来自扫码枪时间间隔在50毫秒以内视为连续输入 if ((DateTime.Now - lastKeyTime).TotalMilliseconds 50) { if (e.KeyChar (char)13) // 回车符条码结束 { ProcessBarcode(barcodeBuffer.ToString()); barcodeBuffer.Clear(); e.Handled true; return; } barcodeBuffer.Append(e.KeyChar); } else { barcodeBuffer.Clear(); if (e.KeyChar ! (char)13) { barcodeBuffer.Append(e.KeyChar); } } lastKeyTime DateTime.Now; e.Handled true; }这里的关键判断是扫码枪输入速度。扫码枪在几十毫秒内输出一个完整条码而人工敲键盘最快也需要100毫秒以上。用50毫秒作为时间阈值来区分“条码输入”和“人工输入”效果很不错。你可以根据实际情况调整阈值有些慢速扫码枪可能超过50毫秒实测后微调即可。2.2 回车键尾缀配置与条码截断处理扫码枪默认在条码末尾会发送一个回车键Enter表示一次扫描结束。这个回车键非常重要它像是一句话的句号告诉程序“这一码读完了”。问题是不同品牌扫码枪的出厂默认设置不一样。霍尼韦尔1900默认是“条码 回车”斑马默认是“条码 Tab”。这就导致同样的程序在斑马枪上跑回车事件收不到缓冲区永远不会被清空。解决方法是手动配置扫码枪的尾缀。霍尼韦尔1900的设置方式是扫描说明书上的配置条码。我手里这台1900需要扫以下配置码禁用默认前缀扫说明书上的“No Prefix”条码设置后缀为回车扫说明书上的“Enter”后缀条码配置完以后可以抓一下数据验证。用记事本打开扫一个条码进去如果看到条码后面光标自动跳到下一行说明回车尾缀生效了。在程序层面有一个细节很容易被忽略Windows键盘事件里回车键有两个标识KeyPress事件中是(char)13KeyDown事件中是Keys.Enter。如果在KeyPress里判断回车就只判断(char)13如果在KeyDown里判断则要用Keys.Enter。我的代码全部统一用KeyPress处理避免混淆。2.3 串口模式的数据接收与解析如果你选择的是串口模式程序开发逻辑就完全不一样了。串口模式下扫码枪是把数据通过COM口发送给电脑不会干扰其他窗口。需要在系统设置里配置串口参数。霍尼韦尔1900的串口参数一般是波特率9600数据位8停止位1无校验。不同型号的波特率可能不同看说明书确认。串口接收的核心是SerialPort控件数据接收使用DataReceived事件。但有一个巨大的坑必须注意DataReceived事件运行在后台线程不能在事件里直接更新UI控件必须Invoke到UI线程。如果偷懒直接改TextBox的Text属性程序会在运行一段时间后随机崩溃报“线程间操作无效”的异常。private void serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { try { string data serialPort.ReadExisting(); if (InvokeRequired) { BeginInvoke(new Action(() { ProcessSerialData(data); })); } } catch (Exception ex) { LogHelper.Error(串口接收异常, ex); } } private void ProcessSerialData(string data) { buffer.Append(data); string content buffer.ToString(); int enterIndex content.IndexOf(\r); if (enterIndex 0) { string barcode content.Substring(0, enterIndex).Trim(); buffer.Clear(); ProcessBarcode(barcode); } }串口数据还有一个粘包/拆包问题。扫码枪可能一次发送的数据没有完全到达回调函数可能一次来了两段数据的拼接。所以必须维护一个循环缓冲区按回车符切分完整条码。这段buffer逻辑在实际项目中很关键直接决定系统的稳定性。2.4 条码格式校验与业务映射条码到了程序里必须做格式校验和业务映射。你扫上来的码是什么格式直接决定了后续逻辑。我在这套系统里把条码分为三种类型通过前缀区分商品条码以“69”开头通常是13位EAN码对应商品档案表包装条码以“P”开头的自定义编码对应一个包装箱里的商品集合库位条码以“LOC”开头对应仓库的物理库位进行出入库时扫商品条码直接找商品扫库位条码则定位库位后续可以做到“商品库位”的精确库存管理。条码数据校验的核心逻辑是前缀匹配和长度校验。比如商品条码必须是数字且长度为8到14位如果不符合就直接弹提示并丢弃不进入后续逻辑这样可以有效避免误扫和扫码不完整的情况。private void ProcessBarcode(string code) { if (string.IsNullOrWhiteSpace(code)) return; if (code.StartsWith(69) code.Length 13) { // 商品条码处理 } else if (code.StartsWith(P)) { // 包装条码处理解析箱子里的商品明细 } else if (code.StartsWith(LOC)) { // 库位条码处理 } else { MessageBox.Show(无法识别的条码: code); } }3. 核心业务模块实现出入库与订单管理3.1 数据库设计与库存流水思路这套系统选择了SQLite作为存储原因很简单单机应用不需要复杂的数据服务一个.db文件搞定备份和迁移。如果你公司有现成的SQL Server也可以平滑切换EF Core对这两种数据库的兼容性都很好。先看核心表结构。第一张是商品表ProductCREATE TABLE Product ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Barcode VARCHAR(20) NOT NULL UNIQUE, Name VARCHAR(100) NOT NULL, Spec VARCHAR(50), Unit VARCHAR(10), DefaultPrice DECIMAL(10,2) DEFAULT 0, MinStock DECIMAL(10,2) DEFAULT 0, Status INTEGER DEFAULT 1, CreatedTime DATETIME DEFAULT CURRENT_TIMESTAMP );第二张是库存表Inventory我用的是“当前库存 流水表”组合模式没有只设计一个数量字段CREATE TABLE Inventory ( ProductId INTEGER PRIMARY KEY, Quantity DECIMAL(10,2) DEFAULT 0, LockedQuantity DECIMAL(10,2) DEFAULT 0, UpdatedTime DATETIME ); CREATE TABLE StockLog ( Id INTEGER PRIMARY KEY AUTOINCREMENT, ProductId INTEGER NOT NULL, ChangeType INTEGER NOT NULL, Quantity DECIMAL(10,2) NOT NULL, BeforeQuantity DECIMAL(10,2) NOT NULL, AfterQuantity DECIMAL(10,2) NOT NULL, RefType VARCHAR(20), RefId INTEGER, Remark VARCHAR(200), CreatedTime DATETIME DEFAULT CURRENT_TIMESTAMP );为什么建议用流水表如果只维护一个数量字段一旦数据录错你根本不知道这个数是哪一次操作产生的无法追溯。有了StockLog每一笔入库、出库、盘点调整都有记录可以随时倒查还能做库存趋势分析。这个设计在我实际使用中帮了大忙至少三次排查数据问题都靠它。3.2 扫码入库流程与实现入库流程设计为多个环节操作员只需要专注扫码。入库主窗体有一个大的DataGridView用于显示当前批次的所有入库明细。操作员每次扫到一个商品条码系统自动查找商品档案如果没有找到提示“未注册商品是否新建”如果找到了先判断当前批次明细里是否存在该商品如果已存在则数量自增1否则新加一行。核心逻辑如下private void BarcodeForInbound(string barcode) { var product productService.GetByBarcode(barcode); if (product null) { var dialog new ProductEditForm(barcode); if (dialog.ShowDialog() DialogResult.OK) { product dialog.Product; } else { return; // 用户取消丢弃该条码 } } var existingRow FindRowByProductId(product.Id); if (existingRow ! null) { int currentQty Convert.ToInt32(existingRow.Cells[colQty].Value); existingRow.Cells[colQty].Value currentQty 1; } else { int index gridInbound.Rows.Add(); gridInbound.Rows[index].Cells[colBarcode].Value product.Barcode; gridInbound.Rows[index].Cells[colName].Value product.Name; gridInbound.Rows[index].Cells[colQty].Value 1; gridInbound.Rows[index].Cells[colPrice].Value product.DefaultPrice; gridInbound.Rows[index].Tag product.Id; } // 扫码成功提示 PlaySuccessSound(); lblLastBarcode.Text barcode; }点击“保存入库”按钮后系统在同一个事务里完成三件事写入入库单主表、写入入库单明细表、更新库存和库存流水。用EF Core事务保证数据一致性using (var db new AppDbContext()) { using (var tx db.Database.BeginTransaction()) { try { var inbound new InboundOrder { OrderNo GenerateOrderNo(IN), TotalQuantity totalQty, CreatedTime DateTime.Now }; db.InboundOrders.Add(inbound); db.SaveChanges(); foreach (DataGridViewRow row in gridInbound.Rows) { int productId Convert.ToInt32(row.Tag); int qty Convert.ToInt32(row.Cells[colQty].Value); var inventory db.Inventories.Find(productId); int beforeQty inventory.Quantity; inventory.Quantity qty; db.StockLogs.Add(new StockLog { ProductId productId, ChangeType 1, // 入库 Quantity qty, BeforeQuantity beforeQty, AfterQuantity inventory.Quantity, RefType Inbound, RefId inbound.Id }); } db.SaveChanges(); tx.Commit(); } catch { tx.Rollback(); throw; } } }事务保证了“要么全部成功要么全部失败”扫了10个品类的货不可能出现主单保存了明细没保存的情况。日志中的RefType和RefId字段是索引做溯源时通过它一键关联到原始单据。3.3 扫码出库流程与库存校验出库流程比入库多了一步库存校验。扫码的时候如果库存不足系统直接拦截不允许出库。出库逻辑和入库类似就是在扣减数量之前先检查当前可用库存Quantity减去LockedQuantityprivate void BarcodeForOutbound(string barcode) { var product productService.GetByBarcode(barcode); if (product null) { MessageBox.Show(未找到商品请先建档); return; } var inventory inventoryService.GetByProductId(product.Id); int availableQty inventory.Quantity - inventory.LockedQuantity; // 检查当前格子里的数量 var existingRow FindRowByProductId(product.Id); int currentRequestedQty existingRow null ? 0 : Convert.ToInt32(existingRow.Cells[colQty].Value); if (availableQty currentRequestedQty 1) { MessageBox.Show($库存不足当前可用库存为 {availableQty}); return; } // 继续添加行或者累加数量 // ... }出库单保存时事务里要执行的操作是写入库单、扣减库存、写流水同时检查是否有关联的订单。如果这个出库来源于订单还要更新订单状态。3.4 订单管理模块扫码选品与自动联动订单管理模块是我觉得整个系统里最出彩的部分。操作员创建一张订单后进入订单明细编辑界面同样用扫码枪扫描商品条码添加商品单价自动带出数量累加金额实时刷新。订单的主流程分四步草稿状态、已审核、部分出库、已完成。订单创建后默认是“草稿”可以修改商品明细点击“审核”后订单不能再修改商品系统自动锁定库存LockedQuantity 1根据订单生成出库单扫码出库后回写订单出库数量出库数量达到订单数量后订单状态变为“已完成”锁定库存的逻辑很关键防止卖出去的东西又被别人拿走。比如库存100件A订单锁了60件B订单再尝试锁定60件时可用库存只剩40件系统拒绝锁定。订单明细的扫码添加逻辑与出入库基本一致但多了一个金额计算。我这里用DataGridView的CellValueChanged事件来计算行金额和总金额private void gridOrder_CellValueChanged(object sender, DataGridViewCellEventArgs e) { if (e.RowIndex 0) return; if (gridOrder.Columns[e.ColumnIndex].Name colPrice || gridOrder.Columns[e.ColumnIndex].Name colQty) { decimal price Convert.ToDecimal(gridOrder.Rows[e.RowIndex].Cells[colPrice].Value ?? 0); int qty Convert.ToInt32(gridOrder.Rows[e.RowIndex].Cells[colQty].Value ?? 0); gridOrder.Rows[e.RowIndex].Cells[colAmount].Value price * qty; // 重新计算订单总金额 decimal total 0; foreach (DataGridViewRow row in gridOrder.Rows) { total Convert.ToDecimal(row.Cells[colAmount].Value ?? 0); } lblTotal.Text total.ToString(F2); } }3.5 DataGridView性能和定位技巧在这个系统里DataGridView用得非常频繁有几个技巧值得分享。扫码后自动滚动到当前行。当订单明细比较多时扫到一个新商品后DataGridView应该自动滚动到新行并选中它方便操作员确认。代码就一行gridOrder.FirstDisplayedScrollingRowIndex index; gridOrder.CurrentCell gridOrder.Rows[index].Cells[colName];缓存商品数据避免频繁查库。扫码枪是很快的一秒可以扫好几件货。如果你每次扫码都去数据库查商品信息数据库压力不小而且界面会卡。我的方案是启动时把商品档案全量加载到一个DataTable或者Dictionary里运行时直接内存查找更新商品档案时刷新缓存private Dictionarystring, Product productCache new Dictionarystring, Product(); private void LoadProductCache() { productCache.Clear(); var products productService.GetAll(); foreach (var p in products) { productCache[p.Barcode] p; } }这段缓存代码在连续扫描的场景下效果非常明显。添加了缓存之后扫30个条码从原来的来回闪烁变成极速命中操作体验完全不一样。4. 常见问题与排查技巧实录4.1 扫码枪扫码“丢码”和“乱码”先说一下丢码问题。现象是扫10个条码有时候只进来8个或者前面几位字符丢了。排查思路分两种情况。如果是键盘模式大概率是焦点问题。扫码枪把字符发给当前活动窗口如果你的窗体没有获得焦点或者某个Button控件抢占了焦点并把回车当成“点击”事件触发条码就丢一半。解决办法是开头提到的窗体级KeyPreview处理并确保主窗体始终处于激活状态。如果是串口模式丢码先查串口参数设置是否与扫码枪匹配。常见的问题是波特率不一致导致数据乱码。用串口调试助手先抓一下原始数据如果抓到的就是乱码那就是硬件参数问题跟代码无关。这里还要特别提醒一点扫码枪对条码质量是有要求的。条码打印不清晰、有污渍磨损会导致扫描器识别错误出现乱码也是在所难免。这种问题只能从源头解决换好一点的条码纸、定期清洁条码枪窗口。4.2 中文输入法导致的扫入问题WinForm下的一个经典坑如果焦点在某个能输入中文的控件上而系统当前输入法是中文输入状态扫码枪的字符输入可能会被输入法“吃掉”一部分导致条码不完整。解决办法有两个。第一个是全局切换到英文输入法在窗体的Shown事件里执行InputLanguage.CurrentInputLanguage InputLanguage.FromCulture(new System.Globalization.CultureInfo(en-US));第二个办法是每次扫码触发KeyPress后将键盘布局强制切到英文。实操时在ProcessBarcode方法开头切一次就行。这个方法解决了我系统里90%以上的“丢码”问题。4.3 UI卡顿与多线程的正确打开方式系统跑了一段时间后订单列表的数据量变大查询可能要好几秒。这时候如果直接放在UI线程里执行界面会“假死”看起来像崩溃了一样。现在的写法是使用async/await配合Entity Framework的异步方法private async void btnQuery_Click(object sender, EventArgs e) { btnQuery.Enabled false; try { var list await Task.Run(() orderService.QueryOrders(txtKeyword.Text, dtpStart.Value, dtpEnd.Value)); gridOrders.DataSource list; } catch (Exception ex) { MessageBox.Show(查询失败: ex.Message); } finally { btnQuery.Enabled true; } }Task.Run把耗时操作放到线程池里await让出UI线程界面在等待期间仍然可以正常响应。这里不要用Thread.Sleep这种阻塞式写法做延时在UI线程里Sleep会让整个窗体冻结即使数据量很小也尽量不要用。4.4 定时任务与库存预警通知系统里有一个库存预警功能当某个商品库存低于最低库存时在启动时和每天18点弹出提醒。我用了WinForm里的Timer控件做定时任务。Timer控件的用法很简单拖到窗体上设置Interval属性为600001分钟然后在Tick事件里做时间判断。但在实际使用中遇到一个问题默认的Timer控件和另外两个Timer类System.Threading.Timer、System.Timers.Timer行为不一样很多初学者搞混。WinForm的Timer依赖Windows消息循环Tick事件一定在UI线程执行可以直接更新控件但精度不高。System.Threading.Timer在后台线程执行精度高但不能直接更新UI。在这套系统里我只在UI线程里做了简单的时间检查所以用WinForm自带的Timer就够了。4.5 扫码重复录入的防抖处理实际操作中一个常见的现象是操作员因为紧张或者习惯问题在一个条码上重复扫码。或者扫码枪灵敏度太高同一个码连续扫了两次。系统里如果不做防重处理就会出现同一商品一秒钟内入库两次。我的方案是加一个扫描去重窗口同一商品ID在3秒内只能扫入一次入库明细重复扫码的会提示“刚刚已扫描过该商品请确认”。核心实现用一个字典保存最近扫描时间private Dictionaryint, DateTime lastScanTimeMap new Dictionaryint, DateTime(); private bool CheckDuplicate(int productId) { if (lastScanTimeMap.ContainsKey(productId)) { DateTime last lastScanTimeMap[productId]; if ((DateTime.Now - last).TotalSeconds 3) { return true; } } lastScanTimeMap[productId] DateTime.Now; return false; }这个3秒的阈值可以根据实际场景调整。如果是那种多个同款货物连续扫描的场景比如一次入库50箱同样的商品阈值设置太长会影响效率我会在这种场景下把它改成1秒或者完全关闭防重。4.6 系统设置界面与PropertyGrid的只读问题系统设置里需要配置扫描枪前缀、尾缀、商品单价默认值、订单流水号格式等信息。之前看到也有不少人碰到PropertyGrid控件只能看不能改的问题其实PropertyGrid本身是支持编辑的不能编辑的原因通常是绑定的对象没有用public属性暴露字段或者属性没有set访问器。我的设置页用了一个简单的Settings类属性全部公开且带setpublic class AppSettings { public string ScanPrefix { get; set; } ; public string ScanSuffix { get; set; } \r; public int DuplicateTimeWindow { get; set; } 3; public string OrderPrefix { get; set; } SO; }PropertyGrid绑定这个类的实例后属性就可以正常编辑了。唯一要注意的是修改后需要重新读取一次设置对象才能刷新界面上其他控件的绑定值。5. 设备扩展与界面优化实践5.1 摄像头拍照存档AForge实践有些仓库场景除了扫码记录还要拍照留底比如验收货物时给货物拍照、给运单拍照。系统里我扩展了一个拍照模块用了AForge库来调用USB摄像头。AForge.Video.FFMPEG和AForge.Video.DirectShow这两个包配合使用。先枚举摄像头设备private FilterInfoCollection videoDevices; private void LoadCameraDevices() { videoDevices new FilterInfoCollection(FilterCategory.VideoInputDevice); foreach (FilterInfo device in videoDevices) { cmbCamera.Items.Add(device.Name); } }选中设备后开始预览VideoCaptureDevice videoSource new VideoCaptureDevice(videoDevices[0].MonikerString); videoSource.NewFrame VideoSource_NewFrame; videoSource.Start(); private void VideoSource_NewFrame(object sender, NewFrameEventArgs eventArgs) { Bitmap bitmap (Bitmap)eventArgs.Frame.Clone(); picCamera.Image bitmap; }注意NewFrame事件也是在后台线程触发的更新PictureBox同样需要Invoke。AForge会持续占用摄像头资源窗体关闭时必须调用SignalToStop和WaitForStop释放资源否则摄像头的灯会一直亮着。5.2 条码标签打印扩展出入库记录生成后如果需要给货物贴标签可以加一个打印模块。最简单的做法是调用Bartender或者斑马打印机自带的打印命令。我采用的办法是生成打印模板后走Windows打印队列利用System.Drawing.Printing的PrintDocument来打印条码标签。不过这个模块在我实际项目中实现得比较浅主要是条码标签格式不统一每家供应商的规格不一样定制化成本较高。如果你有这方面的需求建议单独做一套标签模板配置功能。5.3 WinForm界面美化AntdUI与SunnyUI对比说句实话默认的WinForm控件样式停留在上世纪放到现在真的有点丑。好在现在有第三方控件库可以补救主流的两个是SunnyUI和AntdUI。SunnyUI 是开源免费的控件风格现代自带中文字体优化和主题皮肤封装了按钮、输入框、表格、弹窗等常用控件。适合老项目逐步替换。AntdUI 则是把蚂蚁设计语言的风格带到了WinForm上走扁平化路线控件颜色和交互风格更符合当前审美。但它的内部API设计得很另类有些属性和原生控件差异挺大初学者上手需要一段时间。我的建议是如果项目工期紧要求别太苛刻用SunnyUI最快如果你在乎交互细节、愿意花时间调样式选AntdUI。我自己用的是AntdUI主窗体换成它的控件后整个系统的质感提升明显仓库大姐都反馈“看着舒服了”。5.4 手持PDA与工业安卓终端的对接随着业务扩大有时候工人会拿着PDA在仓库里边走边扫。PDA端普遍是安卓系统这时候WinForm管不了的。我采用的方案是借助Socket服务把PDA扫码数据转发到电脑端。WinForm程序里启动了一个TCP监听服务PDA端扫码后通过TCP/IP发送条码和操作类型WinForm收到数据后解析、入库、返回结果。核心逻辑就是一个轻量级TCP服务端private TcpListener tcpListener; private void StartSocketServer(int port) { tcpListener new TcpListener(IPAddress.Any, port); tcpListener.Start(); var acceptThread new Thread(AcceptClient); acceptThread.IsBackground true; acceptThread.Start(); } private void AcceptClient() { while (true) { TcpClient client tcpListener.AcceptTcpClient(); var thread new Thread(HandleClient); thread.IsBackground true; thread.Start(client); } }这里需要注意TCP服务端不能放在UI线程里否则AcceptTcpClient会阻塞界面。放在后台线程之后收到数据仍要Invoke回UI线程处理业务逻辑。当初加上这个功能以后系统的应用场景一下子拓宽了。工人在货架边扫码录入数据实时同步到电脑端的仓储系统做到了“人在货架、数据在系统”。6. 针对新手的一些建议这篇文章涉及的内容比较多最后再分享一点个人的经验和学习建议。如果你是完全的新手刚接触C#和WinForm建议先做一个小而美的模块练手比如只做一个“扫码录入商品 查询库存”的小工具跑通了再去扩展出入库、订单、打印等模块。不要一上来就想着做大而全的系统WinForm的难点不在于控件用得好而在于你对业务流程的理解和数据结构的设计是否合理。我踩过的最大的坑就是一开始没设计流水表导致库存数据出错时排查困难。后来补上StockLog表之后所有问题都有迹可循系统交付后的维护难度降了一半。还有一个小技巧用WinForm做管理系统时把“窗体切换”这个交互做得简单直接一点。仓库工人的电脑操作水平参差不齐界面花哨不如操作路径短。扫码枪扫一下、回车、下一个三步以内完成一次操作这样的系统才是好用的系统。工人们真正用起来之后项目的价值才能真正体现出来。如果后续有条件建议往三层架构和模块解耦上多花时间把数据访问层独立出来这样以后升级Web管理端、对接API代码复用的成本会低很多。这套WinForm系统目前在公司已经稳定运行一年多每天处理上千次扫码没出现过一次数据错乱核心就在于结构清晰、流水完整、交互直接。本文还有配套的精品资源点击获取
返回列表