This commit is contained in:
2026-04-21 23:38:29 +08:00
commit d5444cdb35
15 changed files with 1236 additions and 0 deletions
+208
View File
@@ -0,0 +1,208 @@
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