1

I have a setup where two QGraphicViews display a single QGraphicsScene. One of these views is an overview the other the detail. Imagine something like:

enter image description here

The rectangle marking the current boundaries of the detailed view is part of the scene. It is the white rectangle in the upper view, which I will call in the text below as "bounding-box".

What I want is to be able to click in the overview- QGraphicsView and drag the bounding-box around to trigger a scrolling of the detail- QGraphicsView. Obviously, the bounding-box has to be only clickable in the overview- QGraphicsView, otherwise I would never be able to do manipulations in the detail- QGraphicsView, because the bounding-box covers the entire detail view.

So how can I make a QGraphicsItem be selectable only from a single QGraphicsView or, alternatively, how do I "insert" a QGraphicsItem only into a single QGraphicsView? Can I perhaps nest QGraphicsScenes so that one is the copy of another plus some extra items?

P.R.
  • 3,785
  • 1
  • 27
  • 47
  • I wrote an answer about what I think you should do but I confess I'm a bit confused about the question. You have one scene and two views on it, is that right? Better would be two scenes and two views (detailed and overview) or how exactly do you overlay the two views? The one view who is in the foreground will consume all mouse events, so why are you worried about the moving of the red rectangle (bounding-box)? – NoDataDumpNoContribution Jul 08 '14 at 11:15
  • I have two views on the same scene, because I change stuff in the scene and I need it to be updated in the overview as well as in the detailed view. Therefore I went for one scene with two views – P.R. Jul 10 '14 at 11:22
  • But the over-view shows a completely different image than the detailed-view (in your example). So is the over-view zoomed out from the scene or at a separate place in the scene. I guess it doesn't make any difference whethere it is one or two scenes - the answer should work with both. I don't want to extend the code example however. – NoDataDumpNoContribution Jul 10 '14 at 11:26
  • I just copy and pasted a map from google images. That is not what I display. I should have used a screen shot. I will deliver this in a sec. – P.R. Jul 10 '14 at 11:27
  • updated the image. It shows now what is displayed in my program. – P.R. Jul 10 '14 at 11:33
  • Ah okay. I might add a bit of code later. – NoDataDumpNoContribution Jul 10 '14 at 11:45

4 Answers4

1

Extending my other answer which only concentrates on the movable QGraphicsItem I made an example specifically for your task.

from PySide import QtGui, QtCore

# special GraphicsRectItem that is aware of its position and does something if the position is changed
class MovableGraphicsRectItem(QtGui.QGraphicsRectItem):

    def __init__(self, callback=None):
        super().__init__()
        self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
        self.setCursor(QtCore.Qt.PointingHandCursor)
        self.callback = callback

    def itemChange(self, change, value):
        if change == QtGui.QGraphicsItem.ItemPositionChange and self.callback:
            self.callback(value)

        return super().itemChange(change, value)

app = QtGui.QApplication([])

# the scene with some rectangles
scene = QtGui.QGraphicsScene()
scene.addRect(30, 30, 100, 50, pen=QtGui.QPen(QtCore.Qt.darkGreen))
scene.addRect(150, 0, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkYellow))
scene.addRect(80, 80, 100, 20, pen=QtGui.QPen(QtCore.Qt.darkMagenta))
scene.addRect(200, 10, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkRed))

window = QtGui.QWidget()

# put two graphicsviews into the window with different scaling for each
layout = QtGui.QVBoxLayout(window)
v1 = QtGui.QGraphicsView(scene)
v1.setFixedSize(500, 100)
v1.scale(0.5, 0.5)
v1.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
v1.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
layout.addWidget(v1)
v2 = QtGui.QGraphicsView(scene)
v2.setFixedSize(500, 500)
v2.scale(5, 5)
v2.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
v2.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
layout.addWidget(v2)

# the tracker rectangle
tracker = MovableGraphicsRectItem(lambda pos: v2.setSceneRect(pos.x(), pos.y(), 100, 100))
tracker.setRect(0, 0, 100, 100)
v2.setSceneRect(0, 0, 100, 100)
tracker.setPen(QtGui.QPen(QtCore.Qt.darkCyan))
scene.addItem(tracker)

window.show()
app.exec_()

