laxer
laxer

Reputation: 760

Pyqt5 QgraphicsView pan past scroll bar limits

I have a set of predetermined X and Y coordinates that I am using to place QGraphicsItem's as points and then placing each of those points in a QGraphicsView. I have my panning button set to be the middle mouse button, but I can only pan if I have zoomed in. Also, I can only pan to my furthers point.

Is there a way to set the QGraphicsView so that it will not stop panning at a point and that I can pan at any zoom level? Also as a side note, Later I would like to be able to select these points to set and get attributes from them, so I would still like to ability to interact with them?

Update

I made a couple of gif's to show how the controls currently work and then how I would like them to work.

Currently

You can see here I am not able to move past the limits of the scroll bars enter image description here

What I would like

I would like to be able to move the screen past where the squares are being placed so that if the user desires they can move some off the screen or they could zoom out further than where fitInView would allow. I know the zoom part is in my wheel event and just haven't changed it yet. The big thing is being able to move them past at any zoom level.

enter image description here

please let me know if you need more explanation.

Code

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
from math import sqrt

class Point(QGraphicsItem):
    def __init__(self, x, y):
        super(Point, self).__init__()
        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
        self.rectF = QRectF(0, 0, 30, 30)
        self.x=x
        self.y=y
        self._brush = QBrush(Qt.black)

    def setBrush(self, brush):
        self._brush = brush
        self.update()

    def boundingRect(self):
        return self.rectF

    def paint(self, painter=None, style=None, widget=None):
        painter.fillRect(self.rectF, self._brush)

    def hoverMoveEvent(self, event):
        point = event.pos().toPoint()
        print(point)
        QGraphicsItem.hoverMoveEvent(self, event)


