import sys import os from PyQt5 import uic from PyQt5.QtCore import Qt, QPropertyAnimation, QRect, QEasingCurve, QTimer, QPoint, pyqtProperty, QThread from PyQt5.QtGui import QColor, QPainter, QBrush, QPixmap from PyQt5.QtWidgets import QWidget, QApplication, QLabel, QFrame, QGraphicsBlurEffect from loguru import logger from qfluentwidgets import setThemeColor import conf from conf import base_directory import list_ from file import config_center from play_audio import PlayAudio import platform # 适配高DPI缩放 if platform.system() == 'Windows' and platform.release() not in ['7', 'XP', 'Vista']: QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) else: logger.warning('不兼容的系统,跳过高DPI标识') prepare_class = config_center.read_conf('Audio', 'prepare_class') attend_class = config_center.read_conf('Audio', 'attend_class') finish_class = config_center.read_conf('Audio', 'finish_class') pushed_notification = False notification_contents = {"state": None, "lesson_name": None, "title": None, "subtitle": None, "content": None} # 波纹效果 normal_color = '#56CFD8' window_list = [] # 窗口列表 active_windows = [] class tip_toast(QWidget): def __init__(self, pos, width, state=1, lesson_name=None, title=None, subtitle=None, content=None, icon=None, duration=2000): super().__init__() for w in active_windows[:]: w.close() active_windows.append(self) self.audio_thread = None uic.loadUi(f"{base_directory}/view/widget-toast-bar.ui", self) try: dpr = self.screen().devicePixelRatio() if self.screen() else QApplication.primaryScreen().devicePixelRatio() except AttributeError: dpr = QApplication.primaryScreen().devicePixelRatio() dpr = max(1.0, dpr) # 窗口位置 if config_center.read_conf('Toast', 'pin_on_top') == '1': self.setWindowFlags( Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.FramelessWindowHint | Qt.X11BypassWindowManagerHint # 绕过窗口管理器以在全屏显示通知 ) else: self.setWindowFlags( Qt.WindowType.WindowStaysOnBottomHint | Qt.WindowType.FramelessWindowHint ) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.move(pos[0], pos[1]) self.resize(width, height) # 标题 title_label = self.findChild(QLabel, 'title') backgnd = self.findChild(QFrame, 'backgnd') lesson = self.findChild(QLabel, 'lesson') subtitle_label = self.findChild(QLabel, 'subtitle') icon_label = self.findChild(QLabel, 'icon') sound_to_play = None if icon: pixmap = QPixmap(icon) icon_size = int(48 * dpr) pixmap = pixmap.scaled(icon_size, icon_size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) icon_label.setPixmap(pixmap) icon_label.setFixedSize(icon_size, icon_size) if state == 1: logger.info('上课铃声显示') title_label.setText('活动开始') # 修正文本,以适应不同场景 subtitle_label.setText('当前课程') lesson.setText(lesson_name) # 课程名 sound_to_play = attend_class setThemeColor(f"#{config_center.read_conf('Color', 'attend_class')}") # 主题色 elif state == 0: logger.info('下课铃声显示') title_label.setText('下课') if lesson_name: subtitle_label.setText('即将进行') else: subtitle_label.hide() lesson.setText(lesson_name) # 课程名 sound_to_play = finish_class setThemeColor(f"#{config_center.read_conf('Color', 'finish_class')}") elif state == 2: logger.info('放学铃声显示') title_label.setText('放学') subtitle_label.setText('当前课程已结束') lesson.setText('') # 课程名 sound_to_play = finish_class setThemeColor(f"#{config_center.read_conf('Color', 'finish_class')}") elif state == 3: logger.info('预备铃声显示') title_label.setText('即将开始') # 同上 subtitle_label.setText('下一节') lesson.setText(lesson_name) sound_to_play = prepare_class setThemeColor(f"#{config_center.read_conf('Color', 'prepare_class')}") elif state == 4: logger.info(f'通知显示: {title}') title_label.setText(title) subtitle_label.setText(subtitle) lesson.setText(content) sound_to_play = prepare_class # 设置样式表 if state == 1: # 上课铃声 bg_color = [ # 1为正常、2为渐变亮色部分、3为渐变暗色部分 generate_gradient_color(attend_class_color)[0], generate_gradient_color(attend_class_color)[1], generate_gradient_color(attend_class_color)[2] ] elif state == 0 or state == 2: # 下课铃声 bg_color = [ generate_gradient_color(finish_class_color)[0], generate_gradient_color(finish_class_color)[1], generate_gradient_color(finish_class_color)[2] ] elif state == 3: # 预备铃声 bg_color = [ generate_gradient_color(prepare_class_color)[0], generate_gradient_color(prepare_class_color)[1], generate_gradient_color(prepare_class_color)[2] ] elif state == 4: # 通知铃声 bg_color = ['rgba(110, 190, 210, 255)', 'rgba(110, 190, 210, 255)', 'rgba(90, 210, 215, 255)'] else: bg_color = ['rgba(110, 190, 210, 255)', 'rgba(110, 190, 210, 255)', 'rgba(90, 210, 215, 255)'] backgnd.setStyleSheet(f'font-weight: bold; border-radius: {radius}; ' 'background-color: qlineargradient(' 'spread:pad, x1:0, y1:0, x2:1, y2:1,' f' stop:0 {bg_color[1]}, stop:0.5 {bg_color[0]}, stop:1 {bg_color[2]}' ');' ) # 模糊效果 self.blur_effect = QGraphicsBlurEffect(self) if config_center.read_conf('Toast', 'wave') == '1': backgnd.setGraphicsEffect(self.blur_effect) mini_size_x = 150 / dpr mini_size_y = 50 / dpr self.timer = QTimer(self) self.timer.setSingleShot(True) self.timer.setInterval(duration) self.timer.timeout.connect(self.close_window) # 放大效果 self.geometry_animation = QPropertyAnimation(self, b"geometry") self.geometry_animation.setDuration(750) # 动画持续时间 start_rect = QRect(int(start_x + mini_size_x / 2), int(start_y + mini_size_y / 2), int(total_width - mini_size_x), int(height - mini_size_y)) self.geometry_animation.setStartValue(start_rect) self.geometry_animation.setEndValue(QRect(start_x, start_y, total_width, height)) self.geometry_animation.setEasingCurve(QEasingCurve.Type.OutCirc) self.geometry_animation.finished.connect(self.timer.start) self.blur_animation = QPropertyAnimation(self.blur_effect, b"blurRadius") self.blur_animation.setDuration(550) self.blur_animation.setStartValue(25) self.blur_animation.setEndValue(0) # 渐显 self.opacity_animation = QPropertyAnimation(self, b"windowOpacity") self.opacity_animation.setDuration(450) self.opacity_animation.setStartValue(0) self.opacity_animation.setEndValue(1) self.opacity_animation.setEasingCurve(QEasingCurve.Type.InOutQuad) if sound_to_play: self.playsound(sound_to_play) self.geometry_animation.start() self.opacity_animation.start() self.blur_animation.start() def close_window(self): try: dpr = self.screen().devicePixelRatio() if self.screen() else QApplication.primaryScreen().devicePixelRatio() except AttributeError: dpr = QApplication.primaryScreen().devicePixelRatio() dpr = max(1.0, dpr) mini_size_x = 120 / dpr mini_size_y = 20 / dpr # 放大效果 self.geometry_animation_close = QPropertyAnimation(self, b"geometry") self.geometry_animation_close.setDuration(500) # 动画持续时间 self.geometry_animation_close.setStartValue(QRect(start_x, start_y, total_width, height)) end_rect = QRect(int(start_x + mini_size_x / 2), int(start_y + mini_size_y / 2), int(total_width - mini_size_x), int(height - mini_size_y)) self.geometry_animation_close.setEndValue(end_rect) self.geometry_animation_close.setEasingCurve(QEasingCurve.Type.InOutQuad) self.blur_animation_close = QPropertyAnimation(self.blur_effect, b"blurRadius") self.blur_animation_close.setDuration(500) self.blur_animation_close.setStartValue(0) self.blur_animation_close.setEndValue(30) self.opacity_animation_close = QPropertyAnimation(self, b"windowOpacity") self.opacity_animation_close.setDuration(500) self.opacity_animation_close.setStartValue(1) self.opacity_animation_close.setEndValue(0) self.geometry_animation_close.start() self.opacity_animation_close.start() self.blur_animation_close.start() self.opacity_animation_close.finished.connect(self.close) def closeEvent(self, event): if self in active_windows: active_windows.remove(self) global window_list # window_list.remove(self) self.hide() self.deleteLater() event.ignore() def playsound(self, filename): try: file_path = os.path.join(base_directory, 'audio', filename) if self.audio_thread and self.audio_thread.isRunning(): self.audio_thread.quit() self.audio_thread.wait() self.audio_thread = PlayAudio(str(file_path)) self.audio_thread.start() self.audio_thread.setPriority(QThread.Priority.HighestPriority) # 设置优先级 except Exception as e: logger.error(f'播放音频文件失败:{e}') class wave_Effect(QWidget): def __init__(self, state=1): super().__init__() if config_center.read_conf('Toast', 'pin_on_top') == '1': self.setWindowFlags( Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.FramelessWindowHint | Qt.X11BypassWindowManagerHint # 绕过窗口管理器以在全屏显示通知 ) else: self.setWindowFlags( Qt.WindowType.WindowStaysOnBottomHint | Qt.WindowType.FramelessWindowHint | Qt.X11BypassWindowManagerHint # 绕过窗口管理器以在全屏显示通知 ) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self._radius = 0 self.duration = 1200 if state == 1: self.color = QColor(attend_class_color) elif state == 0 or state == 2: self.color = QColor(finish_class_color) elif state == 3: self.color = QColor(prepare_class_color) elif state == 4: self.color = QColor(normal_color) else: self.color = QColor(normal_color) screen_geometry = QApplication.primaryScreen().geometry() self.setGeometry(screen_geometry) self.timer = QTimer(self) self.timer.setSingleShot(True) self.timer.setInterval(275) self.timer.timeout.connect(self.showAnimation) self.timer.start() @pyqtProperty(int) def radius(self): return self._radius @radius.setter def radius(self, value): self._radius = value self.update() def showAnimation(self): self.animation = QPropertyAnimation(self, b'radius') self.animation.setDuration(self.duration) self.animation.setStartValue(50) try: dpr = self.screen().devicePixelRatio() if self.screen() else QApplication.primaryScreen().devicePixelRatio() except AttributeError: dpr = QApplication.primaryScreen().devicePixelRatio() dpr = max(1.0, dpr) fixed_end_radius = 1000 * dpr # 动画效果值 self.animation.setEndValue(fixed_end_radius) self.animation.setEasingCurve(QEasingCurve.Type.InOutQuad) self.animation.start() self.fade_animation = QPropertyAnimation(self, b'windowOpacity') self.fade_animation.setDuration(self.duration - 150) self.fade_animation.setKeyValues([ # 关键帧 (0, 0), (0.06, 0.9), (1, 0) ]) self.fade_animation.setEasingCurve(QEasingCurve.Type.InOutQuad) self.fade_animation.finished.connect(self.close) self.fade_animation.start() def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing) painter.setBrush(QBrush(self.color)) painter.setPen(Qt.PenStyle.NoPen) center = self.rect().center() loc = QPoint(center.x(), self.rect().top() + start_y + 50) painter.drawEllipse(loc, self._radius, self._radius) def closeEvent(self, event): if self in active_windows: active_windows.remove(self) global window_list # window_list.remove(self) self.deleteLater() self.hide() event.ignore() def generate_gradient_color(theme_color): # 计算渐变色 def adjust_color(color, factor): r = max(0, min(255, int(color.red() * (1 + factor)))) g = max(0, min(255, int(color.green() * (1 + factor)))) b = max(0, min(255, int(color.blue() * (1 + factor)))) # return QColor(r, g, b) return f'rgba({r}, {g}, {b}, 255)' color = QColor(theme_color) gradient = [adjust_color(color, 0), adjust_color(color, 0.24), adjust_color(color, -0.11)] return gradient def main(state=1, lesson_name='', title='通知示例', subtitle='副标题', content='这是一条通知示例', icon=None, duration=2000): # 0:下课铃声 1:上课铃声 2:放学铃声 3:预备铃 4:其他 if detect_enable_toast(state): return global start_x, start_y, total_width, height, radius, attend_class_color, finish_class_color, prepare_class_color widgets = list_.get_widget_config() for widget in widgets: # 检查组件 if widget not in list_.widget_name: widgets.remove(widget) # 移除不存在的组件(确保移除插件后不会出错) attend_class_color = f"#{config_center.read_conf('Color', 'attend_class')}" finish_class_color = f"#{config_center.read_conf('Color', 'finish_class')}" prepare_class_color = f"#{config_center.read_conf('Color', 'prepare_class')}" theme = config_center.read_conf('General', 'theme') height = conf.load_theme_config(theme)['height'] radius = conf.load_theme_config(theme)['radius'] screen_geometry = QApplication.primaryScreen().geometry() screen_width = screen_geometry.width() spacing = conf.load_theme_config(theme)['spacing'] try: dpr = QApplication.primaryScreen().devicePixelRatio() except AttributeError: dpr = 1.0 dpr = max(1.0, dpr) widgets_width = 0 for widget in widgets: # 计算总宽度(兼容插件) try: widgets_width += conf.load_theme_width(theme)[widget] except KeyError: widgets_width += list_.widget_width[widget] except: widgets_width += 0 total_width = widgets_width + spacing * (len(widgets) - 1) start_x = int((screen_width - total_width) / 2) margin_base = int(config_center.read_conf('General', 'margin')) start_y = int(margin_base * dpr) if state != 4: window = tip_toast((start_x, start_y), total_width, state, lesson_name, duration=duration) else: window = tip_toast( (start_x, start_y), total_width, state, '', title, subtitle, content, icon, duration=duration ) window.show() window_list.append(window) if config_center.read_conf('Toast', 'wave') == '1': wave = wave_Effect(state) wave.show() window_list.append(wave) def detect_enable_toast(state=0): if config_center.read_conf('Toast', 'attend_class') != '1' and state == 1: return True if (config_center.read_conf('Toast', 'finish_class') != '1') and (state in [0, 2]): return True if config_center.read_conf('Toast', 'prepare_class') != '1' and state == 3: return True else: return False def push_notification(state=1, lesson_name='', title=None, subtitle=None, content=None): # 推送通知 global pushed_notification, notification_contents pushed_notification = True notification_contents = { "state": state, "lesson_name": lesson_name, "title": title, "subtitle": subtitle, "content": content } main(state, lesson_name, title, subtitle, content) return notification_contents if __name__ == '__main__': app = QApplication(sys.argv) main( state=4, # 自定义通知 title='天气预报', subtitle='', content='1°~-3° | 3°~-3° | 9°~1°', icon='img/favicon.ico', duration=2000 ) sys.exit(app.exec())