import os from PyQt6.QtWidgets import QWidget, QApplication from PyQt6.QtCore import Qt, QPoint, QTimer, pyqtSignal, QPropertyAnimation, QEasingCurve, QRectF from PyQt6.QtGui import (QPainter, QPixmap, QBrush, QColor, QPen, QPainterPath, QRadialGradient, QIcon) BALL_SIZE = 60 LOGO_SIZE = 34 LOGO_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logo.png") DRAG_THRESHOLD = 6 class FloatBall(QWidget): clicked = pyqtSignal() right_clicked = pyqtSignal(QPoint) def __init__(self, parent=None): super().__init__(parent) self.setWindowFlags( Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Tool ) if os.path.exists(LOGO_PATH): self.setWindowIcon(QIcon(LOGO_PATH)) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground) # 关键:让整个窗口区域都透明,不显示系统边框 self.setFixedSize(BALL_SIZE + 20, BALL_SIZE + 20) # 留出光晕空间 self.setCursor(Qt.CursorShape.PointingHandCursor) self._logo = QPixmap(LOGO_PATH) if os.path.exists(LOGO_PATH) else QPixmap() self._drag_start = QPoint() self._dragging = False self._hovered = False self._glow = 0.0 # 0.0~1.0 光晕强度 # 光晕动画 self._glow_anim = QPropertyAnimation(self, b"_glow_prop") self._glow_anim.setDuration(300) self._glow_anim.setEasingCurve(QEasingCurve.Type.OutCubic) # 闲置淡出 self._idle_timer = QTimer(self) self._idle_timer.setSingleShot(True) self._idle_timer.setInterval(3000) self._idle_timer.timeout.connect(self._fade_out) self._place_default() self._idle_timer.start() # Qt property 用于动画 def _get_glow(self): return self._glow def _set_glow(self, v): self._glow = v; self.update() from PyQt6.QtCore import pyqtProperty _glow_prop = pyqtProperty(float, _get_glow, _set_glow) def _place_default(self): screen = QApplication.primaryScreen().availableGeometry() pad = (self.width() - BALL_SIZE) // 2 self.move(screen.right() - self.width() - 20 + pad, screen.top() + screen.height() // 2 - self.height() // 2) # ── 绘制 ──────────────────────────────────────────── def paintEvent(self, event): p = QPainter(self) p.setRenderHint(QPainter.RenderHint.Antialiasing) pad = (self.width() - BALL_SIZE) // 2 # 光晕留白 cx = self.width() / 2 cy = self.height() / 2 r = BALL_SIZE / 2 - 1 # 1. 外部光晕(hover 时) if self._glow > 0: glow_r = r + 10 * self._glow grad = QRadialGradient(cx, cy, glow_r) grad.setColorAt(0, QColor(255, 255, 255, int(80 * self._glow))) grad.setColorAt(0.5, QColor(255, 255, 255, int(30 * self._glow))) grad.setColorAt(1, QColor(255, 255, 255, 0)) p.setBrush(QBrush(grad)) p.setPen(Qt.PenStyle.NoPen) p.drawEllipse(QRectF(cx - glow_r, cy - glow_r, glow_r * 2, glow_r * 2)) # 2. 圆形裁剪 clip = QPainterPath() clip.addEllipse(QRectF(cx - r, cy - r, r * 2, r * 2)) p.setClipPath(clip) # 3. 白色半透明毛玻璃底 p.setBrush(QBrush(QColor(255, 255, 255, 210))) p.setPen(Qt.PenStyle.NoPen) p.drawEllipse(QRectF(cx - r, cy - r, r * 2, r * 2)) # 4. 顶部高光 hi = QRadialGradient(cx, cy - r * 0.3, r * 0.9) hi.setColorAt(0, QColor(255, 255, 255, 140)) hi.setColorAt(1, QColor(255, 255, 255, 0)) p.setBrush(QBrush(hi)) p.drawEllipse(QRectF(cx - r, cy - r, r * 2, r * 2)) # 5. logo 居中 if not self._logo.isNull(): scaled = self._logo.scaled( LOGO_SIZE, LOGO_SIZE, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation ) lx = int(cx - scaled.width() / 2) ly = int(cy - scaled.height() / 2) p.drawPixmap(lx, ly, scaled) # 6. 边框(取消裁剪后画) p.setClipping(False) p.setBrush(Qt.BrushStyle.NoBrush) p.setPen(QPen(QColor(200, 200, 200, 120), 1.5)) p.drawEllipse(QRectF(cx - r, cy - r, r * 2, r * 2)) # 7. hover 时内圈白色光晕边 if self._glow > 0: p.setPen(QPen(QColor(255, 255, 255, int(160 * self._glow)), 2)) p.drawEllipse(QRectF(cx - r + 1, cy - r + 1, (r - 1) * 2, (r - 1) * 2)) p.end() # ── 鼠标事件 ──────────────────────────────────────── def mousePressEvent(self, event): if event.button() == Qt.MouseButton.LeftButton: self._drag_start = event.globalPosition().toPoint() self._dragging = False self.setWindowOpacity(1.0) self._idle_timer.stop() elif event.button() == Qt.MouseButton.RightButton: self.right_clicked.emit(event.globalPosition().toPoint()) def mouseMoveEvent(self, event): if not (event.buttons() & Qt.MouseButton.LeftButton): return delta = event.globalPosition().toPoint() - self._drag_start if not self._dragging and delta.manhattanLength() > DRAG_THRESHOLD: self._dragging = True if self._dragging: new_pos = self.pos() + delta self._drag_start = event.globalPosition().toPoint() screen = QApplication.primaryScreen().availableGeometry() new_pos.setX(max(screen.left(), min(new_pos.x(), screen.right() - self.width()))) new_pos.setY(max(screen.top(), min(new_pos.y(), screen.bottom() - self.height()))) self.move(new_pos) def mouseReleaseEvent(self, event): if event.button() == Qt.MouseButton.LeftButton: if not self._dragging: self.clicked.emit() else: self._snap_to_edge() self._idle_timer.start() def enterEvent(self, event): self._hovered = True self.setWindowOpacity(1.0) self._idle_timer.stop() self._glow_anim.stop() self._glow_anim.setStartValue(self._glow) self._glow_anim.setEndValue(1.0) self._glow_anim.start() def leaveEvent(self, event): self._hovered = False self._glow_anim.stop() self._glow_anim.setStartValue(self._glow) self._glow_anim.setEndValue(0.0) self._glow_anim.start() self._idle_timer.start() # ── 吸附边缘 ───────────────────────────────────────── def _snap_to_edge(self): screen = QApplication.primaryScreen().availableGeometry() cx = self.x() + self.width() // 2 cy = self.y() + self.height() // 2 dists = { "l": cx - screen.left(), "r": screen.right() - cx, "t": cy - screen.top(), "b": screen.bottom() - cy, } side = min(dists, key=dists.get) ends = { "l": QPoint(screen.left(), self.y()), "r": QPoint(screen.right() - self.width(), self.y()), "t": QPoint(self.x(), screen.top()), "b": QPoint(self.x(), screen.bottom() - self.height()), } anim = QPropertyAnimation(self, b"pos") anim.setDuration(200) anim.setEasingCurve(QEasingCurve.Type.OutCubic) anim.setStartValue(self.pos()) anim.setEndValue(ends[side]) self._snap_anim = anim anim.start() def _fade_out(self): self._anim_fade = QPropertyAnimation(self, b"windowOpacity") self._anim_fade.setDuration(600) self._anim_fade.setStartValue(1.0) self._anim_fade.setEndValue(0.3) self._anim_fade.start()