# Python实现个人支出统计与可视化工具(Tkinter+matplotlib+JSON)


背景介绍

在日常生活中,清晰的支出统计能帮助我们更好地管理个人财务。本文将介绍如何使用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()

代码模块解析

  1. GUI初始化(init_gui
    • 通过ttk.LabelFramettk.Entryttk.Combobox等组件搭建输入区域,预设日期格式和支出类别,提升用户体验。
    • 功能按钮与listbox结合,实时展示支出记录,滚动条保证列表可滚动。
  2. 数据持久化(save_data/load_data
    • 使用json模块的dump/load方法,将支出列表(字典结构)与JSON文件双向转换。
    • 程序启动时自动加载历史数据,添加支出后即时保存,确保数据不丢失。
  3. 统计与可视化(generate_charts
    • 手动遍历支出数据,按类别日期分组求和(也可使用pandas.groupby简化,但本文为降低依赖,采用原生方法)。
    • 通过matplotlibpiebar绘制图表,利用FigureCanvasTkAgg将图表嵌入Tkinter的Toplevel窗口,实现GUI与可视化的无缝衔接。

运行与扩展

  1. 依赖安装:确保Python环境已安装matplotlibpip install matplotlib),Tkinter为Python内置库,无需额外安装。
  2. 功能扩展:可添加“删除支出”“按时间筛选”“导出报表”等功能,或优化日期格式校验(如使用datetime模块)。

总结

本工具通过Tkinter、JSON和matplotlib的结合,实现了个人支出的记录-持久化-统计-可视化全流程。代码结构清晰,各模块职责明确,适合Python初学者学习GUI开发、文件操作和数据可视化的联动应用。通过该项目,可深入理解“数据驱动界面”的开发思路,为后续复杂工具开发打下基础。

如果在运行中遇到问题(如中文乱码、图表不显示),可检查matplotlib字体配置或Tkinter的事件循环逻辑,确保环境兼容。

通过以上步骤,我们成功开发了一个实用的个人支出统计工具,既满足了日常记账需求,又学习了Python GUI、文件操作和数据可视化的核心技术。希望本文能为你的编程学习提供帮助!


发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注