☰
VB.NET进销存源码实战:水晶报表与自定义套打打印方案
2026/10/26 4:49:39 网站建设 项目流程

简介:这份进销存VB.NET源码面向中小企业部署使用,也适合学习WinForm桌面开发的开发者参考,覆盖采购、销售、库存等核心业务流程,帮助解决日常经营数据跟踪与报表输出的问题。资源包共433个文件,约14.2MB,以174个vb源文件与135个resx资源文件为主体,另有35个rpt水晶报表模板、22个mod模块文件及xsd数据集定义,并附带xpMenu、fxyPrintModel等dll组件与可执行程序,结构完整便于二次开发。已有633人学习下载。源码中XP风格菜单、水晶报表实例与自定义套打功能均有完整实现,可据此研究销售、采购、库存报表的生成逻辑与收据发票打印模板定制,在线升级与启动提示模块也提供了可借鉴的工程思路,适合作为VB.NET进销存项目的实践案例。

1. 进销存 VB.NET 源码:一套能直接跑通的水晶报表与自定义套打实战项目

接手一套进销存系统,最怕的不是业务逻辑复杂,而是打印这一关过不去。采购单、销售单、出入库单,客户要的格式千奇百怪,有的要套打针式打印机预印表格,有的要水晶报表导出 PDF 存档。这套 VB.NET 进销存源码把这两条路都走通了:基础单据用 Crystal Reports 出标准报表,特殊格式用自定义套打模块按坐标画。适合两类人——想拿一套完整进销存练手 WinForm 三层架构的开发者,以及被单据打印折磨过、想看看别人怎么把套打做成可配置的熟手。下面按「能干什么、怎么跑、核心怎么实现、怎么改」的顺序拆。

2. 项目定位与技术栈:WinForm 三层架构下的进销存骨架

2.1 这套源码解决什么业务闭环

进销存的核心链路是「采购入库 → 库存变动 → 销售出库 → 往来对账」,这套源码围绕这条链路组织模块。基础资料维护商品、供应商、客户、仓库;采购模块做采购订单和采购入库;销售模块做销售订单和销售出库;库存模块管调拨、盘点、库存查询;报表模块负责各类单据打印和统计。业务上不算庞大,但闭环是完整的,拿来改造成某个细分行业的小型进销存完全够用。

技术栈上,UI 层是 VB.NET WinForm,数据访问走 ADO.NET 直连,报表用 Crystal Reports,套打是自绘。数据库常见做法是 SQL Server,源码里连接字符串集中在配置文件,换库时改一处即可。这种组合在今天看不算新潮,但胜在部署简单——客户端装个运行时,服务器装个数据库就能跑,不需要额外中间件,对中小企业的内网环境很友好。

2.2 技术选型背后的取舍

为什么用 WinForm 而不是 Web?进销存的使用场景大量集中在仓库、门店的固定工位上,操作员要快速录单、快速打印,WinForm 的响应速度和本地打印能力是天然优势。为什么报表用 Crystal Reports?它的设计器成熟,字段拖拽、分组汇总、子报表都能可视化配置,业务人员改个表头不用动代码。为什么套打要自绘?因为预印单据的格式是印刷厂定死的,水晶报表的布局模型对「精确到毫米的绝对定位」支持不够顺手,用 Graphics 直接按坐标画反而更可控。

这里有个血泪经验:很多项目一开始想用一套报表方案通吃所有打印需求,结果标准单据和套打单据互相打架。这套源码把两者分开——标准报表走 Crystal Reports,套打走自定义绘制,各管各的,维护起来清晰得多。

2.3 目录结构速览

源码的目录组织大致如下(不同版本命名略有差异,以实际下载为准):

/进销存系统 /Forms 窗体文件,按模块分子目录 /BasicData 基础资料窗体 /Purchase 采购模块窗体 /Sales 销售模块窗体 /Stock 库存模块窗体 /Report 报表与打印窗体 /Class 业务实体与公共类 /DAL 数据访问层 /BLL 业务逻辑层 /Reports Crystal Reports 报表文件(.rpt) /Resources 图标、图片资源 App.config 连接字符串与全局配置

三层划分不算严格,部分窗体可能直接调 DAL,但整体骨架在。二次开发时建议先把 BLL 补厚,把业务规则从窗体里抽出来,否则后面加审批流、加权限会很难受。

