Files
countdown/countdown.py

436 lines
15 KiB
Python
Raw Normal View History

2026-01-21 18:21:56 +08:00
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
2026-01-21 18:21:56 +08:00
from PySide6.QtCore import QUrl
class ConfigDialog(QDialog):
def __init__(self, parent=None, config=None):
2026-01-21 18:21:56 +08:00
super().__init__(parent)
self.setWindowTitle("配置")
self.config = config
self.init_ui()
def init_ui(self):
self.setStyleSheet("""
QDialog {
background-color: #f5f5f5;
}
QLabel {
color: #333333;
font-size: 14px;
}
QSpinBox, QCheckBox {
padding: 5px;
}
""")
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)
2026-01-21 18:21:56 +08:00
# 提前告警
self.alarm_offset = QSpinBox()
self.alarm_offset.setRange(0, 300)
self.alarm_offset.setValue(self.config['alarm_offset'])
layout.addRow("提前告警(秒):", self.alarm_offset)
2026-01-21 18:21:56 +08:00
# 窗口置顶
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):
2026-01-21 18:21:56 +08:00
def __init__(self):
super().__init__()
self.config = {
'duration': 240,
'alarm_offset': 30,
'stay_on_top': True,
'opacity': 1.0,
'right_margin': 40, # 距离右边缘
'top_margin': 30 # 距离上边缘
}
self.remaining_time = 0
2026-01-21 18:21:56 +08:00
self.is_running = False
self.state = "NORMAL" # NORMAL or MINI
self.drag_position = QPoint()
2026-01-21 18:21:56 +08:00
self.init_ui()
self.setup_timers()
self.setup_audio()
2026-01-21 18:21:56 +08:00
self.apply_config()
2026-01-21 18:21:56 +08:00
def init_ui(self):
self.setWindowFlags(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setStyleSheet("""
QWidget#MainFrame {
background-color: #e8f4f8;
border-radius: 15px;
border: 2px solid #b0d4e3;
2026-01-21 18:21:56 +08:00
}
QLabel { color: #2c3e50; font-family: 'Segoe UI', Arial; }
2026-01-21 18:21:56 +08:00
QPushButton {
background-color: #5dade2;
2026-01-21 18:21:56 +08:00
color: white;
border-radius: 5px;
padding: 8px;
2026-01-21 18:21:56 +08:00
font-weight: bold;
}
QPushButton:hover { background-color: #3498db; }
2026-01-21 18:21:56 +08:00
""")
self.main_layout = QVBoxLayout(self)
self.frame = QWidget()
self.frame.setObjectName("MainFrame")
self.main_layout.addWidget(self.frame)
2026-01-21 18:21:56 +08:00
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_4m = QPushButton("4分钟")
self.btn_other = QPushButton("其它")
self.btn_4m.clicked.connect(lambda: self.start_countdown(240))
self.btn_other.clicked.connect(self.open_config)
self.btn_layout.addWidget(self.btn_4m)
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)
2026-01-21 18:21:56 +08:00
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):
# exe所在目录
external_path = os.path.dirname(sys.executable)
else:
# 当前工作目录
external_path = os.getcwd()
alarm_file = os.path.join(external_path, "alarm.wav")
if not os.path.exists(alarm_file):
# 外部没有则从打包的临时目录加载
if getattr(sys, 'frozen', False):
alarm_file = os.path.join(sys._MEIPASS, "alarm.wav")
else:
alarm_file = os.path.join(os.path.dirname(__file__), "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)
2026-01-21 18:21:56 +08:00
self.setWindowOpacity(self.config['opacity'])
self.show()
def set_normal_state(self):
self.state = "NORMAL"
# 解除固定尺寸限制
self.setMinimumSize(0, 0)
self.setMaximumSize(16777215, 16777215)
2026-01-21 18:21:56 +08:00
self.resize(500, 300)
self.btn_area.show()
self.label_title.show()
2026-01-21 18:21:56 +08:00
# 恢复布局边距
self.main_layout.setContentsMargins(10, 10, 10, 10)
self.content_layout.setContentsMargins(10, 10, 10, 10)
2026-01-21 18:21:56 +08:00
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)
# 将焦点还给上一个进程
self.return_focus_to_previous_window()
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()
2026-01-21 18:21:56 +08:00
self.timer.stop()
self.mini_timer.stop()
2026-01-21 18:21:56 +08:00
if hasattr(self, 'color_anim') and self.color_anim is not None:
self.color_anim.stop()
2026-01-21 18:21:56 +08:00
self.label_time.setStyleSheet("color: #2c3e50;")
2026-01-21 18:21:56 +08:00
self.is_running = True
self.mini_timer.timeout.disconnect()
self.mini_timer.timeout.connect(self.start_timer)
self.mini_timer.start(2000)
2026-01-21 18:21:56 +08:00
self.set_mini_state()
def start_timer(self):
# 开始倒计时
2026-01-21 18:21:56 +08:00
self.timer.start(1000)
2026-01-21 18:21:56 +08:00
def pause_countdown(self):
self.timer.stop()
self.mini_timer.stop()
2026-01-21 18:21:56 +08:00
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()
2026-01-21 18:21:56 +08:00
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("时间已到")
2026-01-21 18:21:56 +08:00
if self.label_time.graphicsEffect():
self.label_time.graphicsEffect().deleteLater()
if self.state == "MINI":
self.color_anim = QVariantAnimation(self)
self.color_anim.setDuration(2000)
2026-01-21 18:21:56 +08:00
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()
2026-01-21 18:21:56 +08:00
else:
jump_offset = -20
2026-01-21 18:21:56 +08:00
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)
2026-01-21 18:21:56 +08:00
opacity_effect = QGraphicsOpacityEffect(self.label_time)
self.label_time.setGraphicsEffect(opacity_effect)
2026-01-21 18:21:56 +08:00
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)
2026-01-21 18:21:56 +08:00
self.anim_group = QSequentialAnimationGroup()
self.anim_group.addAnimation(self.jump_anim)
self.anim_group.addAnimation(self.flash_anim)
self.anim_group.start()
def return_focus_to_previous_window(self):
# 清除当前窗口的焦点
self.clearFocus()
# 将窗口置于其他窗口下方
self.lower()
# 尝试激活其他窗口
app = QApplication.instance()
windows = app.topLevelWindows()
# 找到不是当前窗口的其他窗口
for window in windows:
if window != self.windowHandle() and window.isVisible():
# 尝试激活其他窗口
window.requestActivate()
break
2026-01-21 18:21:56 +08:00
def open_config(self):
dialog = ConfigDialog(self, self.config)
2026-01-21 18:21:56 +08:00
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)
2026-01-21 18:21:56 +08:00
def main():
app = QApplication(sys.argv)
app.setApplicationName("述职计时器")
2026-01-21 18:21:56 +08:00
# 设置窗口图标
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))
2026-01-21 18:21:56 +08:00
timer_app = TimerApp()
timer_app.show()
2026-01-21 18:21:56 +08:00
sys.exit(app.exec())
if __name__ == "__main__":
main()