class PhotosViewer(QGraphicsView): afterLeftClick = pyqtSignal(float, float) afterLeftClickReleased = pyqtSignal(float, float) afterDoubleClick = pyqtSignal(float, float) def __init__(self, selfwindow): QGraphicsView.__init__(self) self.selfwindow = selfwindow self.panSelect = False self.zoomSelect = False self.zoom_data = [] self.scene = QGraphicsScene() self.setScene(self.scene) self.setMouseTracking(False) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setDragMode(QGraphicsView.NoDrag) def mousePressEvent(self, event): sc_pos = self.mapToScene(event.pos()) if self.panSelect: self.setDragMode(QGraphicsView.ScrollHandDrag) if self.zoomSelect: self.setDragMode(QGraphicsView.RubberBandDrag) self.afterLeftClick.emit(sc_pos.x(), sc_pos.y()) QGraphicsView.mousePressEvent(self, event) def mouseDoubleClickEvent(self, event): sc_pos = self.mapToScene(event.pos()) if self.zoomSelect or self.panSelect: self.zoom_data = [] self.fitInView(self.sceneRect(), Qt.KeepAspectRatio) self.afterDoubleClick.emit(sc_pos.x(), sc_pos.y()) QGraphicsView.mouseDoubleClickEvent(self, event) def mouseReleaseEvent(self, event): QGraphicsView.mouseReleaseEvent(self, event) sc_pos = self.mapToScene(event.pos()) if self.zoomSelect: view_bb = self.sceneRect() if self.zoom_data: view_bb = self.zoom_data selection_bb = self.scene.selectionArea().boundingRect( ).intersected(view_bb) self.scene.setSelectionArea(QPainterPath()) if selection_bb.isValid() and (selection_bb != view_bb): self.zoom_data = selection_bb self.fitInView(self.zoom_data, Qt.KeepAspectRatio) self.setDragMode(QGraphicsView.NoDrag) self.afterLeftClickReleased.emit(sc_pos.x(), sc_pos.y()) def resizeEvent(self, event): self.fitInView(self.scene.sceneRect(), Qt.KeepAspectRatio)
class ImageViewerQt(QGraphicsView): """ PyQt image viewer widget for a QPixmap in a QGraphicsView scene with mouse zooming and panning. Displays a QImage or QPixmap (QImage is internally converted to a QPixmap). To display any other image format, you must first convert it to a QImage or QPixmap. Some useful image format conversion utilities: qimage2ndarray: NumPy ndarray <==> QImage (https://github.com/hmeine/qimage2ndarray) ImageQt: PIL Image <==> QImage (https://github.com/python-pillow/Pillow/blob/master/PIL/ImageQt.py) Mouse interaction: Left mouse button drag: Pan image. Right mouse button drag: Zoom box. Right mouse button doubleclick: Zoom to show entire image. """ # Mouse button signals emit image scene (x, y) coordinates. # !!! For image (row, column) matrix indexing, row = y and column = x. leftMouseButtonPressed = pyqtSignal(float, float) rightMouseButtonPressed = pyqtSignal(float, float) leftMouseButtonReleased = pyqtSignal(float, float) rightMouseButtonReleased = pyqtSignal(float, float) leftMouseButtonDoubleClicked = pyqtSignal(float, float) rightMouseButtonDoubleClicked = pyqtSignal(float, float) def __init__(self): QGraphicsView.__init__(self) # Image is displayed as a QPixmap in a QGraphicsScene attached to this QGraphicsView. self.scene = QGraphicsScene() self.setScene(self.scene) # Store a local handle to the scene's current image pixmap. self._pixmapHandle = None # Image aspect ratio mode. # !!! ONLY applies to full image. Aspect ratio is always ignored when zooming. # Qt.IgnoreAspectRatio: Scale image to fit viewport. # Qt.KeepAspectRatio: Scale image to fit inside viewport, preserving aspect ratio. # Qt.KeepAspectRatioByExpanding: Scale image to fill the viewport, preserving aspect ratio. self.aspectRatioMode = Qt.KeepAspectRatio # Scroll bar behaviour. # Qt.ScrollBarAlwaysOff: Never shows a scroll bar. # Qt.ScrollBarAlwaysOn: Always shows a scroll bar. # Qt.ScrollBarAsNeeded: Shows a scroll bar only when zoomed. self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) # Stack of QRectF zoom boxes in scene coordinates. self.zoomStack = [] # Flags for enabling/disabling mouse interaction. self.canZoom = True self.canPan = True def hasImage(self): """ Returns whether or not the scene contains an image pixmap. """ return self._pixmapHandle is not None def clearImage(self): """ Removes the current image pixmap from the scene if it exists. """ if self.hasImage(): self.scene.removeItem(self._pixmapHandle) self._pixmapHandle = None def pixmap(self): """ Returns the scene's current image pixmap as a QPixmap, or else None if no image exists. :rtype: QPixmap | None """ if self.hasImage(): return self._pixmapHandle.pixmap() return None def image(self): """ Returns the scene's current image pixmap as a QImage, or else None if no image exists. :rtype: QImage | None """ if self.hasImage(): return self._pixmapHandle.pixmap().toImage() return None def setImage(self, image): """ Set the scene's current image pixmap to the input QImage or QPixmap. Raises a RuntimeError if the input image has type other than QImage or QPixmap. :type image: QImage | QPixmap """ if type(image) is QPixmap: pixmap = image elif type(image) is QImage: pixmap = QPixmap.fromImage(image) else: raise RuntimeError( "ImageViewer.setImage: Argument must be a QImage or QPixmap.") if self.hasImage(): self._pixmapHandle.setPixmap(pixmap) else: self._pixmapHandle = self.scene.addPixmap(pixmap) self.setSceneRect(QRectF( pixmap.rect())) # Set scene size to image size. self.updateViewer() def loadImageFromFile(self, fileName=""): """ Load an image from file. Without any arguments, loadImageFromFile() will popup a file dialog to choose the image file. With a fileName argument, loadImageFromFile(fileName) will attempt to load the specified image file directly. """ if len(fileName) == 0: if QT_VERSION_STR[0] == '4': fileName = QFileDialog.getOpenFileName(self, "Open image file.") elif QT_VERSION_STR[0] == '5': fileName, dummy = QFileDialog.getOpenFileName( self, "Open image file.") if len(fileName) and os.path.isfile(fileName): image = QImage(fileName) self.setImage(image) def updateViewer(self): """ Show current zoom (if showing entire image, apply current aspect ratio mode). """ if not self.hasImage(): return if len(self.zoomStack) and self.sceneRect().contains( self.zoomStack[-1]): self.fitInView(self.zoomStack[-1], Qt.IgnoreAspectRatio ) # Show zoomed rect (ignore aspect ratio). else: self.zoomStack = [ ] # Clear the zoom stack (in case we got here because of an invalid zoom). self.fitInView( self.sceneRect(), self.aspectRatioMode ) # Show entire image (use current aspect ratio mode). def resizeEvent(self, event): """ Maintain current zoom on resize. """ self.updateViewer() def mousePressEvent(self, event): """ Start mouse pan or zoom mode. """ scenePos = self.mapToScene(event.pos()) if event.button() == Qt.LeftButton: if self.canPan: self.setDragMode(QGraphicsView.ScrollHandDrag) self.leftMouseButtonPressed.emit(scenePos.x(), scenePos.y()) elif event.button() == Qt.RightButton: if self.canZoom: self.setDragMode(QGraphicsView.RubberBandDrag) self.rightMouseButtonPressed.emit(scenePos.x(), scenePos.y()) QGraphicsView.mousePressEvent(self, event) def mouseReleaseEvent(self, event): """ Stop mouse pan or zoom mode (apply zoom if valid). """ QGraphicsView.mouseReleaseEvent(self, event) scenePos = self.mapToScene(event.pos()) if event.button() == Qt.LeftButton: self.setDragMode(QGraphicsView.NoDrag) self.leftMouseButtonReleased.emit(scenePos.x(), scenePos.y()) elif event.button() == Qt.RightButton: if self.canZoom: viewBBox = self.zoomStack[-1] if len( self.zoomStack) else self.sceneRect() selectionBBox = self.scene.selectionArea().boundingRect( ).intersected(viewBBox) self.scene.setSelectionArea( QPainterPath()) # Clear current selection area. if selectionBBox.isValid() and (selectionBBox != viewBBox): self.zoomStack.append(selectionBBox) self.updateViewer() self.setDragMode(QGraphicsView.NoDrag) self.rightMouseButtonReleased.emit(scenePos.x(), scenePos.y()) def mouseDoubleClickEvent(self, event): """ Show entire image. """ scenePos = self.mapToScene(event.pos()) if event.button() == Qt.LeftButton: self.leftMouseButtonDoubleClicked.emit(scenePos.x(), scenePos.y()) elif event.button() == Qt.RightButton: if self.canZoom: self.zoomStack = [] # Clear zoom stack. self.updateViewer() self.rightMouseButtonDoubleClicked.emit(scenePos.x(), scenePos.y()) QGraphicsView.mouseDoubleClickEvent(self, event)