3. 跑起来:环境准备与首次运行

3.1 环境与依赖清单

项目要求说明
开发工具Visual Studio(支持 VB.NET 的版本)打开 .sln 即可
运行时.NET Framework按项目目标框架选择
数据库SQL Server先建库再执行脚本
报表组件Crystal Reports for .NET需单独安装,版本与项目引用一致
打印普通打印机或针式打印机套打建议针式

Crystal Reports 的运行时是新手最容易翻车的地方。项目里引用了 CrystalDecisions 系列程序集,但开发机上没装对应的报表组件,编译能过、运行就报「无法加载程序集」。常见做法是先装与项目引用版本匹配的 Crystal Reports 运行时,再打开解决方案。

3.2 数据库初始化与连接配置

先在 SQL Server 里建一个空库,然后找到源码附带的建表脚本(通常在 /Script 或 /DB 目录,文件名类似 create.sql),在目标库里执行。脚本会建表、建索引,部分版本还会插入基础数据和演示账套。

连接字符串在 App.config 里,形如:

<configuration> <connectionStrings> <!-- 改成自己的服务器、库名、账号密码 --> <add name="ConnStr" connectionString="Data Source=.;Initial Catalog=JXC;User ID=sa;Password=你的密码" providerName="System.Data.SqlClient" /> </connectionStrings> </configuration>

Data Source填服务器地址,本机可用.或localhost;Initial Catalog是数据库名;如果用 Windows 身份验证,把User ID和Password换成Integrated Security=True。改完记得在代码里确认读取的是这个配置项,有些老项目会把连接串硬编码在公共类里,两处都要改,否则改了配置不生效——这是排查「连不上库」时的第一检查点。

3.3 编译与登录

用 VS 打开解决方案,先「重新生成解决方案」,看输出窗口有没有报错。常见编译错误是报表文件引用的程序集版本不匹配,或者某个第三方控件缺失。编译通过后设启动项目为 WinForm 主程序,F5 运行。

首次登录一般有默认账号,源码说明里会写(常见是 admin/空密码或 admin/123)。登录后先别急着录单,去基础资料里把商品、仓库、供应商各建一条测试数据,再走一遍采购入库和销售出库,确认库存数字会变、单据能保存。这一步跑通,说明数据库、业务层、UI 层都通了,后面再研究打印。

4. 核心模块拆解:从单据保存到水晶报表与套打输出

4.1 单据保存的事务处理

进销存里一张入库单保存时,要同时写单据主表、明细表,并更新库存。这三步必须在一个事务里,否则中途失败会出现「单据有了库存没加」的脏数据。DAL 层的典型写法:

' 保存采购入库单:主表 + 明细 + 库存更新,同一事务 Public Function SavePurchaseIn(ByVal head As PurchaseInHead, ByVal details As List(Of PurchaseInDetail)) As Boolean Dim conn As New SqlConnection(ConnStr) Dim tran As SqlTransaction = Nothing Try conn.Open() tran = conn.BeginTransaction() ' 开启事务 ' 1. 插入主表,返回自增单号 Dim billNo As String = InsertHead(conn, tran, head) ' 2. 循环插入明细,并累加库存 For Each d As PurchaseInDetail In details d.BillNo = billNo InsertDetail(conn, tran, d) UpdateStock(conn, tran, d.GoodsId, d.WarehouseId, d.Qty) ' 库存增加 Next tran.Commit() ' 全部成功才提交 Return True Catch ex As Exception If tran IsNot Nothing Then tran.Rollback() ' 任一步失败整体回滚 Throw New Exception("保存入库单失败:" & ex.Message) Finally conn.Close() End Try End Function

关键点在BeginTransaction到Commit之间:主表、明细、库存三步共用同一个conn和tran,任何一步抛异常都走Rollback。参数上,head是单据头实体,details是明细集合,UpdateStock的第三个参数是数量,入库为正、出库为负,这样库存表只需要一个累加逻辑。二次开发时如果要加「审核后才更新库存」,把UpdateStock从保存方法里挪到审核方法里即可,事务边界跟着调整。

4.2 水晶报表的数据绑定

