import tkinter as tk
from tkinter import messagebox, simpledialog
import time
import threading
import os
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import openpyxl
from openpyxl.styles import Alignment
from openpyxl.utils import get_column_letter
from openpyxl.styles import Font, PatternFill
from tkinter import ttk
class FloatTimer:
def init(self):
self.categories = [“学英语”,
“看书”,
“运动”,
‘看文献’,
‘做实验’,
‘摄影/视频剪辑’,
“耍手机”,
“编程”,
“吃饭”,
“课程学习”,
“休息”,
‘冥想’,
‘练字’]
self.root = tk.Tk()
self.root.title(“悬浮计时器”)
self.root.overrideredirect(True)
self.root.attributes(‘-topmost’, True)
self.root.attributes(‘-alpha’, 0.9)
# 计时变量 self.elapsed_time = 0 self.running = False self.start_time = 0 self.last_save_time = 0 self.session_records = [] # 存储计时记录 self.current_segment_start = None # 当前计时段的开始时间 self.current_segment_end = None # 当前计时段的结束时间 # 创建UI self.time_label = tk.Label(self.root, text="00:00:00", font=("Arial", 20), bg='black', fg='white') self.time_label.pack(fill=tk.BOTH) btn_frame = tk.Frame(self.root, bg='black') btn_frame.pack(fill=tk.BOTH) self.start_btn = tk.Button(btn_frame, text="开始", command=self.toggle_start, width=5) self.start_btn.pack(side=tk.LEFT, padx=5, pady=5) reset_btn = tk.Button(btn_frame, text="重置", command=self.reset_timer) reset_btn.pack(side=tk.LEFT, padx=5, pady=5) save_btn = tk.Button(btn_frame, text="保存", command=self.save_current_session) save_btn.pack(side=tk.LEFT, padx=5, pady=5) close_btn = tk.Button(btn_frame, text="关闭", command=self.on_close) close_btn.pack(side=tk.RIGHT, padx=5, pady=5) # 绑定事件 self.time_label.bind("<ButtonPress-1>", self.start_drag) self.time_label.bind("<B1-Motion>", self.on_drag) # 设置位置 self.set_position_top_right() # 设置自定义保存路径 self.save_directory = r"C:\Users\Dell\Desktop\新建文件夹" os.makedirs(self.save_directory, exist_ok=True) # 启动自动保存线程 self.auto_save = True threading.Thread(target=self.auto_save_records, daemon=True).start() def set_position_top_right(self): """将窗口置于屏幕右上角""" self.root.update_idletasks() screen_width = self.root.winfo_screenwidth() window_width = self.root.winfo_reqwidth() x_pos = screen_width - window_width - 20 y_pos = 20 self.root.geometry(f"+{x_pos}+{y_pos}") def start_drag(self, event): self.x_start = event.x self.y_start = event.y def on_drag(self, event): x = self.root.winfo_x() + (event.x - self.x_start) y = self.root.winfo_y() + (event.y - self.y_start) self.root.geometry(f"+{x}+{y}") def toggle_start(self): if not self.running: # 开始计时 self.running = True self.start_btn.config(text="结束") self.start_time = time.time() - self.elapsed_time self.current_segment_start = datetime.now() # 启动计时线程 threading.Thread(target=self.update_timer, daemon=True).start() else: # 结束计时 self.running = False self.start_btn.config(text="开始") self.current_segment_end = datetime.now() # 记录结束时间并获取类型 self.record_session() # 重置当前计时段 self.current_segment_start = None self.current_segment_end = None def reset_timer(self): if self.running: # 如果正在运行,先结束当前计时 self.running = False self.start_btn.config(text="开始") self.current_segment_end = datetime.now() self.record_session() # 记录当前计时段 # 重置所有计时变量 self.elapsed_time = 0 self.time_label.config(text="00:00:00") self.start_btn.config(text="开始") self.current_segment_start = None self.current_segment_end = None def record_session(self): if not self.current_segment_start or not self.current_segment_end: return # 创建选择类型的对话框 dialog = tk.Toplevel(self.root) dialog.title("选择计时类型") dialog.attributes('-topmost', True) dialog.resizable(False, False) # 设置对话框大小 dialog_width = 300 dialog_height = 150 dialog.geometry(f"{dialog_width}x{dialog_height}") # === 新增:计算并设置对话框位置 === # 获取屏幕尺寸 screen_width = self.root.winfo_screenwidth() screen_height = self.root.winfo_screenheight() # 计算右上角位置(向右偏移10px,向下偏移10px) x_position = screen_width - dialog_width - 10 y_position = 10 # 应用位置设置 dialog.geometry(f"+{x_position}+{y_position}") # ============================== # 添加标签 label = tk.Label(dialog, text="请选择计时类型:", font=("Arial", 15)) label.pack(pady=10) # 使用Combobox下拉菜单 category_var = tk.StringVar() category_combo = ttk.Combobox( dialog, textvariable=category_var, values=self.categories, state="readonly", width=20, font=("Arial", 12) ) category_combo.pack(pady=5) category_combo.current(0) # 设置默认选择第一个选项 # 添加确定按钮 def on_ok(): dialog.destroy() category = category_var.get() if not category: category = "未分类" duration = (self.current_segment_end - self.current_segment_start).total_seconds() hours, remainder = divmod(duration, 3600) minutes, seconds = divmod(remainder, 60) formatted_duration = f"{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}" self.session_records.append({ "日期": self.current_segment_start.strftime("%Y-%m-%d"), "开始时间": self.current_segment_start.strftime("%H:%M:%S"), "结束时间": self.current_segment_end.strftime("%H:%M:%S"), "持续时间": formatted_duration, "类型": category, "持续秒数": duration }) btn_frame = tk.Frame(dialog) btn_frame.pack(pady=10) ok_btn = tk.Button(btn_frame, text="确定", command=on_ok, width=10) ok_btn.pack(side=tk.LEFT, padx=10) cancel_btn = tk.Button(btn_frame, text="取消", command=dialog.destroy, width=10) cancel_btn.pack(side=tk.RIGHT, padx=10) # 让对话框模式化 dialog.transient(self.root) dialog.grab_set() self.root.wait_window(dialog) def save_current_session(self): """手动保存当前会话记录""" if self.session_records: self.save_to_excel() self.session_records = [] # 清空记录 messagebox.showinfo("保存成功", "计时记录已保存到Excel文件") else: messagebox.showinfo("无记录", "没有计时记录需要保存") def auto_save_records(self): """自动保存记录到Excel""" while True: if self.auto_save and self.session_records: current_time = time.time() # 每5分钟自动保存一次且记录不为空 if current_time - self.last_save_time > 300: # 300秒=5分钟 self.save_to_excel() self.last_save_time = current_time time.sleep(60) # 每分钟检查一次 def create_summary_sheet(self, workbook, records_df): """创建时间总结工作表(分钟形式)""" # 如果records_df中没有'持续秒数',则从'持续时间'转换 if '持续秒数' not in records_df.columns: # 将持续时间字符串转换为秒数 def to_seconds(t_str): parts = t_str.split(':') if len(parts) == 3: hours, minutes, seconds = map(int, parts) return hours * 3600 + minutes * 60 + seconds return 0 records_df['持续秒数'] = records_df['持续时间'].apply(to_seconds) # 按日期和类型汇总秒数 summary_df = records_df.groupby(['日期', '类型'])['持续秒数'].sum().reset_index() # 创建一个函数将秒数转换为分钟(保留一位小数) def seconds_to_minutes(sec): minutes = sec / 60 return round(minutes, 1) # 保留一位小数 # 添加分钟列 summary_df['时长(分钟)'] = summary_df['持续秒数'].apply(seconds_to_minutes) # 创建透视表:日期为行,类型为列 pivot_df = summary_df.pivot(index='日期', columns='类型', values='时长(分钟)').fillna(0).reset_index() # 计算每日总计(分钟) daily_total = records_df.groupby('日期')['持续秒数'].sum().reset_index() daily_total['每日总计(分钟)'] = daily_total['持续秒数'].apply(seconds_to_minutes) # 合并到透视表中 pivot_df = pivot_df.merge(daily_total[['日期', '每日总计(分钟)']], on='日期', how='left') # 写入新的工作表 ws_summary = workbook.create_sheet(title="时间总结") # 写入标题 ws_summary.append(["日期"] + list(pivot_df.columns[1:])) # 写入数据 for _, row in pivot_df.iterrows(): ws_summary.append(row.tolist()) return ws_summary def save_to_excel(self): """保存记录到Excel文件,并设置列宽为15、内容居中""" if not self.session_records: return # 获取当前日期作为文件名的一部分 current_date = datetime.now().strftime("%Y-%m-%d") filename = f"每日记录.xlsx" file_path = os.path.join(self.save_directory, filename) print(f"{'计时器已退出':-^50}") print(f"计时记录保存位置: {file_path}") # 创建DataFrame df = pd.DataFrame(self.session_records) column_order = ["日期", "开始时间", "结束时间", "持续时间", "类型", "持续秒数"] df = df[column_order] # 使用 openpyxl 引擎写入或追加数据 try: if os.path.exists(file_path): # 读取现有工作簿 workbook = openpyxl.load_workbook(file_path) # 获取或创建时间记录工作表 if '时间记录' in workbook.sheetnames: ws_detail = workbook['时间记录'] # 读取现有时间记录 existing_data = [] for row in ws_detail.iter_rows(values_only=True): existing_data.append(row) # 获取标题行 headers = existing_data[0] # 创建DataFrame合并数据 existing_df = pd.DataFrame(existing_data[1:], columns=headers) combined_df = pd.concat([existing_df, df], ignore_index=True) # 删除原有工作表 workbook.remove(ws_detail) else: combined_df = df # 创建新的时间记录工作表 ws_detail = workbook.create_sheet(title="时间记录", index=0) # 写入合并后的数据 rows = [combined_df.columns.tolist()] + combined_df.values.tolist() for row in rows: ws_detail.append(row) # 删除旧的时间总结工作表(如果存在) if '时间总结' in workbook.sheetnames: workbook.remove(workbook['时间总结']) # 创建新的时间总结工作表 self.create_summary_sheet(workbook, combined_df) # 保存工作簿 workbook.save(file_path) else: # 创建新的工作簿 workbook = openpyxl.Workbook() workbook.remove(workbook.active) # 删除默认工作表 # 创建时间记录工作表 ws_detail = workbook.create_sheet(title="时间记录") rows = [df.columns.tolist()] + df.values.tolist() for row in rows: ws_detail.append(row) # 创建时间总结工作表 self.create_summary_sheet(workbook, df) # 保存工作簿 workbook.save(file_path) def create_summary_sheet(self, workbook, records_df): """创建时间总结工作表(按分钟显示)""" # 确保有'持续秒数'列 if '持续秒数' not in records_df.columns: def to_seconds(t_str): parts = t_str.split(':') if len(parts) == 3: hours, minutes, seconds = map(int, parts) return hours * 3600 + minutes * 60 + seconds return 0 records_df['持续秒数'] = records_df['持续时间'].apply(to_seconds) # 按日期和类型汇总秒数 summary_df = records_df.groupby(['日期', '类型'])['持续秒数'].sum().reset_index() # 将秒数转换为分钟(保留1位小数) summary_df['总分钟数'] = summary_df['持续秒数'] / 60 summary_df['总分钟数'] = summary_df['总分钟数'].round(1) # 四舍五入保留1位小数 # 创建透视表:日期为行,类型为列 pivot_df = summary_df.pivot(index='日期', columns='类型', values='总分钟数').fillna(0).reset_index() # 计算每日总计分钟数 daily_total = records_df.groupby('日期')['持续秒数'].sum().reset_index() daily_total['每日总计(分钟)'] = daily_total['持续秒数'] / 60 daily_total['每日总计(分钟)'] = daily_total['每日总计(分钟)'].round(1) # 合并到透视表 pivot_df = pivot_df.merge(daily_total[['日期', '每日总计(分钟)']], on='日期', how='left') # 写入新的工作表 ws_summary = workbook.create_sheet(title="时间总结") # 写入标题 headers = ["日期"] + [f"{col}(分钟)" for col in pivot_df.columns if col != "日期"] ws_summary.append(headers) # 写入数据 for _, row in pivot_df.iterrows(): # 处理日期列 date_value = row['日期'] # 处理分钟数列(保留1位小数) values = [round(row[col], 1) if isinstance(row[col], float) else row[col] for col in pivot_df.columns if col != "日期"] ws_summary.append([date_value] + values) return ws_summary # 重新加载工作簿设置样式 workbook = openpyxl.load_workbook(file_path) # 设置时间记录工作表样式 ws_detail = workbook['时间记录'] # 设置列宽为15 for i, _ in enumerate(column_order, 1): col_letter = get_column_letter(i) ws_detail.column_dimensions[col_letter].width = 15 # 设置所有单元格居中对齐 center_alignment = Alignment(horizontal='center', vertical='center') for row in ws_detail.iter_rows(): for cell in row: cell.alignment = center_alignment # 设置标题行样式 header_fill = PatternFill(start_color="DDEBF7", end_color="DDEBF7", fill_type="solid") header_font = Font(bold=True) for cell in ws_detail[1]: cell.font = header_font cell.fill = header_fill # 设置时间总结工作表样式 if '时间总结' in workbook.sheetnames: ws_summary = workbook['时间总结'] # 设置列宽 for i in range(1, ws_summary.max_column + 1): col_letter = get_column_letter(i) ws_summary.column_dimensions[col_letter].width = 18 # 设置所有单元格居中对齐 for row in ws_summary.iter_rows(): for cell in row: cell.alignment = center_alignment # 设置标题行样式 for cell in ws_summary[1]: cell.font = header_font cell.fill = header_fill # 保存修改 workbook.save(file_path) print("Excel文件保存成功,包含两个工作表") except Exception as e: # 出错时创建备份文件 error_file = os.path.join(self.save_directory, f"时间记录_出错备份_{int(time.time())}.xlsx") df.to_excel(error_file, index=False) messagebox.showerror("保存失败", f"无法保存或设置样式,已创建备份文件:\n{error_file}\n错误:{str(e)}") def update_timer(self): """更新计时器显示""" while self.running: self.elapsed_time = time.time() - self.start_time hours, remainder = divmod(self.elapsed_time, 3600) minutes, seconds = divmod(remainder, 60) self.time_label.config(text=f"{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}") time.sleep(0.1) def on_close(self): """关闭窗口前的处理""" # 如果正在计时,先结束并记录 if self.running: self.running = False self.current_segment_end = datetime.now() self.record_session() # 保存所有记录 if self.session_records: self.save_to_excel() self.root.destroy() def run(self): self.root.mainloop()
if name == “main”:
timer = FloatTimer()
timer.run()
print(f"{‘计时器已退出’:-^50}")
想要实现在桌面上创造一个快捷键,点击一下就可以进行计时器的运行