专栏导读
🌸 欢迎来到Python办公自动化专栏—Python处理办公问题,解放您的双手
🏳️🌈 博客主页:请点击——> 一晌小贪欢的博客主页求关注
👍 该系列文章专栏:请点击——>Python办公自动化专栏求订阅
🕷 此外还有爬虫专栏:请点击——>Python爬虫基础专栏求订阅
📕 此外还有python基础专栏:请点击——>Python基础学习专栏求订阅
文章作者技术和水平有限,如果文中出现错误,希望大家能指正🙏
❤️ 欢迎各位佬关注! ❤️
前言
-
一个基于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爬虫基础专栏
-
此外还有Python基础专栏,欢迎大家订阅:Python基础学习专栏