Crystal Reports 的用法分两步:设计器里画好布局,代码里推数据。设计阶段在 VS 里打开 .rpt 文件,从「字段资源管理器」拖数据库字段到报表上,设置分组、汇总、页眉页脚。代码里绑定数据源:

' 加载采购入库单报表并绑定数据 Private Sub LoadPurchaseReport(ByVal billNo As String) Dim rpt As New ReportDocument() ' 加载报表模板文件 rpt.Load(Application.StartupPath & "\Reports\PurchaseIn.rpt") ' 取单据头和明细数据 Dim dtHead As DataTable = GetPurchaseHead(billNo) Dim dtDetail As DataTable = GetPurchaseDetail(billNo) ' 推入数据集,表名要与 rpt 设计时的表名一致 rpt.SetDataSource(dtDetail) ' 单据头字段用参数字段传入 rpt.SetParameterValue("BillNo", billNo) rpt.SetParameterValue("SupplierName", dtHead.Rows(0)("SupplierName")) ' 绑定到窗体上的 CrystalReportViewer 控件 CrystalReportViewer1.ReportSource = rpt CrystalReportViewer1.Refresh() End Sub

SetDataSource传的是明细 DataTable,报表主体按明细行循环;单据头这类「每页只显示一次」的信息,用参数字段传更稳,避免主从表关联带来的重复行问题。rpt.Load的路径用Application.StartupPath拼相对路径,部署时把 Reports 目录一起拷过去。如果运行时报「报表未找到」,先查这个路径和文件名大小写。

4.3 自定义套打的坐标绘制

套打的核心是「纸是印好的,程序只往上填字」。做法是拿到预印单据的精确尺寸,用 Graphics 在指定坐标画字符串。典型实现:

' 自定义套打:按毫米坐标在预印单据上绘制内容 Private Sub PrintCustom(ByVal data As BillData) Dim pd As New PrintDocument() ' 设置纸张为自定义尺寸(单位:百分之一英寸) pd.DefaultPageSettings.PaperSize = New PaperSize("Custom", 827, 1169) ' 约 A4 AddHandler pd.PrintPage, Sub(sender, e) Dim g As Graphics = e.Graphics ' 高质量文本渲染,针式打印更清晰 g.TextRenderingHint = Drawing.Text.TextRenderingHint.AntiAlias Dim font As New Font("宋体", 10) Dim brush As Brush = Brushes.Black ' 按毫米换算成像素后定位(假设 96 DPI,1mm ≈ 3.78px) g.DrawString(data.BillNo, font, brush, MmToPx(20), MmToPx(15)) ' 单号位置 g.DrawString(data.SupplierName, font, brush, MmToPx(60), MmToPx(15)) ' 供应商 g.DrawString(data.TotalAmount.ToString("F2"), font, brush, MmToPx(150), MmToPx(120)) ' 金额 ' 明细行按固定行高逐行绘制 Dim y As Single = MmToPx(40) For Each item In data.Items g.DrawString(item.GoodsName, font, brush, MmToPx(20), y) g.DrawString(item.Qty.ToString(), font, brush, MmToPx(100), y) g.DrawString(item.Amount.ToString("F2"), font, brush, MmToPx(130), y) y += MmToPx(8) ' 行高 8mm Next End Sub pd.Print() End Sub ' 毫米转像素 Private Function MmToPx(ByVal mm As Single) As Single Return mm * 96.0F / 25.4F End Function

MmToPx是这套逻辑的命门:设计时按毫米量坐标,运行时统一换算,换打印机不用重调。PaperSize的宽高单位是百分之一英寸,A4 约 827×1169。明细行用固定行高循环,行高要和预印表格的行距对齐,差 1mm 打几行就偏出去了。实际项目里这些坐标最好做成配置表存数据库,不同单据模板各存一套,改格式不用重新编译——这是套打模块最值得改造的点。

4.4 打印参数与预览

正式打印前一般要给用户预览,PrintPreviewDialog直接套用同一个PrintDocument:

' 打印预览,复用同一套绘制逻辑 Dim preview As New PrintPreviewDialog() preview.Document = pd preview.ShowDialog()

预览和打印共用PrintPage事件,所见即所得。注意针式打印机的走纸偏移,很多机器实际打印位置和预览有 1~2mm 偏差,常见做法是在套打配置里加一个「整体偏移 X/Y」参数,让用户自己微调,比在代码里硬改坐标灵活。

