widgets.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. from __future__ import annotations
  2. from typing import Optional
  3. from PyQt6.QtCore import QPoint, QRect, Qt
  4. from PyQt6.QtGui import QPainter, QPixmap
  5. from PyQt6.QtWidgets import QGraphicsPixmapItem, QGraphicsScene, QGraphicsTextItem, QGraphicsView, QRubberBand, QWidget
  6. class InteractiveImageView(QGraphicsView):
  7. """Image view with wheel-zoom and drag-to-zoom rectangle."""
  8. def __init__(self, title: str, parent: Optional[QWidget] = None) -> None:
  9. super().__init__(parent)
  10. self._title = title
  11. self._scene = QGraphicsScene(self)
  12. self.setScene(self._scene)
  13. self._pix_item = QGraphicsPixmapItem()
  14. self._scene.addItem(self._pix_item)
  15. self._text_item = QGraphicsTextItem(f"{title}: no image")
  16. self._scene.addItem(self._text_item)
  17. self._drag_start: Optional[QPoint] = None
  18. self._pan_last: Optional[QPoint] = None
  19. self._rubber = QRubberBand(QRubberBand.Shape.Rectangle, self.viewport())
  20. self.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform)
  21. self.setDragMode(QGraphicsView.DragMode.NoDrag)
  22. self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
  23. self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
  24. self.setMinimumHeight(360)
  25. self.setStyleSheet("border: 1px solid #d0d0d0; background: #fafafa;")
  26. def set_placeholder(self, text: str) -> None:
  27. self._pix_item.setPixmap(QPixmap())
  28. self._text_item.setPlainText(text)
  29. self._text_item.setVisible(True)
  30. self.resetTransform()
  31. def set_image_bytes(self, content: bytes) -> bool:
  32. pix = QPixmap()
  33. if not pix.loadFromData(content):
  34. self.set_placeholder(f"{self._title}: invalid image")
  35. return False
  36. return self.set_pixmap(pix)
  37. def set_pixmap(self, pix: QPixmap) -> bool:
  38. if pix.isNull():
  39. self.set_placeholder(f"{self._title}: invalid image")
  40. return False
  41. self._pix_item.setPixmap(pix)
  42. self._text_item.setVisible(False)
  43. self.resetTransform()
  44. self.fitInView(self._pix_item, Qt.AspectRatioMode.KeepAspectRatio)
  45. return True
  46. def wheelEvent(self, event) -> None: # type: ignore[override]
  47. delta = event.angleDelta().y()
  48. factor = 1.15 if delta > 0 else 1.0 / 1.15
  49. self.scale(factor, factor)
  50. def mousePressEvent(self, event) -> None: # type: ignore[override]
  51. if self._pix_item.pixmap().isNull():
  52. super().mousePressEvent(event)
  53. return
  54. if event.button() == Qt.MouseButton.LeftButton:
  55. self._drag_start = event.position().toPoint()
  56. self._rubber.setGeometry(QRect(self._drag_start, self._drag_start))
  57. self._rubber.show()
  58. event.accept()
  59. return
  60. if event.button() == Qt.MouseButton.RightButton:
  61. self._pan_last = event.position().toPoint()
  62. self.setCursor(Qt.CursorShape.ClosedHandCursor)
  63. event.accept()
  64. return
  65. super().mousePressEvent(event)
  66. def mouseMoveEvent(self, event) -> None: # type: ignore[override]
  67. if self._drag_start is not None:
  68. cur = event.position().toPoint()
  69. self._rubber.setGeometry(QRect(self._drag_start, cur).normalized())
  70. event.accept()
  71. return
  72. if self._pan_last is not None:
  73. cur = event.position().toPoint()
  74. delta = cur - self._pan_last
  75. self._pan_last = cur
  76. self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - delta.x())
  77. self.verticalScrollBar().setValue(self.verticalScrollBar().value() - delta.y())
  78. event.accept()
  79. return
  80. super().mouseMoveEvent(event)
  81. def mouseReleaseEvent(self, event) -> None: # type: ignore[override]
  82. if event.button() == Qt.MouseButton.LeftButton and self._drag_start is not None:
  83. rect = self._rubber.geometry()
  84. self._rubber.hide()
  85. self._drag_start = None
  86. if rect.width() > 10 and rect.height() > 10:
  87. scene_rect = self.mapToScene(rect).boundingRect()
  88. self.fitInView(scene_rect, Qt.AspectRatioMode.KeepAspectRatio)
  89. event.accept()
  90. return
  91. if event.button() == Qt.MouseButton.RightButton and self._pan_last is not None:
  92. self._pan_last = None
  93. self.setCursor(Qt.CursorShape.ArrowCursor)
  94. event.accept()
  95. return
  96. super().mouseReleaseEvent(event)
  97. def mouseDoubleClickEvent(self, event) -> None: # type: ignore[override]
  98. if not self._pix_item.pixmap().isNull():
  99. self.resetTransform()
  100. self.fitInView(self._pix_item, Qt.AspectRatioMode.KeepAspectRatio)
  101. event.accept()
  102. return
  103. super().mouseDoubleClickEvent(event)