简介:这份资源是一套基于 C# WinForms 开发的快递单打印系统完整源码,面向具备一定 C# 基础、希望学习桌面应用与打印技术的开发者,可用于快速搭建或参考快递单模板设置、打印、查询及管理员权限管理等典型业务场景。压缩包共 97 个文件,约 2.73MB,以 34 个 cs 源码文件为核心,配合 14 个 resx 与 14 个 ico 资源、16 个 gif 及若干 bmp、jpg 图片,另有 csproj、sln 工程文件与 mdf、ldf 数据库文件,覆盖界面、数据访问与打印模块。已有 753 人学习下载。读者可从中获取模板设计、PrintDocument 打印与预览、ADO.NET 数据库查询、基于角色的权限控制等实现思路,并参考其分层目录结构理解 WinForms 项目的组织方式,适合作为课程设计或小型管理系统的实践参考。
1. 快递单打印系统:一个被低估的 WinForms 实战切口
电商仓库里最不起眼、却最容易翻车的一环,往往就是那张快递面单。订单系统导出一批数据,操作员点一下打印,热敏打印机吐出一张张带条码的面单——听起来简单,真做起来,纸张偏移、条码扫不出、批量打印卡死、模板改一版就全乱,全是血泪经验。这套 C# WinForms 快递单打印系统,解决的就是「把订单数据稳定、准确地落到热敏面单上」这件事。它适合两类人:一是需要给中小仓库做一套轻量打单工具的后端或全栈开发者,二是想拿一个真实业务场景练手 WinForms + 打印控件的初学者。下面按「它由什么构成、怎么跑起来、参数怎么调、坑在哪」的顺序拆开讲,能直接照着复现。
2. 系统骨架拆解:WinForms 界面、数据层与打印引擎怎么分工
这套系统的核心不是界面多花哨,而是把「数据 → 模板 → 打印」三段解耦。界面只负责录入和触发,数据层负责订单的增删改查与批量导入,打印引擎负责把一条订单记录映射成一张面单。三者分开,改模板不动业务逻辑,换打印机不动数据代码,这是它能长期用的前提。
2.1 三层结构:为什么不做成一个大 Form
很多新手会把所有逻辑塞进Form1.cs,按钮事件里直接拼字符串、直接调PrintDocument.Print()。这样写第一版能跑,第二版加个「批量打印」就崩。合理的做法是拆成三层:
- 表现层:
MainForm负责订单列表(DataGridView)、单条/批量打印按钮、打印机选择下拉框。 - 数据层:
OrderRepository负责读写订单,常见做法是用本地 SQLite 或直接读写 CSV/Excel,仓库场景下 CSV 导入最省事。 - 打印层:
WaybillPrinter封装PrintDocument,对外只暴露PrintOne(Order)和PrintBatch(List<Order>)。
这样拆的好处是,打印层可以单独写单元测试——给定一条订单,断言生成的Graphics绘制指令序列,不用真的连打印机。我一般会先把打印层做成纯函数式的「绘制到 Graphics」,再包一层PrintDocument,调试时先画到Bitmap上预览,确认版式对了再上纸。
2.2 打印引擎的核心:PrintDocument 与 PrintPage 事件
WinForms 打印的入口是System.Drawing.Printing.PrintDocument,真正干活的是PrintPage事件。每打印一页触发一次,你在事件里用e.Graphics画内容,用e.HasMorePages告诉框架还有没有下一页。批量打印就是靠这个标志位循环。
private int _currentIndex = 0; private List<Order> _batchOrders; private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e) { // 取当前要打印的订单 var order = _batchOrders[_currentIndex]; var g = e.Graphics; // 面单常见尺寸 100mm x 150mm,按打印机 DPI 换算成像素 float dpiX = e.Graphics.DpiX; float dpiY = e.Graphics.DpiY; float widthPx = 100f / 25.4f * dpiX; float heightPx = 150f / 25.4f * dpiY; // 绘制收件人信息、条码、运单号等 DrawWaybill(g, order, widthPx, heightPx); _currentIndex++; // 还有订单就继续,否则结束 e.HasMorePages = _currentIndex < _batchOrders.Count; }这段代码的关键点有三个。第一,_currentIndex是跨页状态,必须放在事件外,否则每页都从第一条开始打。第二,尺寸换算必须用e.Graphics.DpiX/DpiY,不能写死 96,热敏打印机常见 203 DPI,写死会导致整体缩小或溢出。第三,HasMorePages是批量打印的开关,忘了设或设错,要么只打一张,要么无限循环。参数上,PrintDocument.DefaultPageSettings.PaperSize要设成自定义尺寸,标准 A4 套不上面单。
2.3 数据层:订单模型与批量导入
订单模型字段不用多,够面单用就行:运单号、收件人、电话、地址、商品简述、重量、备注。批量导入我一般用 CSV,因为仓库导出的原始数据十有八九是表格,CSV 解析不依赖 Office 组件,部署到没装 Excel 的机器上也不报错。
public class Order { public string WaybillNo { get; set; } // 运单号,条码内容 public string Receiver { get; set; } // 收件人 public string Phone { get; set; } // 电话 public string Address { get; set; } // 地址 public string Goods { get; set; } // 商品简述 public decimal Weight { get; set; } // 重量 kg } public List<Order> LoadFromCsv(string path) { var list = new List<Order>(); // 跳过表头,逐行解析,注意处理逗号在引号内的情况 foreach (var line in File.ReadLines(path).Skip(1)) { var cols = SplitCsvLine(line); if (cols.Length < 6) continue; // 脏数据直接跳过,别让一条坏数据卡住整批 list.Add(new Order { WaybillNo = cols[0].Trim(), Receiver = cols[1].Trim(), Phone = cols[2].Trim(), Address = cols[3].Trim(), Goods = cols[4].Trim(), Weight = decimal.TryParse(cols[5], out var w) ? w : 0m }); } return list; }SplitCsvLine要自己处理引号包裹的逗号,别直接用string.Split(','),地址里带逗号是常态,一拆就错位。decimal.TryParse失败给 0 而不是抛异常,批量导入时一条脏数据不该让整批失败。这些细节看着小,实际是仓库现场最常出问题的地方。
3. 从零跑起来:环境、模板与打印参数配置
拿到源码包后,别急着点运行,先把环境和打印参数对齐,否则第一张纸大概率是歪的。这一章按「装环境 → 配模板 → 调打印机 → 跑单条 → 跑批量」的顺序走,每步都能验证。
3.1 环境准备与项目打开
需要 Visual Studio 2019 及以上,目标框架 .NET Framework 4.7.2 或 .NET 6/8 的 Windows 桌面运行时。打开.sln后先还原 NuGet 包,如果数据层用了 SQLite,会依赖System.Data.SQLite或Microsoft.Data.Sqlite。编译前检查平台目标,打印相关 API 在AnyCPU下一般没问题,但如果引用了 32 位打印驱动组件,要改成x86。
# 命令行还原并编译,确认没有缺包 dotnet restore dotnet build -c Release编译报「找不到 PrintDocument」通常是目标框架选成了 .NET Core 跨平台版,System.Drawing.Printing在非 Windows 目标下不可用,把目标框架改回net6.0-windows或net8.0-windows即可。这一步翻车的人不少,报错信息又不直观,先记下来。
3.2 面单模板:坐标、字体与条码三件事
面单版式本质是「在固定尺寸画布上按坐标画元素」。模板参数建议抽成一个配置类,别散落在绘制代码里:
| 参数 | 典型值 | 说明 |
|---|---|---|
| 面单宽 | 100 mm | 热敏常见规格,按实际纸卷改 |
| 面单高 | 150 mm | 同上 |
| 左边距 | 5 mm | 留出撕纸边 |
| 条码高度 | 15 mm | 太矮扫不出,太高挤占地址区 |
| 正文字体 | 宋体 9pt | 热敏分辨率低,别用细体 |
| 条码类型 | Code128 | 运单号字母数字混合首选 |
条码生成不要自己画,用现成库(如ZXing.Net)生成Bitmap再DrawImage到指定矩形。字体一定用系统自带字体,自定义字体在客户机器上大概率缺失,打印出来是方块。坐标全部用毫米定义,绘制时统一乘 DPI 换算,这样换打印机不用改模板。
3.3 打印机选择与页面设置
PrintDocument.PrinterSettings.PrinterName指定打印机,界面上用PrinterSettings.InstalledPrinters填充下拉框。页面设置里最关键的是PaperSize,必须自定义成面单实际尺寸,否则驱动会按默认纸张缩放。
var doc = new PrintDocument(); doc.PrinterSettings.PrinterName = selectedPrinter; // 自定义纸张,单位百分之一英寸 var paper = new PaperSize("Waybill100x150", 394, 591); // 100mm≈394, 150mm≈591 doc.DefaultPageSettings.PaperSize = paper; doc.DefaultPageSettings.Margins = new Margins(0, 0, 0, 0); // 热敏打印机通常不支持双面,显式关掉避免驱动报错 doc.DefaultPageSettings.PrinterSettings.Duplex = Duplex.Simplex;PaperSize的宽高单位是百分之一英寸,100mm 约等于 394,这个换算错了纸就歪。边距设 0,因为面单内容自己控制坐标,留边距反而挤压绘制区。Duplex显式设单面,部分驱动在双面默认值下会弹确认框,批量打印时卡在那里等人点,现场很尴尬。
3.4 单条打印与批量打印的触发逻辑
单条打印直接PrintOne,批量打印把列表塞进_batchOrders、重置_currentIndex、调Print()。批量时建议加个进度提示,PrintPage里更新一个 Label 或进度条,否则几百张纸打下去界面像卡死。
private void btnPrintBatch_Click(object sender, EventArgs e) { var orders = GetSelectedOrders(); // 从 DataGridView 取勾选行 if (orders.Count == 0) return; _batchOrders = orders; _currentIndex = 0; _printDoc.Print(); // 触发 PrintPage 循环 }注意Print()是同步阻塞的,批量大时界面无响应是正常的,别以为死机了去强杀进程。要体验好就放到后台线程,但PrintDocument跨线程有坑,稳妥做法是用BackgroundWorker并在PrintPage里通过Invoke更新 UI。
4. 避坑与排查:五条现场踩出来的经验
打印这类需求,代码写对只是及格,真正决定能不能上线的是这些边角问题。下面五条都是仓库现场反复出现的,按「现象 → 原因 → 解决」记。
4.1 打印内容整体偏移或缩小
现象:面单内容偏左偏上,或整体缩小一圈,条码扫不出。原因:PaperSize没设成自定义尺寸,驱动按 A4 缩放;或坐标换算用了固定 96 DPI,而打印机是 203 DPI。解决:显式设置PaperSize为面单实际尺寸,所有毫米坐标乘e.Graphics.DpiX/25.4换算,打印前先用PrintPreviewDialog预览确认。
4.2 批量打印只出第一张
现象:选了 50 条订单,只打出一张就停。原因:e.HasMorePages没设,或_currentIndex定义在PrintPage方法内部,每页都重置。解决:_currentIndex提到类字段,PrintPage末尾e.HasMorePages = _currentIndex < _batchOrders.Count,并在开始打印前重置为 0。
4.3 条码能显示但扫不出来
现象:面单上条码肉眼可见,扫码枪读不出。原因:条码高度不足、模块宽度太窄,或热敏打印浓度低导致边缘模糊。解决:条码高度不低于 15mm,Code128 模块宽度按打印机 DPI 调整,打印浓度在驱动里调高一档;生成条码时留足静区(左右空白),别贴着文字画。
4.4 换一台打印机版式全乱
现象:开发机正常,客户机器上内容溢出或错位。原因:两台机器 DPI 不同,模板里混用了像素和毫米;或客户机器缺字体。解决:模板全部用毫米定义,绘制时统一换算;字体只用宋体/黑体等系统自带字体,必要时把字体文件随程序分发并注册。
4.5 打印时程序无响应被误判死机
现象:批量打印时界面卡住,操作员强杀进程,打到一半的单子丢了。原因:Print()同步阻塞 UI 线程,批量大时耗时明显。解决:用BackgroundWorker或Task跑打印,PrintPage里更新进度用Invoke;同时在界面上禁用打印按钮,防止重复触发。
5. 进阶:模板可配置化与打印结果自检
系统能跑之后,真正拉开差距的是「模板能不能不改代码就换」和「打完能不能自证没打错」。这两件事做好了,这套工具才算能交给非开发人员用。
5.1 把模板抽成 JSON 配置
硬编码坐标的模板,客户改一版版式就得重新编译发版,维护成本极高。我的做法是把每个元素的类型、坐标、字体、对齐方式抽成 JSON,绘制引擎读配置逐元素渲染。这样运营人员改个地址区位置,改 JSON 就行,不用碰代码。
{ "widthMm": 100, "heightMm": 150, "elements": [ { "type": "text", "field": "Receiver", "xMm": 10, "yMm": 20, "font": "宋体", "sizePt": 10, "bold": true }, { "type": "text", "field": "Address", "xMm": 10, "yMm": 30, "font": "宋体", "sizePt": 9, "maxWidthMm": 80 }, { "type": "barcode", "field": "WaybillNo", "xMm": 10, "yMm": 100, "heightMm": 15 } ] }绘制引擎遍历elements,按type分派到文字或条码绘制,field对应订单属性名,用反射取值。maxWidthMm控制地址自动换行,避免长地址溢出到条码区。这套结构的好处是新增字段只改配置,不动代码;坏处是反射有性能开销,批量打印时可以把订单转成字典缓存,别每张都反射一遍。
5.2 打印结果自检:先画 Bitmap 再上纸
热敏纸不便宜,打废一张就是成本。我习惯在真正Print()之前,先把同一套绘制逻辑画到一个Bitmap上,尺寸按面单像素算,然后在界面上预览。绘制逻辑复用同一个DrawWaybill(Graphics, Order, w, h),保证预览和实际打印一致。
public Bitmap RenderPreview(Order order, float dpi) { int w = (int)(100f / 25.4f * dpi); int h = (int)(150f / 25.4f * dpi); var bmp = new Bitmap(w, h); using (var g = Graphics.FromImage(bmp)) { g.Clear(Color.White); DrawWaybill(g, order, w, h); // 与打印共用同一绘制函数 } return bmp; }预览确认版式、条码位置、文字不溢出之后再批量打,能挡掉八成低级错误。更进一步,可以把预览图存下来做打印日志,出问题时能回溯「当时到底画成什么样」,比翻聊天记录靠谱得多。
5.3 一个我坚持的习惯
从那以后我每次改打印模板,都强制走一遍「预览 → 打一张 → 扫码验证 → 再批量」的流程,哪怕只改了一个坐标。打印这行当,玄学的地方太多——驱动版本、纸张批次、浓度设置,任何一个变量都能让昨天还好的版式今天翻车。把自检做成流程而不是靠记忆,是这套系统能稳定跑下去的真正原因。希望帮到你。
本文还有配套的精品资源,点击获取