好友
阅读权限 10
听众
最后登录 1970-1-1
任务条可以拖拽,看得到进度,
可以透明
https://wwbvp.lanzouu.com/iXP483ngisnc
密码:buon
代码如下:
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
import threading
import time
import json
import os
import sys
import winsound
# --- 解决打包后资源路径问题 ---
def resource_path(relative_path):
"""获取打包后资源的绝对路径"""
base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base_path, relative_path)
# --- 配置文件路径 (隐藏到系统AppData目录) ---
APP_DATA_DIR = os.path.join(os.environ['APPDATA'], "涂涂牌倒计时闹钟")
os.makedirs(APP_DATA_DIR, exist_ok=True)
CONFIG_FILE = os.path.join(APP_DATA_DIR, "countdown_tasks.json")
WINDOW_CONFIG_FILE = os.path.join(APP_DATA_DIR, "window_config.json")
ALARM_SOUND = resource_path("alarm.wav") # 闹钟声音文件路径
class CountdownTask:
def __init__(self, name, total_seconds, remind_before=0):
self.name = name
self.total_seconds = total_seconds
self.remaining = total_seconds
self.remind_before = remind_before
self.is_running = False
self.is_finished = False
self.reminded = False
self.is_alarming = False
def to_dict(self):
return {
"name": self.name,
"total_seconds": self.total_seconds,
"remaining": self.remaining,
"remind_before": self.remind_before,
"is_finished": self.is_finished
}
@staticmethod
def from_dict(data):
task = CountdownTask(data["name"], data["total_seconds"], data.get("remind_before", 0))
task.remaining = data["remaining"]
task.is_finished = data["is_finished"]
return task
class App:
def __init__(self, root):
self.root = root
self.root.title("涂涂牌倒计时闹钟")
self.load_window_config()
self.root.minsize(380, 300)
self.tasks = []
self.task_widgets = []
self.breathing_state = 0
self.breathing_colors = ["#ff3333", "#ff9999"]
self.dragging_index = None
self.load_data()
self.setup_ui()
self.running = True
self.update_thread = threading.Thread(target=self.tick_loop, daemon=True)
self.update_thread.start()
self.breathing_loop()
self.update_clock()
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
def setup_ui(self):
frame_top = tk.Frame(self.root, pady=8, bg="#f0f0f0")
frame_top.pack(fill='x', side='top')
frame_top.grid_columnconfigure(2, weight=1)
btn_group = tk.Frame(frame_top, bg="#f0f0f0")
btn_group.grid(row=0, column=0, sticky='w', padx=10)
btn_add = tk.Button(btn_group, text="➕ 新建", command=self.add_task_dialog,
bg="#4CAF50", fg="white", font=("Arial", 9, "bold"))
btn_add.pack(side='left', padx=2)
btn_pause_all = tk.Button(btn_group, text="⏸️ 全停", command=self.pause_all)
btn_pause_all.pack(side='left', padx=2)
btn_reset_all = tk.Button(btn_group, text="🔄 全重置", command=self.reset_all)
btn_reset_all.pack(side='left', padx=2)
btn_stop_all_alarm = tk.Button(btn_group, text="🔇 全静音", bg="red", fg="white",
command=self.stop_all_alarm)
btn_stop_all_alarm.pack(side='left', padx=2)
setting_group = tk.Frame(frame_top, bg="#f0f0f0")
setting_group.grid(row=0, column=3, sticky='e', padx=10)
tk.Label(setting_group, text="透:", bg="#f0f0f0", font=("Arial", 9)).pack(side='left')
self.alpha_slider = tk.Scale(setting_group, from_=20, to=100, orient='horizontal',
length=60, showvalue=0, command=self.change_alpha)
self.alpha_slider.set(100)
self.alpha_slider.pack(side='left', padx=3)
self.clock_label = tk.Label(setting_group, text="00:00:00",
font=("Arial", 12, "bold"), fg="#2196F3", bg="#f0f0f0")
self.clock_label.pack(side='left', padx=8)
self.canvas = tk.Canvas(self.root, bg="white")
self.scrollbar = ttk.Scrollbar(self.root, orient="vertical", command=self.canvas.yview)
self.scrollable_frame = ttk.Frame(self.canvas)
self.scrollable_frame.bind(
"<Configure>",
lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))
)
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw", tags="frame")
self.canvas.configure(yscrollcommand=self.scrollbar.set)
self.canvas.bind("<Configure>", self.on_canvas_configure)
self.canvas.bind_all("<MouseWheel>", self.on_mousewheel)
self.canvas.pack(side="left", fill="both", expand=True)
self.scrollbar.pack(side="right", fill="y")
self.refresh_task_list()
def on_mousewheel(self, event):
self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")
def on_canvas_configure(self, event):
self.canvas.itemconfig("frame", width=event.width)
def change_alpha(self, value):
alpha_val = int(value) / 100.0
self.root.attributes("-alpha", alpha_val)
def update_clock(self):
current_time = time.strftime("%H:%M:%S")
self.clock_label.config(text=current_time)
self.root.after(1000, self.update_clock)
def add_task_dialog(self):
win = tk.Toplevel(self.root)
win.title("新建任务")
win.geometry("350x300")
win.resizable(False, False)
win.transient(self.root)
tk.Label(win, text="任务名称:", font=("Arial", 10)).pack(pady=5)
name_entry = tk.Entry(win, font=("Arial", 12), width=25)
name_entry.pack()
name_entry.insert(0, "新任务")
name_entry.focus_set()
time_frame = tk.Frame(win)
time_frame.pack(pady=10)
tk.Label(time_frame, text="时:").grid(row=0, column=0)
hour_entry = tk.Entry(time_frame, width=5)
hour_entry.grid(row=0, column=1, padx=5)
hour_entry.insert(0, "0")
tk.Label(time_frame, text="分:").grid(row=0, column=2)
min_entry = tk.Entry(time_frame, width=5)
min_entry.grid(row=0, column=3, padx=5)
min_entry.insert(0, "5")
tk.Label(time_frame, text="秒:").grid(row=0, column=4)
sec_entry = tk.Entry(time_frame, width=5)
sec_entry.grid(row=0, column=5, padx=5)
sec_entry.insert(0, "0")
tk.Label(win, text="提前多少秒提醒:", font=("Arial", 10)).pack(pady=5)
remind_entry = tk.Entry(win, font=("Arial", 12), width=10)
remind_entry.pack()
remind_entry.insert(0, "10")
def confirm(event=None):
try:
name = name_entry.get()
h = int(hour_entry.get() or 0)
m = int(min_entry.get() or 0)
s = int(sec_entry.get() or 0)
remind_s = int(remind_entry.get() or 0)
total_secs = h * 3600 + m * 60 + s
if total_secs <= 0:
messagebox.showerror("错误", "时长必须大于0")
return
if not name: name = "未命名任务"
task = CountdownTask(name, total_secs, remind_s)
self.tasks.append(task)
self.save_data()
self.refresh_task_list()
win.destroy()
except ValueError:
messagebox.showerror("错误", "请输入数字")
win.bind("<Return>", confirm)
tk.Button(win, text="确定", command=confirm, bg="blue", fg="white", height=2, width=15).pack(pady=15)
def refresh_task_list(self):
for widget in self.scrollable_frame.winfo_children():
widget.destroy()
self.task_widgets = []
if not self.tasks:
empty_label = tk.Label(self.scrollable_frame, text="暂无任务\n点击左上角「新建」添加倒计时",
font=("Arial", 14), fg="#999999", pady=50)
empty_label.pack()
return
for i, task in enumerate(self.tasks):
frame = tk.Frame(self.scrollable_frame, bd=2, relief=tk.RIDGE, pady=8)
frame.pack(fill='x', pady=4, padx=8)
frame.grid_columnconfigure(2, weight=1)
drag_handle = tk.Label(frame, text="☰", cursor="hand2",
bg="#d0d0d0", fg="#666666",
font=("Arial", 14, "bold"),
padx=3, pady=2)
drag_handle.grid(row=0, column=0, sticky='ns', padx=3)
drag_handle.bind("<ButtonPress-1>", lambda e, idx=i: self.start_drag(e, idx))
drag_handle.bind("<B1-Motion>", self.do_drag)
drag_handle.bind("<ButtonRelease-1>", self.end_drag)
def show_tooltip(event):
tooltip = tk.Toplevel(frame)
tooltip.wm_overrideredirect(True)
tooltip.geometry(f"+{event.x_root+10}+{event.y_root+10}")
tk.Label(tooltip, text="按住拖动排序", bg="#ffffe0", padx=5, pady=2).pack()
tooltip.after(1000, tooltip.destroy)
drag_handle.bind("<Enter>", show_tooltip)
lbl_name = tk.Label(frame, text=task.name, font=("Arial", 11, "bold"), fg="blue", cursor="hand2")
lbl_name.grid(row=0, column=1, sticky='w', padx=5)
def edit_name(event, t=task, lbl=lbl_name):
new_name = simpledialog.askstring("修改", "输入新名称:", initialvalue=t.name)
if new_name:
t.name = new_name
lbl.config(text=new_name)
self.save_data()
lbl_name.bind("<Button-1>", edit_name)
lbl_time = tk.Label(frame, text="00:00:00", font=("Arial", 16, "bold"))
lbl_time.grid(row=0, column=2, padx=8)
progress_bg = tk.Frame(frame, bg="#e0e0e0", height=8)
progress_bg.grid(row=1, column=1, columnspan=2, sticky='ew', padx=8, pady=4)
progress_fg = tk.Frame(progress_bg, bg="#4CAF50", width=0)
progress_fg.place(x=0, y=0, relheight=1)
btn_frame = tk.Frame(frame)
btn_frame.grid(row=0, column=3, rowspan=2, padx=8)
self.task_widgets.append({
'task': task,
'label': lbl_time,
'progress_bg': progress_bg,
'progress_fg': progress_fg,
'frame': frame
})
if task.is_finished:
frame.config(bg="#e8f5e9")
mins, secs = divmod(task.total_seconds, 60)
hours, mins = divmod(mins, 60)
total_time_str = f"{hours:02d}:{mins:02d}:{secs:02d}"
lbl_time.config(text=total_time_str, fg="green")
if task.is_alarming:
btn_stop_alarm = tk.Button(btn_frame, text="🔇", bg="red", fg="white", width=2,
command=lambda t=task: self.stop_alarm(t))
btn_stop_alarm.pack(side='left', padx=1)
btn_repeat = tk.Button(btn_frame, text="🔁", width=2, command=lambda t=task: self.reset_task(t))
btn_repeat.pack(side='left', padx=1)
btn_del = tk.Button(btn_frame, text="🗑️", fg="red", width=2, command=lambda t=task: self.delete_task(t))
btn_del.pack(side='left', padx=1)
else:
if task.is_running:
frame.config(bd=3, relief=tk.SOLID, highlightbackground="#2196F3", highlightthickness=2)
if task.is_alarming:
frame.config(bg="#ffcdd2")
btn_stop_alarm = tk.Button(btn_frame, text="🔇", bg="red", fg="white", width=2,
command=lambda t=task: self.stop_alarm(t))
btn_stop_alarm.pack(side='left', padx=1)
btn_text = "⏸️" if task.is_running else "▶️"
btn_start = tk.Button(btn_frame, text=btn_text, width=2,
command=lambda t=task: self.toggle_task(t))
btn_start.pack(side='left', padx=1)
btn_reset = tk.Button(btn_frame, text="🔄", width=2, command=lambda t=task: self.reset_task(t))
btn_reset.pack(side='left', padx=1)
btn_del = tk.Button(btn_frame, text="🗑️", fg="red", width=2, command=lambda t=task: self.delete_task(t))
btn_del.pack(side='left', padx=1)
def start_drag(self, event, index):
self.dragging_index = index
self.task_widgets[index]['frame'].config(bg="#ffffcc")
def do_drag(self, event):
if self.dragging_index is None:
return
y = event.y_root
for i, widget in enumerate(self.task_widgets):
if i == self.dragging_index:
continue
frame = widget['frame']
if frame.winfo_rooty() < y < frame.winfo_rooty() + frame.winfo_height():
self.tasks.insert(i, self.tasks.pop(self.dragging_index))
self.dragging_index = i
self.refresh_task_list()
break
def end_drag(self, event):
if self.dragging_index is not None:
self.save_data()
self.dragging_index = None
def toggle_task(self, task):
task.is_running = not task.is_running
self.refresh_task_list()
def reset_task(self, task):
task.remaining = task.total_seconds
task.is_finished = False
task.is_running = False
task.reminded = False
self.stop_alarm(task)
self.save_data()
self.refresh_task_list()
def delete_task(self, task):
if messagebox.askyesno("确认删除", f"确定要删除任务「{task.name}」吗?"):
self.stop_alarm(task)
self.tasks.remove(task)
self.save_data()
self.refresh_task_list()
def stop_alarm(self, task):
task.is_alarming = False
# 立即停止所有声音播放
winsound.PlaySound(None, winsound.SND_ASYNC)
for widget in self.task_widgets:
if widget['task'] == task:
widget['progress_fg'].config(bg="#4CAF50")
break
self.refresh_task_list()
def pause_all(self):
for task in self.tasks:
if task.is_running:
task.is_running = False
self.refresh_task_list()
def reset_all(self):
if messagebox.askyesno("确认", "确定要重置所有任务吗?"):
for task in self.tasks:
task.remaining = task.total_seconds
task.is_finished = False
task.is_running = False
task.reminded = False
task.is_alarming = False
# 停止所有声音
winsound.PlaySound(None, winsound.SND_ASYNC)
self.save_data()
self.refresh_task_list()
def stop_all_alarm(self):
for task in self.tasks:
task.is_alarming = False
# 立即停止所有声音
winsound.PlaySound(None, winsound.SND_ASYNC)
for widget in self.task_widgets:
widget['progress_fg'].config(bg="#4CAF50")
self.refresh_task_list()
def tick_loop(self):
while self.running:
time.sleep(0.1)
self.root.after(0, self.update_ui)
def update_ui(self):
for widget in self.task_widgets:
task = widget['task']
lbl = widget['label']
progress_fg = widget['progress_fg']
progress_bg = widget['progress_bg']
if task.is_running and not task.is_finished:
task.remaining -= 0.1
if not task.reminded and 0 < task.remaining <= task.remind_before:
task.reminded = True
if not task.is_alarming:
task.is_alarming = True
self.start_alarm_thread(task)
if task.remaining <= 0:
task.remaining = 0
task.is_running = False
task.is_finished = True
if not task.is_alarming:
task.is_alarming = True
self.start_alarm_thread(task)
self.save_data()
self.refresh_task_list()
self.canvas.yview_moveto(0)
mins, secs = divmod(int(task.remaining), 60)
hours, mins = divmod(mins, 60)
time_str = f"{hours:02d}:{mins:02d}:{secs:02d}"
if not task.is_finished:
lbl.config(text=time_str)
if task.total_seconds > 0:
progress = (task.total_seconds - task.remaining) / task.total_seconds
progress_fg.config(width=int(progress_bg.winfo_width() * progress))
def breathing_loop(self):
self.breathing_state = 1 - self.breathing_state
current_color = self.breathing_colors[self.breathing_state]
for widget in self.task_widgets:
if widget['task'].is_alarming:
widget['progress_fg'].config(bg=current_color)
self.root.after(300, self.breathing_loop)
def start_alarm_thread(self, task):
def alarm_sound_loop():
# 使用 SND_LOOP 标志实现无缝循环播放
try:
winsound.PlaySound(ALARM_SOUND, winsound.SND_FILENAME | winsound.SND_ASYNC | winsound.SND_LOOP)
except:
# 如果WAV文件播放失败,回退到蜂鸣器循环
while task.is_alarming:
try:
winsound.Beep(1200, 300)
time.sleep(0.1)
winsound.Beep(1000, 300)
time.sleep(0.3)
except:
break
threading.Thread(target=alarm_sound_loop, daemon=True).start()
self.refresh_task_list()
def save_data(self):
data = [t.to_dict() for t in self.tasks]
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f)
def load_data(self):
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
self.tasks = [CountdownTask.from_dict(d) for d in data]
except:
pass
def save_window_config(self):
config = {
"geometry": self.root.geometry(),
"alpha": self.alpha_slider.get()
}
with open(WINDOW_CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(config, f)
def load_window_config(self):
if os.path.exists(WINDOW_CONFIG_FILE):
try:
with open(WINDOW_CONFIG_FILE, 'r', encoding='utf-8') as f:
config = json.load(f)
self.root.geometry(config.get("geometry", "600x700"))
except:
self.root.geometry("600x700")
else:
self.root.geometry("600x700")
def on_closing(self):
# 关闭软件时停止所有声音
winsound.PlaySound(None, winsound.SND_ASYNC)
self.save_window_config()
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = App(root)
root.mainloop()
免费评分
查看全部评分