Created
January 10, 2026 07:10
-
-
Save chunibyo-wly/134cb9fe6fc5e62e1f43ec2fe9867b67 to your computer and use it in GitHub Desktop.
Claude-Opus-4.5 generated
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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() |
Author
chunibyo-wly
commented
Jan 10, 2026
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment