C# WinForms 固定资产管理系统:条码打印与扫码盘点实战
简介这份资源是基于C#开发的带条码打印功能的固定资产管理系统完整源码面向计算机相关专业的毕业设计学生及需要C#桌面应用实战练习的开发者可帮助解决固定资产登记、条码生成打印、设备台账管理等典型业务场景的开发需求。压缩包共196个文件约2.65MB以39个cs源码文件、33个resx与33个resources资源文件、30个ico图标、25个dll依赖库为主另含sln解决方案、csproj项目文件、config配置及exe可执行程序结构完整配置环境后即可本地编译运行。目前已有236人学习下载。源码功能经过实际验证涵盖资产信息录入、条码打印、数据查询与数据库备份等模块读者可据此快速理解C#窗体应用的工程组织方式参考条码打印与数据库交互的实现思路并在此基础上完成功能扩展或论文撰写适合作为毕业设计参考与课程实践素材。1. 从一台标签打印机说起固定资产系统为什么绕不开条码很多做企业内部系统的朋友都遇到过这个场景行政拿着一沓 Excel 来找你说资产盘点又要开始了几百台设备靠人工对编号眼睛都看花了。你心想不就是个增删改查吗两天撸一个。结果真动手才发现麻烦的根本不是数据库而是那台躺在角落里的条码打印机——驱动装不上、中文打出来是乱码、打一张吐三张、条码扫不出来。基于 C# 的带条码打印的固定资产管理系统核心难点恰恰不在「固定资产管理」而在「带条码打印」这五个字。它要解决的是资产从入库、贴标、领用、盘点到报废的全生命周期记录同时把每台设备的唯一身份用条码固化下来让扫码枪一扫就能调出档案。这套东西适合谁适合手里有 C# 基础、想给中小企业或自己单位做一套轻量资产管理工具的开发者也适合已经有一版系统但打印环节天天翻车的同行。下面我按自己踩过的路把选型、建库、打印、扫码、避坑一条条讲清楚源码结构怎么组织、参数怎么调都落到能直接抄的程度。2. 技术选型与数据库设计为什么用 WinForms SQLite 而不是 Web2.1 桌面端还是 Web 端先想清楚打印这件事固定资产管理系统最常见的两种形态B/S 和 C/S。很多人一上来就想用 ASP.NET Core 做 Web 版觉得时髦、好部署。但只要涉及条码打印Web 端就会立刻给你上强度。浏览器调用本地打印机要走打印控件或者中间服务标签的精确排版毫米级定位在 HTML 里极其难控不同浏览器渲染差异能把你对齐好的标签搞歪。而 WinForms 直接调用System.Drawing.Printing命名空间PrintDocument配合Graphics对象想画在哪就画在哪坐标以百分之一英寸为单位标签机走纸精度能对得上。所以这套系统的常见做法是WinForms 做客户端SQLite 做本地库单机或小局域网共享文件夹都能跑零配置部署拷贝即用。选 SQLite 而不是 SQL Server理由很实在固定资产数据量通常几千到几万条SQLite 完全扛得住而且不用装服务、不用配连接串、不用管账号密码。数据库就是一个.db文件备份就是复制文件对中小企业 IT 维护来说这是巨大优势。如果单位已经有 SQL Server把 DAL 层的连接换成SqlConnection即可业务逻辑不用动这也是分层设计的意义。2.2 资产表、分类表、领用记录表怎么建数据库设计决定了后面所有功能顺不顺。核心三张表资产主表、资产分类表、领用/变更记录表。资产主表里必须有一个字段专门存条码内容通常是「资产编号」它是全局唯一的也是打印和扫码的锚点。-- 资产分类表 CREATE TABLE AssetCategory ( CategoryId INTEGER PRIMARY KEY AUTOINCREMENT, CategoryName TEXT NOT NULL, -- 如笔记本电脑、打印机 Prefix TEXT NOT NULL -- 编号前缀如 NB、PR ); -- 资产主表 CREATE TABLE Asset ( AssetId INTEGER PRIMARY KEY AUTOINCREMENT, AssetCode TEXT NOT NULL UNIQUE, -- 资产编号即条码内容 AssetName TEXT NOT NULL, CategoryId INTEGER NOT NULL, Spec TEXT, -- 规格型号 Price REAL, BuyDate TEXT, -- 购入日期 yyyy-MM-dd Status INTEGER DEFAULT 1, -- 1在用 2闲置 3报废 Keeper TEXT, -- 当前保管人 Remark TEXT, FOREIGN KEY(CategoryId) REFERENCES AssetCategory(CategoryId) ); -- 领用/变更记录表 CREATE TABLE AssetLog ( LogId INTEGER PRIMARY KEY AUTOINCREMENT, AssetCode TEXT NOT NULL, Action TEXT NOT NULL, -- 领用/归还/报废/变更 Operator TEXT, LogTime TEXT, Note TEXT );AssetCode上加UNIQUE约束是关键它保证条码不会重复扫码时能唯一定位。Prefix字段让编号有可读性比如笔记本都是NB2024001这种格式人眼也能大致判断类别。Status用整数而不是字符串查询和统计更快界面显示时再映射成中文。AssetLog单独拆表是为了保留完整的历史轨迹盘点时能追溯每台设备经手过谁。2.3 用 Dapper 还是 EF Core我一般这样选数据访问层有两个主流选择EF Core 和 Dapper。EF Core 适合表关系复杂、需要迁移管理的项目但它的启动开销和「黑匣子」感在小型桌面程序里有点重。Dapper 是轻量 ORM本质是IDbConnection的扩展方法SQL 自己写映射自动做性能接近原生 ADO.NET调试时 SQL 一目了然。这套系统我一般用 Dapper因为查询逻辑简单、SQL 可控出问题好排查。// 引入 Dapper 后一个查询方法长这样 public ListAsset SearchAssets(string keyword) { using (var conn new SQLiteConnection(ConnStr)) { // 参数化查询防止注入kw 用模糊匹配 string sql SELECT * FROM Asset WHERE AssetCode LIKE kw OR AssetName LIKE kw ORDER BY AssetCode; return conn.QueryAsset(sql, new { kw % keyword % }).ToList(); } }QueryAsset会自动把结果集列名映射到Asset类的同名属性省掉手写DataReader的循环。using保证连接释放SQLite 连接虽然轻但养成习惯没坏处。参数用匿名对象传Dapper 自动处理类型和转义别用字符串拼接这是血泪经验——早期图省事拼 SQL遇到资产名里带单引号直接报错。3. 条码生成与打印从 Code128 编码到标签排版3.1 条码类型怎么选Code128 为什么是默认答案条码类型一大堆EAN-13、Code39、Code128、QR 码。固定资产场景我几乎只用 Code128原因有三。第一它支持全 ASCII 字符字母数字混合的资产编号如NB2024001直接能编Code39 虽然也支持但密度低、条码长。第二Code128 密度高同样内容占的宽度小适合小标签。第三它有校验位扫码容错比 Code39 好。QR 码适合存更多信息比如带 URL但固定资产只需要一个编号用 QR 有点杀鸡用牛刀而且 QR 对打印精度要求更高廉价标签机容易糊。生成条码不要自己画线条用成熟库。C# 里常用的是 ZXing.Net它能生成位图也能直接输出到Graphics。// 用 ZXing.Net 生成 Code128 条码位图 using ZXing; using ZXing.Common; public Bitmap GenerateBarcode(string content, int width, int height) { var writer new BarcodeWriter { Format BarcodeFormat.CODE_128, Options new EncodingOptions { Width width, // 目标宽度像素 Height height, // 目标高度像素 Margin 2, // 四周留白太小扫不出 PureBarcode true // 不显示下方文字文字自己画 } }; return writer.Write(content); }Margin这个参数特别容易被忽略。条码两侧必须留白静区一般至少 10 倍最小窄条宽度设成 2 到 5 个模块比较稳。PureBarcode true表示只出条码不出文字因为标签上的文字位置我们要自己控制ZXing 自带的文字排版不好看。Width和Height是像素最终打印尺寸取决于打印机的 DPI300 DPI 的机器上 1 毫米约等于 11.8 像素算尺寸时要换算。3.2 用 PrintDocument 画一张标签坐标怎么算WinForms 打印的核心是PrintDocument的PrintPage事件在里面用Graphics画。难点是坐标单位Graphics默认单位是像素但打印时应该用GraphicsUnit.Millimeter或者把PageUnit设成毫米这样排版才和物理尺寸对应。private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e) { var g e.Graphics; g.PageUnit GraphicsUnit.Millimeter; // 关键单位换成毫米 // 假设标签 40mm x 30mm左上角留 3mm 边距 float margin 3f; string code _currentAsset.AssetCode; // 1. 画条码先生成位图再按毫米尺寸绘制 using (var bmp GenerateBarcode(code, 300, 80)) { // 目标区域宽 34mm高 10mm var rect new RectangleF(margin, margin, 34f, 10f); g.DrawImage(bmp, rect); } // 2. 画资产名称字体 8pt using (var font new Font(微软雅黑, 8)) using (var brush new SolidBrush(Color.Black)) { g.DrawString(_currentAsset.AssetName, font, brush, new PointF(margin, margin 11f)); g.DrawString(编号 code, font, brush, new PointF(margin, margin 16f)); } }g.PageUnit GraphicsUnit.Millimeter是整段代码的灵魂设了它之后所有坐标和尺寸都以毫米计和标签纸的实际尺寸一一对应。DrawImage把条码位图缩放到指定矩形区域注意缩放会改变条码的窄条宽度比例如果缩放比例不是整数倍条码可能变形导致扫不出。稳妥做法是让生成位图的宽高比和目标矩形一致或者干脆按目标毫米尺寸反推像素数再生成。字体用「微软雅黑」而不是默认的宋体小字号下雅黑更清晰。文字位置用PointF精确定位别用DrawString的自动换行标签空间寸土寸金。3.3 批量打印和走纸校准参数在哪里调单张打印跑通后批量打印是刚需——入库 50 台设备要一次打完。做法是把待打印列表传给打印逻辑在PrintPage里判断是否还有下一页用e.HasMorePages true继续。private int _printIndex 0; private ListAsset _printList; private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e) { if (_printIndex _printList.Count) { e.HasMorePages false; return; } _currentAsset _printList[_printIndex]; DrawLabel(e.Graphics, _currentAsset); // 抽出来的绘制方法 _printIndex; e.HasMorePages _printIndex _printList.Count; }HasMorePages是分页开关返回 true 系统会自动再触发一次PrintPage。_printIndex要在开始打印前重置为 0否则第二次打印会从中间开始这个坑我踩过。走纸校准是硬件问题标签机有「标签间隙传感器」如果打印内容整体偏移先调打印机驱动的「纸张来源」为「间隙纸」再在驱动里做「校准」让机器重新测纸。软件层面能调的是PrintDocument.DefaultPageSettings.Margins但更推荐在绘制时用边距变量控制别依赖系统边距不同驱动对边距的解释不一致。4. 扫码盘点与数据一致性让扫码枪真正好用4.1 扫码枪本质是键盘别把它当串口设备很多人以为扫码枪要写串口通信其实绝大多数 USB 扫码枪工作在「HID 键盘模式」——扫一下它就模拟键盘把条码内容敲进去再补一个回车。所以对接极其简单界面上放一个TextBox让焦点停在上面用户扫码内容自动填入回车触发查询。private void TxtScan_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode Keys.Enter) { string code TxtScan.Text.Trim(); if (string.IsNullOrEmpty(code)) return; var asset _assetService.GetByCode(code); if (asset null) { // 扫到库里没有的码提示并记录 MessageBox.Show(未找到资产 code); } else { ShowAssetDetail(asset); MarkAsInventoried(asset); // 盘点标记 } TxtScan.Clear(); // 清空准备下一次 e.SuppressKeyPress true; // 屏蔽回车提示音 } }e.SuppressKeyPress true是为了去掉回车时系统那声「叮」盘点时连续扫几百次有提示音很烦。扫码枪的输入速度极快KeyDown事件完全跟得上不需要特殊处理。如果扫码枪配的是串口模式少数工业枪那才需要SerialPort类但那是另一套逻辑普通办公场景用不上。4.2 盘点时的并发与重复扫描怎么处理盘点场景有个现实问题同一台设备可能被扫两次或者多人同时盘点。重复扫描要在逻辑上幂等——第一次扫标记为「已盘」第二次扫提示「已盘点过」而不是报错。public bool MarkAsInventoried(string assetCode) { using (var conn new SQLiteConnection(ConnStr)) { // 先查当前盘点状态 var status conn.ExecuteScalarint?( SELECT Inventoried FROM Asset WHERE AssetCodec, new { c assetCode }); if (status 1) return false; // 已盘过 conn.Execute( UPDATE Asset SET Inventoried1, InventTimet WHERE AssetCodec, new { t DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss), c assetCode }); return true; } }ExecuteScalarint?返回可空整数因为查不到记录时返回 null用int?接住避免异常。先查后改在单机场景够用如果是多人共享数据库文件SQLite 的写锁会串行化操作不会出现脏写但要注意别长时间持有连接。盘点结束后一键导出「未盘资产清单」这才是盘点的价值所在——找出账实不符的设备。4.3 数据导入导出与 Excel 对接资产初始数据往往在 Excel 里导入功能必须有。用 EPPlus 或 NPOI 读 Excel逐行校验后入库。导出同理把盘点结果写成 Excel 交给行政。// 用 EPPlus 读取 Excel 并批量入库 using (var package new ExcelPackage(new FileInfo(path))) { var sheet package.Workbook.Worksheets[0]; int rows sheet.Dimension.Rows; for (int r 2; r rows; r) // 第1行是表头 { string code sheet.Cells[r, 1].Text.Trim(); if (string.IsNullOrEmpty(code)) continue; // 校验编号是否已存在存在则跳过或更新 if (_assetService.Exists(code)) continue; var asset new Asset { AssetCode code, AssetName sheet.Cells[r, 2].Text.Trim(), Spec sheet.Cells[r, 3].Text.Trim(), BuyDate sheet.Cells[r, 4].Text.Trim() }; _assetService.Insert(asset); } }sheet.Dimension.Rows拿到实际有数据的行数比死循环到空行靠谱。从第 2 行开始是因为第 1 行是表头。导入前一定要做编号去重校验否则UNIQUE约束会抛异常中断整个导入。大批量导入建议包在事务里要么全成功要么全回滚避免导入一半失败留下脏数据。5. 避坑与排查条码打印和扫码最常见的五个翻车现场5.1 中文打出来是乱码或方块现象标签上资产名称显示成「口口口」或者乱码。原因通常是字体问题——Graphics.DrawString用的字体在打印机端不存在或者用了不支持中文的字体。解决显式指定「微软雅黑」「宋体」这类系统自带中文字体别用 Arial。如果还乱码检查PrintDocument的Graphics是否被设置了奇怪的TextRenderingHint正常用默认即可。另外某些廉价标签机的驱动会把文字当图形处理这时要确认驱动里没有开启「使用打印机字体」选项。5.2 条码扫不出来扫码枪一直报错现象条码肉眼看着清楚扫码枪就是读不出。原因有三静区不够条码两侧留白太少、条码被缩放变形窄条宽度比例失真、打印浓度太低热敏纸发灰。解决把Margin调到 3 以上生成位图时按目标尺寸反推像素避免非等比缩放在打印机驱动里把「打印浓度」调高热敏机调到 8 到 10 档。还有一个隐蔽原因条码内容里带了空格或特殊字符Code128 虽然支持但某些扫码枪默认配置会过滤尽量让资产编号只含字母数字。5.3 打一张吐三张或者内容整体偏移现象明明只点了一次打印机器吐出好几张空白标签或者内容偏到标签边缘。原因打印机没做纸张校准间隙传感器没识别到标签边界或者PrintDocument的纸张尺寸设成了 A4 而不是标签实际尺寸。解决先在打印机驱动里执行「校准」或「测纸」让机器重新学习标签间隙然后在代码里设置PaperSize为标签的实际毫米尺寸比如new PaperSize(Label, 40, 30)注意这里的单位是百分之一英寸40mm 约等于 157。别用默认纸张默认往往是 A4。5.4 扫码枪扫出来的内容多了回车或少了字符现象扫NB2024001结果 TextBox 里是NB2024001加一个换行或者首字符丢失。原因扫码枪的「后缀」配置问题有的枪默认加回车有的加 Tab首字符丢失通常是焦点还没到 TextBox 就扫码了。解决用扫码枪的配置手册通常扫一个配置条码把后缀设成回车软件层面在KeyDown里处理回车即可。首字符丢失要在窗体激活时就把焦点设到扫码框this.ActiveControl TxtScan别让用户先点一下。5.5 数据库文件被占用程序打不开现象程序启动报「database is locked」或者文件被其他进程占用。原因上一次运行异常退出SQLite 连接没释放或者数据库文件放在共享文件夹多人同时写。解决所有SQLiteConnection都用using包裹确保释放连接串里加Poolingfalse避免连接池持有文件句柄多人场景改用「一写多读」模式或者干脆升级到 SQL Server。备份数据库时先关程序直接复制.db文件别在程序运行时复制可能拿到不完整的数据。6. 把源码结构理顺分层、复用与二次开发的一点心得一套能长期维护的固定资产系统源码结构比功能本身更重要。我一般分四层Models放实体类DAL放 Dapper 数据访问BLL放业务逻辑比如编号生成、盘点状态流转UI放 WinForms 窗体。条码生成和打印单独抽一个BarcodeHelper和LabelPrinter类这样换打印机、换标签尺寸时只改这两个文件不碰业务代码。编号自动生成是个值得单独说的技巧。不要用数据库自增 ID 当资产编号因为自增 ID 不体现类别而且删除后会有空洞。我的做法是按「前缀 年份 三位流水」生成流水号查当前类别当年最大编号再加一。public string GenerateAssetCode(string prefix) { string year DateTime.Now.Year.ToString(); using (var conn new SQLiteConnection(ConnStr)) { // 查该前缀该年份的最大编号 string maxCode conn.ExecuteScalarstring( SELECT MAX(AssetCode) FROM Asset WHERE AssetCode LIKE p, new { p prefix year % }); int seq 1; if (!string.IsNullOrEmpty(maxCode)) { // 取后三位转整数加一 seq int.Parse(maxCode.Substring(maxCode.Length - 3)) 1; } return prefix year seq.ToString(D3); // D3 补零到三位 } }MAX(AssetCode)配合LIKE前缀匹配拿到当前最大编号。Substring取后三位D3格式化成001这种。这个逻辑在单机没问题多人并发时可能撞号所以AssetCode的UNIQUE约束是最后一道防线插入失败就重试一次。二次开发时如果单位要加「折旧计算」在BLL加一个DepreciationService即可UI 加个按钮调用完全不影响打印模块。验证一套系统好不好用我的习惯是拿真实标签纸打 20 张用扫码枪连续扫中间故意扫重复的、扫不存在的看程序反应是否合理。打印偏移就用卡尺量别靠眼睛估。这套基于 C# 的固定资产管理系统难点从来不是写代码而是把打印和扫码这两个「物理世界接口」调顺。我自己的教训是别等到盘点前一天才试打印机提前把标签、驱动、浓度都调好否则现场翻车没有后悔药。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →