Search code examples
pythonpyqt5qgraphicsview

Linking zoom between two qgraphicsview objects


I'm trying to link two qgraphicsview objects so that when one window zooms in the other qgraphicsview zooms as well. I'm trying to accomplish this by calling the test function when one window is going to zoom. I then try to call self.scale(factor,factor) in the other qgraphicsview object. I get an error :AttributeError: 'PhotoViewer' object has no attribute 'viewer'. How do I get I'm trying to call the class ui self.viewer but judging from the error I'm still in the Photoviewer object. How do I call the other windows self.scale from when one window is zoomed in? Thanks in advance.

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtGui import QPixmap, QPainter, QPen
import sys

class PhotoViewer(QtWidgets.QGraphicsView):
    photoClicked = QtCore.pyqtSignal(QtCore.QPoint)

    def __init__(self, parent,num_view):
        super(PhotoViewer, self).__init__(parent)
        #to track which viewer
        self.num_view=num_view
        self.drawmode=0
        self._zoom = 0
        self.drawing = False
        self.lastPoint = QPoint()
        self.image=False
        self.image=QPixmap(r"image.jpg")
        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:
                ui.test(self,self.num_view,factor)

                self.scale(factor, factor)
            elif self._zoom == 0:
                self.fitInView()
            else:
                self._zoom = 0


    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)


class ui(QtWidgets.QWidget):
    def __init__(self):
        super(ui, self).__init__()

        self.viewer = PhotoViewer(self,1)
        self.viewer2 = PhotoViewer(self,2)
        # 'Load image' button
        self.btnLoad = QtWidgets.QToolButton(self)
        self.btnLoad.setText('Load image')
        self.btnLoad.clicked.connect(self.loadImage)
        # draw mode
        self.btndraw = QtWidgets.QToolButton(self)
        self.btndraw.setText('Draw Mode')
        self.btndraw.clicked.connect(self.drawmode)
        # 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)
        HBlayout2 = QtWidgets.QHBoxLayout()
        HBlayout2.setAlignment(QtCore.Qt.AlignLeft)
        HBlayout2.addWidget(self.viewer2)
        HBlayout2.addWidget(self.viewer)
        HBlayout = QtWidgets.QHBoxLayout()
        HBlayout.setAlignment(QtCore.Qt.AlignLeft)
        HBlayout.addWidget(self.btnLoad)
        HBlayout.addWidget(self.btnPixInfo)
        HBlayout.addWidget(self.btndraw)
        HBlayout.addWidget(self.editPixInfo)
        VBlayout.addLayout(HBlayout2)
        VBlayout.addLayout(HBlayout)
        #scroll bars
        self.viewer.horizontalScrollBar().valueChanged.connect(self.horizontal_scroll)
        self.viewer.verticalScrollBar().valueChanged.connect(self.vertical_scroll)
        self.viewer2.horizontalScrollBar().valueChanged.connect(self.horizontal_scroll2)
        self.viewer2.verticalScrollBar().valueChanged.connect(self.vertical_scroll2)

    def test(self,num,factor):
        if num==1:
            self.viewer2.scale(factor,factor)
        if num==2:
            self.viewer.scale(factor,factor)


        #auto scroll
    def horizontal_scroll(self):
        self.viewer2.horizontalScrollBar().setValue(self.viewer.horizontalScrollBar().value())    
    def horizontal_scroll2(self):
        self.viewer.horizontalScrollBar().setValue(self.viewer2.horizontalScrollBar().value())
    def vertical_scroll(self):
        self.viewer2.verticalScrollBar().setValue(self.viewer.verticalScrollBar().value())   
    def vertical_scroll2(self):
        self.viewer.verticalScrollBar().setValue(self.viewer2.verticalScrollBar().value())


    def loadImage(self):
        self.viewer.setPhoto(QtGui.QPixmap(r'temp.png'))
        self.viewer2.setPhoto(QtGui.QPixmap(r'temp.png'))
        self.image=QPixmap(r"image.jpg")

    def drawmode(self):        
        self.viewer.toggleDrawMode()

    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__':

    app = QtWidgets.QApplication(sys.argv)
    window = ui()
    window.setGeometry(500, 300, 800, 600)
    window.show()
    sys.exit(app.exec_())

Solution

  • Instead of trying to scale the view directly, you could use signals for this:

    Add the following code to PhotoViewer:

    class PhotoViewer(QtWidgets.QGraphicsView):
        scaled = QtCore.pyqtSignal(int, int, QtGui.QTransform, int)
    
        ....
    
        def scale(self, horz, vert):
            super().scale(horz, vert)
            self.scaled.emit(self.horizontalScrollBar().value(),
                             self.verticalScrollBar().value(), 
                             self.transform(), 
                             self._zoom
                            )
    
        def set_transform(self, horz_scroll, vert_scroll, transform, zoom):
            # temporary block signals from scroll bars to prevent interference
            horz_blocked = self.horizontalScrollBar().blockSignals(True)
            vert_blocked = self.verticalScrollBar().blockSignals(True)
            self._zoom = zoom
            self.setTransform(transform)
            dx = horz_scroll - self.horizontalScrollBar().value()
            dy = vert_scroll - self.verticalScrollBar().value()
            self.horizontalScrollBar().setValue(dx)
            self.verticalScrollBar().setValue(dy)
            self.horizontalScrollBar().blockSignals(horz_blocked)
            self.verticalScrollBar().blockSignals(vert_blocked)
    

    And to ui:

    class ui(QtWidgets.QWidget):
        def __init__(self):
            ....
            self.viewer.scaled.connect(self.viewer2.set_transform)
            self.viewer2.scaled.connect(self.viewer.set_transform)
    

    Also remove the call to ui.test() form PhotoViewer.wheelEvent.