class Viewer(QGraphicsView):
    photoClicked = pyqtSignal(QPoint)
    rectChanged = pyqtSignal(QRect)

    def __init__(self, parent):
        super(Viewer, self).__init__(parent)
        self.rubberBand = QRubberBand(QRubberBand.Rectangle, self)
        self.setMouseTracking(True)
        self.origin = QPoint()
        self.changeRubberBand = False

        self._zoom = 0
        self._empty = True
        self._scene = QGraphicsScene(self)

        self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
        self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setFrameShape(QFrame.NoFrame)
        self.area = float()
        self.setPoints()
        QTimer.singleShot(0, self.fitInView) # This is done so that it can fit into view on load

    def setItems(self):
            self.data = {'x': [-2414943.8686, -2417160.6592, -2417160.6592, -2417856.1783, -2417054.7618, -2416009.9966, -2416012.5232, -2418160.8952, -2418160.8952, -2416012.5232, -2417094.7694, -2417094.7694], 'y': [10454269.7008,
     10454147.2672, 10454147.2672, 10453285.2456, 10452556.8132, 10453240.2808, 10455255.8752, 10455183.1912, 10455183.1912, 10455255.8752, 10456212.5959, 10456212.5959]}
            maxX = max(self.data['x'])
            minX = min(self.data['x'])
            maxY = max(self.data['y'])
            minY = min(self.data['y'])
            distance = sqrt((maxX-minX)**2+(maxY-minY)**2)

            self.area = QRectF(minX, minY, distance, distance)
            for i,x in enumerate(self.data['x']):
                x = self.data['x'][i]
                y = self.data['y'][i]
                p = Point(x,y)
                p.setPos(x,y)
                self._scene.addItem(p)
            self.setScene(self._scene)



    def fitInView(self, scale=True):
        rect = QRectF(self.area)
        if not rect.isNull():
            self.setSceneRect(rect)

            unity = self.transform().mapRect(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 setPoints(self):
        self._zoom = 0
        self.setItems()
        self.setDragMode(self.ScrollHandDrag)
        # self.fitInView()

    def wheelEvent(self, event):
            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


    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:

            self.origin = event.pos()
            self.rubberBand.setGeometry(QRect(self.origin, QSize()))
            self.rectChanged.emit(self.rubberBand.geometry())
            self.rubberBand.show()
            self.changeRubberBand = True
            return
            #QGraphicsView.mousePressEvent(self,event)
        elif event.button() == Qt.MidButton:
            self.viewport().setCursor(Qt.ClosedHandCursor)
            self.original_event = event
            handmade_event = QMouseEvent(QEvent.MouseButtonPress,QPointF(event.pos()),Qt.LeftButton,event.buttons(),Qt.KeyboardModifiers())
            QGraphicsView.mousePressEvent(self,handmade_event)

        super(Viewer, self).mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.changeRubberBand = False
            QGraphicsView.mouseReleaseEvent(self,event)
        elif event.button() == Qt.MidButton:
            self.viewport().setCursor(Qt.OpenHandCursor)
            handmade_event = QMouseEvent(QEvent.MouseButtonRelease,QPointF(event.pos()),Qt.LeftButton,event.buttons(),Qt.KeyboardModifiers())
            QGraphicsView.mouseReleaseEvent(self,handmade_event)
        super(Viewer, self).mouseReleaseEvent(event)


    def mouseMoveEvent(self, event):
        if self.changeRubberBand:
            self.rubberBand.setGeometry(QRect(self.origin, event.pos()).normalized())
            self.rectChanged.emit(self.rubberBand.geometry())
            QGraphicsView.mouseMoveEvent(self,event)
        super(Viewer, self).mouseMoveEvent(event)

class Window(QWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.viewer = Viewer(self)
        self.btnLoad = QToolButton(self)
        self.btnLoad.setText('Fit Into View')
        self.btnLoad.clicked.connect(self.fitPoints)

        VBlayout = QVBoxLayout(self)
        VBlayout.addWidget(self.viewer)
        HBlayout = QHBoxLayout()
        HBlayout.setAlignment(Qt.AlignLeft)
        HBlayout.addWidget(self.btnLoad)

        VBlayout.addLayout(HBlayout)
        self.viewer.fitInView()

    def fitPoints(self):
        self.viewer.fitInView()



if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    window = Window()
    window.setGeometry(500, 300, 800, 600)
    window.show()
    sys.exit(app.exec_())

Upvotes: 3

Views: 2223

Answers (1)

Haru
Haru

Reputation: 2083

Update2

Please try this Code. Is this appropriate answer for you? If so, I want to add new explanation.

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
from math import sqrt
class Line(QGraphicsLineItem):
    def __init__(self, x1, y1, x2, y2):
        super(Line, self).__init__()
        pen = self.pen()
        pen.setWidth(10)
        pen.setColor(Qt.gray)
        pen.setStyle(Qt.SolidLine)

        self.setPen(pen)
        self.origin = self.pos()
        self.setZValue(1)
        self.setLine(QLineF(x1, y1, x2, y2))
class Point(QGraphicsItem):
    def __init__(self, x, y):
        super(Point, self).__init__()
        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
        self.rectF = QRectF(0, 0, 30, 30)
        self.x=x
        self.y=y
        self.origin = QPointF(self.pos())
        self._brush = QBrush(Qt.black)
        self.setZValue(2)
    def setBrush(self, brush):
        self._brush = brush
        self.update()

    def boundingRect(self):
        return self.rectF

    def paint(self, painter=None, style=None, widget=None):
        painter.fillRect(self.rectF, self._brush)

    def hoverMoveEvent(self, event):
        point = event.pos().toPoint()
        print(point)
        QGraphicsItem.hoverMoveEvent(self, event)


class Viewer(QGraphicsView):
    photoClicked = pyqtSignal(QPoint)
    rectChanged = pyqtSignal(QRect)

    def __init__(self, parent):
        super(Viewer, self).__init__(parent)
        self.rubberBand = QRubberBand(QRubberBand.Rectangle, self)
        self.setMouseTracking(True)
        self.origin = QPoint()
        self.changeRubberBand = False
        self.mid_panning = False     



        self._zoom = 0
        self._empty = True
        self._scene = QGraphicsScene(self)
        self._scene.setBackgroundBrush(Qt.white)
        self.white_board = QGraphicsRectItem()
        self.white_board.setZValue(1)
        self.white_board.setBrush(Qt.white)
        self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
        self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setFrameShape(QFrame.NoFrame)
        self.area = float()
        self.setPoints()
        self._old_x = QCursor.pos().x()
        self._old_y = QCursor.pos().y()

        QTimer.singleShot(0, self.fitInView) # This is done so that it can fit into view on load

    def setItems(self):
            self.data = {'x': [-2414943.8686, -2417160.6592, -2417160.6592, -2417856.1783, -2417054.7618, -2416009.9966, -2416012.5232, -2418160.8952, -2418160.8952, -2416012.5232, -2417094.7694, -2417094.7694], 'y': [10454269.7008,
     10454147.2672, 10454147.2672, 10453285.2456, 10452556.8132, 10453240.2808, 10455255.8752, 10455183.1912, 10455183.1912, 10455255.8752, 10456212.5959, 10456212.5959]}
            maxX = max(self.data['x'])
            minX = min(self.data['x'])
            maxY = max(self.data['y'])
            minY = min(self.data['y'])
            distance = sqrt((maxX-minX)**2+(maxY-minY)**2)

            self.area = QRectF(minX , minY , distance , distance )
            self.white_board.setRect(QRectF(minX , minY , distance , distance ))
            self._scene.addItem(self.white_board)
            line1 = Line(minX, minY, minX+distance, minY+distance)
            line2 = Line(minX+distance, minY, minX, minY+distance)
            self._scene.addItem(line1)
            self._scene.addItem(line2)
            for i,x in enumerate(self.data['x']):
                x = self.data['x'][i]
                y = self.data['y'][i]
                p = Point(x,y)
                p.setPos(x,y)
                self._scene.addItem(p)

            self.setScene(self._scene)


    def make_area2(self, area):
        x = area.x()
        y = area.y()
        width = area.width()
        height = area.height()
        x -= 2*x
        y -= 2*y
        width = width*2
        height = height*2
        area = QRectF(x, y, width, height)
        return area
    def fitInView(self, scale=True):
        rect = QRectF(self.area)
        if not rect.isNull():
            self.setSceneRect(rect)
            unity = self.transform().mapRect(QRectF(0, 0, 1, 1))
            print(unity.width(), unity.height())
            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())
            print(scenerect.width(), scenerect.height())
            self.scale(factor, factor)

            self._zoom = 0



    def setPoints(self):
        self._zoom = 0
        self.setItems()
        self.setDragMode(self.ScrollHandDrag)
        # self.fitInView()

    def wheelEvent(self, event):
            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


    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:

            self.origin = event.pos()
            self.rubberBand.setGeometry(QRect(self.origin, QSize()))
            self.rectChanged.emit(self.rubberBand.geometry())
            self.rubberBand.show()
            self.changeRubberBand = True
            return
            #QGraphicsView.mousePressEvent(self,event)
        elif event.button() == Qt.MidButton:
            self.viewport().setCursor(Qt.ClosedHandCursor)
            self.origin = event.pos()
            self.original_event = event
            self.mid_panning = True
            self.scene_origin = self.mapToScene(event.pos())
            self._old_x = QCursor.pos().x()
            self._old_y = QCursor.pos().y()
            for i in self._scene.items():

                i.origin = i.pos()
            # I recommend that you get the all item position.
            handmade_event = QMouseEvent(QEvent.MouseButtonPress,QPointF(event.pos()),Qt.LeftButton,event.buttons(),Qt.KeyboardModifiers())
            QGraphicsView.mousePressEvent(self,handmade_event)

        super(Viewer, self).mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.changeRubberBand = False
            QGraphicsView.mouseReleaseEvent(self,event)
        elif event.button() == Qt.MidButton:
            self.viewport().setCursor(Qt.OpenHandCursor)
            handmade_event = QMouseEvent(QEvent.MouseButtonRelease,QPointF(event.pos()),Qt.LeftButton,event.buttons(),Qt.KeyboardModifiers())
            self.mid_panning = False
            # here you set the original point.
            for i in self._scene.items():
                i.setPos(i.origin)
            QGraphicsView.mouseReleaseEvent(self,handmade_event)
        super(Viewer, self).mouseReleaseEvent(event)
    def calc_offset(self, x, y):
        offset_x = x - int(self.viewport().width()/2)
        offset_y = y - int(self.viewport().height()/2)
        return offset_x, offset_y

    def mouseMoveEvent(self, event):        
        if self.changeRubberBand:
            self.rubberBand.setGeometry(QRect(self.origin, event.pos()).normalized())
            self.rectChanged.emit(self.rubberBand.geometry())
            QGraphicsView.mouseMoveEvent(self,event)
        elif self.mid_panning:         

            new_x = event.x()
            new_y = event.y()
            offset_x, offset_y = self.calc_offset(new_x, new_y)
            for item in self._scene.items():
                item.setPos(QPointF(item.pos().x() - (new_x - self._old_x)*10, item.pos().y() - (new_y - self._old_y)*10))     


            self._old_x = new_x
            self._old_y = new_y
            return

        super(Viewer, self).mouseMoveEvent(event)

class Window(QWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.viewer = Viewer(self)
        self.btnLoad = QToolButton(self)
        self.btnLoad.setText('Fit Into View')
        self.btnLoad.clicked.connect(self.fitPoints)

        VBlayout = QVBoxLayout(self)
        VBlayout.addWidget(self.viewer)
        HBlayout = QHBoxLayout()
        HBlayout.setAlignment(Qt.AlignLeft)
        HBlayout.addWidget(self.btnLoad)

        VBlayout.addLayout(HBlayout)
        self.viewer.fitInView()

    def fitPoints(self):
        self.viewer.fitInView()



if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    window = Window()
    window.setGeometry(300, 400, 800, 600)
    window.show()
    sys.exit(app.exec_())

Update

The key solution is to use scroll(dx, dy).

Explanation

At the first time, I doubted if there was the place of "offscreen" or not. The reason is QGraphicsView only shows QGraphicsScene and QGraphicsItem on the scene.QGraphicsView can show all the scene or the part of the scene,I thought it cannot show the "offscreen".

I have experienced pygame,so I attempted to implement the idea of "offset",but pygame has originally an infinite vast of screen and only shows the slight part of it.So We can see the "offscreen".On the contrast,QGraphicsScene is a finite screen the user decide the range by setSceneRect.And fitInView means the same range of screen.

But QGraphicsView is a widget for showing the scene.We can scroll the widget. So, We can do the same thing by scrolling the widget itself.

code.

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
from math import sqrt

class Point(QGraphicsItem):
    def __init__(self, x, y):
        super(Point, self).__init__()
        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
        self.rectF = QRectF(0, 0, 30, 30)
        self.x=x
        self.y=y
        self._brush = QBrush(Qt.black)

    def setBrush(self, brush):
        self._brush = brush
        self.update()

    def boundingRect(self):
        return self.rectF

    def paint(self, painter=None, style=None, widget=None):
        painter.fillRect(self.rectF, self._brush)

    def hoverMoveEvent(self, event):
        point = event.pos().toPoint()
        print(point)
        QGraphicsItem.hoverMoveEvent(self, event)


class Viewer(QGraphicsView):
    photoClicked = pyqtSignal(QPoint)
    rectChanged = pyqtSignal(QRect)

    def __init__(self, parent):
        super(Viewer, self).__init__(parent)
        self.rubberBand = QRubberBand(QRubberBand.Rectangle, self)
        self.setMouseTracking(True)
        self.origin = QPoint()
        self.changeRubberBand = False
        self.mid_panning = False     



        self._zoom = 0
        self._empty = True
        self._scene = QGraphicsScene(self)

        self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
        self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setFrameShape(QFrame.NoFrame)
        self.area = float()
        self.setPoints()
        QTimer.singleShot(0, self.fitInView) # This is done so that it can fit into view on load

    def setItems(self):
            self.data = {'x': [-2414943.8686, -2417160.6592, -2417160.6592, -2417856.1783, -2417054.7618, -2416009.9966, -2416012.5232, -2418160.8952, -2418160.8952, -2416012.5232, -2417094.7694, -2417094.7694], 'y': [10454269.7008,
     10454147.2672, 10454147.2672, 10453285.2456, 10452556.8132, 10453240.2808, 10455255.8752, 10455183.1912, 10455183.1912, 10455255.8752, 10456212.5959, 10456212.5959]}
            maxX = max(self.data['x'])
            minX = min(self.data['x'])
            maxY = max(self.data['y'])
            minY = min(self.data['y'])
            distance = sqrt((maxX-minX)**2+(maxY-minY)**2)

            self.area = QRectF(minX, minY, distance, distance)
            for i,x in enumerate(self.data['x']):
                x = self.data['x'][i]
                y = self.data['y'][i]
                p = Point(x,y)
                p.setPos(x,y)
                self._scene.addItem(p)
            self.setScene(self._scene)



    def fitInView(self, scale=True):
        rect = QRectF(self.area)
        if not rect.isNull():
            self.setSceneRect(rect)
            unity = self.transform().mapRect(QRectF(0, 0, 1, 1))
            print(unity.width(), unity.height())
            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 setPoints(self):
        self._zoom = 0
        self.setItems()
        self.setDragMode(self.ScrollHandDrag)
        # self.fitInView()

    def wheelEvent(self, event):
            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


    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:

            self.origin = event.pos()
            self.rubberBand.setGeometry(QRect(self.origin, QSize()))
            self.rectChanged.emit(self.rubberBand.geometry())
            self.rubberBand.show()
            self.changeRubberBand = True
            return
            #QGraphicsView.mousePressEvent(self,event)
        elif event.button() == Qt.MidButton:
            self.viewport().setCursor(Qt.ClosedHandCursor)
            self.origin = event.pos()
            self.original_event = event
            self.mid_panning = True
            self.scene_origin = self.mapToScene(event.pos())
            handmade_event = QMouseEvent(QEvent.MouseButtonPress,QPointF(event.pos()),Qt.LeftButton,event.buttons(),Qt.KeyboardModifiers())
            QGraphicsView.mousePressEvent(self,handmade_event)

        super(Viewer, self).mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.changeRubberBand = False
            QGraphicsView.mouseReleaseEvent(self,event)
        elif event.button() == Qt.MidButton:
            self.viewport().setCursor(Qt.OpenHandCursor)
            handmade_event = QMouseEvent(QEvent.MouseButtonRelease,QPointF(event.pos()),Qt.LeftButton,event.buttons(),Qt.KeyboardModifiers())
            self.mid_panning = False
            QGraphicsView.mouseReleaseEvent(self,handmade_event)
        super(Viewer, self).mouseReleaseEvent(event)
    def calc_offset(self, x, y):
        offset_x = x - int(self.viewport().width()/2)
        offset_y = y - int(self.viewport().height()/2)
        return offset_x, offset_y
    def mouseMoveEvent(self, event):        
        if self.changeRubberBand:
            self.rubberBand.setGeometry(QRect(self.origin, event.pos()).normalized())
            self.rectChanged.emit(self.rubberBand.geometry())
            QGraphicsView.mouseMoveEvent(self,event)
        elif self.mid_panning:         
            offset_x, offset_y = self.calc_offset(event.pos().x(), event.pos().y())
            self.scroll(offset_x,offset_y)
            return

        super(Viewer, self).mouseMoveEvent(event)

class Window(QWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.viewer = Viewer(self)
        self.btnLoad = QToolButton(self)
        self.btnLoad.setText('Fit Into View')
        self.btnLoad.clicked.connect(self.fitPoints)

        VBlayout = QVBoxLayout(self)
        VBlayout.addWidget(self.viewer)
        HBlayout = QHBoxLayout()
        HBlayout.setAlignment(Qt.AlignLeft)
        HBlayout.addWidget(self.btnLoad)

        VBlayout.addLayout(HBlayout)
        self.viewer.fitInView()

    def fitPoints(self):
        self.viewer.fitInView()



if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    window = Window()
    window.setGeometry(300, 400, 800, 600)
    window.show()
    sys.exit(app.exec_())

Upvotes: 3

Related Questions