完成0.01版本
This commit is contained in:
parent
104dfe9693
commit
77f718cca6
@ -1,7 +1,7 @@
|
|||||||
# -*- mode: python ; coding: utf-8 -*-
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
from PyInstaller.utils.hooks import collect_all
|
from PyInstaller.utils.hooks import collect_all
|
||||||
|
|
||||||
datas = [('assets', 'assets'), ('logo.png', '.')]
|
datas = [('assets', 'assets'), ('logo.png', '.'), ('logo.ico', '.')]
|
||||||
binaries = []
|
binaries = []
|
||||||
hiddenimports = ['qtawesome']
|
hiddenimports = ['qtawesome']
|
||||||
tmp_ret = collect_all('qtawesome')
|
tmp_ret = collect_all('qtawesome')
|
||||||
|
|||||||
BIN
logo.ico
BIN
logo.ico
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 146 KiB |
BIN
logo.png
BIN
logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 150 KiB |
29
main.py
29
main.py
@ -1,4 +1,5 @@
|
|||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt6.QtWidgets import QApplication
|
from PyQt6.QtWidgets import QApplication
|
||||||
@ -8,6 +9,17 @@ from ui.ball import FloatBall, BALL_SIZE
|
|||||||
import ui.theme as theme
|
import ui.theme as theme
|
||||||
from db import database
|
from db import database
|
||||||
|
|
||||||
|
__VERSION__ = "0.0.1"
|
||||||
|
|
||||||
|
# ===================== 打包兼容核心函数 =====================
|
||||||
|
def get_resource_path(relative_path):
|
||||||
|
"""
|
||||||
|
打包 EXE 后获取资源路径,开发环境也能用
|
||||||
|
"""
|
||||||
|
if hasattr(sys, '_MEIPASS'):
|
||||||
|
return os.path.join(sys._MEIPASS, relative_path)
|
||||||
|
return os.path.join(os.path.abspath("."), relative_path)
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
def _wake_existing_or_exit() -> bool:
|
def _wake_existing_or_exit() -> bool:
|
||||||
"""
|
"""
|
||||||
@ -22,7 +34,7 @@ def _wake_existing_or_exit() -> bool:
|
|||||||
import ctypes.wintypes
|
import ctypes.wintypes
|
||||||
|
|
||||||
mutex_name = r"Global\CleanDesktopOrganizerSingleton"
|
mutex_name = r"Global\CleanDesktopOrganizerSingleton"
|
||||||
title = "牛马软件柜"
|
title = "牛马软件柜 v" + __VERSION__
|
||||||
|
|
||||||
kernel32 = ctypes.windll.kernel32
|
kernel32 = ctypes.windll.kernel32
|
||||||
user32 = ctypes.windll.user32
|
user32 = ctypes.windll.user32
|
||||||
@ -34,16 +46,14 @@ def _wake_existing_or_exit() -> bool:
|
|||||||
|
|
||||||
last_err = kernel32.GetLastError()
|
last_err = kernel32.GetLastError()
|
||||||
if last_err == ERROR_ALREADY_EXISTS:
|
if last_err == ERROR_ALREADY_EXISTS:
|
||||||
# 轮询一小段时间,避免首次窗口尚未创建
|
|
||||||
hwnd = None
|
hwnd = None
|
||||||
for _ in range(8): # ~2.4s
|
for _ in range(8):
|
||||||
hwnd = user32.FindWindowW(None, title)
|
hwnd = user32.FindWindowW(None, title)
|
||||||
if hwnd:
|
if hwnd:
|
||||||
break
|
break
|
||||||
time.sleep(0.3)
|
time.sleep(0.3)
|
||||||
|
|
||||||
if hwnd:
|
if hwnd:
|
||||||
# SW_SHOW = 5
|
|
||||||
user32.ShowWindow(hwnd, 5)
|
user32.ShowWindow(hwnd, 5)
|
||||||
user32.SetForegroundWindow(hwnd)
|
user32.SetForegroundWindow(hwnd)
|
||||||
return False
|
return False
|
||||||
@ -52,42 +62,41 @@ def _wake_existing_or_exit() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# 必须在创建 QApplication 之前:不按各显示器缩放,逻辑像素固定(跨分辨率/跨屏拖动宽高保持一致)
|
|
||||||
if not _wake_existing_or_exit():
|
if not _wake_existing_or_exit():
|
||||||
return
|
return
|
||||||
|
|
||||||
QApplication.setAttribute(Qt.ApplicationAttribute.AA_Use96Dpi, True)
|
QApplication.setAttribute(Qt.ApplicationAttribute.AA_Use96Dpi, True)
|
||||||
# 这个策略同样要求在创建 QApplication 前设置
|
|
||||||
try:
|
try:
|
||||||
QApplication.setHighDpiScaleFactorRoundingPolicy(
|
QApplication.setHighDpiScaleFactorRoundingPolicy(
|
||||||
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
app.setQuitOnLastWindowClosed(False)
|
app.setQuitOnLastWindowClosed(False)
|
||||||
|
|
||||||
init_db()
|
init_db()
|
||||||
theme.load() # 从数据库读取上次主题
|
theme.load()
|
||||||
_set_autostart(True)
|
_set_autostart(True)
|
||||||
|
|
||||||
panel = PanelWindow()
|
panel = PanelWindow()
|
||||||
ball = FloatBall()
|
ball = FloatBall()
|
||||||
|
|
||||||
panel._ball_ref = ball
|
panel._ball_ref = ball
|
||||||
|
panel._apply_pin_window_layer()
|
||||||
|
|
||||||
ball.clicked.connect(lambda: panel.show_near(ball.pos(), BALL_SIZE))
|
ball.clicked.connect(lambda: panel.show_near(ball.pos(), BALL_SIZE))
|
||||||
ball.right_clicked.connect(lambda pos: panel.tray.contextMenu().exec(pos))
|
ball.right_clicked.connect(lambda pos: panel.tray.contextMenu().exec(pos))
|
||||||
|
|
||||||
# 判断是否有保存的位置:有则直接在原位显示,没有则用悬浮球旁边
|
|
||||||
has_saved = bool(database.get_setting("panel_x", ""))
|
has_saved = bool(database.get_setting("panel_x", ""))
|
||||||
if has_saved:
|
if has_saved:
|
||||||
# _restore_geometry 已在 PanelWindow.__init__ 里执行,直接 show
|
|
||||||
panel.setWindowOpacity(0)
|
panel.setWindowOpacity(0)
|
||||||
panel.show()
|
panel.show()
|
||||||
panel.raise_()
|
panel.raise_()
|
||||||
if hasattr(panel, "_ball_ref"):
|
if hasattr(panel, "_ball_ref"):
|
||||||
panel._ball_ref.hide()
|
panel._ball_ref.hide()
|
||||||
# 淡入
|
|
||||||
from PyQt6.QtCore import QPropertyAnimation
|
from PyQt6.QtCore import QPropertyAnimation
|
||||||
anim = QPropertyAnimation(panel, b"windowOpacity")
|
anim = QPropertyAnimation(panel, b"windowOpacity")
|
||||||
anim.setDuration(180)
|
anim.setDuration(180)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
292
ui/ball.py
292
ui/ball.py
@ -1,55 +1,154 @@
|
|||||||
|
import ctypes
|
||||||
import os
|
import os
|
||||||
from PyQt6.QtWidgets import QWidget, QApplication
|
import sys
|
||||||
from PyQt6.QtCore import Qt, QPoint, QTimer, pyqtSignal, QPropertyAnimation, QEasingCurve, QRectF
|
from ctypes import wintypes
|
||||||
from PyQt6.QtGui import (QPainter, QPixmap, QBrush, QColor, QPen,
|
|
||||||
QPainterPath, QRadialGradient, QIcon)
|
|
||||||
|
|
||||||
BALL_SIZE = 60
|
from PyQt6.QtWidgets import QWidget, QApplication
|
||||||
LOGO_SIZE = 34
|
from PyQt6.QtCore import Qt, QPoint, pyqtSignal, QPropertyAnimation, QEasingCurve, QRectF
|
||||||
LOGO_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logo.png")
|
from PyQt6.QtGui import (QPainter, QPixmap, QBrush, QColor,
|
||||||
|
QPainterPath, QRadialGradient, QLinearGradient,
|
||||||
|
QIcon, QRegion)
|
||||||
|
|
||||||
|
# 球体内容直径;外侧留一圈给投影
|
||||||
|
BALL_DIAM = 96
|
||||||
|
SHADOW_PAD = 5
|
||||||
|
# 窗口边长 = 圆直径(含投影环),main 里 show_near 用此尺寸
|
||||||
|
BALL_SIZE = BALL_DIAM + 1 * SHADOW_PAD
|
||||||
DRAG_THRESHOLD = 6
|
DRAG_THRESHOLD = 6
|
||||||
|
|
||||||
|
|
||||||
|
def _resource_path(*parts: str) -> str:
|
||||||
|
"""开发与 PyInstaller 打包后均能定位项目根目录资源。"""
|
||||||
|
base = sys._MEIPASS if hasattr(sys, "_MEIPASS") else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
return os.path.join(base, *parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_logo_path():
|
||||||
|
for name in ("logo.png", "logo.ico"):
|
||||||
|
p = _resource_path(name)
|
||||||
|
if os.path.isfile(p):
|
||||||
|
return p
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _load_logo_pixmap(path):
|
||||||
|
"""ICO 多尺寸时用 QIcon 取较大图,避免位图过小、放大后发糊或带边线感。"""
|
||||||
|
if not path:
|
||||||
|
return QPixmap()
|
||||||
|
if os.path.splitext(path)[1].lower() == ".ico":
|
||||||
|
pm = QIcon(path).pixmap(512, 512)
|
||||||
|
if not pm.isNull():
|
||||||
|
return pm
|
||||||
|
return QPixmap(path)
|
||||||
|
|
||||||
|
|
||||||
|
def _win32_set_ellipse_window_rgn(hwnd, w, h):
|
||||||
|
"""用系统区域裁切 HWND。"""
|
||||||
|
if not hwnd or w < 2 or h < 2:
|
||||||
|
return
|
||||||
|
gdi32 = ctypes.windll.gdi32
|
||||||
|
user32 = ctypes.windll.user32
|
||||||
|
hrgn = gdi32.CreateEllipticRgn(0, 0, w, h)
|
||||||
|
if not hrgn:
|
||||||
|
return
|
||||||
|
user32.SetWindowRgn(hwnd, hrgn, True)
|
||||||
|
|
||||||
|
|
||||||
|
def _win32_disable_dwm_rounded_shell(hwnd):
|
||||||
|
"""减弱 Win11 给无边框窗套的圆角矩形描边/阴影(与自绘圆叠在一起会像方框)。"""
|
||||||
|
if not hwnd:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
dwm = ctypes.windll.dwmapi
|
||||||
|
DWMWA_WINDOW_CORNER_PREFERENCE = 33
|
||||||
|
DWMWCP_DONOTROUND = 1
|
||||||
|
pref = ctypes.c_uint(DWMWCP_DONOTROUND)
|
||||||
|
dwm.DwmSetWindowAttribute(
|
||||||
|
wintypes.HWND(hwnd),
|
||||||
|
wintypes.DWORD(DWMWA_WINDOW_CORNER_PREFERENCE),
|
||||||
|
ctypes.byref(pref),
|
||||||
|
ctypes.sizeof(pref),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FloatBall(QWidget):
|
class FloatBall(QWidget):
|
||||||
clicked = pyqtSignal()
|
clicked = pyqtSignal()
|
||||||
right_clicked = pyqtSignal(QPoint)
|
right_clicked = pyqtSignal(QPoint)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowFlags(
|
self._stays_on_top = False
|
||||||
Qt.WindowType.FramelessWindowHint |
|
self._apply_window_flags()
|
||||||
Qt.WindowType.WindowStaysOnTopHint |
|
self._logo_path = _resolve_logo_path()
|
||||||
Qt.WindowType.Tool
|
if self._logo_path:
|
||||||
)
|
self.setWindowIcon(QIcon(self._logo_path))
|
||||||
if os.path.exists(LOGO_PATH):
|
|
||||||
self.setWindowIcon(QIcon(LOGO_PATH))
|
|
||||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground)
|
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground)
|
||||||
# 关键:让整个窗口区域都透明,不显示系统边框
|
self.setAutoFillBackground(False)
|
||||||
self.setFixedSize(BALL_SIZE + 20, BALL_SIZE + 20) # 留出光晕空间
|
# 正方形窗口边长 = 圆直径,绘制也用满圆,避免出现「大圆环套小图」
|
||||||
|
self.setFixedSize(BALL_SIZE, BALL_SIZE)
|
||||||
|
self._apply_circular_window_shape()
|
||||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
|
||||||
self._logo = QPixmap(LOGO_PATH) if os.path.exists(LOGO_PATH) else QPixmap()
|
self._logo = _load_logo_pixmap(self._logo_path) if self._logo_path else QPixmap()
|
||||||
self._drag_start = QPoint()
|
self._drag_start = QPoint()
|
||||||
self._dragging = False
|
self._dragging = False
|
||||||
self._hovered = False
|
self._hovered = False
|
||||||
self._glow = 0.0 # 0.0~1.0 光晕强度
|
self._glow = 0.0 # 0.0~1.0 光晕强度
|
||||||
|
|
||||||
# 光晕动画
|
|
||||||
self._glow_anim = QPropertyAnimation(self, b"_glow_prop")
|
self._glow_anim = QPropertyAnimation(self, b"_glow_prop")
|
||||||
self._glow_anim.setDuration(300)
|
self._glow_anim.setDuration(300)
|
||||||
self._glow_anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
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._place_default()
|
||||||
self._idle_timer.start()
|
|
||||||
|
|
||||||
# Qt property 用于动画
|
def set_stays_on_top(self, on_top):
|
||||||
|
"""与主面板图钉联动:仅图钉开启时置顶,避免挡在其他程序前面。"""
|
||||||
|
if on_top == self._stays_on_top:
|
||||||
|
return
|
||||||
|
self._stays_on_top = bool(on_top)
|
||||||
|
was_visible = self.isVisible()
|
||||||
|
geo = self.geometry()
|
||||||
|
self._apply_window_flags()
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground, True)
|
||||||
|
if self._logo_path:
|
||||||
|
self.setWindowIcon(QIcon(self._logo_path))
|
||||||
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
self._apply_circular_window_shape()
|
||||||
|
if was_visible:
|
||||||
|
self.setGeometry(geo)
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
def _apply_circular_window_shape(self):
|
||||||
|
w, h = self.width(), self.height()
|
||||||
|
self.setMask(QRegion(0, 0, w, h, QRegion.RegionType.Ellipse))
|
||||||
|
if sys.platform == "win32":
|
||||||
|
wid = int(self.winId())
|
||||||
|
if wid:
|
||||||
|
_win32_set_ellipse_window_rgn(wid, w, h)
|
||||||
|
_win32_disable_dwm_rounded_shell(wid)
|
||||||
|
|
||||||
|
def showEvent(self, event):
|
||||||
|
super().showEvent(event)
|
||||||
|
if sys.platform == "win32":
|
||||||
|
wid = int(self.winId())
|
||||||
|
if wid:
|
||||||
|
_win32_set_ellipse_window_rgn(wid, self.width(), self.height())
|
||||||
|
_win32_disable_dwm_rounded_shell(wid)
|
||||||
|
|
||||||
|
def _apply_window_flags(self):
|
||||||
|
flags = (
|
||||||
|
Qt.WindowType.FramelessWindowHint
|
||||||
|
| Qt.WindowType.Tool
|
||||||
|
| Qt.WindowType.NoDropShadowWindowHint
|
||||||
|
)
|
||||||
|
if self._stays_on_top:
|
||||||
|
flags |= Qt.WindowType.WindowStaysOnTopHint
|
||||||
|
self.setWindowFlags(flags)
|
||||||
|
|
||||||
def _get_glow(self): return self._glow
|
def _get_glow(self): return self._glow
|
||||||
def _set_glow(self, v): self._glow = v; self.update()
|
def _set_glow(self, v): self._glow = v; self.update()
|
||||||
from PyQt6.QtCore import pyqtProperty
|
from PyQt6.QtCore import pyqtProperty
|
||||||
@ -57,79 +156,118 @@ class FloatBall(QWidget):
|
|||||||
|
|
||||||
def _place_default(self):
|
def _place_default(self):
|
||||||
screen = QApplication.primaryScreen().availableGeometry()
|
screen = QApplication.primaryScreen().availableGeometry()
|
||||||
pad = (self.width() - BALL_SIZE) // 2
|
self.move(
|
||||||
self.move(screen.right() - self.width() - 20 + pad,
|
screen.right() - self.width() - 20,
|
||||||
screen.top() + screen.height() // 2 - self.height() // 2)
|
screen.top() + screen.height() // 2 - self.height() // 2,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _paint_radius_outer(self):
|
||||||
|
w, h = self.width(), self.height()
|
||||||
|
return min(w, h) * 0.5 - 0.5
|
||||||
|
|
||||||
|
def _paint_radius_ball(self):
|
||||||
|
return self._paint_radius_outer() - SHADOW_PAD
|
||||||
|
|
||||||
# ── 绘制 ────────────────────────────────────────────
|
|
||||||
def paintEvent(self, event):
|
def paintEvent(self, event):
|
||||||
p = QPainter(self)
|
p = QPainter(self)
|
||||||
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
|
|
||||||
pad = (self.width() - BALL_SIZE) // 2 # 光晕留白
|
w, h = self.width(), self.height()
|
||||||
cx = self.width() / 2
|
cx = w * 0.5
|
||||||
cy = self.height() / 2
|
cy = h * 0.5
|
||||||
r = BALL_SIZE / 2 - 1
|
r_out = self._paint_radius_outer()
|
||||||
|
r = self._paint_radius_ball()
|
||||||
|
g = self._glow
|
||||||
|
glow_strength = 0.28 + 0.72 * g
|
||||||
|
|
||||||
# 1. 外部光晕(hover 时)
|
disc_out = QRectF(cx - r_out, cy - r_out, r_out * 2, r_out * 2)
|
||||||
if self._glow > 0:
|
disc = QRectF(cx - r, cy - r, r * 2, r * 2)
|
||||||
glow_r = r + 10 * self._glow
|
|
||||||
grad = QRadialGradient(cx, cy, glow_r)
|
# 0. 外环投影(内容圆与窗口边之间的环形:下重上轻,模拟落地阴影)
|
||||||
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.setPen(Qt.PenStyle.NoPen)
|
||||||
p.drawEllipse(QRectF(cx - glow_r, cy - glow_r, glow_r * 2, glow_r * 2))
|
outer_path = QPainterPath()
|
||||||
|
outer_path.addEllipse(disc_out)
|
||||||
|
inner_hole = QPainterPath()
|
||||||
|
inner_hole.addEllipse(disc)
|
||||||
|
ring = outer_path.subtracted(inner_hole)
|
||||||
|
sh = QLinearGradient(0, cy - r_out, 0, cy + r_out)
|
||||||
|
sh.setColorAt(0.0, QColor(0, 0, 0, 0))
|
||||||
|
sh.setColorAt(0.45, QColor(0, 0, 0, 0))
|
||||||
|
sh.setColorAt(0.62, QColor(0, 0, 0, int(18 + 22 * glow_strength)))
|
||||||
|
sh.setColorAt(0.82, QColor(0, 0, 0, int(38 + 35 * g)))
|
||||||
|
sh.setColorAt(1.0, QColor(0, 0, 0, int(52 + 40 * g)))
|
||||||
|
p.fillPath(ring, QBrush(sh))
|
||||||
|
|
||||||
# 2. 圆形裁剪
|
|
||||||
clip = QPainterPath()
|
clip = QPainterPath()
|
||||||
clip.addEllipse(QRectF(cx - r, cy - r, r * 2, r * 2))
|
clip.addEllipse(disc)
|
||||||
p.setClipPath(clip)
|
p.setClipPath(clip)
|
||||||
|
|
||||||
# 3. 白色半透明毛玻璃底
|
# 1. 毛玻璃底:半透明冷白渐变(磨砂基底)
|
||||||
p.setBrush(QBrush(QColor(255, 255, 255, 210)))
|
frost_bg = QLinearGradient(0, cy - r, 0, cy + r)
|
||||||
p.setPen(Qt.PenStyle.NoPen)
|
frost_bg.setColorAt(0.0, QColor(255, 255, 255, int(175 + 35 * glow_strength)))
|
||||||
p.drawEllipse(QRectF(cx - r, cy - r, r * 2, r * 2))
|
frost_bg.setColorAt(0.42, QColor(244, 249, 255, int(130 + 40 * glow_strength)))
|
||||||
|
frost_bg.setColorAt(1.0, QColor(228, 236, 248, int(155 + 45 * glow_strength)))
|
||||||
|
p.setBrush(QBrush(frost_bg))
|
||||||
|
p.drawEllipse(disc)
|
||||||
|
|
||||||
# 4. 顶部高光
|
# 2. 斜向磨砂高光(模拟玻璃反光)
|
||||||
hi = QRadialGradient(cx, cy - r * 0.3, r * 0.9)
|
sheen = QLinearGradient(cx - r, cy - r, cx + r * 0.55, cy + r * 0.85)
|
||||||
hi.setColorAt(0, QColor(255, 255, 255, 140))
|
sheen.setColorAt(0.0, QColor(255, 255, 255, 0))
|
||||||
hi.setColorAt(1, QColor(255, 255, 255, 0))
|
sheen.setColorAt(0.38, QColor(255, 255, 255, int(38 + 45 * glow_strength)))
|
||||||
p.setBrush(QBrush(hi))
|
sheen.setColorAt(0.52, QColor(255, 255, 255, int(14 + 18 * glow_strength)))
|
||||||
p.drawEllipse(QRectF(cx - r, cy - r, r * 2, r * 2))
|
sheen.setColorAt(1.0, QColor(255, 255, 255, 0))
|
||||||
|
p.setCompositionMode(QPainter.CompositionMode.CompositionMode_SoftLight)
|
||||||
|
p.setBrush(QBrush(sheen))
|
||||||
|
p.drawEllipse(disc)
|
||||||
|
p.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
|
||||||
|
|
||||||
# 5. logo 居中
|
# 3. Logo(cover 铺满圆)
|
||||||
if not self._logo.isNull():
|
if not self._logo.isNull():
|
||||||
|
p.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True)
|
||||||
|
cover = max(1, int(2 * r))
|
||||||
scaled = self._logo.scaled(
|
scaled = self._logo.scaled(
|
||||||
LOGO_SIZE, LOGO_SIZE,
|
cover, cover,
|
||||||
Qt.AspectRatioMode.KeepAspectRatio,
|
Qt.AspectRatioMode.KeepAspectRatioByExpanding,
|
||||||
Qt.TransformationMode.SmoothTransformation
|
Qt.TransformationMode.SmoothTransformation
|
||||||
)
|
)
|
||||||
lx = int(cx - scaled.width() / 2)
|
lx = int(cx - scaled.width() / 2)
|
||||||
ly = int(cy - scaled.height() / 2)
|
ly = int(cy - scaled.height() / 2)
|
||||||
p.drawPixmap(lx, ly, scaled)
|
p.drawPixmap(lx, ly, scaled)
|
||||||
|
|
||||||
# 6. 边框(取消裁剪后画)
|
# 4. 覆在图上的雾面(毛玻璃「蒙在内容上」)
|
||||||
|
veil = QLinearGradient(cx, cy - r, cx, cy + r)
|
||||||
|
veil.setColorAt(0.0, QColor(255, 255, 255, int(52 + 38 * g)))
|
||||||
|
veil.setColorAt(0.35, QColor(255, 255, 255, int(28 + 22 * g)))
|
||||||
|
veil.setColorAt(0.75, QColor(248, 252, 255, int(18 + 15 * g)))
|
||||||
|
veil.setColorAt(1.0, QColor(240, 245, 252, int(22 + 18 * g)))
|
||||||
|
p.setBrush(QBrush(veil))
|
||||||
|
p.drawEllipse(disc)
|
||||||
|
|
||||||
|
# 5. 内缘光晕(边缘略亮,悬停时更柔)
|
||||||
|
rim = QRadialGradient(cx, cy, r)
|
||||||
|
rim.setColorAt(0.0, QColor(255, 255, 255, 0))
|
||||||
|
rim.setColorAt(0.62, QColor(255, 255, 255, 0))
|
||||||
|
rim.setColorAt(0.88, QColor(255, 255, 255, int(28 + 55 * glow_strength)))
|
||||||
|
rim.setColorAt(0.97, QColor(255, 255, 255, int(45 + 85 * glow_strength)))
|
||||||
|
rim.setColorAt(1.0, QColor(255, 255, 255, int(22 + 40 * glow_strength)))
|
||||||
|
p.setBrush(QBrush(rim))
|
||||||
|
p.drawEllipse(disc)
|
||||||
|
|
||||||
|
# 6. 顶区高光(玻璃上沿反光)
|
||||||
|
hi = QRadialGradient(cx, cy - r * 0.22, r * 1.02)
|
||||||
|
hi.setColorAt(0.0, QColor(255, 255, 255, int(42 + 55 * glow_strength)))
|
||||||
|
hi.setColorAt(0.45, QColor(255, 255, 255, int(12 + 18 * glow_strength)))
|
||||||
|
hi.setColorAt(1.0, QColor(255, 255, 255, 0))
|
||||||
|
p.setBrush(QBrush(hi))
|
||||||
|
p.drawEllipse(disc)
|
||||||
|
|
||||||
p.setClipping(False)
|
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()
|
p.end()
|
||||||
|
|
||||||
# ── 鼠标事件 ────────────────────────────────────────
|
|
||||||
def mousePressEvent(self, event):
|
def mousePressEvent(self, event):
|
||||||
if event.button() == Qt.MouseButton.LeftButton:
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
self._drag_start = event.globalPosition().toPoint()
|
self._drag_start = event.globalPosition().toPoint()
|
||||||
self._dragging = False
|
self._dragging = False
|
||||||
self.setWindowOpacity(1.0)
|
|
||||||
self._idle_timer.stop()
|
|
||||||
elif event.button() == Qt.MouseButton.RightButton:
|
elif event.button() == Qt.MouseButton.RightButton:
|
||||||
self.right_clicked.emit(event.globalPosition().toPoint())
|
self.right_clicked.emit(event.globalPosition().toPoint())
|
||||||
|
|
||||||
@ -153,12 +291,9 @@ class FloatBall(QWidget):
|
|||||||
self.clicked.emit()
|
self.clicked.emit()
|
||||||
else:
|
else:
|
||||||
self._snap_to_edge()
|
self._snap_to_edge()
|
||||||
self._idle_timer.start()
|
|
||||||
|
|
||||||
def enterEvent(self, event):
|
def enterEvent(self, event):
|
||||||
self._hovered = True
|
self._hovered = True
|
||||||
self.setWindowOpacity(1.0)
|
|
||||||
self._idle_timer.stop()
|
|
||||||
self._glow_anim.stop()
|
self._glow_anim.stop()
|
||||||
self._glow_anim.setStartValue(self._glow)
|
self._glow_anim.setStartValue(self._glow)
|
||||||
self._glow_anim.setEndValue(1.0)
|
self._glow_anim.setEndValue(1.0)
|
||||||
@ -170,9 +305,7 @@ class FloatBall(QWidget):
|
|||||||
self._glow_anim.setStartValue(self._glow)
|
self._glow_anim.setStartValue(self._glow)
|
||||||
self._glow_anim.setEndValue(0.0)
|
self._glow_anim.setEndValue(0.0)
|
||||||
self._glow_anim.start()
|
self._glow_anim.start()
|
||||||
self._idle_timer.start()
|
|
||||||
|
|
||||||
# ── 吸附边缘 ─────────────────────────────────────────
|
|
||||||
def _snap_to_edge(self):
|
def _snap_to_edge(self):
|
||||||
screen = QApplication.primaryScreen().availableGeometry()
|
screen = QApplication.primaryScreen().availableGeometry()
|
||||||
cx = self.x() + self.width() // 2
|
cx = self.x() + self.width() // 2
|
||||||
@ -197,10 +330,3 @@ class FloatBall(QWidget):
|
|||||||
anim.setEndValue(ends[side])
|
anim.setEndValue(ends[side])
|
||||||
self._snap_anim = anim
|
self._snap_anim = anim
|
||||||
anim.start()
|
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()
|
|
||||||
|
|||||||
43
ui/dock.py
43
ui/dock.py
@ -314,9 +314,8 @@ class PanelWindow(QWidget):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setWindowTitle("牛马软件柜")
|
self.setWindowTitle("牛马软件柜")
|
||||||
self.setWindowFlags(
|
# 默认不置顶;仅图钉开启时与悬浮球一并置顶(见 _apply_pin_window_layer)
|
||||||
Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint
|
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
|
||||||
)
|
|
||||||
# 任务栏/开始菜单图标:使用项目 logo.png
|
# 任务栏/开始菜单图标:使用项目 logo.png
|
||||||
logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logo.png")
|
logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logo.png")
|
||||||
if os.path.exists(logo_path):
|
if os.path.exists(logo_path):
|
||||||
@ -522,7 +521,9 @@ class PanelWindow(QWidget):
|
|||||||
|
|
||||||
self.pin_btn = QPushButton()
|
self.pin_btn = QPushButton()
|
||||||
self.pin_btn.setFixedSize(26, 26)
|
self.pin_btn.setFixedSize(26, 26)
|
||||||
self.pin_btn.setToolTip("固定:开启后最小化只收缩内容,不变悬浮球")
|
self.pin_btn.setToolTip(
|
||||||
|
"固定:面板与悬浮球置顶;开启后最小化只收缩内容,不变悬浮球"
|
||||||
|
)
|
||||||
self.pin_btn.setStyleSheet("border:none; background:transparent;")
|
self.pin_btn.setStyleSheet("border:none; background:transparent;")
|
||||||
self.pin_btn.clicked.connect(self._toggle_pin)
|
self.pin_btn.clicked.connect(self._toggle_pin)
|
||||||
|
|
||||||
@ -995,18 +996,45 @@ class PanelWindow(QWidget):
|
|||||||
f"已创建分组「{group_name}」,共 {count} 个文件(实时读取)。",
|
f"已创建分组「{group_name}」,共 {count} 个文件(实时读取)。",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _toggle_pin(self):
|
def _sync_pin_dependent_ui(self):
|
||||||
self._pinned = not self._pinned
|
"""图钉图标、最小化提示、置顶标志(面板 + 悬浮球)。"""
|
||||||
ic = "#cccccc" if theme.name() == "dark" else "#555555"
|
ic = "#cccccc" if theme.name() == "dark" else "#555555"
|
||||||
self.pin_btn.setIcon(
|
self.pin_btn.setIcon(
|
||||||
qta.icon("fa5s.thumbtack", color="#f90" if self._pinned else ic)
|
qta.icon("fa5s.thumbtack", color="#f90" if self._pinned else ic)
|
||||||
)
|
)
|
||||||
self.pin_btn.setIconSize(QSize(13, 13))
|
self.pin_btn.setIconSize(QSize(13, 13))
|
||||||
# 更新最小化按钮 tooltip
|
|
||||||
if self._pinned:
|
if self._pinned:
|
||||||
self._min_btn.setToolTip("收缩内容区(固定模式)")
|
self._min_btn.setToolTip("收缩内容区(固定模式)")
|
||||||
else:
|
else:
|
||||||
self._min_btn.setToolTip("最小化到悬浮球")
|
self._min_btn.setToolTip("最小化到悬浮球")
|
||||||
|
self._apply_pin_window_layer()
|
||||||
|
|
||||||
|
def _apply_pin_window_layer(self):
|
||||||
|
"""图钉开 → 面板与悬浮球 WindowStaysOnTop;关 → 普通叠放,不挡其他程序。"""
|
||||||
|
on_top = self._pinned
|
||||||
|
if getattr(self, "_pin_top_applied", None) is not on_top:
|
||||||
|
self._pin_top_applied = on_top
|
||||||
|
was_visible = self.isVisible()
|
||||||
|
geo = self.geometry()
|
||||||
|
flags = Qt.WindowType.FramelessWindowHint
|
||||||
|
if on_top:
|
||||||
|
flags |= Qt.WindowType.WindowStaysOnTopHint
|
||||||
|
self.setWindowFlags(flags)
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logo.png")
|
||||||
|
if os.path.exists(logo_path):
|
||||||
|
self.setWindowIcon(QIcon(logo_path))
|
||||||
|
if was_visible:
|
||||||
|
self.setGeometry(geo)
|
||||||
|
self.show()
|
||||||
|
ball = getattr(self, "_ball_ref", None)
|
||||||
|
if ball is not None:
|
||||||
|
ball.set_stays_on_top(on_top)
|
||||||
|
|
||||||
|
def _toggle_pin(self):
|
||||||
|
self._pinned = not self._pinned
|
||||||
|
self._sync_pin_dependent_ui()
|
||||||
|
|
||||||
def _on_min_click(self):
|
def _on_min_click(self):
|
||||||
"""图钉开启时收缩内容区,否则最小化到悬浮球"""
|
"""图钉开启时收缩内容区,否则最小化到悬浮球"""
|
||||||
@ -1139,7 +1167,6 @@ class PanelWindow(QWidget):
|
|||||||
if hasattr(self, "_ball_ref"):
|
if hasattr(self, "_ball_ref"):
|
||||||
self._ball_ref.show()
|
self._ball_ref.show()
|
||||||
self._ball_ref.setWindowOpacity(1.0)
|
self._ball_ref.setWindowOpacity(1.0)
|
||||||
self._ball_ref._idle_timer.start()
|
|
||||||
|
|
||||||
def toggle_near(self, ball_pos: QPoint, ball_size: int):
|
def toggle_near(self, ball_pos: QPoint, ball_size: int):
|
||||||
if self.isVisible():
|
if self.isVisible():
|
||||||
|
|||||||
@ -181,7 +181,7 @@ class SettingsWindow(QDialog):
|
|||||||
|
|
||||||
layout.addWidget(self._section_title("窗口行为"))
|
layout.addWidget(self._section_title("窗口行为"))
|
||||||
|
|
||||||
self._pin_check = QCheckBox("固定面板(不自动隐藏)")
|
self._pin_check = QCheckBox("固定面板(置顶、不自动隐藏)")
|
||||||
self._pin_check.setChecked(self._panel._pinned)
|
self._pin_check.setChecked(self._panel._pinned)
|
||||||
self._pin_check.toggled.connect(self._on_pin)
|
self._pin_check.toggled.connect(self._on_pin)
|
||||||
layout.addWidget(self._pin_check)
|
layout.addWidget(self._pin_check)
|
||||||
@ -213,11 +213,7 @@ class SettingsWindow(QDialog):
|
|||||||
|
|
||||||
def _on_pin(self, checked: bool):
|
def _on_pin(self, checked: bool):
|
||||||
self._panel._pinned = checked
|
self._panel._pinned = checked
|
||||||
ic = "#cccccc" if theme.name() == "dark" else "#555555"
|
self._panel._sync_pin_dependent_ui()
|
||||||
import qtawesome as qta
|
|
||||||
self._panel.pin_btn.setIcon(
|
|
||||||
qta.icon("fa5s.thumbtack", color="#f90" if checked else ic)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _on_icon_size(self):
|
def _on_icon_size(self):
|
||||||
val = self._icon_size_spin.value()
|
val = self._icon_size_spin.value()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user