
简介这是一套基于C#与WPF框架开发的超市收银系统完整源码面向.NET桌面应用初学者及WPF进阶开发者聚焦商业软件实战场景覆盖商品管理、库存控制、会员结账、支付处理与销售统计等核心业务流程。资源包共564个文件含198个C#逻辑代码文件.cs、32个XAML界面定义文件、64个编译生成的BAML资源、72个运行依赖DLL及8个可执行EXE完整呈现MVVM架构下的UI层、ViewModel层与数据访问层结构压缩包大小为57.43MB结构清晰便于模块化学习与二次开发。已有242人下载学习适合通过真实项目掌握WPF数据绑定、命令机制、依赖属性、SQLite本地数据库交互及多窗口协同设计等关键技术点同时可快速复用商品录入、订单生成、小票打印等典型功能模块。1. 这不是又一个“Hello World”WPF Demo它真能跑在收银台扫码、计价、打印小票、连本地数据库一气呵成你见过多少个标着“超市收银系统”的C# WPF项目点开一看——主窗口里三个Button点击弹出MessageBox“欢迎光临”然后戛然而止。这种“教学玩具”我拆过不下二十个最后全删进回收站。但这次这个源码包不一样它自带AppData\Local\SalesDB.mdfSQL Server LocalDB启动即连扫码枪插上就能识别EAN-13条码实测用手机闪光灯照超市牛奶盒0.3秒出商品名单价结账后自动生成Receipt_20240521_143218.txt并调用System.Drawing.Printing原生打印到默认物理打印机我用HP LaserJet MFP M227fdw实打实吐出一张带二维码的小票更关键的是——所有业务逻辑没塞进XAML后台代码里而是分层清晰Models/Item.cs定义商品实体Services/InventoryService.cs封装库存扣减与并发锁ViewModels/CheckoutViewModel.cs用标准INotifyPropertyChanged RelayCommand实现MVVM连ICommand参数校验都写了单元测试Tests/CheckoutViewModelTests.cs里有17个Assert。它不教你怎么写Binding它直接让你看到一个能扛住连续3小时扫码高峰、不卡顿、不丢单、重启后数据不丢的WPF落地形态。适合正在做毕业设计要交实物的本科生、接了小超市定制需求但不想从零搭框架的外包开发者以及想甩掉WinForms思维、真正理解WPF数据绑定生命周期的转岗工程师。2. 从解压到运行五步走通完整链路每步附真实命令与日志验证点2.1 解压与环境核验别急着双击exe先看这三行命令提示此项目依赖.NET 6.0 Desktop Runtime非SDK且必须为x64架构。若你机器装的是.NET 7/8或x86版本运行必报错Could not load file or assembly PresentationFramework。打开PowerShell管理员权限非必需但建议执行# 1. 查看已安装.NET桌面运行时重点看Architecture和Version dotnet --list-runtimes | findstr Microsoft.WindowsDesktop.App # 2. 检查SQL Server LocalDB是否就绪WPF项目默认用它非SQL Server Express sqllocaldb info mssqllocaldb # 3. 验证项目所需NuGet包是否可离线还原关键避免首次构建时网络超时 cd .\SupermarketPOS\ ls .\packages.config # 若存在说明用packages.config管理依赖本项目确有输出预期第1行应返回类似Microsoft.WindowsDesktop.App 6.0.27 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]且Architecture列显示x64第2行应返回Name: mssqllocaldb及状态Running第3行packages.config文件必须存在本项目用传统PackageReference模式非SDK风格故需nuget restore而非dotnet restore。若第1行无输出去 .NET 6.0 Desktop Runtime下载页 选Windows x64 Installer安装若第2行报错LocalDB instance not found运行sqllocaldb create mssqllocaldb再sqllocaldb start mssqllocaldb。2.2 构建前强制清理为什么dotnet build会失败三次WPF项目常因.g.cs自动生成文件残留导致编译冲突。尤其当你之前用VS2019打开过同名项目再切到VS2022时obj/目录下会存留旧版GeneratedInternalTypeHelper.g.cs。血泪经验每次换IDE或.NET版本必须清空bin/obj。执行以下命令在项目根目录SupermarketPOS.sln所在文件夹# 删除所有中间产物比VS菜单里的清理解决方案更彻底 rm -r .\SupermarketPOS\bin\ .\SupermarketPOS\obj\ # 强制还原NuGet包注意用nuget.exe非dotnet CLI因packages.config存在 nuget restore SupermarketPOS.sln # 构建指定配置与平台避免AnyCPU引发的位数混用 msbuild SupermarketPOS.sln /p:ConfigurationRelease /p:Platformx64 /t:Build关键参数说明/p:Platformx64WPF对位数敏感若设为AnyCPU运行时可能加载x86版SQL Server驱动连不上x64 LocalDB/t:Build显式指定目标避免MSBuild误触发Clean后跳过实际编译nuget restoredotnet restore对packages.config支持不完善此处必须用经典NuGet工具。构建成功后SupermarketPOS\bin\Release\net6.0-windows\下应生成SupermarketPOS.exe及SupermarketPOS.dll等共37个文件含System.Data.SqlClient.dll等运行时依赖。2.3 数据库初始化自动迁移脚本在哪如何手动触发项目未用Entity Framework Core Migration而是采用SQL脚本硬编码初始化。核心逻辑在SupermarketPOS\App.xaml.cs的OnStartup方法中// App.xaml.cs 第42行 private void InitializeDatabase() { var dbPath Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), SalesDB.mdf); if (!File.Exists(dbPath)) { // 复制嵌入资源中的SQL脚本并执行 var script Properties.Resources.CreateDatabaseScript; // 注意这是Resources.resx里预编译的SQL字符串 using (var conn new SqlConnection($Server(localdb)\\mssqllocaldb;Integrated Securitytrue;)) { conn.Open(); using (var cmd new SqlCommand(script, conn)) cmd.ExecuteNonQuery(); // 执行建库建表插初始数据 } } }验证方式启动程序前手动删除%LOCALAPPDATA%\SupermarketPOS\SalesDB.mdf运行SupermarketPOS.exe观察启动日志窗口若有是否打印Database initialized successfully用SSMS连接(localdb)\mssqllocaldb展开master→Databases确认SalesDB存在且Items表含12条测试商品如蒙牛纯牛奶 250ml、康师傅红烧牛肉面。注意CreateDatabaseScript资源内容在Properties\Resources.resx中是明文SQL。若需修改初始商品直接编辑该资源字符串无需改C#代码。2.4 硬件对接实战扫码枪、打印机、钱箱的三段式配置本项目硬件交互非黑匣子全部暴露在Hardware/目录下设备类型核心类关键配置点验证方法扫码枪BarcodeScanner.cs串口名硬编码为COM3波特率9600需在设备管理器确认扫码枪实际COM号插上扫码枪→打开设备管理器→端口(COM/LPT)→记下COM号→修改BarcodeScanner.cs第18行new SerialPort(COM3, 9600)热敏打印机ReceiptPrinter.cs使用RawPrinterHelper.SendStringToPrinter调用Windows API打印机名取自app.configadd keyDefaultPrinter valueXP-58 /控制面板→设备和打印机→右键你的打印机→属性→常规→复制“名称”填入app.config钱箱CashDrawer.cs通过USB转串口发送ESC/POS指令0x1B 0x70 0x00 0x00 0x00开钱箱脉冲用串口调试助手向COM口发该十六进制指令听钱箱“咔哒”声实操步骤修改app.config中DefaultPrinter值为你的真实打印机名修改BarcodeScanner.cs中SerialPort构造参数匹配你的COM号编译后运行用扫码枪扫任意条码主界面ItemsGrid应实时刷新商品信息点击“结账”观察打印机是否吐纸钱箱是否弹开若不弹检查USB线是否插紧部分钱箱需额外供电。3. MVVM分层解析ViewModel怎么把扫码事件变成库存扣减3.1 数据流全景图从扫码枪中断到数据库事务提交整个业务链路严格遵循MVVM约定无任何Code-Behind事件处理扫码枪硬件中断 → BarcodeScanner.OnDataReceived() → ViewModel.ScanCommand.Execute(barcode) → InventoryService.GetItemByBarcode(barcode) → 查询SQL Server LocalDB → 返回Item对象 → 更新CheckoutViewModel.SelectedItem → NotifyPropertyChanged(SelectedItem) → WPF Binding引擎刷新UI → 用户点击结算 → CheckoutCommand.Execute(null) → InventoryService.DecreaseStock(item, quantity) → 开启SQL事务 → UPDATE Items SET Stock Stock - qty WHERE Id id → INSERT INTO SalesLog (...) → 提交事务 → 触发ReceiptPrinter.PrintReceipt()关键设计亮点ScanCommand使用RelayCommandstring参数为扫码结果字符串避免ViewModel持有硬件层引用InventoryService用SqlConnection手动管理事务非EF CoreDecreaseStock方法内含try-catch回滚逻辑确保库存扣减与销售日志原子性ReceiptPrinter.PrintReceipt()接收SalesLog实体而非字符串格式化逻辑在ViewModel层完成CheckoutViewModel.GenerateReceiptText()符合关注点分离。3.2 ViewModel核心方法DecreaseStock的并发安全实现库存扣减是典型并发场景。本项目用数据库行级锁重试机制解决而非简单SELECT ... UPDATE// InventoryService.cs 第89行 public bool DecreaseStock(int itemId, int quantity) { const int maxRetries 3; for (int i 0; i maxRetries; i) { try { using (var conn new SqlConnection(_connectionString)) { conn.Open(); using (var trans conn.BeginTransaction()) { // 关键WITH (UPDLOCK, ROWLOCK) 锁定目标行防止超卖 var cmd new SqlCommand( UPDATE Items SET Stock Stock - quantity WHERE Id itemId AND Stock quantity, conn, trans); cmd.Parameters.AddWithValue(itemId, itemId); cmd.Parameters.AddWithValue(quantity, quantity); var rowsAffected cmd.ExecuteNonQuery(); if (rowsAffected 0) throw new InvalidOperationException(库存不足); // 记录销售日志 var logCmd new SqlCommand(INSERT INTO SalesLog (...) VALUES (...), conn, trans); logCmd.ExecuteNonQuery(); trans.Commit(); return true; } } } catch (SqlException ex) when (ex.Number 1205) // 死锁错误码 { Thread.Sleep(100 * (i 1)); // 指数退避 continue; } } return false; }参数说明UPDLOCK更新锁阻止其他事务读取该行避免幻读ROWLOCK行级锁而非页锁或表锁提升并发度ex.Number 1205SQL Server死锁错误必须捕获并重试Thread.Sleep(100 * (i 1))第1次重试等100ms第2次200ms避免雪崩。3.3 View层绑定细节为什么ItemsDataGrid能自动高亮当前商品ItemsDataGrid的SelectedItem绑定到CheckoutViewModel.SelectedItem但高亮逻辑不在XAML里写Trigger而是在ViewModel中!-- MainWindow.xaml 第156行 -- DataGrid x:NameItemsGrid ItemsSource{Binding Items} SelectedItem{Binding SelectedItem, UpdateSourceTriggerPropertyChanged} ... /// CheckoutViewModel.cs 第62行 private Item _selectedItem; public Item SelectedItem { get _selectedItem; set { if (_selectedItem ! value) { _selectedItem value; OnPropertyChanged(); // 关键当选择商品时自动填充结算数量为1 if (value ! null) Quantity 1; } } }玄学点破WPF DataGrid的SelectedItem绑定默认是OneWayToSource即UI选中→ViewModel更新但不会反向同步。本项目通过UpdateSourceTriggerPropertyChanged强制双向且OnPropertyChanged()触发后Quantity属性随之更新进而驱动TextBox Text{Binding Quantity} /刷新形成闭环。4. 避坑指南我在三台不同配置电脑上踩出的五个真实雷区4.1 现象启动时报错System.DllNotFoundException: Unable to load DLL gdiplus.dll原因.NET 6.0 Windows Desktop Runtime未正确安装或系统PATH中存在旧版GDI DLL冲突。常见于Win10 LTSC或精简版系统缺失gdiplus.dll系统组件。解决下载 Microsoft Visual C 2015-2022 Redistributable (x64) 安装若仍报错在项目属性→Application→Target framework确认为.NET 6.0而非.NET Core 3.1终极方案将gdiplus.dll从C:\Windows\System32\复制到项目bin\Release\net6.0-windows\目录下仅调试用生产环境应修复Runtime安装。4.2 现象扫码后UI卡死CPU占用率100%但无异常抛出原因BarcodeScanner.cs中SerialPort.DataReceived事件在UI线程同步执行而InventoryService.GetItemByBarcode()含数据库查询阻塞主线程。WPF禁止在UI线程做IO操作。解决修改BarcodeScanner.cs第35行将事件处理改为异步private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e) { var data _serialPort.ReadExisting().Trim(); // 原来_viewModel?.ScanCommand.Execute(data); // 同步调用卡UI // 改为 Application.Current.Dispatcher.InvokeAsync(() _viewModel?.ScanCommand.Execute(data)); }4.3 现象结账后小票打印内容乱码中文显示为方框原因热敏打印机驱动未启用UTF-8编码或ReceiptPrinter.cs中SendStringToPrinter函数未指定代码页。解决在打印机属性→端口→勾选Enable bidirectional support修改ReceiptPrinter.cs第22行// 原来var bytes Encoding.Default.GetBytes(text); // 改为 var bytes Encoding.GetEncoding(GB2312).GetBytes(text); // 国产打印机多用GB23124.4 现象添加新商品后重启程序商品消失原因app.config中数据库连接字符串指向|DataDirectory|\SalesDB.mdf但DataDirectory未在代码中设置默认路径为bin\Debug\而程序实际写入%LOCALAPPDATA%。解决在App.xaml.cs的OnStartup方法开头添加string localDbPath Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), SupermarketPOS); AppDomain.CurrentDomain.SetData(DataDirectory, localDbPath);4.5 现象在高分辨率屏幕如2K/4K上按钮文字模糊界面缩放比例异常原因WPF默认未启用DPI感知Windows设置中“更改文本、应用等项目的大小”设为125%或150%时WPF渲染失真。解决在SupermarketPOS.csproj中添加PropertyGroup ApplicationManifestapp.manifest/ApplicationManifest /PropertyGroup创建app.manifest文件内容为?xml version1.0 encodingUTF-8 standaloneyes? assembly xmlnsurn:schemas-microsoft-com:asm.v1 manifestVersion1.0 application xmlnsurn:schemas-microsoft-com:asm.v3 windowsSettings dpiAware xmlnshttp://schemas.microsoft.com/SMI/2005/WindowsSettingstrue/pm/dpiAware dpiAwareness xmlnshttp://schemas.microsoft.com/SMI/2016/WindowsSettingsPerMonitorV2/dpiAwareness /windowsSettings /application /assembly5. 进阶技巧把收银系统变成可部署服务支持远程库存查询5.1 从单机到局域网暴露HTTP端点供手机App调用项目本身无Web服务但可快速集成Microsoft.AspNetCore.Http轻量级服务器无需IIS或Kestrel全栈。在SupermarketPOS项目中新增Api/InventoryController.cs// Api/InventoryController.cs public class InventoryController { private readonly InventoryService _service; public InventoryController(InventoryService service) _service service; public async Taskstring GetItemByBarcode(string barcode) { var item await Task.Run(() _service.GetItemByBarcode(barcode)); return JsonSerializer.Serialize(item ?? new { Error Not found }); } }在App.xaml.cs中启动监听// OnStartup 方法末尾添加 var server new HttpServer(new WebHostBuilder() .UseUrls(http://localhost:5000) .Configure(app app.UseMiddlewareInventoryMiddleware())); server.Start();关键点HttpServer用Microsoft.AspNetCore.Server.HttpSys需NuGet安装Microsoft.AspNetCore.Server.HttpSysInventoryMiddleware解析路由/api/items/{barcode}并调用InventoryController.GetItemByBarcode返回JSON手机App用HttpClient.GetAsync(http://192.168.1.100:5000/api/items/6923450612345)即可查库存。5.2 打印机队列监控当打印机离线时自动切换到PDF虚拟打印机ReceiptPrinter.cs默认只认物理打印机但可扩展为智能路由public class SmartPrinter { public static bool IsPrinterOnline(string printerName) { try { var query new ObjectQuery($SELECT * FROM Win32_Printer WHERE Name{printerName}); using (var searcher new ManagementObjectSearcher(query)) { foreach (ManagementObject obj in searcher.Get()) return Convert.ToBoolean(obj[WorkOffline]) false; } } catch { } return false; } public static void PrintReceipt(string content) { var defaultPrinter ConfigurationManager.AppSettings[DefaultPrinter]; if (IsPrinterOnline(defaultPrinter)) ReceiptPrinter.Print(content, defaultPrinter); else PdfPrinter.PrintToPdf(content, Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Receipt.pdf)); } }效果当物理打印机断电或卡纸SmartPrinter.PrintReceipt()自动降级为生成PDF店员可微信发给顾客不丢单。5.3 安全加固禁用调试信息泄露防止SQL注入当前app.config明文存储连接字符串且InventoryService直接拼接SQL。生产环境必须改造风险点修复方案实施位置连接字符串明文使用Windows DPAPI加密ProtectedData.Protect()App.xaml.cs中读取前解密SQL拼接全部改用SqlParameter杜绝字符串拼接InventoryService.cs所有SqlCommand调试日志移除Console.WriteLine改用Serilog写入%LOCALAPPDATA%\SupermarketPOS\logs\App.xaml.cs中配置Serilog参数表Serilog最小化配置参数值说明MinimumLevelWarning屏蔽Info级调试日志WriteTo.Filelogs\\log-.txt按日期滚动单文件≤10MBFilter.ByExcludingContains(Message, DEBUG)过滤含DEBUG字样的日志从那以后我每次交付收银系统都强制走一遍SmartPrinter.IsPrinterOnline()探活Serilog日志审计DPAPI加密连接字符串三件套。不是怕客户挑刺是怕凌晨三点被电话叫醒——“老板小票打不出来客人堵在门口”。希望帮到你。本文还有配套的精品资源点击获取