You don't need to have Items that are only visible in one view or the other, you simply restrict the scene rectangle of one view to inside the draggable rectangle in the scene that is visible and draggable in the other view. See the image.

enter image description here

NoDataDumpNoContribution
  • 10,591
  • 9
  • 64
  • 104
  • Thanks for your anser. This shows well how to make a `QGraphicsItem` dragable, but it introduces exactly the problem I stated in the question. Because `tracker` takes the complete space in `v2`, whenever you click in `v2` you drag `tracker`. I, however, need to achieve somehow that the dragging only happens if I click in `v1`. Clicks and actions in `v2`, have different purposes (for instance drawing rectangles). – P.R. Jul 16 '14 at 18:57
  • Thanks. using your code and some extra-bits it is working now. – P.R. Jul 16 '14 at 19:15
  • @P.R. Never checked clicking in v2. Would be interested in getting to know what you added as extra-bits? – NoDataDumpNoContribution Jul 16 '14 at 19:24
  • I posted my complete solution above. Basically, I activate and deactivate the `QtGui.QGraphicsItem.ItemIsMovable` property of `tracker` at the `enter` and `leave` event of the upper graphicview. Ie `tracker` can only be tracked and will only call the `callback` function if the mouse is in the upper `QGraphicsview` – P.R. Jul 17 '14 at 16:19
  • @P.R. Very nice idea. That should finally solve the whole problem. – NoDataDumpNoContribution Jul 17 '14 at 20:27
1

I really like this idea and am trying to generalise it to create a widget which you pass the 'main view' to and it creates an overview which you can use to pan and zoom in. Unfortunately I haven't got it working yet and don't have time to work on it right now but thought I would share the progress so far.

Here is the widget code:

"""
Overview widget
"""
from PyQt4 import QtGui, QtCore

class MovableGraphicsRectItem(QtGui.QGraphicsRectItem):
    '''special GraphicsRectItem that is aware of its position and does
    something if the position is changed'''
    def __init__(self, callback=None):
        super(MovableGraphicsRectItem, self).__init__()
        self.setFlags(QtGui.QGraphicsItem.ItemIsMovable |
                      QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
        self.setCursor(QtCore.Qt.PointingHandCursor)
        self.callback = callback

    def itemChange(self, change, value):
        if change == QtGui.QGraphicsItem.ItemPositionChange and self.callback:
            self.callback(value)

        return super(MovableGraphicsRectItem, self).itemChange(change, value)

    def activate(self):
        self.setFlags(QtGui.QGraphicsItem.ItemIsMovable |
                      QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
        self.setCursor(QtCore.Qt.PointingHandCursor)

    def deactivate(self):
        self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, False)
        self.setFlag(QtGui.QGraphicsItem.ItemSendsScenePositionChanges, False)
        self.setCursor(QtCore.Qt.ArrowCursor)


class MouseInsideFilterObj(QtCore.QObject):
    def __init__(self, enterCallback, leaveCallback):
        QtCore.QObject.__init__(self)
        self.enterCallback = enterCallback
        self.leaveCallback = leaveCallback

    def eventFilter(self, obj, event):
        if event.type() == 10:  # QtCore.QEvent.Type.Enter:
            self.enterCallback(obj)
            print('Enter event')

        if event.type() == 11:  # QtCore.QEvent.Type.Leave:
            self.leaveCallback(obj)
            print('Leave event')

        return False


class Overview(QtGui.QGraphicsView):
    '''provides a view that shows the entire scene and shows the area that
    the main view is zoomed to. Alows user to move the view area around and
    change the zoom level'''

    def __init__(self, mainView):
        QtGui.QGraphicsView.__init__(self)
        self.setWindowTitle('Overview')
        self.resize(QtCore.QSize(400, 300))

        self._mainView = mainView
        self.setScene(mainView.scene())

        mouseFilter = MouseInsideFilterObj(self.enterGV, self.leaveGV)
        self.viewport().installEventFilter(mouseFilter)

        self._tracker = MovableGraphicsRectItem(
           lambda pos: self._mainView.setSceneRect(
               QtCore.QRectF(self._mainView.viewport().geometry())))
        self._tracker.setRect(self._getMainViewArea_())
        self._tracker.setPen(QtGui.QPen(QtCore.Qt.darkCyan))
        self.scene().addItem(self._tracker)

    def _getMainViewArea_(self):
        mainView = self._mainView
        visibleSceneRect = mainView.mapToScene(
            mainView.viewport().geometry()).boundingRect()
        return visibleSceneRect

    def resizeEvent(self, event):
        self.fitInView(self.sceneRect(), QtCore.Qt.KeepAspectRatio)

    def leaveGV(self, gv):
        if gv is self.overview:
            print('exited overview')
            self.tracker.deactivate()

    def enterGV(self, gv):
        if gv is self.overview:
            print('using overview')
            self.tracker.activate()

and here is the test script code:

import sys
from PyQt4 import QtGui, QtCore
import overviewWidget as ov

if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    # the scene with some rectangles
    scene = QtGui.QGraphicsScene()
    scene.addRect(30, 30, 100, 50, pen=QtGui.QPen(QtCore.Qt.darkGreen))
    scene.addRect(150, 0, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkYellow))
    scene.addRect(80, 80, 100, 20, pen=QtGui.QPen(QtCore.Qt.darkMagenta))
    scene.addRect(200, 10, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkRed))

    # the main view
    mainView = QtGui.QGraphicsView(scene)
    mainView.resize(600, 400)
    mainView.update()
    mainView.show()

    # the overview
    overview = ov.Overview(mainView)
    overview.update()
    overview.show()

    sys.exit(app.exec_())
