简介:在仓储管理、门店收银、固定资产盘点等场景中,条码扫描是提升录入效率的关键手段。扫码枪通过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)13,KeyDown事件中是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对这两种数据库的兼容性都很好。
先看核心表结构。第一张是商品表Product:
CREATE 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减去LockedQuantity):
private 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 Dictionary<string, Product> productCache = new Dictionary<string, 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属性为60000(1分钟),然后在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 Dictionary<int, DateTime> lastScanTimeMap = new Dictionary<int, 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类,属性全部公开且带set:
public 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系统目前在公司已经稳定运行一年多,每天处理上千次扫码,没出现过一次数据错乱,核心就在于结构清晰、流水完整、交互直接。
本文还有配套的精品资源,点击获取