""" 述职计时器 - 倒计时应用程序 使用 PySide6 构建的美观倒计时器 """ import sys import json from pathlib import Path from PySide6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QDialog, QSpinBox, QCheckBox, QSlider, QFormLayout, QGroupBox ) from PySide6.QtCore import QTimer, Qt, QPropertyAnimation, QEasingCurve, Property from PySide6.QtGui import QFont from PySide6.QtMultimedia import QSoundEffect, QAudioOutput, QMediaPlayer from PySide6.QtCore import QUrl class ConfigDialog(QDialog): """配置对话框""" def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("其它设置") self.setModal(True) self.resize(400, 300) layout = QFormLayout() # 自定义倒计时时间(秒) self.custom_time_spin = QSpinBox() self.custom_time_spin.setRange(1, 3600) self.custom_time_spin.setValue(300) self.custom_time_spin.setSuffix(" 秒") layout.addRow("自定义倒计时:", self.custom_time_spin) # 提前告警时间(秒) self.alert_time_spin = QSpinBox() self.alert_time_spin.setRange(0, 600) self.alert_time_spin.setValue(60) self.alert_time_spin.setSuffix(" 秒") layout.addRow("提前告警:", self.alert_time_spin) # 置顶选项 self.topmost_checkbox = QCheckBox() self.topmost_checkbox.setChecked(True) layout.addRow("窗口置顶:", self.topmost_checkbox) # 透明度滑块 self.opacity_slider = QSlider(Qt.Horizontal) self.opacity_slider.setRange(10, 100) self.opacity_slider.setValue(90) self.opacity_slider.setTickPosition(QSlider.TicksBelow) self.opacity_slider.setTickInterval(10) self.opacity_label = QLabel("90%") self.opacity_slider.valueChanged.connect( lambda v: self.opacity_label.setText(f"{v}%") ) opacity_layout = QHBoxLayout() opacity_layout.addWidget(self.opacity_slider) opacity_layout.addWidget(self.opacity_label) layout.addRow("窗口透明度:", opacity_layout) # 测试告警按钮 self.test_alert_button = QPushButton("测试告警声音") self.test_alert_button.clicked.connect(self.test_alert_sound) self.test_alert_button.setStyleSheet(""" QPushButton { background-color: #3498db; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-size: 14px; } QPushButton:hover { background-color: #2980b9; } """) layout.addRow("", self.test_alert_button) # 按钮 button_layout = QHBoxLayout() self.ok_button = QPushButton("确定") self.ok_button.clicked.connect(self.accept) self.ok_button.setStyleSheet(""" QPushButton { background-color: #4CAF50; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-size: 14px; } QPushButton:hover { background-color: #45a049; } """) self.cancel_button = QPushButton("取消") self.cancel_button.clicked.connect(self.reject) self.cancel_button.setStyleSheet(""" QPushButton { background-color: #f44336; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-size: 14px; } QPushButton:hover { background-color: #da190b; } """) self.exit_button = QPushButton("退出") self.exit_button.clicked.connect(self.exit_application) self.exit_button.setStyleSheet(""" QPushButton { background-color: #95a5a6; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-size: 14px; } QPushButton:hover { background-color: #7f8c8d; } """) button_layout.addWidget(self.ok_button) button_layout.addWidget(self.cancel_button) button_layout.addWidget(self.exit_button) layout.addRow(button_layout) self.setLayout(layout) # 样式 self.setStyleSheet(""" QDialog { background-color: #f5f5f5; } QLabel { font-size: 13px; color: #333; } QSpinBox { padding: 5px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; } """) def test_alert_sound(self): """测试告警声音""" try: QApplication.beep() # 循环播放2次 QTimer.singleShot(500, QApplication.beep) QTimer.singleShot(1000, QApplication.beep) except Exception as e: print(f"播放测试告警音失败: {e}") def exit_application(self): """退出应用程序""" self.accept() # 关闭对话框 # 退出整个应用程序 QApplication.quit() class CountdownTimer(QMainWindow): """主倒计时器窗口""" def __init__(self): super().__init__() # 配置 self.config_file = Path.home() / ".countdown_timer_config.json" self.load_config() # 状态 self.remaining_seconds = 0 self.total_seconds = 0 self.is_running = False self.alert_played = False self.alert_loop_count = 0 # 定时器 self.timer = QTimer() self.timer.timeout.connect(self.update_timer) # 闪烁动画定时器 self.blink_timer = QTimer() self.blink_timer.timeout.connect(self.toggle_blink) self.blink_state = True # 音频播放器(用于告警提示音) self.media_player = None self.init_ui() self.apply_config() # 鼠标位置跟踪(用于窗口移动) self.drag_position = None # 状态管理 self.current_state = "normal" # normal 或 mini self.state_change_timer = None def mousePressEvent(self, event): """鼠标按下事件 - 开始移动窗口""" if event.button() == Qt.LeftButton: self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft() event.accept() def mouseMoveEvent(self, event): """鼠标移动事件 - 移动窗口""" if self.drag_position is not None and event.buttons() == Qt.LeftButton: self.move(event.globalPosition().toPoint() - self.drag_position) event.accept() def mouseReleaseEvent(self, event): """鼠标释放事件 - 停止移动窗口""" if event.button() == Qt.LeftButton: self.drag_position = None event.accept() def load_config(self): """加载配置""" default_config = { "alert_seconds": 60, "topmost": True, "opacity": 90, "custom_seconds": 300 } if self.config_file.exists(): try: with open(self.config_file, 'r', encoding='utf-8') as f: self.config = json.load(f) # 合并默认值 for key, value in default_config.items(): if key not in self.config: self.config[key] = value except: self.config = default_config else: self.config = default_config def save_config(self): """保存配置""" try: with open(self.config_file, 'w', encoding='utf-8') as f: json.dump(self.config, f, ensure_ascii=False, indent=2) except: pass def apply_config(self): """应用配置""" # 透明度 self.setWindowOpacity(self.config["opacity"] / 100.0) # 置顶 if self.config["topmost"]: self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) else: self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint) self.show() # 保存初始窗口位置(等待窗口显示完成) QTimer.singleShot(100, self.save_initial_position) def save_initial_position(self): """保存初始窗口位置""" self.initial_pos = self.pos() def init_ui(self): """初始化UI""" # 设置无边框窗口 self.setWindowFlags(Qt.FramelessWindowHint) # 设置最小窗口大小(初始状态) self.setMinimumSize(250, 100) # 设置初始窗口大小(倒计时模式) self.resize(500, 300) # 中心部件 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout() main_layout.setSpacing(20) main_layout.setContentsMargins(30, 30, 30, 30) # 倒计时显示 self.time_label = QLabel("00:00") self.time_label.setAlignment(Qt.AlignCenter) self.time_label.setStyleSheet(""" QLabel { font-size: 72px; font-weight: bold; color: #3498db; font-family: 'Courier New', monospace; background-color: #ecf0f1; border-radius: 10px; padding: 20px; } """) main_layout.addWidget(self.time_label) # 完成提示标签(初始隐藏) self.finish_label = QLabel("时间已到") self.finish_label.setAlignment(Qt.AlignCenter) self.finish_label.setStyleSheet(""" QLabel { font-size: 48px; font-weight: bold; color: #e74c3c; background-color: #fee; border-radius: 10px; padding: 20px; } """) self.finish_label.hide() main_layout.addWidget(self.finish_label) # 创建可折叠的按钮区域 self.button_group = QGroupBox() self.button_group.setStyleSheet(""" QGroupBox { border: 2px solid #bdc3c7; border-radius: 8px; margin-top: 10px; padding-top: 10px; background-color: #f8f9fa; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; } """) # 折叠/展开按钮 self.collapse_button = QPushButton("▼") self.collapse_button.setFixedSize(30, 30) self.collapse_button.setStyleSheet(""" QPushButton { background-color: #3498db; color: white; border: none; border-radius: 4px; font-size: 12px; font-weight: bold; } QPushButton:hover { background-color: #2980b9; } """) self.collapse_button.clicked.connect(self.toggle_button_group) # 按钮布局 button_layout = QHBoxLayout() button_layout.setSpacing(15) # 创建按钮 self.btn_5min = self.create_button("5分钟", lambda: self.start_countdown(300)) self.btn_6min = self.create_button("6分钟", lambda: self.start_countdown(360)) self.btn_other = self.create_button("其它", self.open_config, "#95a5a6") button_layout.addWidget(self.btn_5min) button_layout.addWidget(self.btn_6min) button_layout.addWidget(self.btn_other) # 控制按钮 control_layout = QHBoxLayout() self.btn_pause = self.create_button("暂停", self.pause_countdown, "#f39c12") self.btn_reset = self.create_button("重置", self.reset_countdown, "#e74c3c") self.btn_pause.setEnabled(False) self.btn_reset.setEnabled(False) control_layout.addWidget(self.btn_pause) control_layout.addWidget(self.btn_reset) # 添加到按钮组 group_layout = QVBoxLayout() group_layout.addLayout(button_layout) group_layout.addLayout(control_layout) self.button_group.setLayout(group_layout) # 主布局添加折叠按钮和按钮组 main_layout.addWidget(self.collapse_button) main_layout.addWidget(self.button_group) # 默认折叠按钮区 self.button_group.hide() self.collapse_button.setText("▶") central_widget.setLayout(main_layout) # 窗口样式 self.setStyleSheet(""" QMainWindow { background-color: #ffffff; } """) def create_button(self, text, callback, color="#3498db"): """创建样式化按钮""" button = QPushButton(text) button.clicked.connect(callback) button.setMinimumHeight(20) # 减小按钮高度 button.setMinimumWidth(50) # 设置最小宽度 button.setStyleSheet(f""" QPushButton {{ background-color: {color}; color: white; border: none; border-radius: 4px; font-size: 11px; font-weight: bold; padding: 2px 5px; }} QPushButton:hover {{ background-color: {self.darken_color(color)}; }} QPushButton:pressed {{ background-color: {self.darken_color(color, 0.3)}; }} QPushButton:disabled {{ background-color: #bdc3c7; }} """) return button def darken_color(self, hex_color, factor=0.15): """使颜色变暗""" hex_color = hex_color.lstrip('#') r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) r = int(r * (1 - factor)) g = int(g * (1 - factor)) b = int(b * (1 - factor)) return f"#{r:02x}{g:02x}{b:02x}" def start_countdown(self, seconds): """开始倒计时""" self.remaining_seconds = seconds self.total_seconds = seconds self.is_running = True self.alert_played = False self.alert_loop_count = 0 # 折叠按钮区域 self.button_group.hide() self.collapse_button.setText("▶") # 设置2秒后切换到微缩状态的定时器 if self.state_change_timer: self.state_change_timer.stop() self.state_change_timer = QTimer() self.state_change_timer.timeout.connect(self.switch_to_mini_state) self.state_change_timer.start(2000) # 2秒后切换 # 立即切换到微缩状态 self.switch_to_mini_state() self.timer.start(1000) self.update_display() # 隐藏完成标签,显示倒计时 self.finish_label.hide() self.time_label.show() self.blink_timer.stop() # 更新按钮状态 self.btn_pause.setEnabled(True) self.btn_pause.setText("暂停") self.btn_reset.setEnabled(True) def pause_countdown(self): """暂停/继续倒计时""" if self.is_running: self.timer.stop() self.is_running = False self.btn_pause.setText("继续") else: self.timer.start(1000) self.is_running = True self.btn_pause.setText("暂停") def reset_countdown(self): """重置倒计时""" self.timer.stop() self.blink_timer.stop() self.is_running = False self.remaining_seconds = 0 self.alert_played = False self.alert_loop_count = 0 # 恢复原始窗口大小 self.resize(500, 300) # 恢复窗口置顶状态 self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint) self.show() self.time_label.setText("00:00") self.time_label.show() self.finish_label.hide() self.btn_pause.setEnabled(False) self.btn_pause.setText("暂停") self.btn_reset.setEnabled(False) # 停止音频 if self.media_player: self.media_player.stop() def switch_to_mini_state(self): """切换到微缩状态""" self.current_state = "mini" # 调整窗口大小为微缩模式(200x80像素,增加高度以容纳倒计时) self.resize(200, 80) # 移动窗口位置到距离top 50px,距离右边200px screen = QApplication.primaryScreen().geometry() screen_width = screen.width() screen_height = screen.height() new_x = screen_width - 200 - 200 # 距离右边200px new_y = 50 # 距离top 50px self.move(new_x, new_y) # 隐藏按钮区域 self.button_group.hide() self.collapse_button.setText("▼") # 调整布局边距以适应微缩状态 central_widget = self.centralWidget() main_layout = central_widget.layout() main_layout.setSpacing(5) main_layout.setContentsMargins(10, 10, 10, 10) # 调整倒计时标签大小以适应微缩状态 self.time_label.setStyleSheet(""" QLabel { font-size: 12px; font-weight: bold; color: #3498db; font-family: 'Courier New', monospace; background-color: #ecf0f1; border-radius: 3px; padding: 2px; } """) # 隐藏完成提示标签 self.finish_label.hide() # 确保窗口在最前面 self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) self.show() def switch_to_normal_state(self): """切换到正常状态""" self.current_state = "normal" # 取消2秒定时器(如果存在) if self.state_change_timer and self.state_change_timer.isActive(): self.state_change_timer.stop() # 恢复原始窗口大小 self.resize(500, 300) # 恢复初始窗口位置 if hasattr(self, 'initial_pos') and self.initial_pos.x() >= 0 and self.initial_pos.y() >= 0: self.move(self.initial_pos) # 恢复窗口置顶状态 self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint) self.show() # 恢复布局边距 central_widget = self.centralWidget() main_layout = central_widget.layout() main_layout.setSpacing(20) main_layout.setContentsMargins(30, 30, 30, 30) # 恢复倒计时标签的正常样式 self.time_label.setStyleSheet(""" QLabel { font-size: 72px; font-weight: bold; color: #3498db; font-family: 'Courier New', monospace; background-color: #ecf0f1; border-radius: 10px; padding: 20px; } """) # 如果按钮区域之前是展开的,重新展开 if not self.button_group.isHidden(): self.button_group.hide() self.collapse_button.setText("▶") def update_timer(self): """更新计时器""" if self.remaining_seconds > 0: self.remaining_seconds -= 1 self.update_display() # 检查是否需要播放告警 if (not self.alert_played and self.remaining_seconds <= self.config["alert_seconds"] and self.remaining_seconds > 0): self.play_alert() self.alert_played = True else: # 倒计时结束 self.timer.stop() self.is_running = False self.show_finish() def update_display(self): """更新显示""" minutes = self.remaining_seconds // 60 seconds = self.remaining_seconds % 60 self.time_label.setText(f"{minutes:02d}:{seconds:02d}") # 根据剩余时间改变颜色 if self.remaining_seconds <= 10: self.time_label.setStyleSheet(""" QLabel { font-size: 72px; font-weight: bold; color: #e74c3c; font-family: 'Courier New', monospace; background-color: #fee; border-radius: 10px; padding: 20px; } """) elif self.remaining_seconds <= self.config["alert_seconds"]: self.time_label.setStyleSheet(""" QLabel { font-size: 72px; font-weight: bold; color: #f39c12; font-family: 'Courier New', monospace; background-color: #fef5e7; border-radius: 10px; padding: 20px; } """) else: self.time_label.setStyleSheet(""" QLabel { font-size: 72px; font-weight: bold; color: #3498db; font-family: 'Courier New', monospace; background-color: #ecf0f1; border-radius: 10px; padding: 20px; } """) def play_alert(self): """播放告警提示音""" # 使用系统提示音 # 注意:这里使用一个简单的实现 # 实际使用中,你可以替换为自定义音频文件 try: # 尝试播放系统提示音 QApplication.beep() # 设置循环播放2次(总共3次) self.alert_loop_count = 0 self.alert_timer = QTimer() self.alert_timer.timeout.connect(self.play_alert_loop) self.alert_timer.start(1000) # 每秒播放一次 except Exception as e: print(f"播放告警音失败: {e}") def play_alert_loop(self): """循环播放告警""" if self.alert_loop_count < 2: QApplication.beep() self.alert_loop_count += 1 else: self.alert_timer.stop() def show_finish(self): """显示完成提示""" # 调整窗口大小为缩小模式(150x50像素) self.resize(150, 50) # 移动窗口位置到距离top 30px,距离右边边缘30px screen = QApplication.primaryScreen().geometry() screen_width = screen.width() screen_height = screen.height() new_x = screen_width - 150 - 30 # 距离右边30px new_y = 30 # 距离top 30px self.move(new_x, new_y) # 确保窗口在最前面 self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) self.show() self.time_label.hide() self.finish_label.show() # 开始闪烁动画 self.blink_state = True self.blink_timer.start(500) # 每500ms切换一次 # 播放提示音 QApplication.beep() self.btn_pause.setEnabled(False) def toggle_blink(self): """切换闪烁状态""" if self.blink_state: self.finish_label.setStyleSheet(""" QLabel { font-size: 48px; font-weight: bold; color: #e74c3c; background-color: #fee; border-radius: 10px; padding: 20px; } """) else: self.finish_label.setStyleSheet(""" QLabel { font-size: 52px; font-weight: bold; color: #c0392b; background-color: #fadbd8; border-radius: 10px; padding: 25px; } """) self.blink_state = not self.blink_state def toggle_button_group(self): """切换按钮区域的折叠/展开状态""" # 如果当前是微缩状态,点击箭头暂停倒计时并切换回正常状态 if self.current_state == "mini": # 暂停倒计时 self.timer.stop() self.is_running = False self.btn_pause.setText("暂停") # 切换回正常状态 self.switch_to_normal_state() return # 如果当前是正常状态 if self.button_group.isHidden(): self.button_group.show() self.collapse_button.setText("▼") # 如果正在运行倒计时,恢复原始窗口大小 if self.is_running: self.switch_to_normal_state() else: self.button_group.hide() self.collapse_button.setText("▶") # 如果正在运行倒计时,暂停倒计时并切换到微缩状态 if self.is_running: # 暂停倒计时 self.timer.stop() self.is_running = False # 切换到微缩状态 self.switch_to_mini_state() # 按钮文本保持为"暂停",因为倒计时已经被暂停 self.btn_pause.setText("暂停") def open_config(self): """打开配置对话框""" dialog = ConfigDialog(self) # 加载当前配置 dialog.custom_time_spin.setValue(self.config["custom_seconds"]) dialog.alert_time_spin.setValue(self.config["alert_seconds"]) dialog.topmost_checkbox.setChecked(self.config["topmost"]) dialog.opacity_slider.setValue(self.config["opacity"]) if dialog.exec() == QDialog.Accepted: # 保存配置 self.config["custom_seconds"] = dialog.custom_time_spin.value() self.config["alert_seconds"] = dialog.alert_time_spin.value() self.config["topmost"] = dialog.topmost_checkbox.isChecked() self.config["opacity"] = dialog.opacity_slider.value() self.save_config() self.apply_config() # 启动自定义倒计时 self.start_countdown(self.config["custom_seconds"]) def main(): """主函数""" app = QApplication(sys.argv) # 设置应用程序样式 app.setStyle("Fusion") window = CountdownTimer() window.show() sys.exit(app.exec()) if __name__ == "__main__": main()