Source code for canns.pipeline.asa_gui.views.widgets.image_viewer
"""Image viewer widget with zoom and pan."""
from __future__ import annotations
from pathlib import Path
from PySide6.QtCore import QPointF, QRectF, Qt
from PySide6.QtGui import QColor, QPainter, QPixmap
from PySide6.QtWidgets import QGraphicsPixmapItem, QGraphicsScene, QGraphicsTextItem, QGraphicsView
[docs]
class ImageViewer(QGraphicsView):
"""Image viewer with fit-to-view, zoom (wheel), and pan (drag)."""
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
self.setDragMode(QGraphicsView.ScrollHandDrag)
self.setMinimumHeight(200)
self._scene = QGraphicsScene(self)
self.setScene(self._scene)
self._pixmap_item = QGraphicsPixmapItem()
self._scene.addItem(self._pixmap_item)
self._placeholder = QGraphicsTextItem("No image")
self._placeholder.setDefaultTextColor(QColor("#888"))
self._scene.addItem(self._placeholder)
self._has_image = False
self._auto_fit = True
[docs]
def set_image(self, path: str | Path | None) -> None:
if path is None:
self._set_placeholder("No image")
return
path = Path(path)
if not path.exists():
self._set_placeholder(f"Missing: {path}")
return
pixmap = QPixmap(str(path))
if pixmap.isNull():
self._set_placeholder("Failed to load image")
return
self._has_image = True
self._auto_fit = True
self._pixmap_item.setPixmap(pixmap)
self._pixmap_item.setVisible(True)
self._placeholder.setVisible(False)
self._scene.setSceneRect(QRectF(pixmap.rect()))
self.resetTransform()
self.fitInView(self._pixmap_item, Qt.KeepAspectRatio)
[docs]
def wheelEvent(self, event) -> None: # noqa: N802 - Qt naming
if not self._has_image:
return
delta = event.angleDelta().y()
if delta == 0:
return
factor = 1.2 if delta > 0 else 1 / 1.2
self._auto_fit = False
self.scale(factor, factor)
[docs]
def mouseDoubleClickEvent(self, event) -> None: # noqa: N802 - Qt naming
if self._has_image:
self._auto_fit = True
self.resetTransform()
self.fitInView(self._pixmap_item, Qt.KeepAspectRatio)
super().mouseDoubleClickEvent(event)
[docs]
def resizeEvent(self, event) -> None: # noqa: N802 - Qt naming
super().resizeEvent(event)
if self._has_image and self._auto_fit:
self.fitInView(self._pixmap_item, Qt.KeepAspectRatio)
self._center_placeholder()
def _set_placeholder(self, text: str) -> None:
self._has_image = False
self._auto_fit = True
self._pixmap_item.setPixmap(QPixmap())
self._pixmap_item.setVisible(False)
self._placeholder.setPlainText(text)
self._placeholder.setVisible(True)
self._scene.setSceneRect(QRectF(0, 0, self.viewport().width(), self.viewport().height()))
self._center_placeholder()
def _center_placeholder(self) -> None:
if not self._placeholder.isVisible():
return
view_rect = self.viewport().rect()
center_scene = self.mapToScene(view_rect.center())
br = self._placeholder.boundingRect()
self._placeholder.setPos(
QPointF(center_scene.x() - br.width() / 2, center_scene.y() - br.height() / 2)
)