Justin
  • 11
  • 2
0

QGraphicsItems have by default some of their abilities disabled to maximize performance. By enabling these abilities you can make them movable and you can make them aware of their position. Ideally one would then use the Signal/Slot mechanism to notify someone else of changes but again for performance reason QGraphicsItems are not inheriting from QObject. However sending events or manually calling callbacks are always possible.

You have to:

  • Enable flags QGraphicsItem.ItemIsMovable and QGraphicsItem.ItemSendsScenePositionChanges of your QGraphicsItem
  • Provide a custom implementation of method itemChange(change, value) and therein listen to QGraphicsItem.ItemPositionChange changes.
  • Act accordingly to these changes (in your case change the detailed view).

A small example:

from PySide import QtGui, QtCore

class MovableGraphicsRectItem(QtGui.QGraphicsRectItem):
    """
        A QGraphicsRectItem that can be moved and is aware of its position.
    """

    def __init__(self):
        super().__init__()
        # enable moving and position tracking
        self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
        # sets a non-default cursor
        self.setCursor(QtCore.Qt.PointingHandCursor)

    def itemChange(self, change, value):
        if change == QtGui.QGraphicsItem.ItemPositionChange:
            print(value)
        return super().itemChange(change, value)

app = QtGui.QApplication([])

# create our movable rectangle
rectangle = MovableGraphicsRectItem()
rectangle.setRect(0, 0, 100, 100)

# create a scene and add our rectangle
scene = QtGui.QGraphicsScene()
scene.addItem(rectangle)

# create view, set fixed scene rectangle and show
view = QtGui.QGraphicsView(scene)
view.setSceneRect(0, 0, 600, 400)
view.show()

app.exec_()

In this example (Python 3.X) you can drag the rectangle around and the changing positions are printed to the console.

Some more comments:

  • You have two views and two associated scenes.
  • Their display is partly overlapping but this is not a problem because the top view will always consume all mouse events.
  • In order to change something in the other view you just have to send an event from the overriden itemChange method or call a callback.
  • You could also add Signal/Slot ability by inheriting from both, QGraphicsRectItem and QObject and then define a signal and emit it.
  • If by chance you also wanted a movable and position aware ellipse or other item you need to create your custom classes for each xxxItem class. I stumbled upon this problem several times and think it might be a disadvantage of the design.
