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