ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

WPF个人记账系统源码解析:MVVM+SQLite实现桌面财务工具

2026/9/29 1:56:09 拓冰建站 浏览量
WPF个人记账系统源码解析:MVVM+SQLite实现桌面财务工具 简介这是一套基于C#与WPF技术栈构建的个人记账系统完整源码面向具备一定.NET基础的开发者、课程设计学生及需要桌面端记账工具参考实现的技术人员。项目采用分层架构将UI、业务逻辑、数据访问与实体模型分离涵盖登录、账户管理、收支记录、余额统计与操作日志等模块适合用来学习WPF桌面应用从界面到数据库的完整落地方式。压缩包共62个文件约1.97MB以31个cs源码文件为核心配合7个xaml界面文件、4个csproj工程文件、8个ico图标与3个png图片资源另含sln解决方案、mdf与ldf数据库文件及config配置结构完整可直接编译运行。目前已有76人学习下载。读者可从中掌握XAML声明式界面、数据绑定、MVVM设计模式、ADO.NET数据库增删改查、异常处理与ClickOnce部署等关键知识点并借鉴其分层目录组织与版本控制实践快速搭建属于自己的记账类桌面应用。1. 从一份 WPF 个人记账系统源码说起它到底能解决什么很多人第一次看到「用 C# 语言编写的的一个 WPF 个人记账系统.zip」这类标题第一反应是「又一个练手项目」但真正做过桌面端财务工具的人会知道记账系统是少数能把数据绑定、本地存储、表单校验、报表导出、主题切换全部串起来的场景。它解决的不是「记一笔账」这么简单而是让一个 C# 开发者在一套可运行的 WPF 工程里把 MVVM 分层、SQLite 持久化、DataGrid 编辑、图表统计这几件事一次性跑通。适合谁适合刚学完 C# 基础、想找一个能写进简历又能真正用起来的桌面项目的人也适合做上位机、工控 HMI 的同行拿来当 WPF 数据交互的参考骨架。下面我按「先立住结构、再动手复现、最后讲坑」的顺序把这类项目从解压到跑通、再到二次开发的路径讲清楚。2. 拆开这个 WPF 记账系统的工程骨架MVVM 分层与目录约定拿到一个 WPF 记账系统的压缩包别急着 F5。先看目录结构基本能判断作者是不是按主流做法组织的。一个能长期维护的 WPF 记账工程通常会把「界面」「逻辑」「数据」三块彻底分开这也是 MVVM 模式在桌面端最核心的价值——View 只负责长什么样ViewModel 负责状态和命令Model 负责数据结构数据访问单独一层。下面这套目录约定是我见过最稳、也最容易被新手接受的。2.1 一个可维护的目录长什么样BookkeepingApp/ ├── App.xaml / App.xaml.cs # 应用入口全局资源、DI 容器注册 ├── Views/ # 所有 Window / UserControl │ ├── MainWindow.xaml │ ├── DashboardView.xaml # 首页统计 │ ├── TransactionView.xaml # 收支明细录入 │ └── CategoryView.xaml # 分类管理 ├── ViewModels/ │ ├── MainViewModel.cs │ ├── DashboardViewModel.cs │ ├── TransactionViewModel.cs │ └── CategoryViewModel.cs ├── Models/ │ ├── Transaction.cs # 一条收支记录 │ ├── Category.cs # 分类 │ └── Account.cs # 账户现金/银行卡 ├── Services/ │ ├── IDataService.cs │ ├── SqliteDataService.cs # 数据访问实现 │ └── CsvExportService.cs # 导出 ├── Helpers/ │ ├── RelayCommand.cs # ICommand 实现 │ └── ObservableObject.cs # INotifyPropertyChanged 基类 └── Resources/ ├── Styles.xaml └── Icons.xaml这个结构不是摆设。Views 里只放 XAML任何Click事件都不写业务代码ViewModels 里持有ObservableCollectionTransaction通过RelayCommand响应按钮Services 里封装 SQLite 的增删改查。这样做的直接好处是单元测试可以只针对 ViewModel 和 Service不需要启动界面。2.2 Model 与 ViewModel 的边界怎么划新手最容易犯的错是把Transaction这种 Model 直接塞进 DataGrid 然后就地改属性。Model 应该是纯数据最多带一点计算属性比如public class Transaction { public int Id { get; set; } public DateTime Date { get; set; } public decimal Amount { get; set; } // 收入为正支出为负 public string CategoryName { get; set; } public string Remark { get; set; } // 只读计算属性供界面显示不参与持久化 public string AmountDisplay Amount 0 ? ${Amount:F2} : ${Amount:F2}; }AmountDisplay这种只读属性放在 Model 里没问题因为它不涉及业务规则变更。但像「本月结余」「按分类汇总」这类逻辑必须放到 ViewModel 或 Service否则 Model 会越来越臃肿最后变成谁都不敢动的黑匣子。2.3 数据绑定WPF 记账系统的命脉WPF 记账系统能不能用得顺手八成看数据绑定写没写对。核心是INotifyPropertyChanged和ObservableCollection这两样。前者让单个属性变化能通知界面后者让集合增删能自动刷新 DataGrid。下面是一个最小可用的 ViewModel 基类public class ObservableObject : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string name null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } protected bool SetPropertyT(ref T field, T value, [CallerMemberName] string name null) { if (EqualityComparerT.Default.Equals(field, value)) return false; field value; OnPropertyChanged(name); return true; } }SetProperty里先做相等判断再触发通知这一步很关键。DataGrid 编辑时如果每次赋值都触发PropertyChanged会出现光标跳动、输入被吞的玄学问题很多人以为是 WPF 的 bug其实是通知发多了。2.4 命令绑定替代事件处理按钮不要写ClickBtnAdd_Click改成Command{Binding AddCommand}。RelayCommand的标准实现public class RelayCommand : ICommand { private readonly Actionobject _execute; private readonly Predicateobject _canExecute; public RelayCommand(Actionobject execute, Predicateobject canExecute null) { _execute execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute canExecute; } public bool CanExecute(object parameter) _canExecute?.Invoke(parameter) ?? true; public void Execute(object parameter) _execute(parameter); public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested value; } remove { CommandManager.RequerySuggested - value; } } }CommandManager.RequerySuggested会自动在界面交互后重新查询CanExecute省去手动调用RaiseCanExecuteChanged的麻烦。参数说明execute是必填的执行逻辑canExecute可选用来控制按钮是否可点比如「金额为空时禁用保存」。3. 用 SQLite 落地本地账本建表、增删改查与导出记账系统的数据必须落地到本地文件否则关掉就没了。选 SQLite 的理由很直接单文件、零配置、支持事务、和 C# 集成成熟。相比把数据存成 CSV 或 JSONSQLite 在按日期范围查询、按月汇总时优势明显而且不怕程序崩溃写坏文件。下面按建表到查询的顺序走一遍。3.1 建表语句与字段设计CREATE TABLE IF NOT EXISTS Category ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT NOT NULL UNIQUE, Type INTEGER NOT NULL -- 0 支出1 收入 ); CREATE TABLE IF NOT EXISTS Account ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT NOT NULL, Balance REAL NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS [Transaction] ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Date TEXT NOT NULL, -- ISO8601 字符串便于排序 Amount REAL NOT NULL, CategoryId INTEGER NOT NULL, AccountId INTEGER NOT NULL, Remark TEXT, FOREIGN KEY (CategoryId) REFERENCES Category(Id), FOREIGN KEY (AccountId) REFERENCES Account(Id) ); CREATE INDEX IF NOT EXISTS idx_tx_date ON [Transaction](Date);字段说明Date用 TEXT 存 ISO8601如2024-05-01T12:00:00SQLite 没有原生日期类型字符串排序即时间排序跨平台也稳。Amount用 REAL个人记账精度够用如果做多币种或对精度敏感改成 INTEGER 存「分」。Transaction是 SQL 关键字加方括号避免语法冲突这个坑很多人第一次建表就踩。3.2 用参数化命令做增删改查public void AddTransaction(Transaction tx) { using var conn new SqliteConnection(_connectionString); conn.Open(); using var cmd conn.CreateCommand(); cmd.CommandText INSERT INTO [Transaction] (Date, Amount, CategoryId, AccountId, Remark) VALUES (date, amount, cat, acc, remark); cmd.Parameters.AddWithValue(date, tx.Date.ToString(s)); cmd.Parameters.AddWithValue(amount, tx.Amount); cmd.Parameters.AddWithValue(cat, tx.CategoryId); cmd.Parameters.AddWithValue(acc, tx.AccountId); cmd.Parameters.AddWithValue(remark, tx.Remark ?? string.Empty); cmd.ExecuteNonQuery(); }逻辑说明using var保证连接和命令对象在方法结束时释放避免文件被占用导致下次打开失败。参数说明date用ToString(s)输出可排序格式remark做空值兜底防止 NULL 写入后界面绑定报错。查询时同理用ExecuteReader逐行读再映射回Transaction对象。3.3 按月汇总的查询写法SELECT substr(Date, 1, 7) AS Month, SUM(CASE WHEN Amount 0 THEN Amount ELSE 0 END) AS Income, SUM(CASE WHEN Amount 0 THEN -Amount ELSE 0 END) AS Expense FROM [Transaction] GROUP BY substr(Date, 1, 7) ORDER BY Month DESC;substr(Date, 1, 7)直接截出YYYY-MM比在 C# 里循环累加快得多。返回结果可以绑定到 Dashboard 的统计卡片也可以喂给图表控件。3.4 导出 CSV 的注意点导出功能看着简单实际有两个坑一是中文乱码二是文件被 Excel 占用时写入失败。写法上先写 UTF-8 BOMusing var writer new StreamWriter(path, false, new UTF8Encoding(true)); writer.WriteLine(日期,金额,分类,备注); foreach (var tx in list) { writer.WriteLine(${tx.Date:yyyy-MM-dd},{tx.Amount},{tx.CategoryName},{tx.Remark}); }new UTF8Encoding(true)会写入 BOMExcel 打开中文才不乱码。写入前用FileShare.Read检测文件是否被占用被占用就提示用户先关闭而不是直接抛异常。4. 避坑与排查WPF 记账系统最容易翻车的 5 个地方这一章是我自己踩过、也帮别人排查过的真实问题按「现象 → 原因 → 解决」写遇到对应症状直接对号入座。4.1 DataGrid 编辑后数据没保存现象在 DataGrid 里改完金额切到别的页面再切回来改动没了。原因DataGrid 默认的提交时机是「单元格失去焦点」如果直接点导航按钮编辑状态没提交绑定源没更新。解决在 DataGrid 上设UpdateSourceTriggerPropertyChanged或在导航前调用dataGrid.CommitEdit(DataGridEditingUnit.Row, true)。更稳的做法是给列绑定加UpdateSourceTriggerPropertyChanged边输边同步。4.2 SQLite 报「database is locked」现象程序运行中偶尔抛SQLite Error 5: database is locked。原因多个连接同时写或者上一个连接没释放。解决统一用一个连接字符串并开启连接池写操作串行化读操作可以用ModeReadOnly单独开连接。关键是把using写全别让连接对象悬空。4.3 金额用 double 导致对不上账现象几笔支出加起来和显示的总计差一分钱。原因double是二进制浮点0.1 无法精确表示。解决金额统一用decimal数据库 REAL 读出后转decimal再运算或者干脆用 INTEGER 存分。这是财务类程序的铁律别图省事。4.4 界面卡死点按钮没反应现象导入几千条记录时界面冻结。原因数据操作跑在 UI 线程上。解决把耗时逻辑放进Task.Run完成后用Dispatcher.Invoke回 UI 线程更新集合。注意ObservableCollection不能跨线程直接改要么回 UI 线程改要么用BindingOperations.EnableCollectionSynchronization。4.5 发布后换台电脑打不开现象本机跑得好好的拷到别人电脑提示缺少 DLL。原因SQLite 的原生库e_sqlite3.dll没随发布带出去或者目标机没装对应 .NET 运行时。解决发布时选「独立部署」并指定运行时标识或确认runtimes目录完整。用dotnet publish -r win-x64 --self-contained一次搞定。5. 从能跑到好用给记账系统加一层可验证的统计与主题把系统跑通只是起点真正决定它能不能长期用的是「数据可信」和「看着舒服」。我一般会先加一个自检入口再考虑换肤。自检的做法是在启动时跑一遍「期初 收入 - 支出 期末」的校验对不上就在状态栏标红。这个习惯救过我很多次因为记账系统最怕的不是崩溃而是悄悄算错还看不出来。5.1 用一条 SQL 做账目自检SELECT (SELECT IFNULL(SUM(Amount),0) FROM [Transaction]) AS NetChange, (SELECT IFNULL(SUM(Balance),0) FROM Account) AS AccountTotal;两个值理论上应该相等假设账户余额由交易驱动。不相等就说明有交易没同步到账户或者账户被手工改过。把这条查询挂在 Dashboard 的「对账」按钮上点一下就知道账本干不干净。5.2 主题切换的最小实现WPF 换肤不用引入重型框架把颜色定义成ResourceDictionary运行时替换即可var dict new ResourceDictionary { Source new Uri(Resources/DarkTheme.xaml, UriKind.Relative) }; Application.Current.Resources.MergedDictionaries.Clear(); Application.Current.Resources.MergedDictionaries.Add(dict);前提是所有控件颜色都通过{DynamicResource}引用而不是写死#FF0000。这一步如果一开始没做后期改起来要动所有 XAML血泪经验。5.3 图表统计的选型对比方案上手难度适合场景注意点LiveCharts2中折线、饼图、动态刷新版本迭代快锁版本OxyPlot低静态报表、导出图片样式偏朴素自绘 Canvas高极简需求、无依赖坐标换算易错个人记账用 OxyPlot 就够饼图加柱状图两页搞定依赖少、发布体积小。如果要做实时刷新的动态图再考虑 LiveCharts2。5.4 我自己的习惯每次改完数据层我一定先跑自检 SQL 再开界面每次加新控件先确认它用的是DynamicResource。这两个习惯让我少熬了很多夜。记账系统这类工具功能可以慢慢加但账不能算错、界面不能越改越乱。希望帮到你。本文还有配套的精品资源点击获取