Files
countdown/countdown.py
2026-01-21 18:21:56 +08:00

833 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
述职计时器 - 倒计时应用程序
使用 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()