5. 避坑与排查:打印和部署阶段最容易翻车的几处

5.1 报表运行时报程序集加载失败

现象:编译通过,点打印或打开报表窗体时弹「未能加载文件或程序集 CrystalDecisions.CrystalReports.Engine」。原因:开发机装了 Crystal Reports 设计器,但目标机器或当前项目引用的运行时版本不一致,或者 GAC 里没有对应版本。解决:确认项目引用的 CrystalDecisions 程序集版本号,安装同版本运行时;如果项目是 x86 目标,运行时也要装 32 位版本,位数不匹配同样报这个错。

5.2 套打内容整体偏移

现象:预览正常,针式打印机打出来整体偏左或偏上几毫米。原因:不同打印机的可打印区域和走纸起始位置不同,代码里按理想坐标画,实际物理位置有偏差。解决:在套打配置里加全局偏移量,打印时给e.Graphics.TranslateTransform(offsetX, offsetY)做整体平移;先用一张白纸打出来和预印单据叠着看,量出偏差再填参数,别靠猜。

5.3 库存数量出现负数或对不上

现象:出库后库存变成负数,或者账面库存和实际盘点差一截。原因:一是保存单据时库存更新没放进事务,中途失败导致单据和库存不一致;二是并发操作时两个客户端同时读库存再写回,后写的覆盖先写的。解决:库存更新必须和单据写入同事务;并发场景用UPDATE Stock SET Qty = Qty - @qty WHERE GoodsId=@id这种数据库端原子更新,而不是先查再算再写。

5.4 换数据库后中文乱码

现象:迁移到新库后,商品名称、客户地址里的中文变成问号。原因:建库时排序规则不是中文,或者连接字符串没指定字符集。解决:建库时选 Chinese_PRC_CI_AS 之类的中文排序规则;已有库可改排序规则,但要注意索引重建。迁移前先备份,别在正式库上直接试。

5.5 登录后菜单权限不生效

现象:给某个账号取消了采购模块权限,登录后菜单还在。原因:菜单权限判断写在窗体加载时,但权限数据在登录时缓存了,改权限后没刷新缓存。解决:权限变更后强制重新登录,或者在菜单加载时实时查库;如果做了缓存,加一个「刷新权限」的入口。这类问题排查时先看权限判断的代码位置,再看缓存的生命周期。

6. 二次开发:把套打坐标配置化和报表模板外置

拿到这套源码后,最值得先动的是套打模块。原始实现把坐标写死在代码里,每来一种新单据就要改代码、重新编译、重新部署,客户现场等不起。我的改法是建一张套打模板表:

-- 套打模板配置表:一种单据一套坐标 CREATE TABLE PrintTemplate ( TemplateId INT IDENTITY PRIMARY KEY, TemplateName NVARCHAR(50), -- 模板名称,如"销售出库单-针式" BillType NVARCHAR(20), -- 单据类型 FieldName NVARCHAR(50), -- 字段名,如 BillNo、SupplierName PosX DECIMAL(8,2), -- X 坐标(毫米) PosY DECIMAL(8,2), -- Y 坐标(毫米) FontName NVARCHAR(20), -- 字体 FontSize INT, -- 字号 IsDetail BIT -- 是否明细行字段 )

绘制时不再写死MmToPx(20),而是从模板表读出字段的PosX/PosY再换算。明细行字段额外记录行高,循环时按行高递增。这样业务人员改格式只需要在配置界面调数字,不用碰代码。验证方法很直接:同一张单据配两套模板,分别打到白纸和预印纸上,叠起来看对齐度;再换一台打印机打一遍,确认偏移参数能兜住差异。

报表模板外置的思路类似:把 .rpt 文件放在程序目录的 Reports 下,按单据类型命名,代码里用配置项拼路径加载,而不是把报表嵌进程序集。这样改报表布局只需替换文件,不用重新发布整个客户端。改完记得做一次回归——采购、销售、库存各打一张单,确认数据绑定和分页都正常。

从那以后我每次接这类进销存项目,都先把打印配置抽出来单独验证一遍,再谈业务功能,因为打印是客户每天都要用、出问题最容易被投诉的地方。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询