python答疑送福利
本帖最后由 帅哥520 于 2026-2-27 11:25 编辑第一期
本期奖励:30鱼币
本期的题目是:个人资金管理系统
要求
收支记录:记录每笔资金的金额、类型(收入 / 支出)、分类、备注、时间;
分类统计:按日 / 周 / 月 / 分类统计收支情况,计算结余;
预算管理:设置月度分类预算,预警超支情况;
数据可视化:生成收支趋势图、分类占比图;
数据持久化:收支数据保存到本地 JSON 文件,支持导入 / 导出;
使用Tkinter制作可视化界面
代码行数200行以上
加分点:
1.达到上面所有要求
2.额外多出1个功能(如有请标识功能名称及其功能) {:5_106:}
3.界面简洁美观(此点很容易得奖哦)
已开启抢楼,第6、16、36、56层有奖励
补充内容 (2026-2-26 20:59):
欢迎各位参与
本期难度:中等
因为是第一次做这个系列,下一期奖励会提升
我改了下奖励,多了20鱼币,一定要给代码 做出来的程序效果精美,我追加 30RMB 作为奖励~ {:13_453:} 需要学完你的零基础课程才能完成这个任务吗? 如何上传照片文件? import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import json
import datetime
import os
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import calendar
from collections import defaultdict
import shutil
# 设置matplotlib中文显示
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]
plt.rcParams['axes.unicode_minus'] = False
class FinanceManager:
def __init__(self, root):
self.root = root
self.root.title("个人资金管理系统")
self.root.geometry("1000x700")
self.root.resizable(True, True)
# 数据文件路径
self.data_file = "finance_data.json"
self.backup_dir = "finance_backups"
# 初始化数据结构
self.finance_data = {
"records": [],# 收支记录
"budgets": {} # 月度分类预算
}
# 默认分类
self.income_categories = ["工资", "奖金", "理财", "兼职", "其他收入"]
self.expense_categories = ["餐饮", "交通", "购物", "娱乐", "住房", "医疗", "其他支出"]
# 加载数据
self.load_data()
# 创建界面
self.create_widgets()
def load_data(self):
"""加载本地JSON数据"""
if os.path.exists(self.data_file):
try:
with open(self.data_file, "r", encoding="utf-8") as f:
self.finance_data = json.load(f)
except Exception as e:
messagebox.showerror("错误", f"加载数据失败:{str(e)}")
else:
# 初始化空数据文件
self.save_data()
def save_data(self):
"""保存数据到JSON文件"""
try:
with open(self.data_file, "w", encoding="utf-8") as f:
json.dump(self.finance_data, f, ensure_ascii=False, indent=4)
except Exception as e:
messagebox.showerror("错误", f"保存数据失败:{str(e)}")
def create_widgets(self):
"""创建主界面组件"""
# 创建菜单栏
self.create_menu()
# 创建主面板
main_notebook = ttk.Notebook(self.root)
main_notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 1. 收支记录标签页
self.create_record_tab(main_notebook)
# 2. 统计分析标签页
self.create_analysis_tab(main_notebook)
# 3. 预算管理标签页
self.create_budget_tab(main_notebook)
# 4. 数据可视化标签页
self.create_visual_tab(main_notebook)
def create_menu(self):
"""创建菜单栏"""
menubar = tk.Menu(self.root)
# 文件菜单
file_menu = tk.Menu(menubar, tearoff=0)
file_menu.add_command(label="导入数据", command=self.import_data)
file_menu.add_command(label="导出数据", command=self.export_data)
# 新增:导出Excel表格
file_menu.add_command(label="导出Excel表格", command=self.export_to_excel)
file_menu.add_separator()
file_menu.add_command(label="备份数据", command=self.backup_data)
file_menu.add_command(label="恢复数据", command=self.restore_data)
file_menu.add_separator()
file_menu.add_command(label="退出", command=self.root.quit)
menubar.add_cascade(label="文件", menu=file_menu)
# 帮助菜单
help_menu = tk.Menu(menubar, tearoff=0)
help_menu.add_command(label="关于", command=self.show_about)
menubar.add_cascade(label="帮助", menu=help_menu)
self.root.config(menu=menubar)
def create_record_tab(self, parent):
"""创建收支记录标签页"""
record_frame = ttk.Frame(parent)
parent.add(record_frame, text="收支记录")
# 录入区域
input_frame = ttk.LabelFrame(record_frame, text="收支录入")
input_frame.pack(fill=tk.X, padx=10, pady=10)
# 金额
ttk.Label(input_frame, text="金额:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
self.amount_var = tk.StringVar()
ttk.Entry(input_frame, textvariable=self.amount_var).grid(row=0, column=1, padx=5, pady=5, sticky=tk.W)
# 类型(收入/支出)
ttk.Label(input_frame, text="类型:").grid(row=0, column=2, padx=5, pady=5, sticky=tk.W)
self.type_var = tk.StringVar(value="支出")
type_combo = ttk.Combobox(input_frame, textvariable=self.type_var, values=["收入", "支出"], state="readonly")
type_combo.grid(row=0, column=3, padx=5, pady=5, sticky=tk.W)
# 分类
ttk.Label(input_frame, text="分类:").grid(row=0, column=4, padx=5, pady=5, sticky=tk.W)
self.category_var = tk.StringVar()
self.category_combo = ttk.Combobox(input_frame, textvariable=self.category_var, state="readonly")
self.category_combo.grid(row=0, column=5, padx=5, pady=5, sticky=tk.W)
# 绑定类型变更事件,更新分类列表
self.type_var.trace('w', self.update_categories)
self.update_categories()
# 备注
ttk.Label(input_frame, text="备注:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
self.note_var = tk.StringVar()
ttk.Entry(input_frame, textvariable=self.note_var, width=50).grid(row=1, column=1, columnspan=5, padx=5, pady=5, sticky=tk.W)
# 新增:收支日期
ttk.Label(input_frame, text="日期:").grid(row=2, column=0, padx=5, pady=5, sticky=tk.W)
self.date_var = tk.StringVar(value=datetime.datetime.now().strftime("%Y-%m-%d"))
date_entry = ttk.Entry(input_frame, textvariable=self.date_var)
date_entry.grid(row=2, column=1, padx=5, pady=5, sticky=tk.W)
ttk.Label(input_frame, text="格式:YYYY-MM-DD").grid(row=2, column=2, padx=5, pady=5, sticky=tk.W)
# 按钮区域
btn_frame = ttk.Frame(input_frame)
btn_frame.grid(row=3, column=0, columnspan=6, pady=10)
ttk.Button(btn_frame, text="添加记录", command=self.add_record).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="清空输入", command=self.clear_input).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="删除选中记录", command=self.delete_record).pack(side=tk.LEFT, padx=5)
# 记录列表区域
list_frame = ttk.LabelFrame(record_frame, text="收支记录列表")
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 创建表格
columns = ("时间", "类型", "分类", "金额", "备注")
self.record_tree = ttk.Treeview(list_frame, columns=columns, show="headings", height=15)
# 设置列标题
for col in columns:
self.record_tree.heading(col, text=col)
self.record_tree.column(col, width=150 if col == "备注" else 100)
# 滚动条
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.record_tree.yview)
self.record_tree.configure(yscrollcommand=scrollbar.set)
self.record_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(10,0), pady=10)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, padx=(0,10), pady=10)
# 刷新记录列表
self.refresh_record_list()
def create_analysis_tab(self, parent):
"""创建统计分析标签页"""
analysis_frame = ttk.Frame(parent)
parent.add(analysis_frame, text="统计分析")
# 统计选项区域
option_frame = ttk.LabelFrame(analysis_frame, text="统计选项")
option_frame.pack(fill=tk.X, padx=10, pady=10)
# 统计维度
ttk.Label(option_frame, text="统计维度:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
self.analysis_dim_var = tk.StringVar(value="月度")
dim_combo = ttk.Combobox(option_frame, textvariable=self.analysis_dim_var,
values=["按日", "按周", "按月", "按分类"], state="readonly")
dim_combo.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W)
# 时间范围
ttk.Label(option_frame, text="时间范围:").grid(row=0, column=2, padx=5, pady=5, sticky=tk.W)
self.year_var = tk.StringVar(value=str(datetime.datetime.now().year))
year_combo = ttk.Combobox(option_frame, textvariable=self.year_var,
values=, state="readonly")
year_combo.grid(row=0, column=3, padx=5, pady=5, sticky=tk.W)
self.month_var = tk.StringVar(value=str(datetime.datetime.now().month))
month_combo = ttk.Combobox(option_frame, textvariable=self.month_var,
values=, state="readonly")
month_combo.grid(row=0, column=4, padx=5, pady=5, sticky=tk.W)
# 统计按钮
ttk.Button(option_frame, text="开始统计", command=self.run_analysis).grid(row=0, column=5, padx=5, pady=5)
# 统计结果显示区域
result_frame = ttk.LabelFrame(analysis_frame, text="统计结果")
result_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 创建结果文本框
self.analysis_text = tk.Text(result_frame, wrap=tk.WORD, height=20)
scrollbar = ttk.Scrollbar(result_frame, orient=tk.VERTICAL, command=self.analysis_text.yview)
self.analysis_text.configure(yscrollcommand=scrollbar.set)
self.analysis_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(10,0), pady=10)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, padx=(0,10), pady=10)
def create_budget_tab(self, parent):
"""创建预算管理标签页"""
budget_frame = ttk.Frame(parent)
parent.add(budget_frame, text="预算管理")
# 预算设置区域
setup_frame = ttk.LabelFrame(budget_frame, text="月度预算设置")
setup_frame.pack(fill=tk.X, padx=10, pady=10)
# 年份月份
ttk.Label(setup_frame, text="年份:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
self.budget_year_var = tk.StringVar(value=str(datetime.datetime.now().year))
budget_year_combo = ttk.Combobox(setup_frame, textvariable=self.budget_year_var,
values=, state="readonly")
budget_year_combo.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W)
ttk.Label(setup_frame, text="月份:").grid(row=0, column=2, padx=5, pady=5, sticky=tk.W)
self.budget_month_var = tk.StringVar(value=str(datetime.datetime.now().month))
budget_month_combo = ttk.Combobox(setup_frame, textvariable=self.budget_month_var,
values=, state="readonly")
budget_month_combo.grid(row=0, column=3, padx=5, pady=5, sticky=tk.W)
# 分类和金额
ttk.Label(setup_frame, text="支出分类:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
self.budget_category_var = tk.StringVar()
budget_category_combo = ttk.Combobox(setup_frame, textvariable=self.budget_category_var,
values=self.expense_categories, state="readonly")
budget_category_combo.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W)
ttk.Label(setup_frame, text="预算金额:").grid(row=1, column=2, padx=5, pady=5, sticky=tk.W)
self.budget_amount_var = tk.StringVar()
ttk.Entry(setup_frame, textvariable=self.budget_amount_var).grid(row=1, column=3, padx=5, pady=5, sticky=tk.W)
# 按钮
btn_frame = ttk.Frame(setup_frame)
btn_frame.grid(row=1, column=4, padx=5, pady=5)
ttk.Button(btn_frame, text="设置预算", command=self.set_budget).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="查看预算", command=self.view_budget).pack(side=tk.LEFT, padx=5)
# 预算预警区域
alert_frame = ttk.LabelFrame(budget_frame, text="预算预警")
alert_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
self.alert_text = tk.Text(alert_frame, wrap=tk.WORD, height=15)
scrollbar = ttk.Scrollbar(alert_frame, orient=tk.VERTICAL, command=self.alert_text.yview)
self.alert_text.configure(yscrollcommand=scrollbar.set)
self.alert_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(10,0), pady=10)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, padx=(0,10), pady=10)
# 加载预算预警
self.check_budget_alert()
def create_visual_tab(self, parent):
"""创建数据可视化标签页"""
visual_frame = ttk.Frame(parent)
parent.add(visual_frame, text="数据可视化")
# 可视化选项
option_frame = ttk.LabelFrame(visual_frame, text="图表类型")
option_frame.pack(fill=tk.X, padx=10, pady=10)
self.chart_type_var = tk.StringVar(value="收支趋势图")
chart_types = ["收支趋势图", "分类占比图", "月度收支对比图"]
for i, chart_type in enumerate(chart_types):
ttk.Radiobutton(option_frame, text=chart_type, variable=self.chart_type_var,
value=chart_type).grid(row=0, column=i, padx=10, pady=5)
ttk.Button(option_frame, text="生成图表", command=self.generate_chart).grid(row=0, column=3, padx=10, pady=5)
# 图表显示区域
self.chart_frame = ttk.Frame(visual_frame)
self.chart_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# ------------------------ 核心功能实现 ------------------------
def update_categories(self, *args):
"""根据收支类型更新分类列表"""
if self.type_var.get() == "收入":
self.category_combo['values'] = self.income_categories
if self.category_var.get() not in self.income_categories:
self.category_var.set(self.income_categories)
else:
self.category_combo['values'] = self.expense_categories
if self.category_var.get() not in self.expense_categories:
self.category_var.set(self.expense_categories)
def add_record(self):
"""添加收支记录"""
try:
# 验证输入
amount = float(self.amount_var.get())
if amount <= 0:
messagebox.showwarning("警告", "金额必须大于0!")
return
record_type = self.type_var.get()
category = self.category_var.get()
note = self.note_var.get().strip()
# 新增:处理用户输入的日期
date_str = self.date_var.get()
try:
# 解析日期,时间部分默认 00:00:00
record_time = datetime.datetime.strptime(date_str, "%Y-%m-%d")
except ValueError:
messagebox.showerror("错误", "日期格式不正确,请使用 YYYY-MM-DD 格式!")
return
# 创建记录
record = {
"id": len(self.finance_data["records"]) + 1,
"amount": amount,
"type": record_type,
"category": category,
"note": note,
"time": record_time.strftime("%Y-%m-%d %H:%M:%S")# 使用用户选择的日期
}
# 添加到数据并保存
self.finance_data["records"].append(record)
self.save_data()
# 刷新列表并清空输入
self.refresh_record_list()
self.clear_input()
messagebox.showinfo("成功", "记录添加成功!")
# 检查预算预警
self.check_budget_alert()
except ValueError:
messagebox.showerror("错误", "请输入有效的金额数字!")
except Exception as e:
messagebox.showerror("错误", f"添加记录失败:{str(e)}")
def clear_input(self):
"""清空输入框"""
self.amount_var.set("")
self.note_var.set("")
self.type_var.set("支出")
self.date_var.set(datetime.datetime.now().strftime("%Y-%m-%d"))# 重置为今天
def refresh_record_list(self):
"""刷新收支记录列表"""
# 清空现有数据
for item in self.record_tree.get_children():
self.record_tree.delete(item)
# 添加新数据
for record in self.finance_data["records"]:
self.record_tree.insert("", tk.END, values=(
record["time"],
record["type"],
record["category"],
record["amount"],
record["note"]
))
def delete_record(self):
"""删除选中的记录"""
selected = self.record_tree.selection()
if not selected:
messagebox.showwarning("警告", "请先选中要删除的记录!")
return
if messagebox.askyesno("确认", "确定要删除选中的记录吗?"):
# 获取选中行的索引
index = self.record_tree.index(selected)
# 删除记录
del self.finance_data["records"]
# 重新编号
for i, record in enumerate(self.finance_data["records"]):
record["id"] = i + 1
# 保存并刷新
self.save_data()
self.refresh_record_list()
messagebox.showinfo("成功", "记录删除成功!")
def run_analysis(self):
"""执行统计分析"""
self.analysis_text.delete(1.0, tk.END)
try:
year = int(self.year_var.get())
month = int(self.month_var.get())
dim = self.analysis_dim_var.get()
# 筛选指定年月的记录
filtered_records = []
for record in self.finance_data["records"]:
rec_time = datetime.datetime.strptime(record["time"], "%Y-%m-%d %H:%M:%S")
if rec_time.year == year and rec_time.month == month:
filtered_records.append(record)
if not filtered_records:
self.analysis_text.insert(tk.END, f"{year}年{month}月暂无收支记录!")
return
# 按不同维度统计
if dim == "按日":
self.analyze_by_day(filtered_records, year, month)
elif dim == "按周":
self.analyze_by_week(filtered_records, year, month)
elif dim == "按月":
self.analyze_by_month(filtered_records, year, month)
elif dim == "按分类":
self.analyze_by_category(filtered_records, year, month)
except Exception as e:
self.analysis_text.insert(tk.END, f"统计出错:{str(e)}")
def analyze_by_day(self, records, year, month):
"""按日统计"""
daily_stats = defaultdict(lambda: {"收入": 0, "支出": 0, "结余": 0})
for record in records:
day = datetime.datetime.strptime(record["time"], "%Y-%m-%d %H:%M:%S").day
if record["type"] == "收入":
daily_stats["收入"] += record["amount"]
else:
daily_stats["支出"] += record["amount"]
daily_stats["结余"] = daily_stats["收入"] - daily_stats["支出"]
# 生成统计结果
result = f"{year}年{month}月 按日收支统计\n"
result += "-" * 80 + "\n"
result += f"{'日期':<6}{'收入':<10}{'支出':<10}{'结余':<10}\n"
result += "-" * 80 + "\n"
total_income = 0
total_expense = 0
for day in sorted(daily_stats.keys()):
stats = daily_stats
result += f"{day:<6}日{stats['收入']:<10.2f}{stats['支出']:<10.2f}{stats['结余']:<10.2f}\n"
total_income += stats["收入"]
total_expense += stats["支出"]
total_balance = total_income - total_expense
result += "-" * 80 + "\n"
result += f"总计:<6{total_income:<10.2f}{total_expense:<10.2f}{total_balance:<10.2f}\n"
self.analysis_text.insert(tk.END, result)
def analyze_by_week(self, records, year, month):
"""按周统计"""
weekly_stats = defaultdict(lambda: {"收入": 0, "支出": 0, "结余": 0})
for record in records:
rec_time = datetime.datetime.strptime(record["time"], "%Y-%m-%d %H:%M:%S")
week = rec_time.isocalendar()# 获取周数
if record["type"] == "收入":
weekly_stats["收入"] += record["amount"]
else:
weekly_stats["支出"] += record["amount"]
weekly_stats["结余"] = weekly_stats["收入"] - weekly_stats["支出"]
# 生成统计结果
result = f"{year}年{month}月 按周收支统计\n"
result += "-" * 80 + "\n"
result += f"{'周数':<6}{'收入':<10}{'支出':<10}{'结余':<10}\n"
result += "-" * 80 + "\n"
total_income = 0
total_expense = 0
for week in sorted(weekly_stats.keys()):
stats = weekly_stats
result += f"第{week}周:<6{stats['收入']:<10.2f}{stats['支出']:<10.2f}{stats['结余']:<10.2f}\n"
total_income += stats["收入"]
total_expense += stats["支出"]
total_balance = total_income - total_expense
result += "-" * 80 + "\n"
result += f"总计:<6{total_income:<10.2f}{total_expense:<10.2f}{total_balance:<10.2f}\n"
self.analysis_text.insert(tk.END, result)
def analyze_by_month(self, records, year, month):
"""按月统计"""
# 汇总当月所有记录
income = sum(r["amount"] for r in records if r["type"] == "收入")
expense = sum(r["amount"] for r in records if r["type"] == "支出")
balance = income - expense
result = f"{year}年{month}月 收支汇总\n"
result += "-" * 50 + "\n"
result += f"总收入:{income:.2f} 元\n"
result += f"总支出:{expense:.2f} 元\n"
result += f"月结余:{balance:.2f} 元\n"
result += "-" * 50 + "\n"
# 显示各项支出占比
result += "\n支出分类明细:\n"
expense_categories = defaultdict(float)
for r in records:
if r["type"] == "支出":
expense_categories] += r["amount"]
for cat, amt in expense_categories.items():
ratio = (amt / expense) * 100 if expense > 0 else 0
result += f"{cat}:{amt:.2f} 元 ({ratio:.1f}%)\n"
self.analysis_text.insert(tk.END, result)
def analyze_by_category(self, records, year, month):
"""按分类统计"""
# 按分类汇总
category_stats = defaultdict(lambda: {"收入": 0, "支出": 0, "结余": 0})
for record in records:
cat = record["category"]
if record["type"] == "收入":
category_stats["收入"] += record["amount"]
else:
category_stats["支出"] += record["amount"]
category_stats["结余"] = category_stats["收入"] - category_stats["支出"]
# 生成统计结果
result = f"{year}年{month}月 按分类收支统计\n"
result += "-" * 80 + "\n"
result += f"{'分类':<10}{'收入':<10}{'支出':<10}{'结余':<10}\n"
result += "-" * 80 + "\n"
total_income = 0
total_expense = 0
for cat in sorted(category_stats.keys()):
stats = category_stats
result += f"{cat:<10}{stats['收入']:<10.2f}{stats['支出']:<10.2f}{stats['结余']:<10.2f}\n"
total_income += stats["收入"]
total_expense += stats["支出"]
total_balance = total_income - total_expense
result += "-" * 80 + "\n"
result += f"总计:<10{total_income:<10.2f}{total_expense:<10.2f}{total_balance:<10.2f}\n"
self.analysis_text.insert(tk.END, result)
def set_budget(self):
"""设置月度分类预算"""
try:
year = int(self.budget_year_var.get())
month = int(self.budget_month_var.get())
category = self.budget_category_var.get()
amount = float(self.budget_amount_var.get())
if amount < 0:
messagebox.showwarning("警告", "预算金额不能为负数!")
return
# 构建预算键
budget_key = f"{year}-{month:02d}"
if budget_key not in self.finance_data["budgets"]:
self.finance_data["budgets"] = {}
# 设置预算
self.finance_data["budgets"] = amount
self.save_data()
messagebox.showinfo("成功", f"{year}年{month}月 {category} 预算设置为:{amount:.2f} 元")
# 清空输入
self.budget_amount_var.set("")
# 刷新预算预警
self.check_budget_alert()
except ValueError:
messagebox.showerror("错误", "请输入有效的预算金额!")
except Exception as e:
messagebox.showerror("错误", f"设置预算失败:{str(e)}")
def view_budget(self):
"""查看月度预算"""
self.alert_text.delete(1.0, tk.END)
try:
year = int(self.budget_year_var.get())
month = int(self.budget_month_var.get())
budget_key = f"{year}-{month:02d}"
if budget_key not in self.finance_data["budgets"] or not self.finance_data["budgets"]:
self.alert_text.insert(tk.END, f"{year}年{month}月暂无预算设置!")
return
# 显示预算信息
result = f"{year}年{month}月 预算设置\n"
result += "-" * 50 + "\n"
budgets = self.finance_data["budgets"]
for cat, amt in budgets.items():
result += f"{cat}:{amt:.2f} 元\n"
self.alert_text.insert(tk.END, result)
except Exception as e:
self.alert_text.insert(tk.END, f"查看预算失败:{str(e)}")
def check_budget_alert(self):
"""检查预算超支预警"""
self.alert_text.delete(1.0, tk.END)
# 获取当前年月
now = datetime.datetime.now()
year = now.year
month = now.month
budget_key = f"{year}-{month:02d}"
# 筛选当月支出记录
monthly_expenses = defaultdict(float)
for record in self.finance_data["records"]:
rec_time = datetime.datetime.strptime(record["time"], "%Y-%m-%d %H:%M:%S")
if rec_time.year == year and rec_time.month == month and record["type"] == "支出":
monthly_expenses] += record["amount"]
# 检查预算
alert_info = f"{year}年{month}月 预算预警\n"
alert_info += "-" * 60 + "\n"
if budget_key in self.finance_data["budgets"]:
budgets = self.finance_data["budgets"]
has_alert = False
for cat, budget_amt in budgets.items():
actual_amt = monthly_expenses.get(cat, 0)
usage_ratio = (actual_amt / budget_amt) * 100 if budget_amt > 0 else 0
alert_info += f"{cat}:\n"
alert_info += f"预算:{budget_amt:.2f} 元 | 已支出:{actual_amt:.2f} 元 | 使用率:{usage_ratio:.1f}%\n"
if actual_amt > budget_amt:
alert_info += f"⚠️ 超支警告:超出预算 {actual_amt - budget_amt:.2f} 元\n"
has_alert = True
elif usage_ratio >= 90:
alert_info += f"⚠️ 预警:预算即将用尽(剩余 {budget_amt - actual_amt:.2f} 元)\n"
has_alert = True
alert_info += "\n"
if not has_alert:
alert_info += "✅ 所有分类预算使用正常,暂无超支风险\n"
else:
alert_info += "本月暂无预算设置,无法进行超支预警\n"
self.alert_text.insert(tk.END, alert_info)
def generate_chart(self):
"""生成可视化图表"""
# 清空现有图表
for widget in self.chart_frame.winfo_children():
widget.destroy()
chart_type = self.chart_type_var.get()
try:
if chart_type == "收支趋势图":
self.generate_trend_chart()
elif chart_type == "分类占比图":
self.generate_pie_chart()
elif chart_type == "月度收支对比图":
self.generate_monthly_compare_chart()
except Exception as e:
messagebox.showerror("错误", f"生成图表失败:{str(e)}")
def generate_trend_chart(self):
"""生成收支趋势图"""
# 按月份汇总数据
monthly_data = defaultdict(lambda: {"收入": 0, "支出": 0})
for record in self.finance_data["records"]:
rec_time = datetime.datetime.strptime(record["time"], "%Y-%m-%d %H:%M:%S")
month_key = rec_time.strftime("%Y-%m")
if record["type"] == "收入":
monthly_data["收入"] += record["amount"]
else:
monthly_data["支出"] += record["amount"]
if not monthly_data:
messagebox.showwarning("警告", "暂无收支数据,无法生成趋势图!")
return
# 排序月份
sorted_months = sorted(monthly_data.keys())
incomes = ["收入"] for m in sorted_months]
expenses = ["支出"] for m in sorted_months]
# 创建图表
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(sorted_months, incomes, marker='o', label='收入', color='green')
ax.plot(sorted_months, expenses, marker='s', label='支出', color='red')
ax.set_title('收支趋势图', fontsize=14)
ax.set_xlabel('月份', fontsize=12)
ax.set_ylabel('金额 (元)', fontsize=12)
ax.legend()
ax.grid(True, alpha=0.3)
# 旋转x轴标签
plt.xticks(rotation=45)
plt.tight_layout()
# 显示在Tkinter界面
canvas = FigureCanvasTkAgg(fig, master=self.chart_frame)
canvas.draw()
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
def generate_pie_chart(self):
"""生成分类占比图"""
# 汇总支出分类
expense_categories = defaultdict(float)
for record in self.finance_data["records"]:
if record["type"] == "支出":
expense_categories] += record["amount"]
if not expense_categories:
messagebox.showwarning("警告", "暂无支出数据,无法生成分类占比图!")
return
# 准备数据
labels = list(expense_categories.keys())
sizes = list(expense_categories.values())
colors = ['#ff9999','#66b3ff','#99ff99','#ffcc99','#c2c2f0','#ffb3e6', '#c4e17f']
# 创建图表
fig, ax = plt.subplots(figsize=(8, 8))
ax.pie(sizes, labels=labels, colors=colors, autopct='%1.1f%%', startangle=90)
ax.axis('equal')# 保证饼图是正圆形
ax.set_title('支出分类占比图', fontsize=14)
# 显示在Tkinter界面
canvas = FigureCanvasTkAgg(fig, master=self.chart_frame)
canvas.draw()
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
def generate_monthly_compare_chart(self):
"""生成月度收支对比图"""
# 按月份汇总数据
monthly_data = defaultdict(lambda: {"收入": 0, "支出": 0})
for record in self.finance_data["records"]:
rec_time = datetime.datetime.strptime(record["time"], "%Y-%m-%d %H:%M:%S")
month_key = rec_time.strftime("%Y-%m")
if record["type"] == "收入":
monthly_data["收入"] += record["amount"]
else:
monthly_data["支出"] += record["amount"]
if not monthly_data:
messagebox.showwarning("警告", "暂无收支数据,无法生成对比图!")
return
# 排序月份
sorted_months = sorted(monthly_data.keys())
incomes = ["收入"] for m in sorted_months]
expenses = ["支出"] for m in sorted_months]
# 创建图表
fig, ax = plt.subplots(figsize=(10, 6))
x = range(len(sorted_months))
width = 0.35
ax.bar(, incomes, width, label='收入', color='green', alpha=0.7)
ax.bar(, expenses, width, label='支出', color='red', alpha=0.7)
ax.set_title('月度收支对比图', fontsize=14)
ax.set_xlabel('月份', fontsize=12)
ax.set_ylabel('金额 (元)', fontsize=12)
ax.set_xticks(x)
ax.set_xticklabels(sorted_months, rotation=45)
ax.legend()
ax.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
# 显示在Tkinter界面
canvas = FigureCanvasTkAgg(fig, master=self.chart_frame)
canvas.draw()
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# ------------------------ 数据导入导出/备份恢复 ------------------------
def import_data(self):
"""导入JSON数据"""
file_path = filedialog.askopenfilename(
title="选择数据文件",
filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")]
)
if file_path:
try:
with open(file_path, "r", encoding="utf-8") as f:
imported_data = json.load(f)
# 验证数据结构
if "records" not in imported_data or "budgets" not in imported_data:
messagebox.showerror("错误", "导入的文件格式不正确!")
return
# 合并数据
self.finance_data = imported_data
self.save_data()
# 刷新界面
self.refresh_record_list()
self.check_budget_alert()
messagebox.showinfo("成功", "数据导入成功!")
except Exception as e:
messagebox.showerror("错误", f"导入数据失败:{str(e)}")
def export_data(self):
"""导出JSON数据"""
file_path = filedialog.asksaveasfilename(
title="保存数据文件",
defaultextension=".json",
filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")]
)
if file_path:
try:
with open(file_path, "w", encoding="utf-8") as f:
json.dump(self.finance_data, f, ensure_ascii=False, indent=4)
messagebox.showinfo("成功", f"数据已导出到:{file_path}")
except Exception as e:
messagebox.showerror("错误", f"导出数据失败:{str(e)}")
def export_to_excel(self):
"""导出收支数据到Excel表格(.xlsx格式)"""
if not self.finance_data["records"]:
messagebox.showwarning("警告", "暂无收支记录,无法导出Excel!")
return
# 让用户选择保存路径和文件名
file_path = filedialog.asksaveasfilename(
title="导出Excel表格",
defaultextension=".xlsx",
filetypes=[("Excel文件", "*.xlsx"), ("所有文件", "*.*")]
)
if not file_path:
return# 用户取消选择
try:
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, Border, Side
from openpyxl.utils import get_column_letter
# 创建工作簿
wb = Workbook()
ws = wb.active
ws.title = "收支记录"
# 1. 设置表头
headers = ["序号", "时间", "类型", "分类", "金额(元)", "备注", "结余(元)"]
for col, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=header)
# 设置表头样式(加粗、居中、边框)
cell.font = Font(bold=True, size=11)
cell.alignment = Alignment(horizontal="center", vertical="center")
cell.border = Border(
left=Side(style="thin"), right=Side(style="thin"),
top=Side(style="thin"), bottom=Side(style="thin")
)
# 2. 填充数据行
total_income = 0# 累计收入
total_expense = 0 # 累计支出
for row, record in enumerate(self.finance_data["records"], 2):
# 计算累计结余
if record["type"] == "收入":
total_income += record["amount"]
else:
total_expense += record["amount"]
current_balance = total_income - total_expense
# 填充数据
data = [
record["id"],
record["time"],
record["type"],
record["category"],
round(record["amount"], 2),
record["note"] if record["note"] else "-",
round(current_balance, 2)
]
# 写入单元格并设置样式
for col, value in enumerate(data, 1):
cell = ws.cell(row=row, column=col, value=value)
cell.alignment = Alignment(horizontal="center", vertical="center")
cell.border = Border(
left=Side(style="thin"), right=Side(style="thin"),
top=Side(style="thin"), bottom=Side(style="thin")
)
# 3. 自动调整列宽
column_widths =
for col, width in enumerate(column_widths, 1):
ws.column_dimensions.width = width
# 4. 添加汇总行
summary_row = len(self.finance_data["records"]) + 3
ws.cell(row=summary_row, column=1, value="汇总").font = Font(bold=True)
ws.cell(row=summary_row, column=4, value="总收入:").font = Font(bold=True)
ws.cell(row=summary_row, column=5, value=round(total_income, 2))
ws.cell(row=summary_row+1, column=4, value="总支出:").font = Font(bold=True)
ws.cell(row=summary_row+1, column=5, value=round(total_expense, 2))
ws.cell(row=summary_row+2, column=4, value="总结余:").font = Font(bold=True)
ws.cell(row=summary_row+2, column=5, value=round(total_income - total_expense, 2))
# 5. 保存文件
wb.save(file_path)
messagebox.showinfo("成功", f"Excel表格已导出到:\n{file_path}")
except ImportError:
messagebox.showerror("错误", "未安装openpyxl库,请先执行:pip install openpyxl")
except Exception as e:
messagebox.showerror("错误", f"导出Excel失败:{str(e)}")
def backup_data(self):
"""备份数据(加分项功能)"""
# 创建备份目录
if not os.path.exists(self.backup_dir):
os.makedirs(self.backup_dir)
# 生成备份文件名
backup_time = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
backup_file = os.path.join(self.backup_dir, f"finance_backup_{backup_time}.json")
try:
# 复制数据文件到备份文件
shutil.copy2(self.data_file, backup_file)
messagebox.showinfo("成功", f"数据备份成功!\n备份文件:{backup_file}")
except Exception as e:
messagebox.showerror("错误", f"备份数据失败:{str(e)}")
def restore_data(self):
"""恢复数据(加分项功能)"""
if not os.path.exists(self.backup_dir):
messagebox.showwarning("警告", "暂无备份数据!")
return
# 选择备份文件
backup_file = filedialog.askopenfilename(
title="选择备份文件",
initialdir=self.backup_dir,
filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")]
)
if backup_file:
if messagebox.askyesno("确认", "恢复数据将覆盖当前所有数据,确定继续吗?"):
try:
# 复制备份文件到数据文件
shutil.copy2(backup_file, self.data_file)
# 重新加载数据
self.load_data()
# 刷新界面
self.refresh_record_list()
self.check_budget_alert()
messagebox.showinfo("成功", "数据恢复成功!")
except Exception as e:
messagebox.showerror("错误", f"恢复数据失败:{str(e)}")
def show_about(self):
"""显示关于信息"""
about_text = """个人资金管理系统 v1.0
功能说明:
1. 收支记录:记录每笔资金的金额、类型、分类、备注、时间
2. 分类统计:按日/周/月/分类统计收支情况,计算结余
3. 预算管理:设置月度分类预算,预警超支情况
4. 数据可视化:生成收支趋势图、分类占比图、月度收支对比图
5. 数据持久化:收支数据保存到本地JSON文件,支持导入/导出
6. 数据备份与恢复:额外功能,保障数据安全
使用Tkinter开发,界面简洁美观"""
messagebox.showinfo("关于", about_text)
# ------------------------ 程序入口 ------------------------
if __name__ == "__main__":
root = tk.Tk()
app = FinanceManager(root)
root.mainloop()
我记得大学的时候 有同学接过这个类似的单子,因为自己一个人开发,到后期就放弃了
页:
[1]