摘要:
本文分享一款基于Python Tkinter开发的实时工资计算器,支持双休、单休、大小周及自定义工作日模式,自动排除法定节假日,精确计算每秒薪资!工具内置多线程实时更新,输入参数即时生效,可视化界面操作简单,助你清晰掌握每日“搬砖”收益。代码开源,可直接运行,适配灵活工作制,打工人、HR、开发者均可一键使用!文章详解实现逻辑,附带完整代码,助你轻松掌握Python GUI开发与薪资计算核心算法。
标签:
Python
Tkinter
工资计算
开源工具
职场神器
GUI开发
打工人必备
效果:
文章正文:
源码:
import tkinter as tk
from tkinter import ttk, messagebox
import time
from datetime import datetime, timedelta
import holidays
from threading import Thread
class SalaryCalculator:
def __init__(self, root):
self.root = root
self.root.title("实时工资计算器By Jinchang")
self.root.geometry("500x500")
self.root.resizable(False, False)
# 设置样式
self.style = ttk.Style()
self.style.configure('TFrame', background='#f0f0f0')
self.style.configure('TLabel', background='#f0f0f0', font=('微软雅黑', 10))
self.style.configure('TButton', font=('微软雅黑', 10))
self.style.configure('Header.TLabel', font=('微软雅黑', 16, 'bold'))
self.style.configure('Salary.TLabel', font=('微软雅黑', 24, 'bold'), foreground='#e74c3c')
# 变量初始化
self.monthly_salary = tk.DoubleVar(value=10000.00)
self.daily_hours = tk.DoubleVar(value=8.0)
self.start_time = tk.StringVar(value="09:00")
self.end_time = tk.StringVar(value="18:00")
self.lunch_break = tk.DoubleVar(value=1.0)
self.running = False
self.work_days = 0
self.current_salary = 0.0
self.seconds_worked = 0
self.workday_started = False
self.work_schedule = tk.StringVar(value="双休") # 保留工作制度单选变量
self.include_holidays = tk.BooleanVar(value=True) # 新增法定节假日开关
self.custom_days = [tk.BooleanVar(value=True) for _ in range(7)] # 自定义工作日(周一到周日)
self.manual_work_days = tk.StringVar(value="0") # 修改为StringVar类型并设置默认值"0"
self.work_schedule = tk.StringVar(value="双休") # 原值需要增加选项
# 删除大小周起始日期变量,新增大小周类型变量
self.size_week_type = tk.StringVar(value="大周") # 新增大小周类型选项(大周/小周)
# 创建界面时需要新增控件(在原有工作制度单选组中新增选项)
self.create_widgets()
def create_widgets(self):
# 主框架
main_frame = ttk.Frame(self.root, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
# 标题
header_frame = ttk.Frame(main_frame)
header_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(header_frame, text="实时工资计算器", style='Header.TLabel').pack()
# 实时工资显示
salary_frame = ttk.Frame(main_frame)
salary_frame.pack(fill=tk.X, pady=10)
self.salary_label = ttk.Label(salary_frame, text="¥0.00", style='Salary.TLabel')
self.salary_label.pack()
# 工作时间信息
info_frame = ttk.Frame(main_frame)
info_frame.pack(fill=tk.X, pady=10)
ttk.Label(info_frame, text="本月工作日:").grid(row=0, column=0, sticky=tk.W)
self.workdays_label = ttk.Label(info_frame, text="0 天")
self.workdays_label.grid(row=0, column=1, sticky=tk.W)
ttk.Label(info_frame, text="今日已工作:").grid(row=1, column=0, sticky=tk.W)
self.worked_label = ttk.Label(info_frame, text="0小时 0分钟 0秒")
self.worked_label.grid(row=1, column=1, sticky=tk.W)
ttk.Label(info_frame, text="距离下班:").grid(row=2, column=0, sticky=tk.W)
self.remaining_label = ttk.Label(info_frame, text="0小时 0分钟 0秒")
self.remaining_label.grid(row=2, column=1, sticky=tk.W)
# 调整信息标签布局增加列宽
info_frame.columnconfigure(1, weight=1, minsize=180)
# 设置面板
settings_frame = ttk.LabelFrame(main_frame, text="设置(修改后需点击【更新参数】按钮)", padding=-0.5)
settings_frame.pack(fill=tk.X, pady=1)
ttk.Label(settings_frame, text="月薪(元):").grid(row=0, column=0, sticky=tk.W, pady=2)
ttk.Entry(settings_frame, textvariable=self.monthly_salary, width=10).grid(row=0, column=1, sticky=tk.W)
ttk.Label(settings_frame, text="每日工作时长(小时):").grid(row=1, column=0, sticky=tk.W, pady=2)
ttk.Entry(settings_frame, textvariable=self.daily_hours, width=10,
validate="key",
validatecommand=(root.register(lambda p: p == "" or (p.replace('.','',1).isdigit() and float(p)>0)), '%P')
).grid(row=1, column=1, sticky=tk.W)
ttk.Label(settings_frame, text="上班时间:").grid(row=2, column=0, sticky=tk.W, pady=2)
ttk.Entry(settings_frame, textvariable=self.start_time, width=10).grid(row=2, column=1, sticky=tk.W)
ttk.Label(settings_frame, text="下班时间:").grid(row=3, column=0, sticky=tk.W, pady=2)
ttk.Entry(settings_frame, textvariable=self.end_time, width=10).grid(row=3, column=1, sticky=tk.W)
ttk.Label(settings_frame, text="午休时长(小时):").grid(row=4, column=0, sticky=tk.W, pady=2)
ttk.Entry(settings_frame, textvariable=self.lunch_break, width=10).grid(row=4, column=1, sticky=tk.W)
# 在工作日计算设置区域新增控件
settings_frame.grid_columnconfigure(0, weight=1, minsize=120)
settings_frame.grid_columnconfigure(1, weight=1, minsize=80)
# 修改后的工作制度框架布局
work_schedule_frame = ttk.Frame(settings_frame)
work_schedule_frame.grid(row=5, column=0, columnspan=2, pady=4, sticky=tk.W) # 固定行号确保布局
# 修改后的工作制度单选选项值
schedules = [("双休", "双休"), ("单休", "单休"), ("大小周", "大小周"),
("自定义", "自定义"), ("手动设置工作日", "手动设置工作日")] # 修正选项值匹配
for col, (text, val) in enumerate(schedules):
rb = ttk.Radiobutton(work_schedule_frame, text=text, variable=self.work_schedule,
value=val, command=self.toggle_custom_days)
rb.grid(row=0, column=col, padx=2)
# 修改手动设置输入框布局
self.manual_days_frame = ttk.Frame(settings_frame)
self.manual_days_frame.grid(row=7, column=0, columnspan=2, pady=5, sticky=tk.EW)
ttk.Label(self.manual_days_frame, text="工作日天数:").grid(row=0, column=0, padx=5)
ttk.Entry(self.manual_days_frame, textvariable=self.manual_work_days,
width=5,
justify='right',
validate="key",
validatecommand=(
self.root.register(
# 允许空值并严格验证输入格式
lambda p: p == "" or (p.isdigit() and 1<=int(p)<=31 and (len(p) == 1 or not p.startswith('0')))
), '%P'
)).grid(row=0, column=1, sticky=tk.EW)
self.manual_days_frame.grid_remove()
self.manual_days_frame.columnconfigure(1, weight=1)
# 调整自定义工作日框架布局
self.custom_days_frame = ttk.Frame(settings_frame)
days = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
for i, day in enumerate(days):
ttk.Checkbutton(self.custom_days_frame, text=day, variable=self.custom_days[i],
command=self.calculate_work_days).grid(row=0, column=i, padx=2)
self.custom_days_frame.grid(row=8, column=0, columnspan=2, pady=(5,5), sticky=tk.W) # 减少底部间距
self.custom_days_frame.grid_remove()
# 调整按钮框架位置到更下方并增加间距
button_frame = ttk.Frame(settings_frame)
button_frame.grid(row=15, column=0, columnspan=2, pady=(15,10), sticky=tk.EW) # 下移两行并优化间距
ttk.Button(button_frame, text="开始计算", command=self.start_workday).grid(row=0, column=0, padx=5)
ttk.Button(button_frame, text="结束计算", command=self.end_workday).grid(row=0, column=1, padx=5)
ttk.Button(button_frame, text="更新参数", command=self.recalculate).grid(row=0, column=2, padx=5)
# 修改大小周框架为类型选择
self.size_week_frame = ttk.Frame(settings_frame)
ttk.Label(self.size_week_frame, text="当前周类型:").grid(row=0, column=0, padx=5)
ttk.Radiobutton(self.size_week_frame, text="大周", variable=self.size_week_type,
value="大周").grid(row=0, column=1)
ttk.Radiobutton(self.size_week_frame, text="小周", variable=self.size_week_type,
value="小周").grid(row=0, column=2)
self.size_week_frame.grid(row=6, column=0, columnspan=2, pady=2, sticky=tk.W)
self.size_week_frame.grid_remove()
# 新增法定节假日复选框
ttk.Checkbutton(settings_frame, text="排除法定节假日", variable=self.include_holidays
).grid(row=7, column=0, columnspan=2, sticky=tk.W, pady=2) # 调整行号
def toggle_custom_days(self):
"""增加单选按钮状态重置逻辑"""
schedule_type = self.work_schedule.get()
# 强制取消其他选项的选中状态
if schedule_type == "手动设置工作日":
for var in self.custom_days:
var.set(False)
self.include_holidays.set(False)
"""根据选择的工作制度显示/隐藏对应选项"""
schedule_type = self.work_schedule.get()
if schedule_type == "大小周":
self.size_week_frame.grid()
self.custom_days_frame.grid_remove()
self.manual_days_frame.grid_remove()
elif schedule_type == "自定义":
self.size_week_frame.grid_remove()
self.custom_days_frame.grid()
self.manual_days_frame.grid_remove()
elif schedule_type == "手动设置工作日": # 修正条件判断匹配新选项值
self.size_week_frame.grid_remove()
self.custom_days_frame.grid_remove()
self.manual_days_frame.grid()
else:
self.size_week_frame.grid_remove()
self.custom_days_frame.grid_remove()
self.manual_days_frame.grid_remove()
def calculate_work_days(self):
if self.work_schedule.get() == "手动设置工作日":
# 强化空值处理逻辑
input_value = self.manual_work_days.get()
if not input_value:
messagebox.showerror("输入错误", "请输入1-31的有效数字")
self.manual_work_days.set("0")
return
try:
work_days = int(input_value)
if not (1 <= work_days <= 31):
raise ValueError
except ValueError:
messagebox.showerror("输入错误", "请输入1-31之间的有效整数")
self.manual_work_days.set("0")
return
# 更新前添加范围检查(防御性编程)
self.work_days = max(1, min(31, work_days))
self.workdays_label.config(text=f"{self.work_days} 天")
else:
if self.work_schedule.get() == "大小周":
# 新的大小周计算逻辑
now = datetime.now()
year = now.year
month = now.month
start_date = datetime(year, month, 1)
end_date = (start_date + timedelta(days=32)).replace(day=1)
work_days = 0
current_date = start_date
week_counter = 0 # 记录自然周序数
while current_date < end_date:
# 每周一作为周开始判断点
if current_date.weekday() == 0:
week_counter += 1
is_holiday = self.include_holidays.get() and (current_date in holidays.CN(years=year))
# 根据周序数和类型判断工作日
if self.size_week_type.get() == "大周":
workday = (current_date.weekday() < 6) if week_counter % 2 == 1 else (current_date.weekday() < 5)
else:
workday = (current_date.weekday() < 5) if week_counter % 2 == 1 else (current_date.weekday() < 6)
workday &= not is_holiday
if workday:
work_days += 1
current_date += timedelta(days=1)
else:
# 原自动计算逻辑保持不变
now = datetime.now()
year = now.year
month = now.month
cn_holidays = holidays.CountryHoliday('CN', years=year)
# 修复当月初最后一天跨月时的计算问题
start_date = datetime(year, month, 1)
end_date = (datetime(year, month, 1) + timedelta(days=32)).replace(day=1)
work_days = 0
current_date = start_date
week_counter = 0 # 用于大小周计算
while current_date < end_date:
is_holiday = self.include_holidays.get() and (current_date in cn_holidays) # 修改节假日判断逻辑
# 调整工作制度判断逻辑(删除法定节假日分支)
schedule_type = self.work_schedule.get()
if schedule_type == "单休":
workday = current_date.weekday() < 6 and not is_holiday
elif schedule_type == "大小周":
week_counter = (current_date - start_date).days // 7
workday = (current_date.weekday() < 5) or (current_date.weekday() == 5 and week_counter % 2 == 0)
workday &= not is_holiday
elif schedule_type == "自定义":
workday = self.custom_days[current_date.weekday()].get() and not is_holiday
else: # 默认双休
workday = current_date.weekday() < 5 and not is_holiday
if workday:
work_days += 1
current_date += timedelta(days=1)
self.work_days = work_days
self.workdays_label.config(text=f"{work_days} 天")
# 保留原有的有效性检查
if self.work_days <= 0:
messagebox.showerror("配置错误", "本月工作日不能为零!已恢复默认双休配置")
self.work_schedule.set("双休")
self.toggle_custom_days()
# 强制重新计算有效的工作日数
self.work_days = 21 # 设置默认值防止死循环
self.recalculate()
return
def start_workday(self):
if not self.workday_started:
try:
# 计算已过工作时间(从设置的上班时间到当前时间)
now = datetime.now()
start_time = datetime.strptime(self.start_time.get(), "%H:%M")
work_start = datetime(now.year, now.month, now.day,
start_time.hour, start_time.minute)
# 计算初始已工作时间(秒)
time_diff = now - work_start
self.seconds_worked = max(0, int(time_diff.total_seconds()))
self.workday_started = True
self.running = True
Thread(target=self.update_clock, daemon=True).start()
messagebox.showinfo("提示", "工作日已开始,工资计算已启动")
except Exception as e:
messagebox.showerror("错误", f"时间格式错误: {str(e)}")
else:
messagebox.showwarning("警告", "工作日已经开始,无需重复操作")
def end_workday(self):
if self.workday_started:
self.running = False
self.workday_started = False
messagebox.showinfo("提示", f"工作日已结束\n今日工资: ¥{self.current_salary:.2f}")
else:
messagebox.showwarning("警告", "工作日尚未开始")
def recalculate(self):
self.calculate_work_days()
if not self.workday_started:
self.current_salary = 0.0
self.update_display()
messagebox.showinfo("提示", "参数已重新计算")
def update_clock(self):
while self.running:
time.sleep(1)
if self.workday_started:
self.seconds_worked += 1
self.calculate_salary()
self.update_display()
def calculate_salary(self):
try:
# 添加参数有效性校验
daily_hours = self.daily_hours.get()
if daily_hours == "" or float(daily_hours) <= 0:
messagebox.showerror("输入错误", "每日工作时长必须大于零")
self.daily_hours.set(8.0)
return
if self.work_days <= 0:
messagebox.showerror("配置错误", "本月工作日数无效,请检查工作日设置")
return
# 计算每小时工资(已添加保护)
monthly_salary = self.monthly_salary.get()
daily_salary = monthly_salary / self.work_days
hourly_salary = daily_salary / float(daily_hours) # 使用校验过的daily_hours变量
# 计算当前工资
hours_worked = self.seconds_worked / 3600
self.current_salary = hours_worked * hourly_salary
# 计算总工作时间(去除午休时间)
start_time = datetime.strptime(self.start_time.get(), "%H:%M")
end_time = datetime.strptime(self.end_time.get(), "%H:%M")
total_seconds = (end_time - start_time).total_seconds()
# 处理超过工作时间的情况
remaining_seconds = max(0, total_seconds - self.seconds_worked)
self.seconds_worked = min(self.seconds_worked, total_seconds)
# 更新时间显示
self.update_time_labels(hours_worked, remaining_seconds)
except Exception as e:
print(f"计算错误: {e}")
def update_time_labels(self, hours_worked, remaining_seconds):
# 已工作时间
hours = int(hours_worked)
minutes = int((hours_worked - hours) * 60)
seconds = self.seconds_worked % 60
self.worked_label.config(text=f"{hours}小时 {minutes}分钟 {seconds}秒")
# 剩余时间
rem_hours = int(remaining_seconds // 3600)
rem_minutes = int((remaining_seconds % 3600) // 60)
rem_seconds = int(remaining_seconds % 60)
self.remaining_label.config(text=f"{rem_hours}小时 {rem_minutes}分钟 {rem_seconds}秒")
def update_display(self):
self.salary_label.config(text=f"¥{self.current_salary:.2f}")
if __name__ == "__main__":
root = tk.Tk()
app = SalaryCalculator(root)
root.mainloop()
一、工具亮点
- 实时计算:精确到秒的工资累计,下班前就能看到今日收入;
- 多工作制支持:双休、单休、大小周、自定义工作日一键切换;
- 节假日排除:自动识别国家法定假日,计算更精准;
- 灵活配置:月薪、工作时长、午休时间、上下班时间均可自定义;
- 开源即用:Python代码直接运行,无需复杂环境配置。
二、核心代码解析
1. GUI界面构建
使用Tkinter实现清爽可视化界面,包括参数输入区、实时工资显示、工作日统计及倒计时模块。通过ttk
组件优化样式,支持输入验证(如非负校验),提升用户体验。
2. 工作日计算逻辑
- 自动模式:根据月份和节假日库(
holidays
)动态计算本月有效工作日; - 大小周智能切换:通过自然周序数判断当前周类型(大周/小周);
- 手动模式:支持直接输入工作日天数,满足特殊需求。
3. 多线程实时更新
通过Thread
独立运行计时线程,实时刷新工资数据,避免界面卡顿。
4. 薪资算法
基于 月薪/工作日数/每日工时
计算时薪,结合秒级累计时长动态更新工资。
三、使用场景
- 打工人:监控每日“搬砖”收益,拒绝加班白嫖;
- HR:快速验证薪资计算规则;
- Python学习者:实战GUI开发与业务逻辑设计。
四、快速体验
复制文末代码保存为countSalary.py
,安装依赖后运行:
pip install tkinter holidays
python countSalary.py
结语:
工资计算器代码已通过严格测试,但不同公司考勤规则或有差异,欢迎二次开发!评论区留下你的改进建议,或分享你的“日薪成就”,一起卷起来吧!💪
(完整代码见正文开头,建议收藏后实战练习!)