diff --git a/.gitignore b/.gitignore index ff86efa..4f8501b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ __pycache__/ build/ dist/ *.spec +!dist/*.exe # IDE .idea/ diff --git a/README.md b/README.md index 64adeed..a2ee0cf 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,15 @@ #### 1. 正常状态 - 显示倒计时时间和完整的按钮区域 - 可以通过拖动窗口的任何位置来移动窗口 -- 点击"▼"箭头可以折叠/展开按钮区域 +- 显示按钮区域 - 窗口大小:500×300像素 #### 2. 微缩状态 - 点击"5分钟"或"6分钟"按钮开始倒计时后,延迟2秒自动切换到微缩状态 -- 只显示倒计时时间和"▼"箭头 +- 只显示倒计时时间 - 窗口大小:200×80像素 - 位置:距离屏幕上边缘50像素,距离右边缘200像素 -- 点击"▼"箭头可以暂停倒计时并切换回正常状态 +- 双击界面后暂停倒计时,并切换回正常状态 ### 快捷按钮 - **5分钟**:开始5分钟倒计时 @@ -28,7 +28,7 @@ ### 配置功能 点击"其它"按钮可以打开配置对话框: - **自定义倒计时**:设置自定义倒计时时间(秒) -- **提前告警**:设置倒计时结束前多少秒播放告警提示音 +- **提前告警**:设置倒计时结束前多少秒播放告警提示音,默认30秒 - **窗口置顶**:设置窗口是否置顶 - **窗口透明度**:设置窗口透明度(10%-100%) - **测试告警**:测试告警提示音 @@ -43,8 +43,8 @@ 1. 运行程序后,默认显示正常状态 2. 点击"5分钟"或"6分钟"按钮开始倒计时 -3. 2秒后自动切换到微缩状态,只显示时间和箭头 -4. 在微缩状态下点击箭头可以暂停并回到正常状态 +3. 2秒后自动切换到微缩状态,只显示时间 +4. 在微缩状态下双击可以暂停并回到正常状态 5. 需要退出程序时,点击"其它"→"退出" ## 技术栈 diff --git a/alarm.wav b/alarm.wav new file mode 100644 index 0000000..8511ad6 Binary files /dev/null and b/alarm.wav differ diff --git a/countdown.exe b/countdown.exe new file mode 100644 index 0000000..89bdf98 Binary files /dev/null and b/countdown.exe differ diff --git a/countdown.py b/countdown.py index cbedc6a..7ebb457 100644 --- a/countdown.py +++ b/countdown.py @@ -1,832 +1,394 @@ -""" -述职计时器 - 倒计时应用程序 -使用 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 +import os +from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, + QPushButton, QLabel, QDialog, QFormLayout, + QSpinBox, QCheckBox, QSlider, QGraphicsOpacityEffect) +from PySide6.QtCore import Qt, QTimer, QPoint, QPropertyAnimation, QEasingCurve, QSequentialAnimationGroup, QVariantAnimation +from PySide6.QtGui import QFont, QColor, QMouseEvent +from PySide6.QtMultimedia import QSoundEffect from PySide6.QtCore import QUrl class ConfigDialog(QDialog): - """配置对话框""" - - def __init__(self, parent=None): + def __init__(self, parent=None, config=None): super().__init__(parent) - self.setWindowTitle("其它设置") - self.setModal(True) - self.resize(400, 300) + self.setWindowTitle("配置") + self.config = config + self.init_ui() + + def init_ui(self): + layout = QFormLayout(self) - layout = QFormLayout() + # 自定义倒计时 + self.custom_time = QSpinBox() + self.custom_time.setRange(1, 3600) + self.custom_time.setValue(self.config['duration']) - # 自定义倒计时时间(秒) - 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) + btn_start = QPushButton("开始倒计时") + btn_start.setStyleSheet("background-color: #27ae60; color: white;") + btn_start.clicked.connect(self.start_countdown) - # 提前告警时间(秒) - 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) + time_layout = QHBoxLayout() + time_layout.addWidget(self.custom_time) + time_layout.addWidget(btn_start) + time_widget = QWidget() + time_widget.setLayout(time_layout) + layout.addRow("自定义秒数:", time_widget) + + # 提前告警 + self.alarm_offset = QSpinBox() + self.alarm_offset.setRange(0, 300) + self.alarm_offset.setValue(self.config['alarm_offset']) + layout.addRow("提前告警(秒):", self.alarm_offset) + + # 窗口置顶 + self.stay_on_top = QCheckBox() + self.stay_on_top.setChecked(self.config['stay_on_top']) + layout.addRow("窗口置顶:", self.stay_on_top) + + # 透明度 + self.opacity = QSlider(Qt.Horizontal) + self.opacity.setRange(10, 100) + self.opacity.setValue(int(self.config['opacity'] * 100)) + layout.addRow("透明度:", self.opacity) + + # 微缩窗口位置 + self.right_margin = QSpinBox() + self.right_margin.setRange(0, 1000) + self.right_margin.setValue(self.config['right_margin']) + layout.addRow("距离右边缘(像素):", self.right_margin) + + self.top_margin = QSpinBox() + self.top_margin.setRange(0, 1000) + self.top_margin.setValue(self.config['top_margin']) + layout.addRow("距离上边缘(像素):", self.top_margin) + + # 按钮组 + btn_test = QPushButton("测试告警音") + btn_test.clicked.connect(self.parent().play_alarm) + layout.addRow(btn_test) + + btn_exit = QPushButton("退出程序") + btn_exit.setStyleSheet("background-color: #ff4d4f; color: white;") + btn_exit.clicked.connect(QApplication.instance().quit) + layout.addRow(btn_exit) + + save_btn = QPushButton("确认并保存") + save_btn.clicked.connect(self.save_and_close) + layout.addRow(save_btn) + + def save_and_close(self): + self.config['duration'] = self.custom_time.value() + self.config['alarm_offset'] = self.alarm_offset.value() + self.config['stay_on_top'] = self.stay_on_top.isChecked() + self.config['opacity'] = self.opacity.value() / 100.0 + self.config['right_margin'] = self.right_margin.value() + self.config['top_margin'] = self.top_margin.value() + self.accept() + + def start_countdown(self): + self.save_and_close() + self.parent().start_countdown(self.custom_time.value()) + + +class TimerApp(QWidget): + def __init__(self): + super().__init__() + self.config = { + 'duration': 300, + 'alarm_offset': 30, + 'stay_on_top': True, + 'opacity': 1.0, + 'right_margin': 200, # 距离右边缘 + 'top_margin': 50 # 距离上边缘 + } + self.remaining_time = 0 + self.is_running = False + self.state = "NORMAL" # NORMAL or MINI + self.drag_position = QPoint() - # 置顶选项 - 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(""" + self.init_ui() + self.setup_timers() + self.setup_audio() + self.apply_config() + + def init_ui(self): + self.setWindowFlags(Qt.FramelessWindowHint) + self.setAttribute(Qt.WA_TranslucentBackground) + self.setStyleSheet(""" + QWidget#MainFrame { + background-color: #2c3e50; + border-radius: 15px; + border: 2px solid #34495e; + } + QLabel { color: #ecf0f1; font-family: 'Segoe UI', Arial; } QPushButton { background-color: #3498db; color: white; - border: none; - padding: 8px 16px; - border-radius: 4px; - font-size: 14px; - } - QPushButton:hover { - background-color: #2980b9; + border-radius: 5px; + padding: 8px; + font-weight: bold; } + 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() + self.main_layout = QVBoxLayout(self) + self.frame = QWidget() + self.frame.setObjectName("MainFrame") + self.main_layout.addWidget(self.frame) + + self.content_layout = QVBoxLayout(self.frame) -class CountdownTimer(QMainWindow): - """主倒计时器窗口""" - - def __init__(self): - super().__init__() + # 标题标签 + self.label_title = QLabel("述职倒计时") + self.label_title.setAlignment(Qt.AlignCenter) + self.label_title.setFont(QFont("Microsoft YaHei", 24, QFont.Bold)) + self.content_layout.addWidget(self.label_title) + + # 倒计时显示 + self.label_time = QLabel("00:00") + self.label_time.setAlignment(Qt.AlignCenter) + self.label_time.setFont(QFont("Consolas", 60, QFont.Bold)) + self.label_time.setMinimumSize(120, 40) # 设置最小尺寸 + self.label_time.setMaximumSize(480, 180) # 设置最大尺寸 + self.content_layout.addWidget(self.label_time) + + # 按钮区域 + self.btn_area = QWidget() + self.btn_layout = QHBoxLayout(self.btn_area) + self.btn_5m = QPushButton("5分钟") + self.btn_6m = QPushButton("6分钟") + self.btn_other = QPushButton("其它") - # 配置 - self.config_file = Path.home() / ".countdown_timer_config.json" - self.load_config() + self.btn_5m.clicked.connect(lambda: self.start_countdown(300)) + self.btn_6m.clicked.connect(lambda: self.start_countdown(360)) + self.btn_other.clicked.connect(self.open_config) - # 状态 - self.remaining_seconds = 0 - self.total_seconds = 0 - self.is_running = False - self.alert_played = False - self.alert_loop_count = 0 + self.btn_layout.addWidget(self.btn_5m) + self.btn_layout.addWidget(self.btn_6m) + self.btn_layout.addWidget(self.btn_other) + self.content_layout.addWidget(self.btn_area) + + self.set_normal_state() + + def setup_timers(self): + self.timer = QTimer(self) + self.timer.timeout.connect(self.update_countdown) - # 定时器 - 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 + self.mini_timer = QTimer(self) + self.mini_timer.setSingleShot(True) + self.mini_timer.timeout.connect(self.set_mini_state) + + def setup_audio(self): + self.sound = QSoundEffect() + if getattr(sys, 'frozen', False): + base_path = sys._MEIPASS 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 - + base_path = os.path.dirname(__file__) + alarm_file = os.path.join(base_path, "alarm.wav") + if os.path.exists(alarm_file): + self.sound.setSource(QUrl.fromLocalFile(alarm_file)) + self.sound.setLoopCount(3) + self.sound.setVolume(0.8) + def apply_config(self): - """应用配置""" - # 透明度 - self.setWindowOpacity(self.config["opacity"] / 100.0) - - # 置顶 - if self.config["topmost"]: + if self.config['stay_on_top']: self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) else: self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint) + self.setWindowOpacity(self.config['opacity']) self.show() + + def set_normal_state(self): + self.state = "NORMAL" + # 解除固定尺寸限制 + self.setMinimumSize(0, 0) + self.setMaximumSize(16777215, 16777215) - # 保存初始窗口位置(等待窗口显示完成) - 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() + self.btn_area.show() + self.label_title.show() # 恢复布局边距 - central_widget = self.centralWidget() - main_layout = central_widget.layout() - main_layout.setSpacing(20) - main_layout.setContentsMargins(30, 30, 30, 30) + self.main_layout.setContentsMargins(10, 10, 10, 10) + self.content_layout.setContentsMargins(10, 10, 10, 10) - # 恢复倒计时标签的正常样式 - 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() + self.label_time.setFont(QFont("Consolas", 60, QFont.Bold)) + self.label_time.setFixedSize(480, 180) + self.center_on_screen() + + def set_mini_state(self): + if not self.is_running: return - # 如果当前是正常状态 - if self.button_group.isHidden(): - self.button_group.show() - self.collapse_button.setText("▼") - - # 如果正在运行倒计时,恢复原始窗口大小 - if self.is_running: - self.switch_to_normal_state() + self.state = "MINI" + self.btn_area.hide() + self.label_title.hide() + + # 1. 关键:将布局边距设为0,否则控件会被挤出窗口 + self.main_layout.setContentsMargins(0, 0, 0, 0) + self.content_layout.setContentsMargins(5, 5, 5, 5) # 留一点小边距即可 + + # 2. 关键:先设置内部控件尺寸 + self.label_time.setFont(QFont("Consolas", 18, QFont.Bold)) + self.label_time.setFixedSize(110, 40) + + # 3. 关键:强制设置整个窗口的固定大小 + self.setFixedSize(120, 50) + + # 定位 + screen = QApplication.primaryScreen() + screen_geometry = screen.availableGeometry() + x = screen_geometry.width() - self.width() - self.config['right_margin'] + y = self.config['top_margin'] + self.move(x, y) + + def center_on_screen(self): + screen = QApplication.primaryScreen() + screen_geometry = screen.availableGeometry() + x = (screen_geometry.width() - self.width()) // 2 + y = (screen_geometry.height() - self.height()) // 2 + self.move(x, y) + + def mousePressEvent(self, event: QMouseEvent): + if event.button() == Qt.LeftButton: + self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft() + event.accept() + + def mouseMoveEvent(self, event: QMouseEvent): + if event.buttons() == Qt.LeftButton and self.drag_position: + self.move(event.globalPosition().toPoint() - self.drag_position) + event.accept() + + def mouseDoubleClickEvent(self, event: QMouseEvent): + if self.state == "MINI": + self.pause_countdown() + self.set_normal_state() + event.accept() + + def start_countdown(self, seconds): + self.remaining_time = seconds + self.update_label() + + self.timer.stop() + self.mini_timer.stop() + + if hasattr(self, 'color_anim') and self.color_anim is not None: + self.color_anim.stop() + + self.label_time.setStyleSheet("color: #ecf0f1;") + + self.is_running = True + + self.mini_timer.timeout.disconnect() + self.mini_timer.timeout.connect(self.start_timer) + self.mini_timer.start(2000) + + self.set_mini_state() + + def start_timer(self): + # 开始倒计时 + self.timer.start(1000) + + def pause_countdown(self): + self.timer.stop() + self.mini_timer.stop() + self.is_running = False + + def update_countdown(self): + if self.remaining_time > 0: + self.remaining_time -= 1 + self.update_label() + if self.remaining_time == self.config['alarm_offset']: + self.play_alarm() else: - self.button_group.hide() - self.collapse_button.setText("▶") + self.timer.stop() + self.is_running = False + self.show_finished_anim() + + def update_label(self): + m, s = divmod(self.remaining_time, 60) + self.label_time.setText(f"{m:02d}:{s:02d}") + + def play_alarm(self): + if hasattr(self, 'sound') and self.sound.source().isValid(): + self.sound.play() + + def show_finished_anim(self): + self.label_time.setText("时间已到") + + if self.label_time.graphicsEffect(): + self.label_time.graphicsEffect().deleteLater() + + if self.state == "MINI": + self.color_anim = QVariantAnimation(self) + self.color_anim.setDuration(2000) - # 如果正在运行倒计时,暂停倒计时并切换到微缩状态 - if self.is_running: - # 暂停倒计时 - self.timer.stop() - self.is_running = False - - # 切换到微缩状态 - self.switch_to_mini_state() - - # 按钮文本保持为"暂停",因为倒计时已经被暂停 - self.btn_pause.setText("暂停") - + self.color_anim.setStartValue(QColor("#ff0000")) + self.color_anim.setKeyValueAt(0.5, QColor("#ffff00")) + self.color_anim.setEndValue(QColor("#ff0000")) + + def update_style(color): + self.label_time.setStyleSheet(f"color: {color.name()};") + + self.color_anim.valueChanged.connect(update_style) + self.color_anim.setLoopCount(-1) + self.color_anim.start() + + else: + jump_offset = -20 + + self.jump_anim = QPropertyAnimation(self.label_time, b"pos") + self.jump_anim.setDuration(200) + curr_pos = self.label_time.pos() + self.jump_anim.setKeyValueAt(0, curr_pos) + self.jump_anim.setKeyValueAt(0.5, curr_pos + QPoint(0, jump_offset)) + self.jump_anim.setKeyValueAt(1, curr_pos) + self.jump_anim.setEasingCurve(QEasingCurve.OutBounce) + + opacity_effect = QGraphicsOpacityEffect(self.label_time) + self.label_time.setGraphicsEffect(opacity_effect) + + self.flash_anim = QPropertyAnimation(opacity_effect, b"opacity") + self.flash_anim.setDuration(1000) + self.flash_anim.setStartValue(1.0) + self.flash_anim.setEndValue(0.3) + self.flash_anim.setLoopCount(-1) + + self.anim_group = QSequentialAnimationGroup() + self.anim_group.addAnimation(self.jump_anim) + self.anim_group.addAnimation(self.flash_anim) + self.anim_group.start() + 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"]) - + dialog = ConfigDialog(self, self.config) 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"]) + custom_seconds = dialog.custom_time.value() + if custom_seconds != self.config['duration']: + self.start_countdown(custom_seconds) def main(): - """主函数""" app = QApplication(sys.argv) + app.setApplicationName("述职计时器") - # 设置应用程序样式 - app.setStyle("Fusion") + # 设置窗口图标 + if getattr(sys, 'frozen', False): + base_path = sys._MEIPASS + else: + base_path = os.path.dirname(__file__) + icon_file = os.path.join(base_path, "Timer.ico") + if os.path.exists(icon_file): + from PySide6.QtGui import QIcon + app.setWindowIcon(QIcon(icon_file)) - window = CountdownTimer() - window.show() + timer_app = TimerApp() + timer_app.show() sys.exit(app.exec()) if __name__ == "__main__": - main() + main() \ No newline at end of file