【上班定时休息提醒小程序】附python代码
- 2026-09-25 08:53:26
【上班定时休息提醒小程序】附python代码许多脑力劳动岗位需长久地坐在电子屏幕前。 长期如此,可能导致视力下降、肩颈腰背酸痛、代谢变慢易发胖等健康问题。 对此,本程序应运而生,引入定时提醒功能: 




每隔一小时设置一个闹铃,屏幕出现大量闪烁字体“王子公主请休息”,同时系统发出尖锐爆鸣声(闹铃)。

点击字体关闭闹铃,要求眼疾手快(要在字体实体化时点到)。 如果不人为关闭,闹铃将持续40s,不会再次触发,闹钟列表中相应条目变灰。 点击左下角齿轮,进入后台修改设置。可修改字体内容、铃声音量、闹铃开关状态等。

过午夜12点重置所有闹钟。
为了不妨碍工作,小程序页面可最小化到托盘。

本程序在上上上篇文章的【上班摸鱼小程序】基础上修改得到。
-END-
代码:
import tkinter as tkfrom tkinter import messageboxfrom datetime import datetime, timedeltaimport osimport sysimport mathimport tkinter.font as tkFontfrom zhdate import ZhDateimport randomimport threadingimport timeimport tempfileimport waveimport structtry:import winsoundexcept ImportError:winsound = Nonetry:import pystrayexcept ImportError:pystray = Nonetry:from PIL import Image, ImageDrawexcept ImportError:Image = NoneImageDraw = NoneDEFAULT_OFF_WORK_TIME = "17:00:00"off_work_time_str = DEFAULT_OFF_WORK_TIMEBG_COLOR = "#000000"FG_COLOR = "#cccccc"TITLE_COLOR = "#ffffff"ACCENT_RED = "#ff6b6b"ACCENT_CYAN = "#ff6b6b"ACCENT_YELLOW = "#ff6b6b"BUTTON_COLOR = "#333333"BUTTON_TEXT = "white"POPUP_BG = "#1a1a1a"MASK_COLOR = "#0a0a0a"DIVIDER_COLOR = "#397a9b"CHINESE_FONT = "STZhongsong"DEFAULT_ALARMS = ["08:00", "09:00", "10:00", "11:00","15:00", "16:00", "17:00", "18:30", "19:30",]DEFAULT_ALARM_MESSAGE = "王子公主请休息!"ALARM_MEAL_LABELS = {"11:00": "午饭", "17:00": "晚饭"}CAT_IMAGE_PATH = r"D:\BaiduNetdiskDownload\Cat.png"def get_next_holiday_info():now = datetime.now()current_year = now.yearspring_festival = ZhDate(current_year, 1, 1).to_datetime()dragon_boat = ZhDate(current_year, 5, 5).to_datetime()mid_autumn = ZhDate(current_year, 8, 15).to_datetime()qingming = datetime(current_year, 4, 4)holidays = [(datetime(current_year, 1, 1), "元旦"),(spring_festival, "春节"),(qingming, "清明节"),(datetime(current_year, 5, 1), "劳动节"),(dragon_boat, "端午节"),(datetime(current_year, 10, 1), "国庆节"),(mid_autumn, "中秋节"),]min_diff = Nonenext_holiday_name = ""for year_offset in [0, 1]:year = current_year + year_offsetfor holiday_date, name in holidays:if name == "元旦":date_obj = datetime(year, 1, 1)elif name == "春节":date_obj = ZhDate(year, 1, 1).to_datetime()elif name == "清明节":date_obj = datetime(year, 4, 4)elif name == "劳动节":date_obj = datetime(year, 5, 1)elif name == "端午节":date_obj = ZhDate(year, 5, 5).to_datetime()elif name == "国庆节":date_obj = datetime(year, 10, 1)elif name == "中秋节":date_obj = ZhDate(year, 8, 15).to_datetime()else:continueif date_obj > now:diff = date_obj - nowdays = diff.daysif min_diff is None or days < min_diff:min_diff = daysnext_holiday_name = nameif min_diff is not None:return min_diff, next_holiday_namereturn 0, "暂无数据"def calculate_time_diff(target_time_str):now = datetime.now()try:h, m, s = map(int, target_time_str.split(':'))target_time_today = now.replace(hour=h, minute=m, second=s, microsecond=0)except ValueError:return "时间格式错误", ACCENT_REDif now >= target_time_today:return "已经下班啦!", ACCENT_REDdelta = target_time_today - nowtotal_seconds = int(delta.total_seconds())hours, remainder = divmod(total_seconds, 3600)minutes, seconds = divmod(remainder, 60)return f"{hours:02d}时{minutes:02d}分{seconds:02d}秒", ACCENT_REDdef get_weekend_diff(target_time_str):now = datetime.now()weekday = now.weekday()if weekday >= 5:return "现在就是周末哦~", ACCENT_CYANtry:h, m, s = map(int, target_time_str.split(':'))except ValueError:return "时间格式错误", ACCENT_CYANif weekday == 4:target = now.replace(hour=h, minute=m, second=s, microsecond=0)delta = target - nowtotal_seconds = int(delta.total_seconds())if total_seconds < 0:return "周末已开始~", ACCENT_CYANhours, remainder = divmod(total_seconds, 3600)minutes, seconds = divmod(remainder, 60)return f"{hours}小时{minutes}分{seconds}秒", ACCENT_CYANelse:days_to_friday = 4 - weekdaytarget = now + timedelta(days=days_to_friday)target = target.replace(hour=h, minute=m, second=s, microsecond=0)delta = target - nowreturn f"{delta.days}天", ACCENT_CYANclass RoundedButton(tk.Canvas):def __init__(self, parent, text, command=None, radius=3,bg=BUTTON_COLOR, fg="white", font=(CHINESE_FONT, 11)):tmp_font = tk.font.Font(family=font[0], size=font[1])text_w = tmp_font.measure(text)w = text_w + 50h = 40super().__init__(parent, width=w, height=h,bg=parent["bg"], highlightthickness=0, cursor="hand2")self.command = commandself.radius = radiusself.bg = bgself.fg = fgself.text = textself.font = fontself._hover = Falseself._draw()self.bind("<Button-1>", lambda e: self.command() if self.command else None)self.bind("<Enter>", lambda e: self._set_hover(True))self.bind("<Leave>", lambda e: self._set_hover(False))def _draw(self):self.delete("all")w = self.winfo_reqwidth()h = self.winfo_reqheight()r = self.radiuscolor = "#454545" if self._hover else self.bgself.create_oval(0, 0, 2*r, 2*r, fill=color, outline=color)self.create_oval(w-2*r, 0, w, 2*r, fill=color, outline=color)self.create_oval(0, h-2*r, 2*r, h, fill=color, outline=color)self.create_oval(w-2*r, h-2*r, w, h, fill=color, outline=color)self.create_rectangle(r, 0, w-r, h, fill=color, outline=color)self.create_rectangle(0, r, w, h-r, fill=color, outline=color)self.create_text(w/2, h/2, text=self.text, fill=self.fg, font=self.font)def _set_hover(self, hover):self._hover = hoverself._draw()def create_rounded_image(image_path, size, radius=10):try:from PIL import Image, ImageDraw, ImageTkimg = Image.open(image_path).convert("RGBA")img = img.resize(size, Image.LANCZOS)mask = Image.new('L', size, 0)draw = ImageDraw.Draw(mask)draw.rounded_rectangle([(0, 0), size], radius=radius, fill=255)result = Image.new('RGBA', size, (0, 0, 0, 0))result.paste(img, (0, 0), mask)return ImageTk.PhotoImage(result)except ImportError:return Noneexcept Exception as e:print(f"图片处理错误: {e}")return Noneclass MiniGameApp:def __init__(self, parent):self.top = tk.Toplevel(parent)self.top.title("摸鱼小游戏")self.top.geometry("1200x500")self.top.configure(bg="black")self.top.resizable(False, False)self.canvas = tk.Canvas(self.top, width=1200, height=500, bg="black", highlightthickness=0)self.canvas.pack()self.top.bind("<KeyPress>", self.on_key_press)self.top.bind("<KeyRelease>", self.on_key_release)self.top.bind("<r>", self.restart_game)self.top.bind("<t>", self.close_game)self.top.bind("<R>", self.restart_game)self.top.bind("<T>", self.close_game)self.GRAVITY = 0.8self.MAX_CHARGE = 22self.CHARGE_RATE = 0.4self.player = {'x': 0, 'y': 0, 'w': 20, 'h': 20, 'vx': 0, 'vy': 0}self.platforms = []self.is_charging = Falseself.charge_power = 0self.on_ground = Trueself.elasticing = Falseself.elastic_timer = 0self.elastic_duration = 18self.breaking = Falseself.fragments = []self.break_timer = 0self.game_over = Falseself.game_won = Falseself.game_state = 'playing'self.text_fade = 0.0self.reset_game()self.game_loop()def generate_platforms(self):self.platforms = []current_x = 0self.platforms.append({'x': 0, 'y': 350, 'w': 120, 'h': 50})current_x = 120while current_x < 1200:gap = random.randint(80, 160)width = random.randint(60, 100)next_y = random.randint(280, 360)self.platforms.append({'x': current_x + gap,'y': next_y,'w': width,'h': 500 - next_y})current_x += gap + widthdef reset_game(self):self.generate_platforms()start_plat = self.platforms[0]self.player['x'] = start_plat['x'] + 10self.player['y'] = start_plat['y'] - self.player['h']self.player['vx'] = 0self.player['vy'] = 0self.game_over = Falseself.game_won = Falseself.is_charging = Falseself.charge_power = 0self.on_ground = Trueself.elasticing = Falseself.elastic_timer = 0self.breaking = Falseself.fragments = []self.break_timer = 0self.game_state = 'playing'self.text_fade = 0.0self.draw()def on_key_press(self, event):if event.keysym == 'space' and not self.game_over and not self.game_won and self.on_ground:self.is_charging = Truedef on_key_release(self, event):if event.keysym == 'space' and self.is_charging:self.jump()self.is_charging = Falseself.charge_power = 0def jump(self):self.elasticing = Trueself.elastic_timer = 0self.on_ground = Falseself.player['vx'] = 5 + (self.charge_power * 0.6)self.player['vy'] = -(7 + (self.charge_power * 0.6))def start_break_animation(self):self.breaking = Trueself.break_timer = 0self.fragments = []for i in range(14):angle = random.uniform(0, 2 * math.pi)speed = random.uniform(3.5, 8.0)size = random.randint(4, 8)self.fragments.append({'x': self.player['x'] + self.player['w'] / 2,'y': self.player['y'] + self.player['h'] / 2,'vx': math.cos(angle) * speed,'vy': math.sin(angle) * speed,'size': size,'alpha': 1.0})def _get_faded_color(self, target_color_hex, progress):if not target_color_hex.startswith('#'):color_map = {"red": (255, 0, 0),"gray": (128, 128, 128)}if target_color_hex in color_map:rgb = color_map[target_color_hex]else:rgb = (255, 255, 255)else:h = target_color_hex.lstrip('#')rgb = tuple(int(h[i:i+2], 16) for i in (0, 2, 4))r = int(rgb[0] * progress)g = int(rgb[1] * progress)b = int(rgb[2] * progress)return f'#{r:02x}{g:02x}{b:02x}'def update(self):if self.game_over or self.game_won:if self.breaking:self.break_timer += 1for frag in self.fragments:frag['vy'] += 0.3frag['x'] += frag['vx']frag['y'] += frag['vy']frag['alpha'] = max(0.0, 1.0 - self.break_timer / 30.0)else:passelse:self.on_ground = Falseif self.is_charging:if self.charge_power < self.MAX_CHARGE:self.charge_power += self.CHARGE_RATEself.player['vy'] += self.GRAVITYself.player['x'] += self.player['vx']self.player['y'] += self.player['vy']if self.elasticing:self.elastic_timer += 1if self.elastic_timer >= self.elastic_duration:self.elastic_timer = self.elastic_durationself.elasticing = Falseplayer_bottom = self.player['y'] + self.player['h']player_top = self.player['y']player_left = self.player['x']player_right = self.player['x'] + self.player['w']prev_left = player_left - self.player['vx']prev_right = player_right - self.player['vx']prev_bottom = player_bottom - self.player['vy']for p in self.platforms:plat_top = p['y']plat_left = p['x']plat_right = p['x'] + p['w']plat_bottom = p['y'] + p['h']if player_right > plat_left and player_left < plat_right and player_bottom > plat_top and player_top < plat_bottom:if self.player['vy'] > 0 and prev_bottom <= plat_top:self.player['y'] = plat_top - self.player['h']self.player['vy'] = 0self.player['vx'] = 0self.on_ground = Truebreakelif prev_right <= plat_left and player_right > plat_left:self.player['vx'] = 0self.player['vy'] = 0self.start_break_animation()self.breaking = Trueself.game_over = Truebreakelif prev_left >= plat_right and player_left < plat_right:self.player['vx'] = 0self.player['vy'] = 0self.start_break_animation()self.breaking = Trueself.game_over = Truebreakelse:self.game_over = Truebreakif self.player['x'] > 1200:self.game_won = Trueif self.player['y'] > 500:if not self.breaking:self.start_break_animation()self.breaking = Trueself.game_over = Trueif self.elasticing and self.elastic_timer >= self.elastic_duration:self.elasticing = Falsecurrent_state = 'won' if self.game_won else ('over' if self.game_over else 'playing')if current_state != self.game_state:self.game_state = current_stateself.text_fade = 0.0if self.text_fade < 1.0:self.text_fade += 0.03if self.text_fade > 1.0:self.text_fade = 1.0self.draw()def draw(self):self.canvas.delete("all")for p in self.platforms:self.canvas.create_rectangle(p['x'], p['y'],p['x'] + p['w'], p['y'] + p['h'],fill="#222222", outline="")if not self.breaking:p = self.playerdraw_w, draw_h = self._get_player_draw_dims()draw_x = p['x'] - (draw_w - p['w']) / 2draw_y = p['y'] + (p['h'] - draw_h)self.canvas.create_rectangle(draw_x, draw_y,draw_x + draw_w, draw_y + draw_h,fill="white", outline="")if self.is_charging:ratio = self.charge_power / self.MAX_CHARGEbar_width = ratio * 40if ratio <= 0.5:local_ratio = ratio * 2r = 255g = int(255 * (1 - local_ratio))b = 0else:local_ratio = (ratio - 0.5) * 2r = int(255 - (255 - 139) * local_ratio)g = 0b = 0color_hex = f'#{r:02x}{g:02x}{b:02x}'self.canvas.create_rectangle(p['x'], p['y'] - 10,p['x'] + bar_width, p['y'] - 5,fill=color_hex, outline="")if self.breaking:for frag in self.fragments:alpha = int(frag['alpha'] * 255)color = f"#{alpha:02x}{alpha:02x}{alpha:02x}"self.canvas.create_rectangle(frag['x'], frag['y'],frag['x'] + frag['size'], frag['y'] + frag['size'],fill=color, outline="")if self.game_over:text_color = self._get_faded_color("red", self.text_fade)self.canvas.create_text(580, 200, text="万物皆生,唯你独枯。茫茫天地间,竟全然无你托身之所……", fill=text_color, font=("STKaiti", 30))text_color = self._get_faded_color("grey", self.text_fade)self.canvas.create_text(90, 30, text="R: retry, T: exit", fill=text_color, font=("times", 12))elif self.game_won:text_color = self._get_faded_color("#00ff00", self.text_fade)self.canvas.create_text(450, 200, text="穿尽千山与万水,始知天地本来宽.", fill=text_color, font=("STKaiti", 30))text_color = self._get_faded_color("grey", self.text_fade)self.canvas.create_text(90, 30, text="R: retry, T: exit", fill=text_color, font=("times", 12))else:text_color = self._get_faded_color("white", self.text_fade)self.canvas.create_text(450, 70, text="正入万山圈子里,一山放过一山拦——", fill=text_color, font=("STKaiti", 20))def _get_player_draw_dims(self):base_w = self.player['w']base_h = self.player['h']if self.is_charging:ratio = min(self.charge_power / self.MAX_CHARGE, 1.0)return base_w + ratio * 18, base_h - ratio * 10if self.elasticing:progress = self.elastic_timer / self.elastic_durationeffect = math.sin(progress * math.pi)scale_x = 1.0 + 0.16 * effectscale_y = 1.0 - 0.16 * effectreturn base_w * scale_x, base_h * scale_yreturn base_w, base_hdef game_loop(self):self.update()self.top.after(20, self.game_loop)def restart_game(self, event=None):self.reset_game()def close_game(self, event=None):self.top.destroy()class OffWorkCountdownApp:def __init__(self, root):self.root = rootself.root.title("下班倒计时界面")self.root.geometry("1030x430")self.root.configure(bg=BG_COLOR)self.title_font = (CHINESE_FONT, 18, "bold")self.label_font = (CHINESE_FONT, 12)self.time_font = (CHINESE_FONT, 22, "bold")self.btn_font = (CHINESE_FONT, 11)self.alarms = [{"time": alarm_time, "enabled": True, "fired_date": None}for alarm_time in DEFAULT_ALARMS]self.alarm_reset_date = datetime.now().date()self.alarm_volume = 70self.alarm_message = DEFAULT_ALARM_MESSAGEself.active_alarm = Noneself.alarm_popup = Noneself.alarm_stop_event = Noneself.tray_icon = Noneself.is_exiting = Falseself.root.protocol("WM_DELETE_WINDOW", self.on_window_close)self.root.bind("<Unmap>", self.on_window_minimize)self.create_widgets()self.update_time()self.start_tray_icon()def create_tray_image(self):if Image is None or ImageDraw is None:return Noneimage = Image.new("RGBA", (64, 64), (0, 0, 0, 0))draw = ImageDraw.Draw(image)draw.ellipse((5, 5, 59, 59), fill="#397a9b", outline="#ffdf6b", width=3)draw.text((19, 13), "休", fill="#ffffff")return imagedef start_tray_icon(self):if pystray is None:returntray_image = self.create_tray_image()if tray_image is None:returntry:menu = pystray.Menu(pystray.MenuItem("显示主界面", self.restore_from_tray, default=True),pystray.MenuItem("退出程序", self.exit_from_tray),)self.tray_icon = pystray.Icon("off_work_countdown", tray_image, "下班倒计时", menu)except Exception:self.tray_icon = Nonereturnthreading.Thread(target=self.tray_icon.run, daemon=True).start()def on_window_minimize(self, event=None):if not self.is_exiting and self.root.state() == "iconic":self.root.after(80, self.hide_to_tray)def hide_to_tray(self):if self.is_exiting:returnif self.tray_icon is None:self.show_main_window()returnself.root.withdraw()def on_window_close(self):if self.tray_icon is None:self.exit_application()else:self.hide_to_tray()def restore_from_tray(self, icon=None, item=None):self.root.after(0, self.show_main_window)def show_main_window(self):if self.is_exiting:returnself.root.deiconify()self.root.state("normal")self.root.lift()self.root.focus_force()def exit_from_tray(self, icon=None, item=None):self.root.after(0, self.exit_application)def exit_application(self):if self.is_exiting:returnself.is_exiting = Trueself.stop_alarm()if self.tray_icon:self.tray_icon.stop()self.root.destroy()def create_widgets(self):main = tk.Frame(self.root, bg=BG_COLOR)main.pack(fill="both", expand=True, padx=15, pady=10)alarm_panel = tk.Frame(main, bg=BG_COLOR, width=230)alarm_panel.pack(side="left", fill="y", padx=(0, 18))alarm_panel.pack_propagate(False)tk.Label(alarm_panel, text="休息提醒", font=self.title_font,bg=BG_COLOR, fg=TITLE_COLOR).pack(pady=(0, 10), anchor="w")self.alarm_list = tk.Frame(alarm_panel, bg=BG_COLOR)self.alarm_list.pack(fill="both", expand=True)RoundedButton(alarm_panel, text="⚙", font=(CHINESE_FONT, 17),command=self.open_alarm_settings).pack(side="bottom", pady=(8, 0), anchor="w")tk.Frame(main, width=2, bg=DIVIDER_COLOR).pack(side="left", fill="y", padx=(0, 18))left = tk.Frame(main, bg=BG_COLOR)left.pack(side="left", fill="both", expand=True, padx=(0, 18))tk.Label(left, text=" ~下班倒计时~ ",font=self.title_font, bg=BG_COLOR, fg=TITLE_COLOR).pack(pady=(0, 20), anchor="w")f1 = tk.Frame(left, bg=BG_COLOR)f1.pack(pady=8, fill="x")tk.Label(f1, text="离下班还有:", font=self.label_font,bg=BG_COLOR, fg=FG_COLOR).pack(anchor="w")self.lbl_off_work = tk.Label(f1, text="计算中...", font=self.time_font,bg=BG_COLOR, fg=ACCENT_RED)self.lbl_off_work.pack(anchor="w", pady=(5, 0))f2 = tk.Frame(left, bg=BG_COLOR)f2.pack(pady=8, fill="x")tk.Label(f2, text="离周末还有:", font=self.label_font,bg=BG_COLOR, fg=FG_COLOR).pack(anchor="w")self.lbl_friday = tk.Label(f2, text="计算中...", font=self.time_font,bg=BG_COLOR, fg=ACCENT_CYAN)self.lbl_friday.pack(anchor="w", pady=(5, 0))f3 = tk.Frame(left, bg=BG_COLOR)f3.pack(pady=8, fill="x")tk.Label(f3, text="离节假日还有:", font=self.label_font,bg=BG_COLOR, fg=FG_COLOR).pack(anchor="w")self.lbl_holiday = tk.Label(f3, text="计算中...", font=(CHINESE_FONT, 16),bg=BG_COLOR, fg=ACCENT_YELLOW)self.lbl_holiday.pack(anchor="w", pady=(5, 0))RoundedButton(left, text="⚙", font=(CHINESE_FONT, 17),command=self.open_settings).pack(side="bottom", pady=(8, 0), anchor="w")tk.Frame(main, width=2, bg=DIVIDER_COLOR).pack(side="left", fill="y", padx=(0, 18))right = tk.Frame(main, bg=BG_COLOR, width=390, height=390)right.pack(side="right", fill="y")right.pack_propagate(False)mask = tk.Frame(right, bg=MASK_COLOR, width=360, height=370)mask.place(relx=0.5, rely=0.5, anchor="center")self.cat_img = Noneif os.path.exists(CAT_IMAGE_PATH):try:self.cat_img = create_rounded_image(CAT_IMAGE_PATH, size=(320, 320), radius=10)if self.cat_img:img_label = tk.Label(right, image=self.cat_img, bg=MASK_COLOR, bd=0, cursor="hand2")img_label.place(relx=0.5, rely=0.5, anchor="center")img_label.bind("<Button-1>", lambda e: self.open_game())else:from PIL import Image, ImageTkimg = Image.open(CAT_IMAGE_PATH)img = img.resize((320, 320), Image.LANCZOS)self.cat_img = ImageTk.PhotoImage(img)img_label = tk.Label(right, image=self.cat_img, bg=MASK_COLOR, bd=0, cursor="hand2")img_label.place(relx=0.5, rely=0.5, anchor="center")img_label.bind("<Button-1>", lambda e: self.open_game())except ImportError:tk.Label(right, text="(需安装Pillow才能显示图片哦)\npip install pillow",bg=MASK_COLOR, fg=FG_COLOR, font=(CHINESE_FONT, 11),justify="center").place(relx=0.5, rely=0.5, anchor="center")else:label = tk.Label(right, text="这里本该有只哈基米。点击进入游戏",bg=MASK_COLOR, fg=FG_COLOR, font=(CHINESE_FONT, 10),justify="center", cursor="hand2")label.place(relx=0.5, rely=0.5, anchor="center")label.bind("<Button-1>", lambda e: self.open_game())def update_time(self):global off_work_time_strnow = datetime.now()self.reset_alarms_for_new_day(now)self.check_alarms(now)self.refresh_alarm_list(now)if now.weekday() >= 5:self.lbl_off_work.config(text="休息中", fg=ACCENT_CYAN)else:text, color = calculate_time_diff(off_work_time_str)self.lbl_off_work.config(text=text, fg=color)text, color = get_weekend_diff(off_work_time_str)self.lbl_friday.config(text=text, fg=color)days, name = get_next_holiday_info()self.lbl_holiday.config(text=f"{days}天 ({name})")self.root.after(1000, self.update_time)def reset_alarms_for_new_day(self, now):if now.date() == self.alarm_reset_date:returnfor alarm in self.alarms:alarm["fired_date"] = Noneself.alarm_reset_date = now.date()def refresh_alarm_list(self, now=None):now = now or datetime.now()today = now.date()for child in self.alarm_list.winfo_children():child.destroy()for alarm in self.alarms:is_done = alarm["fired_date"] == todaycolor = "#666666" if is_done else (FG_COLOR if alarm["enabled"] else "#555555")status = "已提醒" if is_done else ("开启" if alarm["enabled"] else "关闭")row = tk.Frame(self.alarm_list, bg=BG_COLOR)row.pack(fill="x", pady=2)tk.Label(row, text=alarm["time"], width=7, anchor="w",font=(CHINESE_FONT, 14, "bold"), bg=BG_COLOR, fg=color).pack(side="left")meal_label = ALARM_MEAL_LABELS.get(alarm["time"])if meal_label:tk.Label(row, text=meal_label, padx=5, pady=1,font=(CHINESE_FONT, 9), bg=BG_COLOR, fg=color,highlightthickness=1, highlightbackground=DIVIDER_COLOR,highlightcolor=DIVIDER_COLOR).pack(side="left", padx=(2, 0))tk.Label(row, text=status, width=6, anchor="e",font=(CHINESE_FONT, 9), bg=BG_COLOR, fg=color).pack(side="right")def check_alarms(self, now):current_time = now.strftime("%H:%M")for index, alarm in enumerate(self.alarms):if (alarm["enabled"] and alarm["time"] == current_timeand alarm["fired_date"] != now.date()):alarm["fired_date"] = now.date()self.start_alarm(index)def start_alarm(self, index):if self.active_alarm is not None:returnself.active_alarm = indexself.alarm_stop_event = threading.Event()threading.Thread(target=self.play_alarm_sound, args=(self.alarm_stop_event,), daemon=True).start()self.show_alarm_popup()def play_alarm_sound(self, stop_event):if winsound is None:returnwav_path = os.path.join(tempfile.gettempdir(), "rest_alarm.wav")try:sample_rate = 44100duration = 0.5frequency = 880amplitude = int(32767 * max(0, min(100, self.alarm_volume)) / 100)with wave.open(wav_path, "w") as audio:audio.setnchannels(1)audio.setsampwidth(2)audio.setframerate(sample_rate)frames = bytearray()for sample in range(int(sample_rate * duration)):value = int(amplitude * math.sin(2 * math.pi * frequency * sample / sample_rate))frames.extend(struct.pack("<h", value))audio.writeframes(frames)end_time = time.time() + 40while time.time() < end_time and not stop_event.is_set():winsound.PlaySound(wav_path, winsound.SND_FILENAME)except Exception as error:print(f"播放提醒音失败: {error}")def show_alarm_popup(self):if self.alarm_popup and self.alarm_popup.winfo_exists():returnpopup = tk.Toplevel(self.root)self.alarm_popup = popuptransparent_color = "#010101"popup.overrideredirect(True)popup_width = 1000popup_height = 260popup.geometry("{}x{}+{}+{}".format(popup_width, popup_height,max((popup.winfo_screenwidth() - popup_width) // 2, 0),max((popup.winfo_screenheight() - popup_height) // 2, 0)))popup.configure(bg=transparent_color)popup.attributes("-topmost", True)popup.protocol("WM_DELETE_WINDOW", self.stop_alarm)try:popup.attributes("-transparentcolor", transparent_color)except tk.TclError:passmessage = self.alarm_message.rstrip("!!") + "!"canvas = tk.Canvas(popup, bg=transparent_color, highlightthickness=0,width=popup_width, height=popup_height, cursor="hand2")canvas.pack(fill="both", expand=True)canvas.bind("<Button-1>", lambda event: self.close_alarm_by_text(canvas, event))self.draw_alarm_text(canvas, message, 0)popup.after(40000, self.stop_alarm)def close_alarm_by_text(self, canvas, event):text_items = canvas.find_withtag("alarm-text")for text_item in text_items:text_box = canvas.bbox(text_item)if text_box and text_box[0] <= event.x <= text_box[2] and text_box[1] <= event.y <= text_box[3]:self.stop_alarm()returndef draw_alarm_text(self, canvas, message, frame):if not self.alarm_popup or not self.alarm_popup.winfo_exists():returncanvas.delete("all")width = max(canvas.winfo_width(), 1000)height = max(canvas.winfo_height(), 260)text_specs = [(0.14, 0.18, "#ff2020", 28, 4, 0),(0.50, 0.16, "#ff4d4d", 42, 7, 1),(0.86, 0.20, "#dc143c", 24, 5, 0),(0.10, 0.76, "#ff3333", 38, 9, 1),(0.50, 0.84, "#ff6666", 30, 6, 0),(0.89, 0.73, "#b22222", 46, 11, 1),]for x_ratio, y_ratio, color, size, blink_period, blink_phase in text_specs:if (frame // blink_period + blink_phase) % 2 == 0:canvas.create_text(width * x_ratio, height * y_ratio, text=message,fill=color, font=(CHINESE_FONT, size, "bold"),tags="alarm-text")self.alarm_popup.after(80, self.draw_alarm_text,canvas, message, frame + 1)def stop_alarm(self):if self.alarm_stop_event:self.alarm_stop_event.set()if self.alarm_popup and self.alarm_popup.winfo_exists():self.alarm_popup.destroy()self.alarm_popup = Noneself.active_alarm = Nonedef open_alarm_settings(self):top = tk.Toplevel(self.root)top.title("休息提醒设置")top.geometry("560x560")top.minsize(560, 560)top.resizable(False, False)top.configure(bg=POPUP_BG)top.transient(self.root)tk.Label(top, text="闹铃时间、开关和提醒文案",bg=POPUP_BG, fg=TITLE_COLOR, font=(CHINESE_FONT, 16, "bold")).pack(pady=(10, 5))header = tk.Frame(top, bg=POPUP_BG)header.pack(fill="x", padx=22)for text, width in (("时间", 12), ("开启", 8), ("状态", 10)):tk.Label(header, text=text, width=width, anchor="w", bg=POPUP_BG,fg=FG_COLOR, font=self.label_font).pack(side="left")rows = []for alarm in self.alarms:row = tk.Frame(top, bg=POPUP_BG)row.pack(fill="x", padx=22, pady=1)time_var = tk.StringVar(value=alarm["time"])enabled_var = tk.BooleanVar(value=alarm["enabled"])tk.Entry(row, textvariable=time_var, width=10, justify="center",bg="#3c3f41", fg="white", insertbackground="white").pack(side="left", padx=(0, 18))tk.Checkbutton(row, variable=enabled_var, bg=POPUP_BG, activebackground=POPUP_BG,selectcolor="#3c3f41").pack(side="left", padx=(0, 28))tk.Label(row, text="HH:MM", bg=POPUP_BG, fg="#888888").pack(side="left")rows.append((alarm, time_var, enabled_var))message_frame = tk.Frame(top, bg=POPUP_BG)message_frame.pack(fill="x", padx=22, pady=(10, 3))tk.Label(message_frame, text="显示字样", bg=POPUP_BG, fg=FG_COLOR).pack(side="left")message_var = tk.StringVar(value=self.alarm_message)tk.Entry(message_frame, textvariable=message_var, width=34,bg="#3c3f41", fg="white", insertbackground="white").pack(side="left", padx=12)volume_frame = tk.Frame(top, bg=POPUP_BG)volume_frame.pack(fill="x", padx=22, pady=2)tk.Label(volume_frame, text="闹铃响度", bg=POPUP_BG, fg=FG_COLOR).pack(side="left")volume_var = tk.IntVar(value=self.alarm_volume)tk.Scale(volume_frame, from_=0, to=100, variable=volume_var, orient="horizontal",length=300, bg=POPUP_BG, fg=FG_COLOR, troughcolor="#3c3f41",highlightthickness=0).pack(side="left", padx=12)def save_alarm_settings():for alarm, time_var, enabled_var in rows:try:hour, minute = map(int, time_var.get().strip().split(":"))if not (0 <= hour < 24 and 0 <= minute < 60):raise ValueErrorexcept ValueError:messagebox.showerror("错误", "闹铃时间请使用 HH:MM 格式。", parent=top)returnalarm["time"] = f"{hour:02d}:{minute:02d}"alarm["enabled"] = enabled_var.get()self.alarm_volume = volume_var.get()self.alarm_message = message_var.get().strip() or DEFAULT_ALARM_MESSAGEself.refresh_alarm_list()top.destroy()tk.Button(top, text="保存设置", command=save_alarm_settings,bg=BUTTON_COLOR, fg=BUTTON_TEXT, font=self.label_font,relief="flat").pack(pady=10)def open_settings(self):top = tk.Toplevel(self.root)top.title("设置")top.geometry("320x160")top.configure(bg=POPUP_BG)tk.Label(top, text="请输入下班时间 (HH:MM:SS):",bg=POPUP_BG, fg=FG_COLOR, font=(CHINESE_FONT, 10)).pack(pady=15)entry = tk.Entry(top, font=(CHINESE_FONT, 12), justify="center",bg="#3c3f41", fg="white", insertbackground="white")entry.insert(0, off_work_time_str)entry.pack(pady=5)entry.focus_set()def save():global off_work_time_strnew_time = entry.get()try:h, m, s = map(int, new_time.split(':'))if 0 <= h < 24 and 0 <= m < 60 and 0 <= s < 60:off_work_time_str = new_timemessagebox.showinfo("成功", "下班时间已更新!")top.destroy()else:messagebox.showerror("错误", "时间数值不合法!")except Exception:messagebox.showerror("错误", "格式错误,请使用 HH:MM:SS")tk.Button(top, text="确定", command=save,bg=BUTTON_COLOR, fg=BUTTON_TEXT, font=(CHINESE_FONT, 10),relief="flat").pack(pady=15)def open_game(self):MiniGameApp(self.root)if __name__ == "__main__":root = tk.Tk()app = OffWorkCountdownApp(root)root.mainloop()
往期:
本文来自网友投稿或网络内容,如有侵犯您的权益请联系我们删除,联系邮箱:wyl860211@qq.com 。