Files
2026-04-21 23:38:29 +08:00

209 lines
6.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLabel
from PyQt6.QtCore import Qt, pyqtSignal, QPoint
from PyQt6.QtGui import QColor, QTextCursor, QMouseEvent
from .config import OverlayCfg
_TITLE = "AI 答题助手"
_LOADING_TEXT = "正在分析截图,请稍候..."
class _CloseBtn(QLabel):
def __init__(self, on_click, parent=None):
super().__init__("×", parent)
self._on_click = on_click
self.setCursor(Qt.CursorShape.PointingHandCursor)
def mousePressEvent(self, event) -> None:
if event.button() == Qt.MouseButton.LeftButton:
self._on_click()
class _TitleBar(QWidget):
def __init__(self, win: "OverlayWindow", cfg: OverlayCfg):
super().__init__(win)
self._win = win
self.setFixedHeight(24)
self.setStyleSheet(f"background-color: {cfg.bg_color}; border-radius: 6px 6px 0 0;")
self.setMouseTracking(True)
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
self._drag_pos = None
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 0, 6, 0)
title_lbl = QLabel(f"· {_TITLE}")
title_lbl.setStyleSheet(f"color: {cfg.accent_color}; font-size: 11px;")
# 让标签对鼠标透明,事件穿透到 _TitleBar
title_lbl.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
close_btn = _CloseBtn(win._on_hide)
close_btn.setStyleSheet(f"color: {cfg.accent_color}; padding: 0 4px; font-size: 14px;")
layout.addWidget(title_lbl)
layout.addStretch()
layout.addWidget(close_btn)
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
# 👇 就是在这里用 frameGeometry()
self._drag_pos = event.globalPosition().toPoint() - self._win.frameGeometry().topLeft()
def mouseMoveEvent(self, event):
if self._drag_pos is not None and event.buttons() & Qt.MouseButton.LeftButton:
self._win.move(event.globalPosition().toPoint() - self._drag_pos)
def mouseReleaseEvent(self, event):
self._drag_pos = None
class OverlayWindow(QWidget):
_sig_append = pyqtSignal(str)
_sig_loading = pyqtSignal()
_sig_clear = pyqtSignal()
_sig_show = pyqtSignal()
_sig_hide = pyqtSignal()
_sig_toggle = pyqtSignal()
def __init__(self, cfg: OverlayCfg):
self._app = QApplication.instance() or QApplication([])
super().__init__()
self._cfg = cfg
self._drag_pos: QPoint | None = None
self._sig_append.connect(self._on_append)
self._sig_loading.connect(self._on_loading)
self._sig_clear.connect(self._on_clear)
self._sig_show.connect(self._on_show)
self._sig_hide.connect(self._on_hide)
self._sig_toggle.connect(self._on_toggle)
self._setup_window()
self._setup_ui()
self._position_bottom_right()
self.show()
def _setup_window(self) -> None:
self.setWindowFlags(
Qt.WindowType.WindowStaysOnTopHint |
Qt.WindowType.Tool
)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setWindowOpacity(self._cfg.alpha)
self.resize(self._cfg.width, self._cfg.height)
def _setup_ui(self) -> None:
cfg = self._cfg
outer = QVBoxLayout(self)
outer.setContentsMargins(0, 0, 0, 0)
container = QWidget()
container.setObjectName("container")
container.setStyleSheet(f"""
#container {{
background-color: {cfg.bg_color};
border-radius: 6px;
}}
""")
outer.addWidget(container)
inner = QVBoxLayout(container)
inner.setContentsMargins(0, 0, 0, 0)
inner.setSpacing(0)
inner.addWidget(_TitleBar(self, cfg))
self._text = QTextEdit()
self._text.setReadOnly(True)
self._text.setStyleSheet(f"""
QTextEdit {{
background-color: {cfg.bg_color};
color: {cfg.fg_color};
border: none;
padding: 6px 8px;
font-size: 13px;
}}
QScrollBar:vertical {{
background: transparent;
width: 4px;
border: none;
}}
QScrollBar::handle:vertical {{
background: {cfg.accent_color};
border-radius: 2px;
}}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
height: 0;
}}
""")
self._text.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
inner.addWidget(self._text)
def _position_bottom_right(self) -> None:
cfg = self._cfg
screen = QApplication.primaryScreen().availableGeometry()
x = screen.width() - cfg.width - cfg.margin_right
y = screen.height() - cfg.height - cfg.margin_bottom
self.move(x, y)
# ------------------------------------------------------------------
# 公开 API
# ------------------------------------------------------------------
def set_loading(self) -> None:
self._sig_loading.emit()
def append_text(self, chunk: str) -> None:
self._sig_append.emit(chunk)
def clear(self) -> None:
self._sig_clear.emit()
def toggle(self) -> None:
self._sig_toggle.emit()
# ------------------------------------------------------------------
# 槽
# ------------------------------------------------------------------
def _on_loading(self) -> None:
self._text.clear()
self._text.setTextColor(QColor(self._cfg.accent_color))
self._text.insertPlainText(_LOADING_TEXT)
self._text.setTextColor(QColor(self._cfg.fg_color))
self._on_show()
def _on_append(self, chunk: str) -> None:
current = self._text.toPlainText()
if current in (_LOADING_TEXT, _LOADING_TEXT + "\n"):
self._text.clear()
cursor = self._text.textCursor()
cursor.movePosition(QTextCursor.MoveOperation.End)
self._text.setTextCursor(cursor)
self._text.insertPlainText(chunk)
self._text.ensureCursorVisible()
def _on_clear(self) -> None:
self._text.clear()
def _on_show(self) -> None:
self.show()
self.raise_()
def _on_hide(self) -> None:
self.hide()
def _on_toggle(self) -> None:
if self.isVisible():
self.hide()
else:
self.show()
self.raise_()
@property
def app(self) -> QApplication:
return self._app