NoDataDumpNoContribution
  • 10,591
  • 9
  • 64
  • 104
  • For this answer I used: [How to enable dragging in QGraphicsScene?](http://stackoverflow.com/questions/3062465/how-to-enable-dragging-in-qgraphicsscene), [QGraphicsItem move event - get absolute position](http://stackoverflow.com/questions/21555639/qgraphicsitem-move-event-get-absolute-position), [Moving QGraphicsItem by mouse in a QGraphicsScene](http://stackoverflow.com/questions/19668164/moving-qgraphicsitem-by-mouse-in-a-qgraphicsscene) ... – NoDataDumpNoContribution Jul 08 '14 at 11:07
  • ...and [Events and signals in Qt's QGraphicsItem: How is this *supposed* to work?](http://stackoverflow.com/questions/10590881/events-and-signals-in-qts-qgraphicsitem-how-is-this-supposed-to-work). Look there for further information. – NoDataDumpNoContribution Jul 08 '14 at 11:07
0

Extending the answer of Trilarion, I was able to solve the problem, by installing a Eventfilter on the overview QgraphcisView. On the Enter event, the dragging is enabled, on the Leave event the dragging is disabled.

from PySide import QtGui, QtCore

# special GraphicsRectItem that is aware of its position and does something if the position is changed
class MovableGraphicsRectItem(QtGui.QGraphicsRectItem):

    def __init__(self, callback=None):
        super(MovableGraphicsRectItem, self).__init__()
        self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
        self.setCursor(QtCore.Qt.PointingHandCursor)
        self.callback = callback

    def itemChange(self, change, value):
        if change == QtGui.QGraphicsItem.ItemPositionChange and self.callback:
            self.callback(value)

        return super(MovableGraphicsRectItem, self).itemChange(change, value)

    def activate(self):
        self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
        self.setCursor(QtCore.Qt.PointingHandCursor)

    def deactivate(self):
        self.setFlags(not QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
        self.setCursor(QtCore.Qt.ArrowCursor)


class MouseInsideFilterObj(QtCore.QObject):#And this one
    def __init__(self, enterCallback, leaveCallback):
        QtCore.QObject.__init__(self)
        self.enterCallback = enterCallback
        self.leaveCallback = leaveCallback

    def eventFilter(self, obj, event):
        if event.type() == QtCore.QEvent.Type.Enter:
            self.enterCallback(obj)

        if event.type() == QtCore.QEvent.Type.Leave:
            self.leaveCallback(obj)

        return True


class TestClass:

    def __init__(self):

        self.app = QtGui.QApplication([])

        # the scene with some rectangles
        self.scene = QtGui.QGraphicsScene()
        self.scene.addRect(30, 30, 100, 50, pen=QtGui.QPen(QtCore.Qt.darkGreen))
        self.scene.addRect(150, 0, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkYellow))
        self.scene.addRect(80, 80, 100, 20, pen=QtGui.QPen(QtCore.Qt.darkMagenta))
        self.scene.addRect(200, 10, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkRed))

        self.window = QtGui.QWidget()

        # put two graphicsviews into the window with different scaling for each
        self.layout = QtGui.QVBoxLayout(self.window)
        self.v1 = QtGui.QGraphicsView(self.scene)
        self.v1.setFixedSize(500, 100)
        self.v1.scale(0.5, 0.5)
        self.v1.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.v1.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.layout.addWidget(self.v1) 
        self.v2 = QtGui.QGraphicsView(self.scene)
        self.v2.setFixedSize(500, 500)
        self.v2.scale(5, 5)
        self.v2.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.v2.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.layout.addWidget(self.v2)

        mouseFilter = MouseInsideFilterObj(self.enterGV, self.leaveGV)
        self.v1.installEventFilter(mouseFilter)

        # the tracker rectangle
        self.tracker = MovableGraphicsRectItem(lambda pos: self.v2.setSceneRect(pos.x(), pos.y(), 100, 100))
        self.tracker.setRect(0, 0, 100, 100)
        self.v2.setSceneRect(0, 0, 100, 100)
        self.tracker.setPen(QtGui.QPen(QtCore.Qt.darkCyan))
        self.scene.addItem(self.tracker)

        self.window.show()
        self.app.exec_()

    def leaveGV(self, gv):
        if gv is self.v1:
            self.tracker.deactivate()

    def enterGV(self, gv):
        if gv is self.v1:
            self.tracker.activate()





TestClass()
P.R.
  • 3,785
  • 1
  • 27
  • 47