import sys 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, config=None): super().__init__(parent) self.setWindowTitle("配置") self.config = config self.init_ui() def init_ui(self): layout = QFormLayout(self) # 自定义倒计时 self.custom_time = QSpinBox() self.custom_time.setRange(1, 3600) self.custom_time.setValue(self.config['duration']) btn_start = QPushButton("开始倒计时") btn_start.setStyleSheet("background-color: #27ae60; color: white;") btn_start.clicked.connect(self.start_countdown) 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.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-radius: 5px; padding: 8px; font-weight: bold; } QPushButton:hover { background-color: #2980b9; } """) self.main_layout = QVBoxLayout(self) self.frame = QWidget() self.frame.setObjectName("MainFrame") self.main_layout.addWidget(self.frame) self.content_layout = QVBoxLayout(self.frame) # 标题标签 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.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.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.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: 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): 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) self.resize(500, 300) self.btn_area.show() self.label_title.show() # 恢复布局边距 self.main_layout.setContentsMargins(10, 10, 10, 10) self.content_layout.setContentsMargins(10, 10, 10, 10) 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 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.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) 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, self.config) if dialog.exec() == QDialog.Accepted: self.apply_config() 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("述职计时器") # 设置窗口图标 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)) timer_app = TimerApp() timer_app.show() sys.exit(app.exec()) if __name__ == "__main__": main()