first commit
This commit is contained in:
+206
@@ -0,0 +1,206 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user