I would like to show two images next to each other, such that when I zoom or pan on one image the other image follows along. My current approach is to emit a viewUpdated
event after resolving mouse events. The event contains the viewportTransformation
and is used to update the transform
in the other view. This sort of works for the zoom part, but panning does not work.
The code below is based on the qt5 version provided in this answer: https://stackoverflow.com/a/35514531/185475
from PyQt5 import QtCore, QtGui, QtWidgets
# Code from https://stackoverflow.com/a/35514531
class PhotoViewer(QtWidgets.QGraphicsView):
photoClicked = QtCore.pyqtSignal(QtCore.QPoint)
viewUpdated = QtCore.pyqtSignal(QtGui.QTransform)
def __init__(self, parent):
super(PhotoViewer, self).__init__(parent)
self._zoom = 0
self._empty = True
self._scene = QtWidgets.QGraphicsScene(self)
self._photo = QtWidgets.QGraphicsPixmapItem()
self._scene.addItem(self._photo)
self.setScene(self._scene)
self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(30, 30, 30)))
self.setFrameShape(QtWidgets.QFrame.NoFrame)
def hasPhoto(self):
return not self._empty
def fitInView(self, scale=True):
rect = QtCore.QRectF(self._photo.pixmap().rect())
if not rect.isNull():
self.setSceneRect(rect)
if self.hasPhoto():
unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1))
self.scale(1 / unity.width(), 1 / unity.height())
viewrect = self.viewport().rect()
scenerect = self.transform().mapRect(rect)
factor = min(viewrect.width() / scenerect.width(),
viewrect.height() / scenerect.height())
self.scale(factor, factor)
self._zoom = 0
def setPhoto(self, pixmap=None):
self._zoom = 0
if pixmap and not pixmap.isNull():
self._empty = False
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
self._photo.setPixmap(pixmap)
else:
self._empty = True
self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
self._photo.setPixmap(QtGui.QPixmap())
self.fitInView()
def wheelEvent(self, event):
if self.hasPhoto():
if event.angleDelta().y() > 0:
factor = 1.25
self._zoom += 1
else:
factor = 0.8
self._zoom -= 1
if self._zoom > 0:
self.scale(factor, factor)
elif self._zoom == 0:
self.fitInView()
else:
self._zoom = 0
self.viewUpdated.emit(self.viewportTransform())
def toggleDragMode(self):
if self.dragMode() == QtWidgets.QGraphicsView.ScrollHandDrag:
self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
elif not self._photo.pixmap().isNull():
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
def mousePressEvent(self, event):
if self._photo.isUnderMouse():
self.photoClicked.emit(self.mapToScene(event.pos()).toPoint())
super(PhotoViewer, self).mousePressEvent(event)
self.viewUpdated.emit(self.viewportTransform())
def set_transform(self, transform):
self.setTransform(transform)
class Window(QtWidgets.QWidget):
def __init__(self):
super(Window, self).__init__()
self.viewer = PhotoViewer(self)
self.viewerSecondImage = PhotoViewer(self)
self.viewer.viewUpdated.connect(self.viewerSecondImage.set_transform)
self.viewerSecondImage.viewUpdated.connect(self.viewer.set_transform)
# 'Load image' button
self.btnLoad = QtWidgets.QToolButton(self)
self.btnLoad.setText('Load image')
self.btnLoad.clicked.connect(self.loadImage)
# Button to change from drag/pan to getting pixel info
self.btnPixInfo = QtWidgets.QToolButton(self)
self.btnPixInfo.setText('Enter pixel info mode')
self.btnPixInfo.clicked.connect(self.pixInfo)
self.editPixInfo = QtWidgets.QLineEdit(self)
self.editPixInfo.setReadOnly(True)
self.viewer.photoClicked.connect(self.photoClicked)
# Arrange layout
VBlayout = QtWidgets.QVBoxLayout(self)
HBlayoutImageViewers = QtWidgets.QHBoxLayout()
HBlayoutImageViewers.addWidget(self.viewer)
HBlayoutImageViewers.addWidget(self.viewerSecondImage)
VBlayout.addLayout(HBlayoutImageViewers)
HBlayout = QtWidgets.QHBoxLayout()
HBlayout.setAlignment(QtCore.Qt.AlignLeft)
HBlayout.addWidget(self.btnLoad)
HBlayout.addWidget(self.btnPixInfo)
HBlayout.addWidget(self.editPixInfo)
VBlayout.addLayout(HBlayout)
def loadImage(self):
self.viewer.setPhoto(QtGui.QPixmap('input/490px-Dostojka_adype.jpg'))
self.viewerSecondImage.setPhoto(QtGui.QPixmap('input/490px-Dostojka_adype.jpg'))
def pixInfo(self):
self.viewer.toggleDragMode()
def photoClicked(self, pos):
if self.viewer.dragMode() == QtWidgets.QGraphicsView.NoDrag:
self.editPixInfo.setText('%d, %d' % (pos.x(), pos.y()))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 800, 600)
window.show()
sys.exit(app.exec_())
You only need to syncronize scrollbar values of two QGraphicsViews
def bindScrollBars(scrollBar1, scrollBar2):
# syncronizing scrollbars syncrnonously somehow breaks zooming and doesn't work
# scrollBar1.valueChanged.connect(lambda value: scrollBar2.setValue(value))
# scrollBar2.valueChanged.connect(lambda value: scrollBar1.setValue(value))
# syncronizing scrollbars asyncronously works ok
scrollBar1.valueChanged.connect(lambda _: QtCore.QTimer.singleShot(0, lambda: scrollBar2.setValue(scrollBar1.value())))
scrollBar2.valueChanged.connect(lambda _: QtCore.QTimer.singleShot(0, lambda: scrollBar1.setValue(scrollBar2.value())))
class Window(QtWidgets.QWidget):
def __init__(self):
...
bindScrollBars(self.viewer.horizontalScrollBar(), self.viewerSecondImage.horizontalScrollBar())
bindScrollBars(self.viewer.verticalScrollBar(), self.viewerSecondImage.verticalScrollBar())
Also wheelEvent
can be simplified
def wheelEvent(self, event):
if self.hasPhoto():
factor = 1.25
if event.angleDelta().y() > 0:
self.scale(factor, factor)
else:
self.scale(1/factor, 1/factor)
self.viewUpdated.emit(self.transform())