| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- from __future__ import annotations
- from typing import Optional
- from PyQt6.QtCore import QPoint, QRect, Qt
- from PyQt6.QtGui import QPainter, QPixmap
- from PyQt6.QtWidgets import QGraphicsPixmapItem, QGraphicsScene, QGraphicsTextItem, QGraphicsView, QRubberBand, QWidget
- class InteractiveImageView(QGraphicsView):
- """Image view with wheel-zoom and drag-to-zoom rectangle."""
- def __init__(self, title: str, parent: Optional[QWidget] = None) -> None:
- super().__init__(parent)
- self._title = title
- self._scene = QGraphicsScene(self)
- self.setScene(self._scene)
- self._pix_item = QGraphicsPixmapItem()
- self._scene.addItem(self._pix_item)
- self._text_item = QGraphicsTextItem(f"{title}: no image")
- self._scene.addItem(self._text_item)
- self._drag_start: Optional[QPoint] = None
- self._pan_last: Optional[QPoint] = None
- self._rubber = QRubberBand(QRubberBand.Shape.Rectangle, self.viewport())
- self.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform)
- self.setDragMode(QGraphicsView.DragMode.NoDrag)
- self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
- self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
- self.setMinimumHeight(360)
- self.setStyleSheet("border: 1px solid #d0d0d0; background: #fafafa;")
- def set_placeholder(self, text: str) -> None:
- self._pix_item.setPixmap(QPixmap())
- self._text_item.setPlainText(text)
- self._text_item.setVisible(True)
- self.resetTransform()
- def set_image_bytes(self, content: bytes) -> bool:
- pix = QPixmap()
- if not pix.loadFromData(content):
- self.set_placeholder(f"{self._title}: invalid image")
- return False
- return self.set_pixmap(pix)
- def set_pixmap(self, pix: QPixmap) -> bool:
- if pix.isNull():
- self.set_placeholder(f"{self._title}: invalid image")
- return False
- self._pix_item.setPixmap(pix)
- self._text_item.setVisible(False)
- self.resetTransform()
- self.fitInView(self._pix_item, Qt.AspectRatioMode.KeepAspectRatio)
- return True
- def wheelEvent(self, event) -> None: # type: ignore[override]
- delta = event.angleDelta().y()
- factor = 1.15 if delta > 0 else 1.0 / 1.15
- self.scale(factor, factor)
- def mousePressEvent(self, event) -> None: # type: ignore[override]
- if self._pix_item.pixmap().isNull():
- super().mousePressEvent(event)
- return
- if event.button() == Qt.MouseButton.LeftButton:
- self._drag_start = event.position().toPoint()
- self._rubber.setGeometry(QRect(self._drag_start, self._drag_start))
- self._rubber.show()
- event.accept()
- return
- if event.button() == Qt.MouseButton.RightButton:
- self._pan_last = event.position().toPoint()
- self.setCursor(Qt.CursorShape.ClosedHandCursor)
- event.accept()
- return
- super().mousePressEvent(event)
- def mouseMoveEvent(self, event) -> None: # type: ignore[override]
- if self._drag_start is not None:
- cur = event.position().toPoint()
- self._rubber.setGeometry(QRect(self._drag_start, cur).normalized())
- event.accept()
- return
- if self._pan_last is not None:
- cur = event.position().toPoint()
- delta = cur - self._pan_last
- self._pan_last = cur
- self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - delta.x())
- self.verticalScrollBar().setValue(self.verticalScrollBar().value() - delta.y())
- event.accept()
- return
- super().mouseMoveEvent(event)
- def mouseReleaseEvent(self, event) -> None: # type: ignore[override]
- if event.button() == Qt.MouseButton.LeftButton and self._drag_start is not None:
- rect = self._rubber.geometry()
- self._rubber.hide()
- self._drag_start = None
- if rect.width() > 10 and rect.height() > 10:
- scene_rect = self.mapToScene(rect).boundingRect()
- self.fitInView(scene_rect, Qt.AspectRatioMode.KeepAspectRatio)
- event.accept()
- return
- if event.button() == Qt.MouseButton.RightButton and self._pan_last is not None:
- self._pan_last = None
- self.setCursor(Qt.CursorShape.ArrowCursor)
- event.accept()
- return
- super().mouseReleaseEvent(event)
- def mouseDoubleClickEvent(self, event) -> None: # type: ignore[override]
- if not self._pix_item.pixmap().isNull():
- self.resetTransform()
- self.fitInView(self._pix_item, Qt.AspectRatioMode.KeepAspectRatio)
- event.accept()
- return
- super().mouseDoubleClickEvent(event)
|