基于Python开发一个日历记账小工具
作者:小庄-Python办公
这篇文章主要为大家详细介绍了一个基于Python和tkinter开发的简洁实用的日历记账工具,可以帮助您轻松管理日常收支记录,感兴趣的小伙伴可以了解一下
前言
一个基于Python和tkinter开发的简洁实用的日历记账工具,帮助您轻松管理日常收支记录。
功能特点
日历界面
- 直观的月历显示
- 不同颜色标识收支状态:
- 绿色:当日收入大于支出
- 红色:当日支出大于收入
- 黄色:当日收支平衡
- 灰色:无记录
记账功能
- 添加收入记录
- 添加支出记录
- 为每笔记录添加详细描述
- 查看和删除当日记录
统计功能
- 实时显示总收入、总支出和净收入
- 底部统计栏一目了然
导出功能
- 支持选择日期范围导出
- 导出为CSV格式,便于进一步分析
- 包含完整的记录信息和时间戳
安装和运行
系统要求
Python 3.6 或更高版本
tkinter(通常随Python一起安装)
运行步骤
确保已安装Python
下载项目文件
在项目目录中运行:
python calendar_accounting.py
使用说明
基本操作
选择日期:击日历上的任意日期
添加记录:
- 输入金额(必须为正数)
- 输入描述信息
- 点击"添加收入"或"添加支出"
查看记录:选择日期后,当日记录会显示在右侧列表中
删除记录:选中列表中的记录,点击"删除选中记录"
导航操作
使用"◀"和"▶"按钮切换月份
日历会自动更新显示当前月份的收支状态
导出数据
- 设置导出的开始和结束日期
- 点击"导出CSV"按钮
- 选择保存位置和文件名
- 数据将以CSV格式保存,可用Excel等软件打开
数据存储
所有记账数据保存在accounting_data.json文件中
数据格式为JSON,便于备份和迁移
程序启动时自动加载历史数据
完整代码
import tkinter as tk from tkinter import ttk, messagebox, filedialog import calendar import json import os from datetime import datetime, date from collections import defaultdict class CalendarAccountingApp: def __init__(self, root): self.root = root self.root.title("💰 日历记账小工具") self.root.geometry("1200x800") self.root.resizable(True, True) # 设置现代化主题色彩 self.colors = { 'primary': '#2E86AB', # 主色调 - 蓝色 'secondary': '#A23B72', # 次要色 - 紫红色 'success': '#F18F01', # 成功色 - 橙色 'danger': '#C73E1D', # 危险色 - 红色 'light': '#F5F5F5', # 浅色背景 'dark': '#2C3E50', # 深色文字 'income': '#27AE60', # 收入色 - 绿色 'expense': '#E74C3C', # 支出色 - 红色 'balance': '#F39C12', # 平衡色 - 橙色 'card_bg': '#FFFFFF', # 卡片背景 'border': '#E0E0E0' # 边框色 } # 设置根窗口样式 self.root.configure(bg=self.colors['light']) # 配置ttk样式 self.setup_styles() # 数据存储文件 self.data_file = "accounting_data.json" self.load_data() # 当前显示的年月 self.current_year = datetime.now().year self.current_month = datetime.now().month # 选中的日期 self.selected_date = None self.create_widgets() self.update_calendar() self.update_statistics() def setup_styles(self): """设置现代化样式主题""" style = ttk.Style() # 配置LabelFrame样式 style.configure('Card.TLabelframe', background=self.colors['card_bg'], borderwidth=1, relief='solid') style.configure('Card.TLabelframe.Label', background=self.colors['card_bg'], foreground=self.colors['primary'], font=('Microsoft YaHei UI', 11, 'bold')) # 配置按钮样式 style.configure('Primary.TButton', background=self.colors['primary'], foreground='white', font=('Microsoft YaHei UI', 9, 'bold'), padding=(10, 5)) style.configure('Success.TButton', background=self.colors['income'], foreground='white', font=('Microsoft YaHei UI', 9, 'bold'), padding=(10, 5)) style.configure('Danger.TButton', background=self.colors['expense'], foreground='white', font=('Microsoft YaHei UI', 9, 'bold'), padding=(10, 5)) # 配置Entry样式 style.configure('Modern.TEntry', fieldbackground='white', borderwidth=1, relief='solid', padding=(8, 5)) # 配置Treeview样式 style.configure('Modern.Treeview', background='white', foreground=self.colors['dark'], rowheight=25, fieldbackground='white') style.configure('Modern.Treeview.Heading', background=self.colors['primary'], foreground='white', font=('Microsoft YaHei UI', 9, 'bold')) # 配置Label样式 style.configure('Title.TLabel', background=self.colors['card_bg'], foreground=self.colors['dark'], font=('Microsoft YaHei UI', 10, 'bold')) style.configure('Stats.TLabel', background=self.colors['card_bg'], foreground=self.colors['primary'], font=('Microsoft YaHei UI', 12, 'bold')) def load_data(self): """加载记账数据""" if os.path.exists(self.data_file): try: with open(self.data_file, 'r', encoding='utf-8') as f: self.data = json.load(f) except: self.data = {} else: self.data = {} def save_data(self): """保存记账数据""" with open(self.data_file, 'w', encoding='utf-8') as f: json.dump(self.data, f, ensure_ascii=False, indent=2) def create_widgets(self): """创建界面组件""" # 主框架 main_frame = tk.Frame(self.root, bg=self.colors['light'], padx=20, pady=20) main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) # 配置网格权重 self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) main_frame.columnconfigure(1, weight=1) main_frame.rowconfigure(1, weight=1) # 左侧日历区域 calendar_frame = ttk.LabelFrame(main_frame, text="📅 日历视图", padding="15", style='Card.TLabelframe') calendar_frame.grid(row=0, column=0, rowspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 15)) # 年月选择 date_frame = tk.Frame(calendar_frame, bg=self.colors['card_bg']) date_frame.grid(row=0, column=0, columnspan=7, pady=(0, 15)) prev_btn = tk.Button(date_frame, text="◀", command=self.prev_month, bg=self.colors['primary'], fg='white', font=('Microsoft YaHei UI', 12, 'bold'), relief='flat', padx=15, pady=5, cursor='hand2') prev_btn.grid(row=0, column=0, padx=(0, 10)) self.month_label = tk.Label(date_frame, text="", font=('Microsoft YaHei UI', 14, 'bold'), bg=self.colors['card_bg'], fg=self.colors['dark']) self.month_label.grid(row=0, column=1, padx=20) next_btn = tk.Button(date_frame, text="▶", command=self.next_month, bg=self.colors['primary'], fg='white', font=('Microsoft YaHei UI', 12, 'bold'), relief='flat', padx=15, pady=5, cursor='hand2') next_btn.grid(row=0, column=2, padx=(10, 0)) # 日历网格 self.calendar_buttons = {} # 星期标题 weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] for i, day in enumerate(weekdays): label = tk.Label(calendar_frame, text=day, font=('Microsoft YaHei UI', 10, 'bold'), bg=self.colors['secondary'], fg='white', pady=8) label.grid(row=1, column=i, padx=2, pady=2, sticky=(tk.W, tk.E)) # 日期按钮 for week in range(6): for day in range(7): btn = tk.Button(calendar_frame, text="", width=6, height=2, font=('Microsoft YaHei UI', 10, 'bold'), relief='flat', cursor='hand2', command=lambda w=week, d=day: self.select_date(w, d)) btn.grid(row=week+2, column=day, padx=2, pady=2, sticky=(tk.W, tk.E)) self.calendar_buttons[(week, day)] = btn # 右侧操作区域 right_frame = tk.Frame(main_frame, bg=self.colors['light']) right_frame.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S)) right_frame.columnconfigure(0, weight=1) # 记账操作区 account_frame = ttk.LabelFrame(right_frame, text="💳 记账操作", padding="15", style='Card.TLabelframe') account_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 15)) account_frame.columnconfigure(1, weight=1) # 选中日期显示 ttk.Label(account_frame, text="📅 选中日期:", style='Title.TLabel').grid(row=0, column=0, sticky=tk.W) self.selected_date_label = tk.Label(account_frame, text="请选择日期", fg=self.colors['primary'], bg=self.colors['card_bg'], font=('Microsoft YaHei UI', 10, 'bold')) self.selected_date_label.grid(row=0, column=1, sticky=tk.W, padx=(10, 0)) # 金额输入 ttk.Label(account_frame, text="💰 金额:", style='Title.TLabel').grid(row=1, column=0, sticky=tk.W, pady=(15, 0)) self.amount_var = tk.StringVar() amount_entry = ttk.Entry(account_frame, textvariable=self.amount_var, width=20, style='Modern.TEntry') amount_entry.grid(row=1, column=1, sticky=tk.W, padx=(10, 0), pady=(15, 0)) # 描述输入 ttk.Label(account_frame, text="📝 描述:", style='Title.TLabel').grid(row=2, column=0, sticky=tk.W, pady=(15, 0)) self.desc_var = tk.StringVar() desc_entry = ttk.Entry(account_frame, textvariable=self.desc_var, width=35, style='Modern.TEntry') desc_entry.grid(row=2, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(15, 0)) # 按钮 btn_frame = tk.Frame(account_frame, bg=self.colors['card_bg']) btn_frame.grid(row=3, column=0, columnspan=2, pady=(20, 0)) income_btn = tk.Button(btn_frame, text="💚 添加收入", command=lambda: self.add_record('income'), bg=self.colors['income'], fg='white', font=('Microsoft YaHei UI', 10, 'bold'), relief='flat', padx=20, pady=8, cursor='hand2') income_btn.grid(row=0, column=0, padx=(0, 15)) expense_btn = tk.Button(btn_frame, text="❤️ 添加支出", command=lambda: self.add_record('expense'), bg=self.colors['expense'], fg='white', font=('Microsoft YaHei UI', 10, 'bold'), relief='flat', padx=20, pady=8, cursor='hand2') expense_btn.grid(row=0, column=1) # 当日记录显示区 records_frame = ttk.LabelFrame(right_frame, text="📋 当日记录", padding="15", style='Card.TLabelframe') records_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 15)) records_frame.columnconfigure(0, weight=1) records_frame.rowconfigure(0, weight=1) # 记录列表 self.records_tree = ttk.Treeview(records_frame, columns=('type', 'amount', 'desc'), show='headings', height=8, style='Modern.Treeview') self.records_tree.heading('type', text='💼 类型') self.records_tree.heading('amount', text='💰 金额') self.records_tree.heading('desc', text='📝 描述') self.records_tree.column('type', width=80) self.records_tree.column('amount', width=100) self.records_tree.column('desc', width=250) scrollbar = ttk.Scrollbar(records_frame, orient=tk.VERTICAL, command=self.records_tree.yview) self.records_tree.configure(yscrollcommand=scrollbar.set) self.records_tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) # 删除按钮 delete_btn = tk.Button(records_frame, text="🗑️ 删除选中记录", command=self.delete_record, bg=self.colors['danger'], fg='white', font=('Microsoft YaHei UI', 9, 'bold'), relief='flat', padx=15, pady=5, cursor='hand2') delete_btn.grid(row=1, column=0, pady=(15, 0)) # 底部统计和导出区 bottom_frame = tk.Frame(main_frame, bg=self.colors['light']) bottom_frame.grid(row=1, column=1, sticky=(tk.W, tk.E)) bottom_frame.columnconfigure(0, weight=1) # 统计信息 stats_frame = ttk.LabelFrame(bottom_frame, text="📊 统计信息", padding="15", style='Card.TLabelframe') stats_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 15)) self.stats_label = tk.Label(stats_frame, text="总收入: ¥0.00 | 总支出: ¥0.00 | 净收入: ¥0.00", font=('Microsoft YaHei UI', 12, 'bold'), bg=self.colors['card_bg'], fg=self.colors['primary']) self.stats_label.grid(row=0, column=0) # 导出功能 export_frame = ttk.LabelFrame(bottom_frame, text="📤 导出功能", padding="15", style='Card.TLabelframe') export_frame.grid(row=1, column=0, sticky=(tk.W, tk.E)) ttk.Label(export_frame, text="📅 导出范围:", style='Title.TLabel').grid(row=0, column=0, sticky=tk.W) date_range_frame = tk.Frame(export_frame, bg=self.colors['card_bg']) date_range_frame.grid(row=0, column=1, sticky=tk.W, padx=(15, 0)) self.start_date_var = tk.StringVar(value=f"{self.current_year}-{self.current_month:02d}-01") ttk.Entry(date_range_frame, textvariable=self.start_date_var, width=12, style='Modern.TEntry').grid(row=0, column=0) tk.Label(date_range_frame, text=" 至 ", bg=self.colors['card_bg'], fg=self.colors['dark'], font=('Microsoft YaHei UI', 10)).grid(row=0, column=1, padx=10) last_day = calendar.monthrange(self.current_year, self.current_month)[1] self.end_date_var = tk.StringVar(value=f"{self.current_year}-{self.current_month:02d}-{last_day:02d}") ttk.Entry(date_range_frame, textvariable=self.end_date_var, width=12, style='Modern.TEntry').grid(row=0, column=2) export_btn = tk.Button(export_frame, text="📊 导出CSV", command=self.export_csv, bg=self.colors['success'], fg='white', font=('Microsoft YaHei UI', 10, 'bold'), relief='flat', padx=20, pady=5, cursor='hand2') export_btn.grid(row=0, column=2, padx=(25, 0)) def prev_month(self): """上一个月""" if self.current_month == 1: self.current_month = 12 self.current_year -= 1 else: self.current_month -= 1 self.update_calendar() self.update_date_range() def next_month(self): """下一个月""" if self.current_month == 12: self.current_month = 1 self.current_year += 1 else: self.current_month += 1 self.update_calendar() self.update_date_range() def update_date_range(self): """更新导出日期范围""" self.start_date_var.set(f"{self.current_year}-{self.current_month:02d}-01") last_day = calendar.monthrange(self.current_year, self.current_month)[1] self.end_date_var.set(f"{self.current_year}-{self.current_month:02d}-{last_day:02d}") def update_calendar(self): """更新日历显示""" # 更新月份标签 self.month_label.config(text=f"📅 {self.current_year}年{self.current_month}月") # 获取月历 cal = calendar.monthcalendar(self.current_year, self.current_month) # 清空所有按钮 for btn in self.calendar_buttons.values(): btn.config(text="", state=tk.DISABLED, bg="SystemButtonFace") # 填充日期 for week_num, week in enumerate(cal): for day_num, day in enumerate(week): if day == 0: continue btn = self.calendar_buttons[(week_num, day_num)] btn.config(text=str(day), state=tk.NORMAL) # 检查是否有记录 date_str = f"{self.current_year}-{self.current_month:02d}-{day:02d}" if date_str in self.data: # 计算当日收支 daily_income = sum(record['amount'] for record in self.data[date_str] if record['type'] == 'income') daily_expense = sum(record['amount'] for record in self.data[date_str] if record['type'] == 'expense') if daily_income > daily_expense: btn.config(bg=self.colors['income'], fg='white', font=('Microsoft YaHei UI', 10, 'bold')) # 收入大于支出 elif daily_expense > daily_income: btn.config(bg=self.colors['expense'], fg='white', font=('Microsoft YaHei UI', 10, 'bold')) # 支出大于收入 else: btn.config(bg=self.colors['balance'], fg='white', font=('Microsoft YaHei UI', 10, 'bold')) # 收支平衡 else: btn.config(bg='white', fg=self.colors['dark'], font=('Microsoft YaHei UI', 10), relief='solid', bd=1, activebackground=self.colors['light']) def select_date(self, week, day): """选择日期""" btn = self.calendar_buttons[(week, day)] if btn['text'] == "": return day_num = int(btn['text']) self.selected_date = f"{self.current_year}-{self.current_month:02d}-{day_num:02d}" self.selected_date_label.config(text=self.selected_date) # 更新当日记录显示 self.update_daily_records() def update_daily_records(self): """更新当日记录显示""" # 清空列表 for item in self.records_tree.get_children(): self.records_tree.delete(item) if not self.selected_date or self.selected_date not in self.data: return # 添加记录 for record in self.data[self.selected_date]: type_text = "收入" if record['type'] == 'income' else "支出" amount_text = f"¥{record['amount']:.2f}" self.records_tree.insert('', tk.END, values=(type_text, amount_text, record['description'])) def add_record(self, record_type): """添加记录""" if not self.selected_date: messagebox.showwarning("警告", "请先选择日期") return try: amount = float(self.amount_var.get()) if amount <= 0: raise ValueError() except ValueError: messagebox.showerror("错误", "请输入有效的金额") return description = self.desc_var.get().strip() if not description: messagebox.showwarning("警告", "请输入描述") return # 添加记录 if self.selected_date not in self.data: self.data[self.selected_date] = [] record = { 'type': record_type, 'amount': amount, 'description': description, 'timestamp': datetime.now().isoformat() } self.data[self.selected_date].append(record) self.save_data() # 清空输入 self.amount_var.set("") self.desc_var.set("") # 更新显示 self.update_calendar() self.update_daily_records() self.update_statistics() messagebox.showinfo("成功", f"已添加{('收入' if record_type == 'income' else '支出')}记录") def delete_record(self): """删除选中的记录""" selection = self.records_tree.selection() if not selection: messagebox.showwarning("警告", "请选择要删除的记录") return if messagebox.askyesno("确认", "确定要删除选中的记录吗?"): # 获取选中项的索引 item = selection[0] index = self.records_tree.index(item) # 删除数据 if self.selected_date in self.data and index < len(self.data[self.selected_date]): del self.data[self.selected_date][index] # 如果该日期没有记录了,删除日期键 if not self.data[self.selected_date]: del self.data[self.selected_date] self.save_data() # 更新显示 self.update_calendar() self.update_daily_records() self.update_statistics() messagebox.showinfo("成功", "记录已删除") def update_statistics(self): """更新统计信息""" total_income = 0 total_expense = 0 for date_records in self.data.values(): for record in date_records: if record['type'] == 'income': total_income += record['amount'] else: total_expense += record['amount'] net_income = total_income - total_expense # 根据净收入设置颜色 if net_income > 0: color = self.colors['income'] icon = "📈" elif net_income < 0: color = self.colors['expense'] icon = "📉" else: color = self.colors['balance'] icon = "⚖️" stats_text = f"💰 总收入: ¥{total_income:.2f} | 💸 总支出: ¥{total_expense:.2f} | {icon} 净收入: ¥{net_income:.2f}" self.stats_label.config(text=stats_text, fg=color) def export_csv(self): """导出CSV文件""" try: start_date = datetime.strptime(self.start_date_var.get(), "%Y-%m-%d").date() end_date = datetime.strptime(self.end_date_var.get(), "%Y-%m-%d").date() except ValueError: messagebox.showerror("错误", "日期格式不正确,请使用YYYY-MM-DD格式") return if start_date > end_date: messagebox.showerror("错误", "开始日期不能大于结束日期") return # 选择保存文件 filename = filedialog.asksaveasfilename( defaultextension=".csv", filetypes=[("CSV文件", "*.csv"), ("所有文件", "*.*")], title="保存导出文件" ) if not filename: return try: import csv with open(filename, 'w', newline='', encoding='utf-8-sig') as csvfile: writer = csv.writer(csvfile) writer.writerow(['日期', '类型', '金额', '描述', '时间戳']) # 按日期排序导出 current_date = start_date while current_date <= end_date: date_str = current_date.strftime("%Y-%m-%d") if date_str in self.data: for record in self.data[date_str]: type_text = "收入" if record['type'] == 'income' else "支出" writer.writerow([ date_str, type_text, f"{record['amount']:.2f}", record['description'], record.get('timestamp', '') ]) # 下一天 from datetime import timedelta current_date += timedelta(days=1) messagebox.showinfo("成功", f"数据已导出到: {filename}") except Exception as e: messagebox.showerror("错误", f"导出失败: {str(e)}") def main(): root = tk.Tk() app = CalendarAccountingApp(root) root.mainloop() if __name__ == "__main__": main()
效果图
文件结构
日历记账小工具/
├── calendar_accounting.py # 主程序文件
└── accounting_data.json # 数据文件(运行后自动生成)
技术特点
使用Python标准库开发,无需安装额外依赖
基于tkinter构建GUI界面,跨平台兼容
JSON格式数据存储,轻量且易于维护
响应式界面设计,支持窗口大小调整
注意事项
请确保输入的金额为有效数字
建议定期备份accounting_data.json文件
导出的CSV文件使用UTF-8编码,确保中文正常显示
到此这篇关于基于Python开发一个日历记账小工具的文章就介绍到这了,更多相关Python日历记账内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!