[Python] 纯文本查看 复制代码
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from datetime import datetime, timedelta
import hashlib
import socket
import sys
import atexit
import re
BG_COLOR = "#E8F5E9"
FRAME_BG = "#F1F8E9"
BTN_BG = "#2C7A47"
BTN_FG = "white"
ENTRY_BG = "#FFFFFF"
LABEL_FG = "#1B5E20"
DB_FILE = "finance.db"
LOCK_PORT = 54321
_single_lock_socket = None
def init_db():
try:
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL,
type TEXT NOT NULL,
category TEXT NOT NULL,
amount REAL NOT NULL,
description TEXT)''')
cursor.execute('''CREATE TABLE IF NOT EXISTS auth (
id INTEGER PRIMARY KEY,
password TEXT NOT NULL)''')
cursor.execute("SELECT COUNT(*) FROM auth")
if cursor.fetchone()[0] == 0:
default_pwd = hashlib.md5("000000".encode()).hexdigest()
cursor.execute("INSERT INTO auth (id, password) VALUES (1, ?)", (default_pwd,))
conn.commit()
except Exception as e:
messagebox.showerror("数据库错误", f"初始化数据库失败:{str(e)}")
finally:
conn.close()
def verify_password(input_pwd):
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute("SELECT password FROM auth WHERE id=1")
stored = cursor.fetchone()[0]
conn.close()
if stored and '$' in stored:
salt, hash_val = stored.split('$', 1)
return hashlib.sha256((salt + input_pwd).encode()).hexdigest() == hash_val
else:
return hashlib.md5(input_pwd.encode()).hexdigest() == stored
def change_password(new_pwd):
salt = hashlib.sha256(str(datetime.now().timestamp()).encode()).hexdigest()[:16]
hashed = hashlib.sha256((salt + new_pwd).encode()).hexdigest()
stored = f"{salt}${hashed}"
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute("UPDATE auth SET password = ? WHERE id=1", (stored,))
conn.commit()
conn.close()
def add_record(date, type_, category, amount, desc):
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute("INSERT INTO records (date, type, category, amount, description) VALUES (?,?,?,?,?)",
(date, type_, category, amount, desc))
conn.commit()
conn.close()
def delete_record(record_id):
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute("DELETE FROM records WHERE id=?", (record_id,))
conn.commit()
conn.close()
def update_record(record_id, date, type_, category, amount, desc):
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute("UPDATE records SET date=?, type=?, category=?, amount=?, description=? WHERE id=?",
(date, type_, category, amount, desc, record_id))
conn.commit()
conn.close()
def get_all_records():
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute("SELECT id, date, type, category, amount, description FROM records ORDER BY date DESC")
rows = cursor.fetchall()
conn.close()
return rows
def get_stats():
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute("SELECT SUM(amount) FROM records WHERE type='income'")
income = cursor.fetchone()[0] or 0.0
cursor.execute("SELECT SUM(amount) FROM records WHERE type='expense'")
expense = cursor.fetchone()[0] or 0.0
conn.close()
return income, expense, income - expense
def plot_yearly_monthly_trend():
conn = sqlite3.connect(DB_FILE)
query = """
SELECT strftime('%Y-%m', date) as month,
SUM(CASE WHEN type='income' THEN amount ELSE 0 END) as income,
SUM(CASE WHEN type='expense' THEN amount ELSE 0 END) as expense
FROM records
WHERE date >= date('now', '-10 years')
GROUP BY month
ORDER BY month
"""
df = pd.read_sql_query(query, conn)
conn.close()
if df.empty:
messagebox.showinfo("提示", "没有足够数据绘制10年趋势图,请先添加记录。")
return
end_date = datetime.now().replace(day=1)
start_date = end_date - timedelta(days=365*10)
all_months = pd.date_range(start=start_date, end=end_date, freq='MS').strftime('%Y-%m')
df_full = pd.DataFrame({'month': all_months})
df_full = df_full.merge(df, on='month', how='left').fillna(0)
fig, ax = plt.subplots(figsize=(14, 6), facecolor=BG_COLOR)
ax.set_facecolor(FRAME_BG)
x = range(len(df_full))
width = 0.35
ax.bar([i - width/2 for i in x], df_full['income'], width, label='收入', color='#2E7D32', edgecolor='white')
ax.bar([i + width/2 for i in x], df_full['expense'], width, label='支出', color='#C62828', edgecolor='white')
ax.set_xlabel('月份', fontsize=11)
ax.set_ylabel('金额 (元)', fontsize=11)
ax.set_title('近10年收支月度趋势', fontsize=13, fontweight='bold')
ax.legend()
ticks = range(0, len(df_full), 12)
labels = [df_full['month'].iloc[i][:7] for i in ticks]
ax.set_xticks(ticks)
ax.set_xticklabels(labels, rotation=45)
plt.tight_layout()
top = tk.Toplevel()
top.title("10年收支趋势图")
top.configure(bg=BG_COLOR)
canvas = FigureCanvasTkAgg(fig, master=top)
canvas.draw()
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
class FinanceApp:
def __init__(self, root):
self.root = root
self.root.title("个人财务管理系统V1.01")
self.root.geometry("1100x700")
self.root.minsize(900, 600)
self.root.configure(bg=BG_COLOR)
self.root.resizable(True, True)
self.style = ttk.Style()
self.style.theme_use('clam')
self.style.configure('TFrame', background=BG_COLOR)
self.style.configure('TLabel', background=BG_COLOR, foreground=LABEL_FG, font=('微软雅黑', 10))
self.style.configure('TButton', background=BTN_BG, foreground=BTN_FG, font=('微软雅黑', 9), borderwidth=0, focuscolor='none')
self.style.map('TButton', background=[('active', '#1B5E20')])
self.style.configure('Treeview', background=ENTRY_BG, foreground='#1E2A1E', fieldbackground=ENTRY_BG, font=('微软雅黑', 9))
self.style.configure('Treeview.Heading', font=('微软雅黑', 10, 'bold'), foreground=LABEL_FG)
self.login_frame = tk.Frame(self.root, bg=BG_COLOR)
self.main_frame = tk.Frame(self.root, bg=BG_COLOR)
self.show_login()
def show_login(self):
if hasattr(self, 'main_frame'):
self.main_frame.destroy()
self.main_frame = tk.Frame(self.root, bg=BG_COLOR)
self.main_frame.pack_forget()
self.login_frame.pack(fill="both", expand=True)
tk.Label(self.login_frame, text="个人财务管理系统", font=("微软雅黑", 24, "bold"), bg=BG_COLOR, fg=LABEL_FG).pack(pady=40)
tk.Label(self.login_frame, text="密码:", font=("微软雅黑", 12), bg=BG_COLOR, fg=LABEL_FG).pack(pady=5)
self.pwd_entry = tk.Entry(self.login_frame, show="*", width=20, font=("微软雅黑", 11), relief=tk.FLAT, bg=ENTRY_BG)
self.pwd_entry.pack(pady=5, ipady=4)
self.pwd_entry.bind("<Return>", lambda e: self.do_login())
btn_login = tk.Button(self.login_frame, text="登录", command=self.do_login, width=12, height=1,
bg=BTN_BG, fg=BTN_FG, font=("微软雅黑", 11, "bold"), relief=tk.FLAT, bd=0)
btn_login.pack(pady=20)
self.login_error = tk.Label(self.login_frame, text="", fg="red", bg=BG_COLOR, font=("微软雅黑", 10))
self.login_error.pack()
def do_login(self):
pwd = self.pwd_entry.get()
if verify_password(pwd):
self.login_frame.pack_forget()
self.init_main_ui()
self.main_frame.pack(fill="both", expand=True)
self.refresh_record_list()
self.refresh_stats()
else:
self.login_error.config(text="密码错误,初始密码: 000000")
def init_main_ui(self):
toolbar = tk.Frame(self.main_frame, bg=BG_COLOR, pady=8)
toolbar.pack(fill=tk.X, padx=10)
btn_style = {'bg': BTN_BG, 'fg': BTN_FG, 'font': ('微软雅黑', 9), 'relief': tk.FLAT, 'bd': 0, 'padx': 12, 'pady': 4}
tk.Button(toolbar, text="修改密码", command=self.change_pwd, **btn_style).pack(side=tk.LEFT, padx=5)
tk.Button(toolbar, text="添加记录", command=self.add_record_dialog, **btn_style).pack(side=tk.LEFT, padx=5)
tk.Button(toolbar, text="导出Excel", command=self.export_excel, **btn_style).pack(side=tk.LEFT, padx=5)
tk.Button(toolbar, text="导入Excel", command=self.import_excel, **btn_style).pack(side=tk.LEFT, padx=5)
tk.Button(toolbar, text="10年趋势图", command=plot_yearly_monthly_trend, **btn_style).pack(side=tk.LEFT, padx=5)
tk.Button(toolbar, text="退出登录", command=self.logout, **btn_style).pack(side=tk.RIGHT, padx=5)
stats_card = tk.Frame(self.main_frame, bg=FRAME_BG, relief=tk.RAISED, bd=1, highlightthickness=0)
stats_card.pack(fill=tk.X, padx=15, pady=10)
self.income_label = tk.Label(stats_card, text="总收入: ¥0.00", font=("微软雅黑", 12, "bold"), bg=FRAME_BG, fg="#2E7D32")
self.income_label.pack(side=tk.LEFT, padx=25, pady=8, expand=True)
self.expense_label = tk.Label(stats_card, text="总支出: ¥0.00", font=("微软雅黑", 12, "bold"), bg=FRAME_BG, fg="#C62828")
self.expense_label.pack(side=tk.LEFT, padx=25, pady=8, expand=True)
self.balance_label = tk.Label(stats_card, text="总余额: ¥0.00", font=("微软雅黑", 12, "bold"), bg=FRAME_BG, fg="#0D47A1")
self.balance_label.pack(side=tk.LEFT, padx=25, pady=8, expand=True)
filter_frame = tk.Frame(self.main_frame, bg=BG_COLOR)
filter_frame.pack(fill=tk.X, padx=15, pady=5)
tk.Label(filter_frame, text="类型筛选:", bg=BG_COLOR, fg=LABEL_FG, font=("微软雅黑", 10)).pack(side=tk.LEFT)
self.type_var = tk.StringVar(value="全部")
type_combo = ttk.Combobox(filter_frame, textvariable=self.type_var, values=["全部", "收入", "支出"], width=8, state="readonly")
type_combo.pack(side=tk.LEFT, padx=5)
type_combo.bind("<<ComboboxSelected>>", lambda e: self.refresh_record_list())
tk.Label(filter_frame, text="关键词:", bg=BG_COLOR, fg=LABEL_FG, font=("微软雅黑", 10)).pack(side=tk.LEFT, padx=(15,0))
self.keyword_entry = tk.Entry(filter_frame, width=15, font=("微软雅黑", 10), bg=ENTRY_BG, relief=tk.FLAT)
self.keyword_entry.pack(side=tk.LEFT, padx=5, ipady=2)
self.keyword_entry.bind("<KeyRelease>", lambda e: self.refresh_record_list())
tk.Label(filter_frame, text="年份-月份:", bg=BG_COLOR, fg=LABEL_FG, font=("微软雅黑", 10)).pack(side=tk.LEFT, padx=(15,0))
self.month_entry = tk.Entry(filter_frame, width=10, font=("微软雅黑", 10), bg=ENTRY_BG, relief=tk.FLAT)
self.month_entry.pack(side=tk.LEFT, padx=5, ipady=2)
self.month_entry.bind("<KeyRelease>", lambda e: self.refresh_record_list())
tk.Label(filter_frame, text="(例: 2025-03)", fg="#6A8E6A", bg=BG_COLOR, font=("微软雅黑", 9)).pack(side=tk.LEFT, padx=2)
columns = ("ID", "日期", "类型", "类别", "金额", "备注")
self.tree = ttk.Treeview(self.main_frame, columns=columns, show="headings", height=18)
for col in columns:
self.tree.heading(col, text=col, command=lambda c=col: self.sort_treeview(c))
self.tree.column(col, width=100 if col != "备注" else 200, anchor="center")
self.tree.pack(fill=tk.BOTH, expand=True, padx=15, pady=5)
self.context_menu = tk.Menu(self.root, tearoff=0, bg=ENTRY_BG, fg=LABEL_FG)
self.context_menu.add_command(label="编辑", command=self.edit_record)
self.context_menu.add_command(label="删除", command=self.delete_record)
self.tree.bind("<Button-3>", self.show_context_menu)
self.sort_column = None
self.sort_reverse = False
def sort_treeview(self, col):
if self.sort_column == col:
self.sort_reverse = not self.sort_reverse
else:
self.sort_column = col
self.sort_reverse = False
data = [(self.tree.item(item)['values'], item) for item in self.tree.get_children('')]
col_index = {"ID":0, "日期":1, "类型":2, "类别":3, "金额":4, "备注":5}[col]
def try_float(v):
try:
return float(str(v).replace('¥','').replace(',',''))
except:
return v
data.sort(key=lambda x: try_float(x[0][col_index]), reverse=self.sort_reverse)
for idx, (values, item) in enumerate(data):
self.tree.move(item, '', idx)
def refresh_stats(self):
inc, exp, bal = get_stats()
self.income_label.config(text=f"总收入: ¥{inc:.2f}")
self.expense_label.config(text=f"总支出: ¥{exp:.2f}")
self.balance_label.config(text=f"总余额: ¥{bal:.2f}")
def refresh_record_list(self):
for row in self.tree.get_children():
self.tree.delete(row)
records = get_all_records()
type_filter = self.type_var.get()
keyword = self.keyword_entry.get().strip().lower()
month_filter = self.month_entry.get().strip()
for rec in records:
rec_id, date, typ, cat, amt, desc = rec
if type_filter != "全部":
if type_filter == "收入" and typ != "income":
continue
if type_filter == "支出" and typ != "expense":
continue
if keyword and not (keyword in cat.lower() or keyword in desc.lower()):
continue
if month_filter and not date.startswith(month_filter):
continue
typ_display = "收入" if typ == "income" else "支出"
self.tree.insert("", tk.END, iid=rec_id, values=(rec_id, date, typ_display, cat, f"¥{amt:.2f}", desc))
def show_context_menu(self, event):
item = self.tree.identify_row(event.y)
if item:
self.tree.selection_set(item)
self.context_menu.post(event.x_root, event.y_root)
def add_record_dialog(self):
self._record_form(is_edit=False)
def edit_record(self):
selected = self.tree.selection()
if not selected:
messagebox.showwarning("警告", "请先选择一条记录")
return
rec_id = int(selected[0])
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute("SELECT date, type, category, amount, description FROM records WHERE id=?", (rec_id,))
row = cursor.fetchone()
conn.close()
if row:
type_display = "收入" if row[1] == "income" else "支出"
self._record_form(is_edit=True, rec_id=rec_id, default_date=row[0], default_type=type_display,
default_cat=row[2], default_amt=row[3], default_desc=row[4])
def _record_form(self, is_edit, rec_id=None, default_date="", default_type="收入", default_cat="餐饮", default_amt=0.0, default_desc=""):
dialog = tk.Toplevel(self.root)
dialog.title("编辑记录" if is_edit else "新增记录")
dialog.geometry("420x450")
dialog.configure(bg=BG_COLOR)
dialog.grab_set()
tk.Label(dialog, text="日期 (YYYY-MM-DD):", bg=BG_COLOR, fg=LABEL_FG, font=("微软雅黑", 10)).pack(pady=(15,5))
date_entry = tk.Entry(dialog, font=("微软雅黑", 10), bg=ENTRY_BG, relief=tk.FLAT, width=25)
date_entry.insert(0, default_date or datetime.today().strftime("%Y-%m-%d"))
date_entry.pack(ipady=3)
tk.Label(dialog, text="类型:", bg=BG_COLOR, fg=LABEL_FG, font=("微软雅黑", 10)).pack(pady=(10,5))
type_var = tk.StringVar(value=default_type)
type_combo = ttk.Combobox(dialog, textvariable=type_var, values=["收入", "支出"], state="readonly", width=20)
type_combo.pack()
tk.Label(dialog, text="类别:", bg=BG_COLOR, fg=LABEL_FG, font=("微软雅黑", 10)).pack(pady=(10,5))
cat_entry = tk.Entry(dialog, font=("微软雅黑", 10), bg=ENTRY_BG, relief=tk.FLAT, width=25)
cat_entry.insert(0, default_cat)
cat_entry.pack(ipady=3)
tk.Label(dialog, text="金额 (数字):", bg=BG_COLOR, fg=LABEL_FG, font=("微软雅黑", 10)).pack(pady=(10,5))
amt_entry = tk.Entry(dialog, font=("微软雅黑", 10), bg=ENTRY_BG, relief=tk.FLAT, width=25)
amt_entry.insert(0, str(default_amt))
amt_entry.pack(ipady=3)
def validate_amount(char):
return char.isdigit() or char == '.' or char == ''
vcmd = (dialog.register(validate_amount), '%S')
amt_entry.config(validate="key", validatecommand=vcmd)
tk.Label(dialog, text="备注:", bg=BG_COLOR, fg=LABEL_FG, font=("微软雅黑", 10)).pack(pady=(10,5))
desc_entry = tk.Entry(dialog, font=("微软雅黑", 10), bg=ENTRY_BG, relief=tk.FLAT, width=25)
desc_entry.insert(0, default_desc)
desc_entry.pack(ipady=3)
def save():
date = date_entry.get().strip()
try:
datetime.strptime(date, "%Y-%m-%d")
except ValueError:
messagebox.showerror("错误", "日期格式不正确,请使用 YYYY-MM-DD")
return
typ_cn = type_var.get()
typ_en = "income" if typ_cn == "收入" else "expense"
cat = cat_entry.get().strip()
amt_str = amt_entry.get().strip()
if not amt_str:
messagebox.showerror("错误", "金额不能为空")
return
try:
amt = float(amt_str)
except:
messagebox.showerror("错误", "金额必须是数字")
return
desc = desc_entry.get().strip()
if not cat:
messagebox.showerror("错误", "类别不能为空")
return
if is_edit:
update_record(rec_id, date, typ_en, cat, amt, desc)
messagebox.showinfo("成功", "修改成功")
else:
add_record(date, typ_en, cat, amt, desc)
messagebox.showinfo("成功", "添加成功")
dialog.destroy()
self.refresh_record_list()
self.refresh_stats()
btn_save = tk.Button(dialog, text="保存", command=save, bg=BTN_BG, fg=BTN_FG, font=("微软雅黑", 10, "bold"),
relief=tk.FLAT, padx=20, pady=5)
btn_save.pack(pady=20)
def delete_record(self):
selected = self.tree.selection()
if not selected:
return
if messagebox.askyesno("确认", "确定删除选中的记录吗?"):
for item in selected:
rec_id = int(item)
delete_record(rec_id)
self.refresh_record_list()
self.refresh_stats()
def export_excel(self):
records = get_all_records()
if not records:
messagebox.showinfo("提示", "无数据可导出")
return
data = []
for r in records:
data.append({
"日期": r[1],
"类型": "收入" if r[2] == "income" else "支出",
"类别": r[3],
"金额": r[4],
"备注": r[5]
})
df = pd.DataFrame(data)
file_path = filedialog.asksaveasfilename(defaultextension=".xlsx", filetypes=[("Excel文件", "*.xlsx")])
if file_path:
try:
df.to_excel(file_path, index=False)
messagebox.showinfo("成功", f"已导出到 {file_path}")
except Exception as e:
messagebox.showerror("导出失败", str(e))
def import_excel(self):
file_path = filedialog.askopenfilename(filetypes=[("Excel文件", "*.xlsx *.xls")])
if not file_path:
return
try:
df = pd.read_excel(file_path)
required = ["日期", "类型", "类别", "金额"]
for col in required:
if col not in df.columns:
messagebox.showerror("错误", f"Excel缺少必要列: {col}\n请确保有:日期、类型、类别、金额、备注(可选)")
return
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
try:
cursor.execute("BEGIN")
count = 0
for _, row in df.iterrows():
date = str(row["日期"])[:10]
try:
datetime.strptime(date, "%Y-%m-%d")
except:
continue
typ_raw = str(row["类型"])
typ = "income" if "收入" in typ_raw else "expense"
category = str(row["类别"])
try:
amount = float(row["金额"])
except:
continue
desc = str(row.get("备注", "")) if pd.notna(row.get("备注")) else ""
cursor.execute("INSERT INTO records (date, type, category, amount, description) VALUES (?,?,?,?,?)",
(date, typ, category, amount, desc))
count += 1
conn.commit()
messagebox.showinfo("成功", f"成功导入 {count} 条记录")
except Exception as e:
conn.rollback()
messagebox.showerror("导入失败", f"数据写入错误:{str(e)}")
finally:
conn.close()
self.refresh_record_list()
self.refresh_stats()
except Exception as e:
messagebox.showerror("导入失败", str(e))
def change_pwd(self):
dialog = tk.Toplevel(self.root)
dialog.title("修改密码")
dialog.geometry("320x280")
dialog.configure(bg=BG_COLOR)
dialog.grab_set()
tk.Label(dialog, text="原密码:", bg=BG_COLOR, fg=LABEL_FG, font=("微软雅黑", 10)).pack(pady=(15,5))
old_entry = tk.Entry(dialog, show="*", font=("微软雅黑", 10), bg=ENTRY_BG, relief=tk.FLAT, width=20)
old_entry.pack(ipady=3)
tk.Label(dialog, text="新密码:", bg=BG_COLOR, fg=LABEL_FG, font=("微软雅黑", 10)).pack(pady=(10,5))
new_entry = tk.Entry(dialog, show="*", font=("微软雅黑", 10), bg=ENTRY_BG, relief=tk.FLAT, width=20)
new_entry.pack(ipady=3)
tk.Label(dialog, text="确认新密码:", bg=BG_COLOR, fg=LABEL_FG, font=("微软雅黑", 10)).pack(pady=(10,5))
confirm_entry = tk.Entry(dialog, show="*", font=("微软雅黑", 10), bg=ENTRY_BG, relief=tk.FLAT, width=20)
confirm_entry.pack(ipady=3)
def do_change():
old = old_entry.get()
if not verify_password(old):
messagebox.showerror("错误", "原密码错误")
return
new_pwd = new_entry.get()
if len(new_pwd) < 4:
messagebox.showerror("错误", "新密码长度至少4位")
return
if new_pwd != confirm_entry.get():
messagebox.showerror("错误", "两次输入的新密码不一致")
return
change_password(new_pwd)
messagebox.showinfo("成功", "密码已修改,下次登录生效")
dialog.destroy()
btn_confirm = tk.Button(dialog, text="确认修改", command=do_change, bg=BTN_BG, fg=BTN_FG,
font=("微软雅黑", 10), relief=tk.FLAT, padx=15, pady=4)
btn_confirm.pack(pady=20)
def logout(self):
if messagebox.askyesno("退出", "确定退出登录吗?"):
self.main_frame.pack_forget()
self.show_login()
self.pwd_entry.delete(0, tk.END)
self.login_error.config(text="")
def acquire_single_instance():
global _single_lock_socket
try:
_single_lock_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
_single_lock_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
_single_lock_socket.bind(('127.0.0.1', LOCK_PORT))
_single_lock_socket.listen(1)
return True
except socket.error:
return False
def release_single_instance():
global _single_lock_socket
if _single_lock_socket:
try:
_single_lock_socket.close()
except:
pass
atexit.register(release_single_instance)
if __name__ == "__main__":
init_db()
root = tk.Tk()
root.withdraw()
if not acquire_single_instance():
messagebox.showerror("启动失败", "程序已经在运行中,请不要重复打开!")
root.destroy()
sys.exit(0)
root.deiconify()
app = FinanceApp(root)
root.mainloop()