Skip to content

Instantly share code, notes, and snippets.

@chunibyo-wly
Created January 10, 2026 07:10
Show Gist options
  • Select an option

  • Save chunibyo-wly/134cb9fe6fc5e62e1f43ec2fe9867b67 to your computer and use it in GitHub Desktop.

Select an option

Save chunibyo-wly/134cb9fe6fc5e62e1f43ec2fe9867b67 to your computer and use it in GitHub Desktop.
Claude-Opus-4.5 generated
# year_progress.py
import sys
import os
import json
import winreg
from datetime import datetime, date
from PyQt5.QtWidgets import (
QApplication,
QWidget,
QLabel,
QVBoxLayout,
QHBoxLayout,
QSystemTrayIcon,
QMenu,
QAction,
QDialog,
QSpinBox,
QFormLayout,
QMessageBox,
QFrame,
QComboBox,
)
from PyQt5.QtCore import Qt, QTimer, QPoint, QSize, QRectF
from PyQt5.QtGui import QFont, QIcon, QPainter, QColor, QPixmap, QPen, QBrush
class SettingsDialog(QDialog):
"""设置对话框"""
def __init__(self, parent=None, current_year=None, grid_size=20):
super().__init__(parent)
self.setWindowTitle("设置")
self.setFixedSize(300, 180)
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
layout = QFormLayout(self)
layout.setSpacing(15)
layout.setContentsMargins(20, 20, 20, 20)
# 年份选择
self.year_spin = QSpinBox()
self.year_spin.setRange(2000, 2100)
self.year_spin.setValue(current_year or datetime.now().year)
layout.addRow("年份:", self.year_spin)
# 网格大小选择
self.grid_combo = QComboBox()
self.grid_combo.addItems(["小 (15x25)", "中 (20x20)", "大 (25x15)"])
if grid_size == 15:
self.grid_combo.setCurrentIndex(0)
elif grid_size == 25:
self.grid_combo.setCurrentIndex(2)
else:
self.grid_combo.setCurrentIndex(1)
layout.addRow("网格样式:", self.grid_combo)
# 按钮
btn_layout = QHBoxLayout()
from PyQt5.QtWidgets import QPushButton
self.ok_btn = QPushButton("确定")
self.ok_btn.clicked.connect(self.accept)
self.cancel_btn = QPushButton("取消")
self.cancel_btn.clicked.connect(self.reject)
btn_layout.addWidget(self.ok_btn)
btn_layout.addWidget(self.cancel_btn)
layout.addRow(btn_layout)
# 样式
self.setStyleSheet(
"""
QDialog { background-color: #1a1a1a; color: white; }
QLabel { color: #888; font-size: 13px; }
QSpinBox, QComboBox {
background-color: #2a2a2a; color: white;
border: 1px solid #444; border-radius: 4px;
padding: 6px; font-size: 13px;
}
QPushButton {
background-color: #333; color: white;
border: 1px solid #444; border-radius: 4px;
padding: 8px 20px; font-size: 13px;
}
QPushButton:hover { background-color: #444; }
"""
)
def get_values(self):
grid_sizes = [15, 20, 25]
return self.year_spin.value(), grid_sizes[self.grid_combo.currentIndex()]
class YearProgressWidget(QWidget):
"""年度进度主窗口"""
def __init__(self):
super().__init__()
self.config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "year_progress_config.json")
# 默认配置
self.year = datetime.now().year
self.cols = 20 # 列数
self.rows = 19 # 行数 (365/20 ≈ 19)
self.dot_radius = 4
self.dot_spacing = 18
self.window_pos = None
self.load_config()
self.calculate_grid()
self.init_ui()
self.init_tray()
# 定时器 - 每分钟更新一次
self.timer = QTimer(self)
self.timer.timeout.connect(self.update)
self.timer.start(60000)
# 拖拽和缩放
self.dragging = False
self.drag_position = QPoint()
def calculate_grid(self):
"""计算网格参数"""
# 计算该年总天数
year_start = date(self.year, 1, 1)
year_end = date(self.year, 12, 31)
self.total_days = (year_end - year_start).days + 1
# 计算已过天数
today = date.today()
if today.year == self.year:
self.passed_days = (today - year_start).days + 1
elif today.year > self.year:
self.passed_days = self.total_days
else:
self.passed_days = 0
self.remaining_days = self.total_days - self.passed_days
# 计算行数
self.rows = (self.total_days + self.cols - 1) // self.cols
def init_ui(self):
"""初始化界面"""
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
self.setAttribute(Qt.WA_TranslucentBackground)
# 计算窗口大小
self.update_window_size()
if self.window_pos:
self.move(self.window_pos)
def update_window_size(self):
"""更新窗口大小"""
padding = 25
grid_width = self.cols * self.dot_spacing
grid_height = self.rows * self.dot_spacing
# 底部文字区域高度
footer_height = 30
width = grid_width + padding * 2
height = grid_height + padding * 2 + footer_height
self.setFixedSize(width, height)
def init_tray(self):
"""初始化系统托盘"""
self.tray_icon = QSystemTrayIcon(self)
# 创建图标
pixmap = QPixmap(64, 64)
pixmap.fill(Qt.transparent)
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.Antialiasing)
painter.setBrush(QColor("#333"))
painter.setPen(Qt.NoPen)
painter.drawRoundedRect(4, 4, 56, 56, 8, 8)
# 绘制小点阵
painter.setBrush(QColor("#666"))
for i in range(4):
for j in range(4):
if i * 4 + j < 10: # 已过去的点
painter.setBrush(QColor("#fff"))
else:
painter.setBrush(QColor("#444"))
painter.drawEllipse(12 + j * 10, 12 + i * 10, 6, 6)
painter.end()
self.tray_icon.setIcon(QIcon(pixmap))
self.tray_icon.setToolTip("年度进度")
# 托盘菜单
tray_menu = QMenu()
show_action = QAction("显示窗口", self)
show_action.triggered.connect(self.show_window)
tray_menu.addAction(show_action)
settings_action = QAction("设置", self)
settings_action.triggered.connect(self.show_settings)
tray_menu.addAction(settings_action)
tray_menu.addSeparator()
# autostart_action = QAction("开机自启动", self)
# autostart_action.setCheckable(True)
# autostart_action.setChecked(self.is_autostart_enabled())
# autostart_action.triggered.connect(self.toggle_autostart)
# tray_menu.addAction(autostart_action)
# self.autostart_action = autostart_action
# tray_menu.addSeparator()
quit_action = QAction("退出", self)
quit_action.triggered.connect(self.quit_app)
tray_menu.addAction(quit_action)
self.tray_icon.setContextMenu(tray_menu)
self.tray_icon.activated.connect(self.tray_activated)
self.tray_icon.show()
def paintEvent(self, event):
"""绘制事件"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# 绘制背景
bg_rect = self.rect()
painter.setBrush(QColor(20, 20, 20, 240))
painter.setPen(Qt.NoPen)
painter.drawRoundedRect(bg_rect, 15, 15)
# 绘制边框
painter.setPen(QPen(QColor(60, 60, 60), 1))
painter.setBrush(Qt.NoBrush)
painter.drawRoundedRect(bg_rect.adjusted(0, 0, -1, -1), 15, 15)
# 绘制点阵
padding = 25
start_x = padding
start_y = padding
for day in range(self.total_days):
row = day // self.cols
col = day % self.cols
x = start_x + col * self.dot_spacing + self.dot_spacing // 2
y = start_y + row * self.dot_spacing + self.dot_spacing // 2
# 判断是已过去还是剩余
if day < self.passed_days:
# 已过去的天 - 亮色
color = QColor(255, 255, 255, 230)
else:
# 剩余的天 - 暗色
color = QColor(80, 80, 80, 180)
painter.setBrush(color)
painter.setPen(Qt.NoPen)
painter.drawEllipse(
QRectF(x - self.dot_radius, y - self.dot_radius, self.dot_radius * 2, self.dot_radius * 2)
)
# 绘制底部文字
footer_y = start_y + self.rows * self.dot_spacing + 15
# 左侧年份
painter.setPen(QColor(200, 200, 200))
font = QFont("SF Pro Display", 16)
font.setWeight(QFont.Medium)
painter.setFont(font)
painter.drawText(padding, footer_y + 20, str(self.year))
# 右侧剩余天数
right_text = f"{self.passed_days / self.total_days * 100:.1f}"
days_label = " %"
font_metrics = painter.fontMetrics()
# 绘制数字(白色)
painter.setPen(QColor(200, 200, 200))
right_x = self.width() - padding - font_metrics.horizontalAdvance(right_text + days_label)
painter.drawText(right_x, footer_y + 20, right_text)
# 绘制 "days left"(灰色)
painter.setPen(QColor(100, 100, 100))
painter.drawText(right_x + font_metrics.horizontalAdvance(right_text), footer_y + 20, days_label)
# 绘制底部标签
# painter.setPen(QColor(150, 150, 150))
# font = QFont("SF Pro Display", 12)
# painter.setFont(font)
# label_text = "one year"
# label_width = painter.fontMetrics().horizontalAdvance(label_text)
# painter.drawText((self.width() - label_width) // 2, self.height() - 10, label_text)
def mousePressEvent(self, event):
"""鼠标按下"""
if event.button() == Qt.LeftButton:
self.dragging = True
self.drag_position = event.globalPos() - self.frameGeometry().topLeft()
elif event.button() == Qt.RightButton:
self.show_settings()
def mouseDoubleClickEvent(self, event):
if event.button() == Qt.LeftButton:
self.hide()
def mouseMoveEvent(self, event):
"""鼠标移动"""
if self.dragging:
self.move(event.globalPos() - self.drag_position)
def mouseReleaseEvent(self, event):
"""鼠标释放"""
if event.button() == Qt.LeftButton:
self.dragging = False
self.save_config()
def tray_activated(self, reason):
if reason == QSystemTrayIcon.DoubleClick:
self.show_window()
def show_window(self):
self.show()
self.activateWindow()
def show_settings(self):
dialog = SettingsDialog(self, self.year, self.cols)
if dialog.exec_() == QDialog.Accepted:
self.year, self.cols = dialog.get_values()
self.calculate_grid()
self.update_window_size()
self.update()
self.save_config()
def closeEvent(self, event):
event.ignore()
self.hide()
self.tray_icon.showMessage("年度进度", "程序已最小化到系统托盘", QSystemTrayIcon.Information, 2000)
def quit_app(self):
self.save_config()
self.tray_icon.hide()
QApplication.quit()
def save_config(self):
config = {"year": self.year, "cols": self.cols, "window_pos": {"x": self.x(), "y": self.y()}}
try:
with open(self.config_file, "w", encoding="utf-8") as f:
json.dump(config, f)
except Exception as e:
print(f"保存配置失败: {e}")
def load_config(self):
try:
if os.path.exists(self.config_file):
with open(self.config_file, "r", encoding="utf-8") as f:
config = json.load(f)
self.year = config.get("year", self.year)
self.cols = config.get("cols", self.cols)
if "window_pos" in config:
pos = config["window_pos"]
self.window_pos = QPoint(pos["x"], pos["y"])
except Exception as e:
print(f"加载配置失败: {e}")
def is_autostart_enabled(self):
try:
key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run", 0, winreg.KEY_READ
)
try:
winreg.QueryValueEx(key, "YearProgress")
return True
except WindowsError:
return False
finally:
winreg.CloseKey(key)
except Exception:
return False
def toggle_autostart(self, enabled):
try:
key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run", 0, winreg.KEY_SET_VALUE
)
if enabled:
app_path = os.path.abspath(sys.argv[0])
if app_path.endswith(".py"):
python_path = sys.executable.replace("python.exe", "pythonw.exe")
value = f'"{python_path}" "{app_path}"'
else:
value = f'"{app_path}"'
winreg.SetValueEx(key, "YearProgress", 0, winreg.REG_SZ, value)
else:
try:
winreg.DeleteValue(key, "YearProgress")
except WindowsError:
pass
winreg.CloseKey(key)
except Exception as e:
QMessageBox.warning(self, "错误", f"设置失败: {e}")
self.autostart_action.setChecked(not enabled)
def main():
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)
widget = YearProgressWidget()
widget.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
@chunibyo-wly
Copy link
Author

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment