延續 Day 28 的基礎版,今天把鬧鐘升級為可持久化+更貼日常的版本:
今天要達成
程式碼(存成 alarm_pro.py)
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from datetime import datetime
import csv, re
HHMM_RE = re.compile(r"^(?:[01]\d|2[0-3]):[0-5]\d$")
MUTED = False
# 鬧鐘結構:{"time":"HH:MM", "days": set[int 0..6] or None(每日), "note": str}
ALARMS: list[dict] = []
FIRED_TODAY: set[tuple[str, str]] = set() # (time, note) 當天已觸發
def weekday_idx() -> int:
# 週一=0, 週日=6
return datetime.now().weekday()
def parse_days(s: str|None):
if not s: return None
# "135" -> {0,2,4} (一三五)
m = set()
for ch in s:
if ch.isdigit():
d = int(ch)
if 1 <= d <= 7: m.add((d-1) % 7)
return m or None
def days_to_string(ds: set[int]|None):
return "每日" if ds is None else "每週" + "".join("一二三四五六日"[d] for d in sorted(ds))
def add_alarm():
hhmm = time_entry.get().strip()
note = note_entry.get().strip()
if not HHMM_RE.match(hhmm):
messagebox.showerror("格式錯誤","請輸入 24 小時制時間,如 08:30"); return
sel_days = {i for i, v in enumerate(day_vars) if v.get()}
days = sel_days if sel_days else None
# 去重:相同時間+相同 days+相同 note 視為重複
for a in ALARMS:
if a["time"]==hhmm and a["days"]==days and a["note"]==note:
messagebox.showinfo("提示","相同鬧鐘已存在"); return
ALARMS.append({"time": hhmm, "days": days, "note": note})
refresh_list(); time_entry.delete(0, tk.END); note_entry.delete(0, tk.END)
def delete_selected():
sel = listbox.curselection()
if not sel: return
idx = sel[0]; ALARMS.pop(idx); refresh_list()
def refresh_list():
listbox.delete(0, tk.END)
for a in sorted(ALARMS, key=lambda x: x["time"]):
ds = days_to_string(a["days"])
note = f"|{a['note']}" if a["note"] else ""
listbox.insert(tk.END, f"{a['time']} {ds}{note}")
def toggle_mute():
global MUTED
MUTED = not MUTED
mute_btn.config(text=("解除靜音" if MUTED else "靜音"))
def tick_clock():
clock_var.set(f"現在時間:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
root.after(1000, tick_clock)
def check_loop():
# 每秒檢查鬧鐘;跨日 00:00:00 清空 FIRED_TODAY
now = datetime.now()
if now.strftime("%H:%M:%S") == "00:00:00":
FIRED_TODAY.clear()
hhmm = now.strftime("%H:%M")
wd = weekday_idx()
for a in ALARMS:
if a["time"] != hhmm: continue
if a["days"] is not None and wd not in a["days"]: continue
key = (a["time"], a["note"])
if key in FIRED_TODAY: continue
FIRED_TODAY.add(key); notify(a)
root.after(1000, check_loop)
def notify(a: dict):
if not MUTED:
try: root.bell()
except Exception: pass
ds = days_to_string(a["days"])
msg = f"{a['time']} 到點囉!\n{ds}"
if a["note"]: msg += f"\n備註:{a['note']}"
messagebox.showinfo("時間到!", msg)
def export_csv():
path = filedialog.asksaveasfilename(defaultextension=".csv",
filetypes=[("CSV","*.csv")],
title="匯出鬧鐘")
if not path: return
with open(path, "w", encoding="utf-8", newline="") as f:
w = csv.writer(f); w.writerow(["time", "days", "note"])
for a in ALARMS:
days = "" if a["days"] is None else "".join(str(d+1) for d in sorted(a["days"]))
w.writerow([a["time"], days, a["note"]])
messagebox.showinfo("完成", f"已匯出 {len(ALARMS)} 筆至:\n{path}")
def import_csv():
path = filedialog.askopenfilename(filetypes=[("CSV","*.csv"), ("All","*.*")], title="匯入鬧鐘")
if not path: return
count = 0
try:
with open(path, "r", encoding="utf-8-sig", newline="") as f:
rdr = csv.DictReader(f)
for row in rdr:
t = (row.get("time") or "").strip()
ds = parse_days((row.get("days") or "").strip())
note = (row.get("note") or "").strip()
if not HHMM_RE.match(t): continue
# 去重
dup = any(a["time"]==t and a["days"]==ds and a["note"]==note for a in ALARMS)
if not dup:
ALARMS.append({"time": t, "days": ds, "note": note}); count += 1
except Exception as e:
messagebox.showerror("匯入失敗", str(e)); return
refresh_list(); messagebox.showinfo("完成", f"匯入 {count} 筆")
# ----- GUI -----
root = tk.Tk(); root.title("多鬧鐘(進階版)")
main = ttk.Frame(root, padding=16); main.grid(sticky="nsew")
root.columnconfigure(0, weight=1); root.rowconfigure(0, weight=1)
main.columnconfigure(0, weight=3); main.columnconfigure(1, weight=2)
main.rowconfigure(3, weight=1)
# 輸入區
inp = ttk.Frame(main); inp.grid(row=0, column=0, sticky="we", padx=(0,12))
ttk.Label(inp, text="時間 HH:MM").grid(row=0, column=0, padx=(0,8))
time_entry = ttk.Entry(inp, width=8, justify="center"); time_entry.grid(row=0, column=1)
ttk.Label(inp, text="備註").grid(row=0, column=2, padx=(12,6))
note_entry = ttk.Entry(inp, width=24); note_entry.grid(row=0, column=3)
# 每週選擇
days_frame = ttk.Frame(main); days_frame.grid(row=1, column=0, sticky="w", padx=(0,12))
ttk.Label(days_frame, text="重複:").grid(row=0, column=0)
day_vars = [tk.BooleanVar() for _ in range(7)]
for i, ch in enumerate("一二三四五六日"):
ttk.Checkbutton(days_frame, text=ch, variable=day_vars[i]).grid(row=0, column=i+1, padx=2)
btns = ttk.Frame(main); btns.grid(row=2, column=0, sticky="w", padx=(0,12), pady=(6,0))
ttk.Button(btns, text="加入", command=add_alarm).grid(row=0, column=0, padx=4)
mute_btn = ttk.Button(btns, text="靜音", command=toggle_mute); mute_btn.grid(row=0, column=1, padx=4)
ttk.Button(btns, text="匯入", command=import_csv).grid(row=0, column=2, padx=4)
ttk.Button(btns, text="匯出", command=export_csv).grid(row=0, column=3, padx=4)
# 清單 + 時鐘
clock_var = tk.StringVar(value=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
ttk.Label(main, textvariable=clock_var).grid(row=0, column=1, sticky="e")
listbox = tk.Listbox(main, height=12)
listbox.grid(row=1, column=1, rowspan=3, sticky="nsew")
scroll = ttk.Scrollbar(main, orient="vertical", command=listbox.yview)
scroll.grid(row=1, column=2, rowspan=3, sticky="ns")
listbox.configure(yscrollcommand=scroll.set)
ops = ttk.Frame(main); ops.grid(row=3, column=0, sticky="we", pady=(8,0))
ttk.Button(ops, text="刪除選取", command=delete_selected).grid(row=0, column=0, padx=4)
ttk.Button(ops, text="離開", command=root.destroy).grid(row=0, column=1, padx=4)
# 快捷鍵
root.bind("<Return>", lambda e: add_alarm())
root.bind("<Delete>", lambda e: delete_selected())
# 啟動
tick_clock(); check_loop(); time_entry.focus()
root.mainloop()
使用方式
python alarm_pro.py
操作範例
實作:
常見問題(FAQ)
1.為什麼同分鐘只提醒一次?
使用 FIRED_TODAY 記錄((time, note)),避免「剛好切到秒」連跳;到隔天 00:00 自動清空。
2.想週期更細(隔天、每兩週)?
可把 days 改為更通用的 RRULE 設計;但對日常使用,週一~週日已覆蓋 90% 場景。
3.想常駐系統匣/自訂鈴聲?
可結合 winsound(Windows)或播放簡短 WAV 檔、搭配 pystray 做托盤圖示(屬進階內容)。
4.鬧鐘會存檔嗎?
是的,透過 CSV 匯入/匯出就能保存與移轉設定;若要「自動重載」,可以在程式啟動時檢查 alarms.csv 並自動 import_csv()。
今日小結