背景介绍
在日常生活中,清晰的支出统计能帮助我们更好地管理个人财务。本文将介绍如何使用Python开发一个个人支出统计与可视化工具,实现支出记录、数据持久化、统计分析和可视化展示的全流程。该工具基于Tkinter构建GUI界面,通过JSON文件持久化数据,利用matplotlib绘制直观的饼图(类别占比)和柱状图(每日趋势),适合个人或家庭快速掌握支出分布。
技术思路分析
该工具需整合四大核心模块:
1. 数据记录层:通过Tkinter的输入组件(日期、类别下拉框、金额输入框)采集支出信息。
2. 数据持久化层:使用Python的json模块读写本地JSON文件,确保数据在程序重启后不丢失。
3. 统计分析层:遍历支出数据,按类别和日期分组求和,为可视化提供数据支撑。
4. 可视化层:借助matplotlib绘制饼图(类别占比)和柱状图(每日支出),并通过FigureCanvasTkAgg嵌入Tkinter窗口,实现GUI与图表的联动。
代码实现(Python)
下面是完整的代码实现,包含详细注释:
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
# 设置matplotlib字体,确保中文显示正常
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]
plt.rcParams["axes.unicode_minus"] = False # 解决负号显示问题
class ExpenseTracker:
def __init__(self, root):
self.root = root
self.root.title("个人支出统计与可视化工具")
self.expenses = [] # 存储所有支出记录的列表,每个元素为字典
self.json_file = "expenses.json" # 数据持久化的JSON文件路径
self.init_gui() # 初始化GUI界面
def init_gui(self):
# ========== 1. 输入区域:日期、类别、金额 ==========
input_frame = ttk.LabelFrame(self.root, text="支出记录")
input_frame.pack(fill="x", padx=10, pady=5)
# 日期输入
ttk.Label(input_frame, text="日期:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.date_entry = ttk.Entry(input_frame, width=15)
self.date_entry.grid(row=0, column=1, padx=5, pady=5)
self.date_entry.insert(0, "2024-09-15") # 默认日期
# 类别下拉框(预设常见类别)
categories = ["餐饮", "交通", "购物", "娱乐", "住房", "其他"]
ttk.Label(input_frame, text="类别:").grid(row=0, column=2, padx=5, pady=5, sticky="w")
self.category_combo = ttk.Combobox(input_frame, values=categories, width=10)
self.category_combo.grid(row=0, column=3, padx=5, pady=5)
self.category_combo.current(0) # 默认选中第一个类别
# 金额输入
ttk.Label(input_frame, text="金额:").grid(row=0, column=4, padx=5, pady=5, sticky="w")
self.amount_entry = ttk.Entry(input_frame, width=10)
self.amount_entry.grid(row=0, column=5, padx=5, pady=5)
# ========== 2. 功能按钮:添加、加载、统计 ==========
button_frame = ttk.Frame(self.root)
button_frame.pack(fill="x", padx=10, pady=5)
ttk.Button(button_frame, text="添加支出", command=self.add_expense).pack(side="left", padx=5)
ttk.Button(button_frame, text="加载历史数据", command=self.load_data).pack(side="left", padx=5)
ttk.Button(button_frame, text="生成统计图表", command=self.generate_charts).pack(side="left", padx=5)
# ========== 3. 支出列表显示区域 ==========
list_frame = ttk.LabelFrame(self.root, text="支出记录列表")
list_frame.pack(fill="both", expand=True, padx=10, pady=5)
self.listbox = tk.Listbox(list_frame, width=60, height=10)
self.listbox.pack(side="left", fill="both", expand=True, padx=5, pady=5)
# 滚动条
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=self.listbox.yview)
scrollbar.pack(side="right", fill="y")
self.listbox.config(yscrollcommand=scrollbar.set)
# 初始化时加载历史数据
self.load_data()
def add_expense(self):
"""添加新的支出记录"""
# 获取输入内容
date = self.date_entry.get().strip()
category = self.category_combo.get().strip()
try:
amount = float(self.amount_entry.get().strip())
if amount <= 0:
messagebox.showerror("错误", "金额必须大于0!")
return
except ValueError:
messagebox.showerror("错误", "金额必须是有效数字!")
return
# 构建支出字典
expense = {"date": date, "category": category, "amount": amount}
self.expenses.append(expense)
# 更新列表显示
self.update_listbox()
# 清空金额输入框
self.amount_entry.delete(0, tk.END)
# 保存到JSON文件
self.save_data()
def save_data(self):
"""将支出数据保存到JSON文件"""
try:
with open(self.json_file, "w", encoding="utf-8") as f:
json.dump(self.expenses, f, ensure_ascii=False, indent=2)
except Exception as e:
messagebox.showerror("错误", f"保存数据失败:{str(e)}")
def load_data(self):
"""从JSON文件加载历史支出数据"""
if os.path.exists(self.json_file):
try:
with open(self.json_file, "r", encoding="utf-8") as f:
self.expenses = json.load(f)
self.update_listbox()
except Exception as e:
messagebox.showerror("错误", f"加载数据失败:{str(e)}")
self.expenses = []
else:
self.expenses = []
self.update_listbox()
def update_listbox(self):
"""更新列表框,显示所有支出记录"""
self.listbox.delete(0, tk.END) # 清空原有内容
for expense in self.expenses:
date = expense["date"]
category = expense["category"]
amount = expense["amount"]
self.listbox.insert(tk.END, f"{date} {category} {amount:.2f}")
def generate_charts(self):
"""生成类别占比饼图和每日支出柱状图"""
if not self.expenses:
messagebox.showinfo("提示", "暂无支出数据,无法生成图表!")
return
# ========== 统计分析:按类别和日期分组求和 ==========
# 1. 按类别统计总金额
category_total = {}
for expense in self.expenses:
cat = expense["category"]
amt = expense["amount"]
category_total[cat] = category_total.get(cat, 0) + amt
total_all = sum(category_total.values())
if total_all == 0:
messagebox.showinfo("提示", "总支出为0,无法生成图表!")
return
# 2. 按日期统计总金额(并按日期排序)
date_total = {}
for expense in self.expenses:
dt = expense["date"]
amt = expense["amount"]
date_total[dt] = date_total.get(dt, 0) + amt
sorted_dates = sorted(date_total.keys()) # 按日期字符串排序(假设格式为YYYY-MM-DD)
date_amounts = [date_total[dt] for dt in sorted_dates]
# ========== 绘制图表:使用matplotlib ==========
# 创建新窗口显示图表
chart_window = tk.Toplevel(self.root)
chart_window.title("支出统计图表")
chart_window.geometry("800x400") # 调整窗口大小
# ---------- 子图1:类别占比饼图 ----------
fig = Figure(figsize=(4, 4), dpi=100)
ax1 = fig.add_subplot(121)
labels = list(category_total.keys())
sizes = [category_total[cat] for cat in labels]
ax1.pie(sizes, labels=labels, autopct="%1.1f%%", startangle=90)
ax1.axis("equal") # 保证饼图为正圆形
ax1.set_title("支出类别占比")
# 嵌入Tkinter窗口
canvas1 = FigureCanvasTkAgg(fig, master=chart_window)
canvas1.draw()
canvas1.get_tk_widget().pack(side="left", fill="both", expand=True)
# ---------- 子图2:每日支出柱状图 ----------
fig2 = Figure(figsize=(4, 4), dpi=100)
ax2 = fig2.add_subplot(122)
ax2.bar(sorted_dates, date_amounts, color="skyblue")
ax2.set_title("每日支出总额")
ax2.set_xlabel("日期")
ax2.set_ylabel("金额(元)")
plt.xticks(rotation=45) # 旋转日期标签,避免重叠
# 嵌入Tkinter窗口
canvas2 = FigureCanvasTkAgg(fig2, master=chart_window)
canvas2.draw()
canvas2.get_tk_widget().pack(side="right", fill="both", expand=True)
if __name__ == "__main__":
root = tk.Tk()
app = ExpenseTracker(root)
root.mainloop()
代码模块解析
- GUI初始化(
init_gui):- 通过
ttk.LabelFrame、ttk.Entry、ttk.Combobox等组件搭建输入区域,预设日期格式和支出类别,提升用户体验。 - 功能按钮与
listbox结合,实时展示支出记录,滚动条保证列表可滚动。
- 通过
- 数据持久化(
save_data/load_data):- 使用
json模块的dump/load方法,将支出列表(字典结构)与JSON文件双向转换。 - 程序启动时自动加载历史数据,添加支出后即时保存,确保数据不丢失。
- 使用
- 统计与可视化(
generate_charts):- 手动遍历支出数据,按类别和日期分组求和(也可使用
pandas.groupby简化,但本文为降低依赖,采用原生方法)。 - 通过
matplotlib的pie和bar绘制图表,利用FigureCanvasTkAgg将图表嵌入Tkinter的Toplevel窗口,实现GUI与可视化的无缝衔接。
- 手动遍历支出数据,按类别和日期分组求和(也可使用
运行与扩展
- 依赖安装:确保Python环境已安装
matplotlib(pip install matplotlib),Tkinter为Python内置库,无需额外安装。 - 功能扩展:可添加“删除支出”“按时间筛选”“导出报表”等功能,或优化日期格式校验(如使用
datetime模块)。
总结
本工具通过Tkinter、JSON和matplotlib的结合,实现了个人支出的记录-持久化-统计-可视化全流程。代码结构清晰,各模块职责明确,适合Python初学者学习GUI开发、文件操作和数据可视化的联动应用。通过该项目,可深入理解“数据驱动界面”的开发思路,为后续复杂工具开发打下基础。
如果在运行中遇到问题(如中文乱码、图表不显示),可检查matplotlib字体配置或Tkinter的事件循环逻辑,确保环境兼容。
通过以上步骤,我们成功开发了一个实用的个人支出统计工具,既满足了日常记账需求,又学习了Python GUI、文件操作和数据可视化的核心技术。希望本文能为你的编程学习提供帮助!