AI写代码!基于Python+ Tkinter + SQLite + Matplotlib 实现个人财务管理系统
·
大体实现的功能参照以下链接:
个人财务管理系统设计与实现_个人账务管理系统python课设mysql-CSDN博客
使用AI写代码,迅速实现了全部功能。
"""
个人财务管理系统
基于 Tkinter + SQLite + Matplotlib 实现
功能:注册登录、个人资料、账户管理、收支管理、借入借出、统计图表
"""
import tkinter as tk
from tkinter import ttk, messagebox
import sqlite3
import datetime
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import sys
# ==================== 数据库管理 ====================
class DatabaseManager:
"""SQLite数据库操作封装"""
def __init__(self, db_path="finance.db"):
self.db_path = db_path
self._init_db()
def _init_db(self):
"""初始化数据库表结构"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 用户表
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
name TEXT,
school TEXT,
signature TEXT,
avatar TEXT
)
''')
# 账户表(银行卡)
cursor.execute('''
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
card_number TEXT,
holder_name TEXT,
bank_name TEXT,
balance REAL DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
# 收入表
cursor.execute('''
CREATE TABLE IF NOT EXISTS incomes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
amount REAL NOT NULL,
type TEXT NOT NULL,
time TEXT NOT NULL,
description TEXT,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
# 支出表
cursor.execute('''
CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
amount REAL NOT NULL,
type TEXT NOT NULL,
time TEXT NOT NULL,
description TEXT,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
# 借入表
cursor.execute('''
CREATE TABLE IF NOT EXISTS borrow_in (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
amount REAL NOT NULL,
lender TEXT NOT NULL,
borrow_date TEXT NOT NULL,
due_date TEXT,
description TEXT,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
# 借出表
cursor.execute('''
CREATE TABLE IF NOT EXISTS borrow_out (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
amount REAL NOT NULL,
borrower TEXT NOT NULL,
lend_date TEXT NOT NULL,
due_date TEXT,
description TEXT,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
# 收支类型预设
cursor.execute('''
CREATE TABLE IF NOT EXISTS income_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS expense_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL
)
''')
# 插入默认类型
default_income = ['兼职收入', '奖学金', '生活费', '红包', '投资收益', '其他']
default_expense = ['餐饮', '购物', '交通', '娱乐', '学习', '医疗', '通讯', '其他']
for t in default_income:
cursor.execute('INSERT OR IGNORE INTO income_types (name) VALUES (?)', (t,))
for t in default_expense:
cursor.execute('INSERT OR IGNORE INTO expense_types (name) VALUES (?)', (t,))
conn.commit()
conn.close()
def get_connection(self):
return sqlite3.connect(self.db_path)
# ---------- 用户操作 ----------
def register_user(self, username, password):
"""注册新用户"""
conn = self.get_connection()
cursor = conn.cursor()
try:
cursor.execute(
'INSERT INTO users (username, password, name) VALUES (?, ?, ?)',
(username, password, username)
)
conn.commit()
user_id = cursor.lastrowid
conn.close()
return True, user_id
except sqlite3.IntegrityError:
conn.close()
return False, "用户名已存在"
def login_user(self, username, password):
"""用户登录验证"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(
'SELECT id, username, name, school, signature FROM users WHERE username=? AND password=?',
(username, password)
)
user = cursor.fetchone()
conn.close()
return user
def get_user_info(self, user_id):
"""获取用户详细信息"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(
'SELECT id, username, name, school, signature FROM users WHERE id=?',
(user_id,)
)
user = cursor.fetchone()
conn.close()
return user
def update_user_info(self, user_id, name, school, signature):
"""更新用户资料"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(
'UPDATE users SET name=?, school=?, signature=? WHERE id=?',
(name, school, signature, user_id)
)
conn.commit()
conn.close()
# ---------- 账户操作 ----------
def add_account(self, user_id, card_number, holder_name, bank_name, balance):
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(
'INSERT INTO accounts (user_id, card_number, holder_name, bank_name, balance) VALUES (?, ?, ?, ?, ?)',
(user_id, card_number, holder_name, bank_name, balance)
)
conn.commit()
conn.close()
def get_accounts(self, user_id):
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(
'SELECT id, card_number, holder_name, bank_name, balance FROM accounts WHERE user_id=?',
(user_id,)
)
data = cursor.fetchall()
conn.close()
return data
def delete_account(self, account_id):
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('DELETE FROM accounts WHERE id=?', (account_id,))
conn.commit()
conn.close()
# ---------- 收入操作 ----------
def add_income(self, user_id, amount, type_name, time_str, description):
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(
'INSERT INTO incomes (user_id, amount, type, time, description) VALUES (?, ?, ?, ?, ?)',
(user_id, amount, type_name, time_str, description)
)
conn.commit()
conn.close()
def get_incomes(self, user_id, start_date=None, end_date=None):
conn = self.get_connection()
cursor = conn.cursor()
if start_date and end_date:
cursor.execute(
'SELECT id, amount, type, time, description FROM incomes WHERE user_id=? AND time BETWEEN ? AND ? ORDER BY time DESC',
(user_id, start_date, end_date)
)
else:
cursor.execute(
'SELECT id, amount, type, time, description FROM incomes WHERE user_id=? ORDER BY time DESC',
(user_id,)
)
data = cursor.fetchall()
conn.close()
return data
def delete_income(self, income_id):
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('DELETE FROM incomes WHERE id=?', (income_id,))
conn.commit()
conn.close()
# ---------- 支出操作 ----------
def add_expense(self, user_id, amount, type_name, time_str, description):
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(
'INSERT INTO expenses (user_id, amount, type, time, description) VALUES (?, ?, ?, ?, ?)',
(user_id, amount, type_name, time_str, description)
)
conn.commit()
conn.close()
def get_expenses(self, user_id, start_date=None, end_date=None):
conn = self.get_connection()
cursor = conn.cursor()
if start_date and end_date:
cursor.execute(
'SELECT id, amount, type, time, description FROM expenses WHERE user_id=? AND time BETWEEN ? AND ? ORDER BY time DESC',
(user_id, start_date, end_date)
)
else:
cursor.execute(
'SELECT id, amount, type, time, description FROM expenses WHERE user_id=? ORDER BY time DESC',
(user_id,)
)
data = cursor.fetchall()
conn.close()
return data
def delete_expense(self, expense_id):
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('DELETE FROM expenses WHERE id=?', (expense_id,))
conn.commit()
conn.close()
# ---------- 借入操作 ----------
def add_borrow_in(self, user_id, amount, lender, borrow_date, due_date, description):
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(
'INSERT INTO borrow_in (user_id, amount, lender, borrow_date, due_date, description) VALUES (?, ?, ?, ?, ?, ?)',
(user_id, amount, lender, borrow_date, due_date, description)
)
conn.commit()
conn.close()
def get_borrow_in(self, user_id):
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(
'SELECT id, amount, lender, borrow_date, due_date, description FROM borrow_in WHERE user_id=? ORDER BY borrow_date DESC',
(user_id,)
)
data = cursor.fetchall()
conn.close()
return data
# ---------- 借出操作 ----------
def add_borrow_out(self, user_id, amount, borrower, lend_date, due_date, description):
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(
'INSERT INTO borrow_out (user_id, amount, borrower, lend_date, due_date, description) VALUES (?, ?, ?, ?, ?, ?)',
(user_id, amount, borrower, lend_date, due_date, description)
)
conn.commit()
conn.close()
def get_borrow_out(self, user_id):
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(
'SELECT id, amount, borrower, lend_date, due_date, description FROM borrow_out WHERE user_id=? ORDER BY lend_date DESC',
(user_id,)
)
data = cursor.fetchall()
conn.close()
return data
# ---------- 统计查询 ----------
def get_income_by_type(self, user_id, start_date=None, end_date=None):
conn = self.get_connection()
cursor = conn.cursor()
if start_date and end_date:
cursor.execute(
'SELECT type, SUM(amount) FROM incomes WHERE user_id=? AND time BETWEEN ? AND ? GROUP BY type',
(user_id, start_date, end_date)
)
else:
cursor.execute(
'SELECT type, SUM(amount) FROM incomes WHERE user_id=? GROUP BY type',
(user_id,)
)
data = cursor.fetchall()
conn.close()
return data
def get_expense_by_type(self, user_id, start_date=None, end_date=None):
conn = self.get_connection()
cursor = conn.cursor()
if start_date and end_date:
cursor.execute(
'SELECT type, SUM(amount) FROM expenses WHERE user_id=? AND time BETWEEN ? AND ? GROUP BY type',
(user_id, start_date, end_date)
)
else:
cursor.execute(
'SELECT type, SUM(amount) FROM expenses WHERE user_id=? GROUP BY type',
(user_id,)
)
data = cursor.fetchall()
conn.close()
return data
def get_monthly_summary(self, user_id, year):
"""获取月度收支汇总"""
conn = self.get_connection()
cursor = conn.cursor()
monthly = {}
for month in range(1, 13):
month_str = f"{year:04d}-{month:02d}"
# 收入
cursor.execute(
'SELECT SUM(amount) FROM incomes WHERE user_id=? AND time LIKE ?',
(user_id, f"{month_str}%")
)
income = cursor.fetchone()[0] or 0
# 支出
cursor.execute(
'SELECT SUM(amount) FROM expenses WHERE user_id=? AND time LIKE ?',
(user_id, f"{month_str}%")
)
expense = cursor.fetchone()[0] or 0
monthly[month] = (income, expense)
conn.close()
return monthly
def get_income_types(self):
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('SELECT name FROM income_types')
data = [row[0] for row in cursor.fetchall()]
conn.close()
return data
def get_expense_types(self):
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('SELECT name FROM expense_types')
data = [row[0] for row in cursor.fetchall()]
conn.close()
return data
# ==================== 主应用程序 ====================
class FinanceApp:
"""个人财务管理系统主界面"""
def __init__(self, root):
self.root = root
self.root.title("个人财务管理系统")
self.root.geometry("1100x700")
self.root.minsize(1000, 650)
# 设置背景色
self.bg_color = "#f0f4f8"
self.root.configure(bg=self.bg_color)
# 统一字体(中文字体)
self.font_family = "Microsoft YaHei" # Windows
# 若为 macOS 可改为 "PingFang SC",Linux 可改为 "WenQuanYi Micro Hei"
self.default_font = (self.font_family, 10)
self.title_font = (self.font_family, 14, "bold")
self.head_font = (self.font_family, 18, "bold")
self.db = DatabaseManager()
self.current_user = None
self.user_id = None
# 显示登录界面
self.show_login()
# ==================== 界面辅助 ====================
def clear_window(self):
for widget in self.root.winfo_children():
widget.destroy()
def create_title(self, parent, text):
"""创建标题标签"""
label = tk.Label(
parent, text=text, font=self.head_font,
bg=self.bg_color, fg="#2c3e50"
)
return label
def create_label(self, parent, text, **kwargs):
return tk.Label(
parent, text=text, font=self.default_font,
bg=self.bg_color, fg="#34495e", **kwargs
)
def create_entry(self, parent, **kwargs):
return tk.Entry(
parent, font=self.default_font,
relief="solid", bd=1, **kwargs
)
def create_button(self, parent, text, command, **kwargs):
return tk.Button(
parent, text=text, font=self.default_font,
command=command, relief="raised", bd=2,
bg="#3498db", fg="white", activebackground="#2980b9",
activeforeground="white", **kwargs
)
def create_treeview(self, parent, columns, headings, widths=None):
"""创建表格视图"""
tree = ttk.Treeview(parent, columns=columns, show="headings", height=12)
if widths is None:
widths = [100] * len(columns)
for col, head, w in zip(columns, headings, widths):
tree.heading(col, text=head)
tree.column(col, width=w, anchor="center")
# 设置滚动条
scrollbar = ttk.Scrollbar(parent, orient="vertical", command=tree.yview)
tree.configure(yscrollcommand=scrollbar.set)
return tree, scrollbar
# ==================== 登录/注册界面 ====================
def show_login(self):
self.clear_window()
self.current_user = None
self.user_id = None
# 主容器 - 居中
main_frame = tk.Frame(self.root, bg=self.bg_color)
main_frame.place(relx=0.5, rely=0.5, anchor="center")
# 标题
title = self.create_title(main_frame, "个人财务管理系统")
title.pack(pady=(0, 30))
# 登录框
login_frame = tk.Frame(main_frame, bg="white", relief="solid", bd=1, padx=40, pady=30)
login_frame.pack()
tk.Label(login_frame, text="用户登录", font=self.title_font,
bg="white", fg="#2c3e50").grid(row=0, column=0, columnspan=2, pady=(0, 20))
tk.Label(login_frame, text="用户名:", font=self.default_font,
bg="white").grid(row=1, column=0, sticky="e", padx=5, pady=8)
self.entry_login_user = self.create_entry(login_frame, width=20)
self.entry_login_user.grid(row=1, column=1, padx=5, pady=8)
self.entry_login_user.focus()
tk.Label(login_frame, text="密码:", font=self.default_font,
bg="white").grid(row=2, column=0, sticky="e", padx=5, pady=8)
self.entry_login_pass = self.create_entry(login_frame, width=20, show="*")
self.entry_login_pass.grid(row=2, column=1, padx=5, pady=8)
btn_login = self.create_button(login_frame, "登 录", self.do_login, width=10)
btn_login.grid(row=3, column=0, padx=10, pady=20)
btn_register = self.create_button(login_frame, "注 册", self.show_register, width=10)
btn_register.grid(row=3, column=1, padx=10, pady=20)
# 绑定回车键
self.root.bind('<Return>', lambda e: self.do_login())
# 底部提示
tk.Label(main_frame, text="个人财务管理系统 v1.0",
font=("Microsoft YaHei", 8), bg=self.bg_color, fg="#95a5a6").pack(pady=(20, 0))
def show_register(self):
"""注册窗口"""
reg_window = tk.Toplevel(self.root)
reg_window.title("注册新用户")
reg_window.geometry("400x350")
reg_window.configure(bg="white")
reg_window.resizable(False, False)
reg_window.grab_set()
main_frame = tk.Frame(reg_window, bg="white", padx=40, pady=30)
main_frame.pack(fill="both", expand=True)
tk.Label(main_frame, text="用户注册", font=self.title_font,
bg="white", fg="#2c3e50").grid(row=0, column=0, columnspan=2, pady=(0, 20))
tk.Label(main_frame, text="用户名:", font=self.default_font,
bg="white").grid(row=1, column=0, sticky="e", padx=5, pady=8)
entry_user = self.create_entry(main_frame, width=20)
entry_user.grid(row=1, column=1, padx=5, pady=8)
entry_user.focus()
tk.Label(main_frame, text="密码:", font=self.default_font,
bg="white").grid(row=2, column=0, sticky="e", padx=5, pady=8)
entry_pass = self.create_entry(main_frame, width=20, show="*")
entry_pass.grid(row=2, column=1, padx=5, pady=8)
tk.Label(main_frame, text="确认密码:", font=self.default_font,
bg="white").grid(row=3, column=0, sticky="e", padx=5, pady=8)
entry_pass2 = self.create_entry(main_frame, width=20, show="*")
entry_pass2.grid(row=3, column=1, padx=5, pady=8)
def do_register():
username = entry_user.get().strip()
password = entry_pass.get()
password2 = entry_pass2.get()
if not username or not password:
messagebox.showerror("错误", "用户名和密码不能为空")
return
if password != password2:
messagebox.showerror("错误", "两次输入的密码不一致")
return
if len(password) < 4:
messagebox.showerror("错误", "密码长度至少4位")
return
success, result = self.db.register_user(username, password)
if success:
messagebox.showinfo("成功", f"注册成功!用户ID: {result}")
reg_window.destroy()
else:
messagebox.showerror("错误", result)
btn_reg = self.create_button(main_frame, "注 册", do_register, width=10)
btn_reg.grid(row=4, column=0, columnspan=2, pady=20)
btn_cancel = tk.Button(main_frame, text="取消", command=reg_window.destroy,
font=self.default_font, bg="#95a5a6", fg="white")
btn_cancel.grid(row=5, column=0, columnspan=2)
def do_login(self):
username = self.entry_login_user.get().strip()
password = self.entry_login_pass.get()
if not username or not password:
messagebox.showerror("错误", "请输入用户名和密码")
return
user = self.db.login_user(username, password)
if user:
self.user_id = user[0]
self.current_user = {
'id': user[0], 'username': user[1],
'name': user[2], 'school': user[3], 'signature': user[4]
}
self.root.unbind('<Return>')
self.show_main()
else:
messagebox.showerror("错误", "用户名或密码错误")
# ==================== 主界面 ====================
def show_main(self):
self.clear_window()
# 主容器
main_frame = tk.Frame(self.root, bg=self.bg_color)
main_frame.pack(fill="both", expand=True)
# 顶部标题栏
header = tk.Frame(main_frame, bg="#2c3e50", height=60)
header.pack(fill="x")
header.pack_propagate(False)
tk.Label(header, text="个人财务管理系统",
font=self.head_font, bg="#2c3e50", fg="white").pack(side="left", padx=20)
tk.Label(header, text=f"欢迎,{self.current_user['name']}",
font=("Microsoft YaHei", 11), bg="#2c3e50", fg="#ecf0f1").pack(side="right", padx=20)
btn_logout = tk.Button(header, text="退出登录", command=self.logout,
font=self.default_font, bg="#e74c3c", fg="white",
activebackground="#c0392b", activeforeground="white")
btn_logout.pack(side="right", padx=10)
# 主体:左侧菜单 + 右侧内容
body = tk.Frame(main_frame, bg=self.bg_color)
body.pack(fill="both", expand=True, padx=10, pady=10)
# 左侧菜单
menu_frame = tk.Frame(body, bg="white", relief="solid", bd=1, width=180)
menu_frame.pack(side="left", fill="y", padx=(0, 10))
menu_frame.pack_propagate(False)
menu_items = [
("个人资料", self.show_profile),
("账户管理", self.show_accounts),
("收入管理", self.show_incomes),
("支出管理", self.show_expenses),
("借入管理", self.show_borrow_in),
("借出管理", self.show_borrow_out),
("统计查询", self.show_statistics),
]
for text, cmd in menu_items:
btn = tk.Button(menu_frame, text=text, command=cmd,
font=("Microsoft YaHei", 11), bg="white", fg="#2c3e50",
relief="flat", anchor="w", padx=15, pady=12,
activebackground="#3498db", activeforeground="white")
btn.pack(fill="x")
# 右侧内容区域
self.content_frame = tk.Frame(body, bg="white", relief="solid", bd=1)
self.content_frame.pack(side="left", fill="both", expand=True)
# 默认显示个人资料
self.show_profile()
def logout(self):
if messagebox.askyesno("确认", "确定要退出登录吗?"):
self.current_user = None
self.user_id = None
self.show_login()
# ==================== 个人资料 ====================
def show_profile(self):
self.clear_content()
info = self.db.get_user_info(self.user_id)
if not info:
return
frame = tk.Frame(self.content_frame, bg="white", padx=40, pady=30)
frame.pack(fill="both", expand=True)
self.create_title(frame, "个人资料").grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 20))
labels = [("用户名:", info[1]), ("姓名:", info[2] or ""), ("学校:", info[3] or ""), ("个性签名:", info[4] or "")]
for i, (label, value) in enumerate(labels, start=1):
tk.Label(frame, text=label, font=self.default_font,
bg="white", fg="#34495e").grid(row=i, column=0, sticky="e", padx=10, pady=8)
tk.Label(frame, text=value, font=self.default_font,
bg="white", fg="#2c3e50").grid(row=i, column=1, sticky="w", padx=10, pady=8)
# 编辑按钮
def edit_profile():
edit_win = tk.Toplevel(self.root)
edit_win.title("编辑个人资料")
edit_win.geometry("400x350")
edit_win.configure(bg="white")
edit_win.resizable(False, False)
edit_win.grab_set()
ef = tk.Frame(edit_win, bg="white", padx=30, pady=20)
ef.pack(fill="both", expand=True)
tk.Label(ef, text="编辑个人资料", font=self.title_font,
bg="white", fg="#2c3e50").grid(row=0, column=0, columnspan=2, pady=(0, 20))
tk.Label(ef, text="姓名:", bg="white", font=self.default_font).grid(row=1, column=0, sticky="e", padx=5, pady=8)
e_name = self.create_entry(ef, width=25)
e_name.insert(0, info[2] or "")
e_name.grid(row=1, column=1, padx=5, pady=8)
tk.Label(ef, text="学校:", bg="white", font=self.default_font).grid(row=2, column=0, sticky="e", padx=5, pady=8)
e_school = self.create_entry(ef, width=25)
e_school.insert(0, info[3] or "")
e_school.grid(row=2, column=1, padx=5, pady=8)
tk.Label(ef, text="个性签名:", bg="white", font=self.default_font).grid(row=3, column=0, sticky="ne", padx=5, pady=8)
e_sig = tk.Text(ef, width=25, height=4, font=self.default_font, relief="solid", bd=1)
e_sig.insert("1.0", info[4] or "")
e_sig.grid(row=3, column=1, padx=5, pady=8)
def save_profile():
name = e_name.get().strip()
school = e_school.get().strip()
sig = e_sig.get("1.0", "end").strip()
self.db.update_user_info(self.user_id, name, school, sig)
self.current_user['name'] = name
self.current_user['school'] = school
self.current_user['signature'] = sig
edit_win.destroy()
self.show_profile()
messagebox.showinfo("成功", "资料已更新")
btn_save = self.create_button(ef, "保存修改", save_profile, width=10)
btn_save.grid(row=4, column=0, columnspan=2, pady=20)
btn_cancel = tk.Button(ef, text="取消", command=edit_win.destroy,
font=self.default_font, bg="#95a5a6", fg="white")
btn_cancel.grid(row=5, column=0, columnspan=2)
btn_edit = self.create_button(frame, "编辑资料", edit_profile, width=12)
btn_edit.grid(row=len(labels)+1, column=0, columnspan=2, pady=20)
# ==================== 账户管理 ====================
def show_accounts(self):
self.clear_content()
frame = tk.Frame(self.content_frame, bg="white", padx=20, pady=15)
frame.pack(fill="both", expand=True)
self.create_title(frame, "我的账户").pack(anchor="w", pady=(0, 10))
# 表格
columns = ("id", "card_number", "holder_name", "bank_name", "balance")
headings = ("ID", "卡号", "持卡人", "开户行", "余额(¥)")
widths = (50, 160, 100, 120, 100)
tree, scrollbar = self.create_treeview(frame, columns, headings, widths)
tree_frame = tk.Frame(frame)
tree_frame.pack(fill="both", expand=True)
tree.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# 加载数据
def load_accounts():
for item in tree.get_children():
tree.delete(item)
data = self.db.get_accounts(self.user_id)
for row in data:
tree.insert("", "end", values=row)
load_accounts()
# 操作按钮
btn_frame = tk.Frame(frame, bg="white")
btn_frame.pack(fill="x", pady=10)
def add_account():
win = tk.Toplevel(self.root)
win.title("添加账户")
win.geometry("400x320")
win.configure(bg="white")
win.resizable(False, False)
win.grab_set()
af = tk.Frame(win, bg="white", padx=30, pady=20)
af.pack(fill="both", expand=True)
tk.Label(af, text="添加银行卡", font=self.title_font,
bg="white", fg="#2c3e50").grid(row=0, column=0, columnspan=2, pady=(0, 15))
fields = [("卡号:", "card"), ("持卡人:", "holder"), ("开户行:", "bank"), ("余额:", "balance")]
entries = {}
for i, (label, key) in enumerate(fields, start=1):
tk.Label(af, text=label, bg="white", font=self.default_font).grid(row=i, column=0, sticky="e", padx=5, pady=6)
e = self.create_entry(af, width=25)
e.grid(row=i, column=1, padx=5, pady=6)
entries[key] = e
def do_add():
card = entries['card'].get().strip()
holder = entries['holder'].get().strip()
bank = entries['bank'].get().strip()
try:
balance = float(entries['balance'].get() or 0)
except ValueError:
messagebox.showerror("错误", "请输入有效的余额数字")
return
if not card or not holder or not bank:
messagebox.showerror("错误", "请填写完整信息")
return
self.db.add_account(self.user_id, card, holder, bank, balance)
win.destroy()
load_accounts()
messagebox.showinfo("成功", "账户添加成功")
btn_add = self.create_button(af, "添 加", do_add, width=10)
btn_add.grid(row=5, column=0, columnspan=2, pady=15)
btn_cancel = tk.Button(af, text="取消", command=win.destroy,
font=self.default_font, bg="#95a5a6", fg="white")
btn_cancel.grid(row=6, column=0, columnspan=2)
def delete_account():
selected = tree.selection()
if not selected:
messagebox.showerror("错误", "请先选择要删除的账户")
return
item = tree.item(selected[0])
acc_id = item['values'][0]
if messagebox.askyesno("确认", "确定要删除该账户吗?"):
self.db.delete_account(acc_id)
load_accounts()
messagebox.showinfo("成功", "账户已删除")
self.create_button(btn_frame, "+ 添加账户", add_account, width=12).pack(side="left", padx=5)
self.create_button(btn_frame, "删除账户", delete_account, width=12).pack(side="left", padx=5)
# ==================== 收入管理 ====================
def show_incomes(self):
self._show_transaction("incomes", "收入管理")
def show_expenses(self):
self._show_transaction("expenses", "支出管理")
def _show_transaction(self, tx_type, title):
"""通用收支管理界面"""
self.clear_content()
frame = tk.Frame(self.content_frame, bg="white", padx=20, pady=15)
frame.pack(fill="both", expand=True)
self.create_title(frame, title).pack(anchor="w", pady=(0, 10))
# 筛选条件
filter_frame = tk.Frame(frame, bg="white")
filter_frame.pack(fill="x", pady=(0, 10))
tk.Label(filter_frame, text="开始日期:", bg="white", font=self.default_font).pack(side="left", padx=5)
entry_start = self.create_entry(filter_frame, width=12)
entry_start.insert(0, (datetime.datetime.now() - datetime.timedelta(days=30)).strftime("%Y-%m-%d"))
entry_start.pack(side="left", padx=5)
tk.Label(filter_frame, text="结束日期:", bg="white", font=self.default_font).pack(side="left", padx=5)
entry_end = self.create_entry(filter_frame, width=12)
entry_end.insert(0, datetime.datetime.now().strftime("%Y-%m-%d"))
entry_end.pack(side="left", padx=5)
# 表格
columns = ("id", "amount", "type", "time", "description")
headings = ("ID", "金额(¥)", "类型", "时间", "备注")
widths = (40, 80, 100, 120, 150)
tree, scrollbar = self.create_treeview(frame, columns, headings, widths)
tree_frame = tk.Frame(frame)
tree_frame.pack(fill="both", expand=True)
tree.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
def load_data():
for item in tree.get_children():
tree.delete(item)
start = entry_start.get().strip()
end = entry_end.get().strip()
if tx_type == "incomes":
data = self.db.get_incomes(self.user_id, start, end)
else:
data = self.db.get_expenses(self.user_id, start, end)
for row in data:
tree.insert("", "end", values=row)
load_data()
# 操作按钮
btn_frame = tk.Frame(frame, bg="white")
btn_frame.pack(fill="x", pady=10)
def add_transaction():
win = tk.Toplevel(self.root)
win.title(f"添加{title}")
win.geometry("420x350")
win.configure(bg="white")
win.resizable(False, False)
win.grab_set()
af = tk.Frame(win, bg="white", padx=30, pady=20)
af.pack(fill="both", expand=True)
tk.Label(af, text=f"添加{title}", font=self.title_font,
bg="white", fg="#2c3e50").grid(row=0, column=0, columnspan=2, pady=(0, 15))
# 类型下拉
tk.Label(af, text="类型:", bg="white", font=self.default_font).grid(row=1, column=0, sticky="e", padx=5, pady=6)
type_var = tk.StringVar()
type_combo = ttk.Combobox(af, textvariable=type_var, width=22, font=self.default_font)
if tx_type == "incomes":
type_combo['values'] = self.db.get_income_types()
else:
type_combo['values'] = self.db.get_expense_types()
type_combo.grid(row=1, column=1, padx=5, pady=6)
type_combo.current(0)
tk.Label(af, text="金额:", bg="white", font=self.default_font).grid(row=2, column=0, sticky="e", padx=5, pady=6)
e_amount = self.create_entry(af, width=25)
e_amount.grid(row=2, column=1, padx=5, pady=6)
tk.Label(af, text="日期:", bg="white", font=self.default_font).grid(row=3, column=0, sticky="e", padx=5, pady=6)
e_time = self.create_entry(af, width=25)
e_time.insert(0, datetime.datetime.now().strftime("%Y-%m-%d"))
e_time.grid(row=3, column=1, padx=5, pady=6)
tk.Label(af, text="备注:", bg="white", font=self.default_font).grid(row=4, column=0, sticky="ne", padx=5, pady=6)
e_desc = tk.Text(af, width=25, height=3, font=self.default_font, relief="solid", bd=1)
e_desc.grid(row=4, column=1, padx=5, pady=6)
def do_add():
try:
amount = float(e_amount.get().strip())
except ValueError:
messagebox.showerror("错误", "请输入有效的金额数字")
return
if amount <= 0:
messagebox.showerror("错误", "金额必须大于0")
return
tx_type_name = type_var.get()
time_str = e_time.get().strip()
desc = e_desc.get("1.0", "end").strip()
if not time_str:
time_str = datetime.datetime.now().strftime("%Y-%m-%d")
if tx_type == "incomes":
self.db.add_income(self.user_id, amount, tx_type_name, time_str, desc)
else:
self.db.add_expense(self.user_id, amount, tx_type_name, time_str, desc)
win.destroy()
load_data()
messagebox.showinfo("成功", f"{title}添加成功")
btn_add = self.create_button(af, "添 加", do_add, width=10)
btn_add.grid(row=5, column=0, columnspan=2, pady=15)
btn_cancel = tk.Button(af, text="取消", command=win.destroy,
font=self.default_font, bg="#95a5a6", fg="white")
btn_cancel.grid(row=6, column=0, columnspan=2)
def delete_transaction():
selected = tree.selection()
if not selected:
messagebox.showerror("错误", "请先选择要删除的记录")
return
item = tree.item(selected[0])
tx_id = item['values'][0]
if messagebox.askyesno("确认", "确定要删除该记录吗?"):
if tx_type == "incomes":
self.db.delete_income(tx_id)
else:
self.db.delete_expense(tx_id)
load_data()
messagebox.showinfo("成功", "记录已删除")
def refresh_data():
load_data()
self.create_button(btn_frame, "+ 添加记录", add_transaction, width=12).pack(side="left", padx=5)
self.create_button(btn_frame, "删除记录", delete_transaction, width=12).pack(side="left", padx=5)
self.create_button(btn_frame, "刷新", refresh_data, width=12).pack(side="left", padx=5)
# 绑定筛选事件
def on_filter_changed(event=None):
load_data()
entry_start.bind('<KeyRelease>', on_filter_changed)
entry_end.bind('<KeyRelease>', on_filter_changed)
# ==================== 借入管理 ====================
def show_borrow_in(self):
self._show_borrow("borrow_in", "借入管理")
def show_borrow_out(self):
self._show_borrow("borrow_out", "借出管理")
def _show_borrow(self, borrow_type, title):
"""通用借入借出管理界面"""
self.clear_content()
frame = tk.Frame(self.content_frame, bg="white", padx=20, pady=15)
frame.pack(fill="both", expand=True)
self.create_title(frame, title).pack(anchor="w", pady=(0, 10))
if borrow_type == "borrow_in":
columns = ("id", "amount", "lender", "borrow_date", "due_date", "description")
headings = ("ID", "金额(¥)", "借款人", "借款日期", "还款日期", "备注")
else:
columns = ("id", "amount", "borrower", "lend_date", "due_date", "description")
headings = ("ID", "金额(¥)", "出借人", "出借日期", "还款日期", "备注")
widths = (40, 80, 100, 100, 100, 120)
tree, scrollbar = self.create_treeview(frame, columns, headings, widths)
tree_frame = tk.Frame(frame)
tree_frame.pack(fill="both", expand=True)
tree.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
def load_data():
for item in tree.get_children():
tree.delete(item)
if borrow_type == "borrow_in":
data = self.db.get_borrow_in(self.user_id)
else:
data = self.db.get_borrow_out(self.user_id)
for row in data:
tree.insert("", "end", values=row)
load_data()
btn_frame = tk.Frame(frame, bg="white")
btn_frame.pack(fill="x", pady=10)
def add_borrow():
win = tk.Toplevel(self.root)
win.title(f"添加{title}")
win.geometry("420x380")
win.configure(bg="white")
win.resizable(False, False)
win.grab_set()
af = tk.Frame(win, bg="white", padx=30, pady=20)
af.pack(fill="both", expand=True)
tk.Label(af, text=f"添加{title}", font=self.title_font,
bg="white", fg="#2c3e50").grid(row=0, column=0, columnspan=2, pady=(0, 15))
if borrow_type == "borrow_in":
label_person = "借款人:"
else:
label_person = "出借人:"
tk.Label(af, text=label_person, bg="white", font=self.default_font).grid(row=1, column=0, sticky="e", padx=5, pady=6)
e_person = self.create_entry(af, width=25)
e_person.grid(row=1, column=1, padx=5, pady=6)
tk.Label(af, text="金额:", bg="white", font=self.default_font).grid(row=2, column=0, sticky="e", padx=5, pady=6)
e_amount = self.create_entry(af, width=25)
e_amount.grid(row=2, column=1, padx=5, pady=6)
tk.Label(af, text="日期:", bg="white", font=self.default_font).grid(row=3, column=0, sticky="e", padx=5, pady=6)
e_date = self.create_entry(af, width=25)
e_date.insert(0, datetime.datetime.now().strftime("%Y-%m-%d"))
e_date.grid(row=3, column=1, padx=5, pady=6)
tk.Label(af, text="还款日期:", bg="white", font=self.default_font).grid(row=4, column=0, sticky="e", padx=5, pady=6)
e_due = self.create_entry(af, width=25)
e_due.insert(0, (datetime.datetime.now() + datetime.timedelta(days=30)).strftime("%Y-%m-%d"))
e_due.grid(row=4, column=1, padx=5, pady=6)
tk.Label(af, text="备注:", bg="white", font=self.default_font).grid(row=5, column=0, sticky="ne", padx=5, pady=6)
e_desc = tk.Text(af, width=25, height=3, font=self.default_font, relief="solid", bd=1)
e_desc.grid(row=5, column=1, padx=5, pady=6)
def do_add():
person = e_person.get().strip()
try:
amount = float(e_amount.get().strip())
except ValueError:
messagebox.showerror("错误", "请输入有效的金额数字")
return
if not person:
messagebox.showerror("错误", "请输入姓名")
return
if amount <= 0:
messagebox.showerror("错误", "金额必须大于0")
return
date_str = e_date.get().strip() or datetime.datetime.now().strftime("%Y-%m-%d")
due_str = e_due.get().strip() or ""
desc = e_desc.get("1.0", "end").strip()
if borrow_type == "borrow_in":
self.db.add_borrow_in(self.user_id, amount, person, date_str, due_str, desc)
else:
self.db.add_borrow_out(self.user_id, amount, person, date_str, due_str, desc)
win.destroy()
load_data()
messagebox.showinfo("成功", f"{title}添加成功")
btn_add = self.create_button(af, "添 加", do_add, width=10)
btn_add.grid(row=6, column=0, columnspan=2, pady=15)
btn_cancel = tk.Button(af, text="取消", command=win.destroy,
font=self.default_font, bg="#95a5a6", fg="white")
btn_cancel.grid(row=7, column=0, columnspan=2)
self.create_button(btn_frame, "+ 添加记录", add_borrow, width=12).pack(side="left", padx=5)
self.create_button(btn_frame, "刷新", load_data, width=12).pack(side="left", padx=5)
# ==================== 统计查询 ====================
def show_statistics(self):
self.clear_content()
frame = tk.Frame(self.content_frame, bg="white", padx=20, pady=15)
frame.pack(fill="both", expand=True)
self.create_title(frame, "统计查询").pack(anchor="w", pady=(0, 10))
# 筛选
filter_frame = tk.Frame(frame, bg="white")
filter_frame.pack(fill="x", pady=(0, 10))
tk.Label(filter_frame, text="年份:", bg="white", font=self.default_font).pack(side="left", padx=5)
year_var = tk.StringVar()
year_combo = ttk.Combobox(filter_frame, textvariable=year_var, width=10, font=self.default_font)
year_combo['values'] = [str(y) for y in range(2020, 2031)]
year_combo.set(str(datetime.datetime.now().year))
year_combo.pack(side="left", padx=5)
def update_charts():
year = int(year_var.get())
self._render_charts(frame, year)
btn_update = self.create_button(filter_frame, "更新图表", update_charts, width=10)
btn_update.pack(side="left", padx=10)
# 图表容器
chart_frame = tk.Frame(frame, bg="white")
chart_frame.pack(fill="both", expand=True)
self._render_charts(chart_frame, int(year_var.get()))
def _render_charts(self, parent, year):
"""渲染统计图表"""
# 清除旧图表
for widget in parent.winfo_children():
widget.destroy()
# 获取数据
income_by_type = self.db.get_income_by_type(self.user_id)
expense_by_type = self.db.get_expense_by_type(self.user_id)
monthly = self.db.get_monthly_summary(self.user_id, year)
# 创建图表
fig, axes = plt.subplots(1, 2, figsize=(10, 4.5))
fig.patch.set_facecolor('white')
# 饼图:收入分类
ax1 = axes[0]
if income_by_type:
labels = [row[0] for row in income_by_type if row[1] and row[1] > 0]
sizes = [row[1] for row in income_by_type if row[1] and row[1] > 0]
if sizes:
ax1.pie(sizes, labels=labels, autopct='%1.1f%%', startangle=90,
colors=plt.cm.Paired(range(len(sizes))))
ax1.set_title('收入分类占比', fontsize=12, fontweight='bold')
else:
ax1.text(0.5, 0.5, '暂无收入数据', ha='center', va='center', fontsize=12)
ax1.set_title('收入分类占比', fontsize=12, fontweight='bold')
else:
ax1.text(0.5, 0.5, '暂无收入数据', ha='center', va='center', fontsize=12)
ax1.set_title('收入分类占比', fontsize=12, fontweight='bold')
# 饼图:支出分类
ax2 = axes[1]
if expense_by_type:
labels = [row[0] for row in expense_by_type if row[1] and row[1] > 0]
sizes = [row[1] for row in expense_by_type if row[1] and row[1] > 0]
if sizes:
ax2.pie(sizes, labels=labels, autopct='%1.1f%%', startangle=90,
colors=plt.cm.Set3(range(len(sizes))))
ax2.set_title('支出分类占比', fontsize=12, fontweight='bold')
else:
ax2.text(0.5, 0.5, '暂无支出数据', ha='center', va='center', fontsize=12)
ax2.set_title('支出分类占比', fontsize=12, fontweight='bold')
else:
ax2.text(0.5, 0.5, '暂无支出数据', ha='center', va='center', fontsize=12)
ax2.set_title('支出分类占比', fontsize=12, fontweight='bold')
plt.tight_layout()
# 嵌入到Tkinter
canvas = FigureCanvasTkAgg(fig, master=parent)
canvas.draw()
canvas.get_tk_widget().pack(side="top", fill="both", expand=True, pady=5)
# 月度趋势图(柱状图)
fig2, ax3 = plt.subplots(figsize=(10, 3))
fig2.patch.set_facecolor('white')
months = list(range(1, 13))
incomes = [monthly[m][0] for m in months]
expenses = [monthly[m][1] for m in months]
x = range(len(months))
width = 0.35
ax3.bar([i - width/2 for i in x], incomes, width, label='收入', color='#2ecc71')
ax3.bar([i + width/2 for i in x], expenses, width, label='支出', color='#e74c3c')
ax3.set_xlabel('月份', fontsize=10)
ax3.set_ylabel('金额 (¥)', fontsize=10)
ax3.set_title(f'{year}年 月度收支趋势', fontsize=12, fontweight='bold')
ax3.set_xticks(x)
ax3.set_xticklabels([f'{m}月' for m in months])
ax3.legend()
ax3.grid(axis='y', linestyle='--', alpha=0.7)
plt.tight_layout()
canvas2 = FigureCanvasTkAgg(fig2, master=parent)
canvas2.draw()
canvas2.get_tk_widget().pack(side="top", fill="both", expand=True, pady=5)
# ==================== 辅助 ====================
def clear_content(self):
for widget in self.content_frame.winfo_children():
widget.destroy()
# ==================== 主程序入口 ====================
if __name__ == "__main__":
# 检查必要的库
try:
import matplotlib
import PIL
except ImportError:
print("请安装依赖库: pip install matplotlib Pillow")
sys.exit(1)
root = tk.Tk()
app = FinanceApp(root)
root.mainloop()





更多推荐


所有评论(0)