
简介这是一套基于C#与WPF技术栈构建的个人记账系统完整源码面向具备一定.NET基础的开发者、课程设计学生及需要桌面端财务工具练手的编程学习者。项目以WPF的XAML声明式界面搭配C#业务逻辑覆盖登录、账户管理、收支记录、余额统计与操作日志等模块并采用MVVM思路分层组织便于理解数据绑定、样式模板与控件库的实际用法。压缩包共62个文件以31个cs源码、7个xaml界面文件、4个csproj工程文件为主另含图标、图片、数据库文件与解决方案配置整体约1.97MB结构清晰、便于按层阅读。数据库部分包含mdf与ldf文件可直接还原运行配合BLL、DAL、Model分层设计能帮助读者掌握ADO.NET增删改查、异常处理与轻量级数据库集成等关键环节。目前已有76人学习下载适合作为WPF入门到进阶的实战参考。1. 从一份 WPF 记账系统压缩包说起它到底能跑起来什么很多人第一次拿到「用 C# 语言编写的的一个 WPF 个人记账系统.zip」这类压缩包第一反应是双击 .sln 然后 F5结果要么缺 SDK要么 NuGet 还原失败要么数据库文件路径写死在作者机器上直接抛异常。这个标题背后其实是一个很典型的 C# 桌面端落地场景用 WPF 做界面、用本地数据库存流水、用 MVVM 把 UI 和业务拆开。它能解决的是「我想有一个自己完全掌控、不依赖云端、数据落在本机的记账工具」同时也是一个练手 WPF 数据绑定、命令、EF Core 或 SQLite 的完整小项目。适合谁适合刚学完 C# 基础、想找一个有真实增删改查和报表的练手项目的人也适合已经会 WinForm、想切到 WPF 看看 XAML 和 MVVM 到底怎么落地的人。下面我按「先跑通、再拆结构、再改功能、最后避坑」的顺序把这类项目从压缩包到能日常用起来的路径讲清楚。2. 把压缩包变成能调试的工程环境、还原与首次运行2.1 先确认你装的是 .NET SDK 还是 .NET FrameworkWPF 项目分两种目标框架老项目多是 .NET Framework 4.x新项目是 .NET 6/7/8 的net6.0-windows这种。判断方法很简单用文本编辑器打开 .csproj看TargetFramework这一行。如果是net48之类你需要装 Visual Studio 的「.NET 桌面开发」工作负载如果是net8.0-windows装对应版本的 .NET SDK 即可。很多人翻车就翻在这里用 VS Code 打开一个 .NET Framework 的 WPF 项目怎么都编译不过因为 VS Code 不带 MSBuild 的桌面目标包。# 查看本机已安装的 .NET SDK 版本 dotnet --list-sdks # 查看某个项目实际使用的目标框架在项目根目录执行 find . -name *.csproj -maxdepth 2 -exec grep -H TargetFramework {} \;第一条命令列出所有 SDK如果只有 6.0 而项目要 8.0就得去装 8.0。第二条命令帮你快速定位 csproj 里的目标框架避免逐个文件点开。参数上没什么可调的关键是看清net8.0-windows后面的-windows不能少少了就编译不了 WPF。2.2 NuGet 还原失败时先看包源而不是删缓存还原失败最常见的原因是包源指向了一个内网地址或者已经失效的镜像。打开项目根目录的NuGet.config看packageSources节点。如果没有这个文件就在%AppData%\NuGet\NuGet.Config里看全局配置。常见做法是把官方源保留把不可用的源注释掉。!-- NuGet.config 放在解决方案根目录优先级高于全局配置 -- configuration packageSources clear / add keynuget.org valuehttps://api.nuget.org/v3/index.json / /packageSources /configurationclear /会清掉继承来的所有源只留你写的这个避免内网源干扰。改完执行dotnet restore如果还失败看报错里具体是哪个包、哪个版本再去 nuget.org 搜这个包是否存在。注意不要一上来就删~/.nuget/packages那会把所有项目的缓存都清掉代价很大。2.3 数据库文件路径写死是首次运行最大的坑这类记账系统多半用 SQLite 或 LocalDB。作者在自己机器上调试时连接字符串可能是Data SourceD:\code\account.db这种绝对路径。你拿到手一跑就报「无法打开数据库文件」。解决办法是把它改成相对路径并在启动时确保目录存在。// App.xaml.cs 或 DbContext 的构造函数里 var baseDir AppDomain.CurrentDomain.BaseDirectory; var dbPath Path.Combine(baseDir, Data, account.db); Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); var options new DbContextOptionsBuilderAppDbContext() .UseSqlite($Data Source{dbPath}) .Options;AppDomain.CurrentDomain.BaseDirectory指向 bin 目录保证调试和发布后路径一致。Directory.CreateDirectory防止 Data 目录不存在导致 SQLite 直接抛异常。参数上如果你希望数据库放在用户文档目录而不是程序目录把baseDir换成Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)这样发布后不会被权限问题卡住。3. 拆开 WPF 记账系统的骨架MVVM 到底落在哪几个文件3.1 先认清 View、ViewModel、Model 三层各自的职责WPF 项目最容易写成「把逻辑全塞进 xaml.cs」但一个像样的记账系统通常会分三层。View 是 .xaml 文件只负责布局和绑定ViewModel 是继承INotifyPropertyChanged的类持有ObservableCollectionTransaction这类集合和ICommandModel 是实体类比如Transaction、Category。判断一个项目结构好不好就看 xaml.cs 里有没有大段业务代码。如果MainWindow.xaml.cs里全是 SQL 和计算那这个项目基本没按 MVVM 写你改起来会很痛苦。// ViewModel 里典型的属性与命令写法 public class MainViewModel : INotifyPropertyChanged { private decimal _totalIncome; public decimal TotalIncome { get _totalIncome; set { _totalIncome value; OnPropertyChanged(); } } public ObservableCollectionTransaction Transactions { get; } new(); public ICommand AddTransactionCommand { get; } public MainViewModel() { AddTransactionCommand new RelayCommand(AddTransaction); } private void AddTransaction() { Transactions.Add(new Transaction { Amount 0, Date DateTime.Today }); TotalIncome Transactions.Where(t t.Amount 0).Sum(t t.Amount); } public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string? name null) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }ObservableCollection保证新增记录后 DataGrid 自动刷新这是 WPF 数据绑定的核心。RelayCommand是常见的ICommand实现按钮的Command属性绑到它上面点击就触发AddTransaction。[CallerMemberName]让OnPropertyChanged()不用手写属性名减少拼写错误。参数上TotalIncome每次增删后重算数据量大时可以改成增量更新但个人记账几千条以内直接重算完全够用。3.2 DataGrid 绑定与金额、日期的显示格式记账系统的主界面通常是一个 DataGrid 列出流水。绑定本身简单但金额显示两位小数、日期显示yyyy-MM-dd需要写StringFormat。另外 DataGrid 默认允许用户直接编辑单元格如果你希望只通过弹窗编辑要把IsReadOnly设为 True。DataGrid ItemsSource{Binding Transactions} AutoGenerateColumnsFalse IsReadOnlyTrue CanUserAddRowsFalse DataGrid.Columns DataGridTextColumn Header日期 Binding{Binding Date, StringFormatyyyy-MM-dd} / DataGridTextColumn Header金额 Binding{Binding Amount, StringFormat{}{0:N2}} / DataGridTextColumn Header分类 Binding{Binding Category} / DataGridTextColumn Header备注 Binding{Binding Note} / /DataGrid.Columns /DataGridAutoGenerateColumnsFalse防止实体新增属性后界面多出莫名其妙的列。StringFormat{}{0:N2}前面的{}是转义否则 XAML 会把大括号当标记扩展解析。CanUserAddRowsFalse去掉底部那行空白新增行避免用户误操作产生空记录。这些参数不改也能跑但改完界面会干净很多。3.3 用 EF Core 还是直接 ADO.NET个人记账的选型理由个人记账系统数据量小、表结构简单常见做法是用 EF Core SQLite写起来快迁移方便。但如果项目里已经用了System.Data.SQLite手写 SQL也不用强行改。判断标准是如果你要加一个「按月份统计分类支出」的功能EF Core 用 LINQ 几行就出来手写 SQL 要拼字符串还容易注入。下面是一个按月统计的例子。var monthly await db.Transactions .Where(t t.Date.Year year t.Date.Month month t.Amount 0) .GroupBy(t t.Category) .Select(g new { Category g.Key, Total g.Sum(t -t.Amount) }) .ToListAsync();Where先过滤出当月支出金额为负GroupBy按分类聚合Sum(t -t.Amount)把负数转成正数方便显示。参数上year和month从界面传入注意月份是 1 到 12 而不是 0 到 11。如果项目用的是 SQLiteEF Core 对DateTime的支持没问题但要注意 SQLite 没有原生 decimal金额字段建议用decimal映射到 TEXT 或 REALREAL 会有精度问题记账场景建议存整数分。4. 改出一个能日常用的功能新增流水与月度统计4.1 新增流水弹窗从命令到保存的完整链路主界面点「新增」弹出一个 Window填完金额、分类、日期后保存并刷新列表。这条链路涉及命令绑定、弹窗、数据校验和数据库写入。很多人卡在弹窗关闭后主界面不刷新原因是弹窗直接操作了数据库但没通知主 ViewModel。// 主 ViewModel 中打开弹窗并等待结果 private void AddTransaction() { var dialog new AddTransactionWindow(); var vm new AddTransactionViewModel(); dialog.DataContext vm; vm.Saved (s, e) { Transactions.Add(e.Transaction); TotalIncome Transactions.Where(t t.Amount 0).Sum(t t.Amount); dialog.Close(); }; dialog.ShowDialog(); }ShowDialog是模态弹窗会阻塞主窗口直到关闭。vm.Saved是自定义事件弹窗内保存成功后触发把新记录传给主 ViewModel 并关闭窗口。参数上如果你不想用事件也可以在弹窗关闭后重新从数据库查一遍列表简单但多一次查询。个人记账数据量小两种都行事件方式更即时。4.2 月度统计用 LINQ 还是写 SQL 视图月度统计是记账系统的核心价值。除了上面按分类聚合还常见「近 6 个月收支趋势」。用 LINQ 写趋势需要先按月份分组再排序。注意 SQLite 对DateTime的分组在 EF Core 里可能翻译不完美必要时先取到内存再分组。var sixMonthsAgo DateTime.Today.AddMonths(-5); var raw await db.Transactions .Where(t t.Date sixMonthsAgo) .Select(t new { t.Date, t.Amount }) .ToListAsync(); var trend raw .GroupBy(t new { t.Date.Year, t.Date.Month }) .Select(g new { Month ${g.Key.Year}-{g.Key.Month:D2}, Income g.Where(t t.Amount 0).Sum(t t.Amount), Expense g.Where(t t.Amount 0).Sum(t -t.Amount) }) .OrderBy(x x.Month) .ToList();先ToListAsync把数据拉到内存再分组避免 EF Core 翻译GroupBy加DateTime属性时生成奇怪 SQL。D2保证月份显示成01而不是1。参数上AddMonths(-5)表示含当月共 6 个月想改范围就调这个数。如果数据量上万建议在数据库加Date索引否则每次统计全表扫描会变慢。4.3 分类管理别把分类写死在代码里很多练手项目把分类写成enum或硬编码字符串数组用起来才发现想加一个「宠物」分类要改代码重新编译。正确做法是建一张Category表界面提供增删改。这样你日常用的时候才能随时调整。public class Category { public int Id { get; set; } public string Name { get; set; } string.Empty; public bool IsExpense { get; set; } // true 表示支出分类 }IsExpense区分收入和支出分类新增流水时根据类型过滤下拉框。参数上Name建议加唯一索引防止重复但 SQLite 的唯一索引对大小写敏感如果不想区分大小写存的时候统一转小写或加COLLATE NOCASE。5. 避坑与排查这类 WPF 记账项目最常见的 5 个翻车点5.1 现象编译报错「找不到 WindowsDesktop SDK」原因项目是 WPF 但你的环境只装了 .NET SDK 没装桌面工作负载或者用dotnet build时目标框架是net8.0-windows但 SDK 版本不匹配。解决装 Visual Studio 的「.NET 桌面开发」工作负载或者用dotnet workload install补对应组件。确认dotnet --list-sdks里有匹配的大版本。5.2 现象界面能显示但按钮点了没反应原因按钮的Command绑定写错或者 ViewModel 没设成 DataContext。常见的是在 xaml 里写Command{Binding AddCommand}但 DataContext 是 code-behind 里this.DataContext new MainViewModel()忘了写。解决在 xaml 根节点加d:DataContext{d:DesignInstance vm:MainViewModel}方便设计时提示运行时确认 DataContext 已赋值。另外检查RelayCommand的CanExecute是否返回了 false。5.3 现象DataGrid 编辑后数据库没更新原因用了ObservableCollection但实体没实现INotifyPropertyChanged或者改了之后没调SaveChanges。解决实体属性加INotifyPropertyChanged或者在保存按钮里显式db.SaveChanges()。注意 EF Core 的变更跟踪只对通过 DbContext 查出来的实体有效如果你手动new一个实体加到集合里要db.Add再保存。5.4 现象发布后运行报「找不到 SQLite.Interop.dll」原因SQLite 的原生依赖在发布时没被复制到输出目录尤其是System.Data.SQLite这种老包。解决在 csproj 里确认PlatformTarget是 x64 或 x86 而不是 AnyCPU或者改用Microsoft.Data.Sqlite它对 .NET Core 支持更好。发布时用dotnet publish -r win-x64指定运行时。5.5 现象金额计算出现 0.01 的误差原因用double存金额浮点累加误差。解决金额字段用decimal数据库存整数分。EF Core 映射 SQLite 时decimal会警告可以配HasConversionlong()存分显示时除以 100。这个坑在记账系统里是血泪经验一旦数据多了对不上账非常难查。6. 让这套记账系统真正顺手的一个技巧用附加属性做全局快捷键最后一个技巧是我自己用下来最提升体验的给主窗口加一个全局快捷键比如CtrlN直接弹新增流水不用鼠标点。WPF 里可以用InputBindings配合KeyBinding但如果你想在任意控件聚焦时都生效用附加属性把命令挂到 Window 上更稳。public static class HotkeyHelper { public static readonly DependencyProperty AddCommandProperty DependencyProperty.RegisterAttached( AddCommand, typeof(ICommand), typeof(HotkeyHelper), new PropertyMetadata(null, OnAddCommandChanged)); public static void SetAddCommand(DependencyObject obj, ICommand value) obj.SetValue(AddCommandProperty, value); public static ICommand GetAddCommand(DependencyObject obj) (ICommand)obj.GetValue(AddCommandProperty); private static void OnAddCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is Window window e.NewValue is ICommand command) { window.InputBindings.Add(new KeyBinding(command, Key.N, ModifierKeys.Control)); } } }在 xaml 里写local:HotkeyHelper.AddCommand{Binding AddTransactionCommand}窗口一加载就绑定了CtrlN。DependencyProperty的PropertyChangedCallback在附加属性赋值时触发把KeyBinding加进InputBindings。参数上ModifierKeys.Control可以换成Alt或组合键注意不要和系统快捷键冲突。这个技巧同样适用于CtrlS保存、CtrlF搜索。我自己维护这类小工具的习惯是每加一个功能先问「这个操作我一天要做几次」超过三次的就给它配快捷键。记账系统每天记两三笔快捷键省下的时间不多但那种「不用找鼠标」的顺手感是决定你会不会坚持用下去的关键。希望帮到你。本文还有配套的精品资源点击获取