i added a draggable red circle into my main window. The red circle can be moved to anywhere you want on the main window. But i want to set a border where the red circle is allowed be move. Probably it should be done with a sub window? Anyone that has an idea how to do this? I come this far:
import sys
from PyQt5.QtWidgets import QApplication, QGraphicsView, QWidget, QGraphicsEllipseItem, QMainWindow, QGroupBox, QGraphicsScene, QHBoxLayout
from PyQt5.QtCore import Qt, QPointF, QRect
class MovingObject(QGraphicsEllipseItem):
def __init__(self, x, y, r):
#de meegegeven waardes gebruiken om beginpositie en grootte ellips te bepalen
super().__init__(0, 0, r, r)
self.setPos(x, y)
self.setBrush(Qt.red)
##mousePressEvent checkt of er wel of niet wordt geklikt
def mousePressEvent(self, event):
pass
##mouseMoveEvent is om de item te kunnen draggen
def mouseMoveEvent(self, event):
orig_cursor_position = event.lastScenePos()
updated_cursor_position = event.scenePos()
orig_position = self.scenePos()
updated_cursor_x = updated_cursor_position.x() - orig_cursor_position.x() + orig_position.x()
updated_cursor_y = updated_cursor_position.y() - orig_cursor_position.y() + orig_position.y()
self.setPos(QPointF(updated_cursor_x, updated_cursor_y))
class GraphicView(QGraphicsView):
def __init__(self):
super().__init__()
self.scene = QGraphicsScene()
self.setScene(self.scene)
self.setSceneRect(0, 0, 60, 60)
#waardes x, y, r waarvan x en y beginpositie van ellips is en r is straal van ellips
self.scene.addItem(MovingObject(0, 0, 40))
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(800, 500, 400, 400)
self.setWindowTitle("MainWindow")
#set GraphicView in Window
self.graphicView = GraphicView()
self.setCentralWidget(self.graphicView)
app = QApplication(sys.argv)
GUI = Window()
GUI.show()
sys.exit(app.exec_())
First of all, there's no need to reimplement mouse events to allow movements of a QGraphicsItem using the mouse, as setting the ItemIsMovable flag is enough.
Once the flag is set, what is needed is to filter geometry changes and react to them.
This is achieved by setting the ItemSendsGeometryChanges flag and reimplementing the itemChange() function. Using the ItemPositionChange change, you can receive the "future" position before it's applied and eventually change (and return) the received value; the returned value is the actual final position that will be applied during the movement.
What remains is to give a reference to check it.
In the following example I'm setting the scene rect (not the view rect as you did) with bigger margins, and set those margins for the item; you obviously can set any QRectF for that.
I also implemented the drawBackground() function in order to show the scene rect that are used as limits.
class MovingObject(QGraphicsEllipseItem):
def __init__(self, x, y, r):
super().__init__(0, 0, r, r)
self.setPos(x, y)
self.setBrush(Qt.red)
self.setFlag(self.ItemIsMovable, True)
self.setFlag(self.ItemSendsGeometryChanges, True)
self.margins = None
def setMargins(self, margins):
self.margins = margins
def itemChange(self, change, value):
if change == self.ItemPositionChange and self.margins:
newRect = self.boundingRect().translated(value)
if newRect.x() < self.margins.x():
# beyond left margin, reset
value.setX(self.margins.x())
elif newRect.right() > self.margins.right():
# beyond right margin
value.setX(self.margins.right() - newRect.width())
if newRect.y() < self.margins.y():
# beyond top margin
value.setY(self.margins.y())
elif newRect.bottom() > self.margins.bottom():
# beyond bottom margin
value.setY(self.margins.bottom() - newRect.height())
return super().itemChange(change, value)
class GraphicView(QGraphicsView):
def __init__(self):
super().__init__()
self.scene = QGraphicsScene()
self.setScene(self.scene)
self.scene.setSceneRect(0, 0, 120, 120)
self.movingObject = MovingObject(0, 0, 40)
self.scene.addItem(self.movingObject)
self.movingObject.setMargins(self.scene.sceneRect())
def drawBackground(self, painter, rect):
painter.drawRect(self.sceneRect())
Do note that the graphics view framework is as powerful as it is hard to really know and understand. Given the question you're asking ("Probably it should be done with a sub window?") it's clear that you still need to understand how it works, as using sub windows is a completely different thing.
I strongly suggest you to carefully its documentation and everything (functions and properties) related to the QGraphicsItem, which is the base class for all graphics items.
Existing properties should never be overwritten; scene() is a basic property of QGraphicsView, so you either choose another name as an instance attribute (self.myScene = QGraphicsScene()), or you just use a local variable (scene = QGraphicsScene()) and always use self.scene() outside the __init__.
Related
I have a QGraphicsScene where I have QGraphicsItems. These items are movable and I can move them all over the QGraphicsScene but I would like to limit the area where these items can be moved. The sizes of the QGraphicsScene don't have to change. I would really appreciate if someone gave me an example of how to do it in python.
Here's what I have now
from PySide2.QtCore import QPointF
from PySide2.QtWidgets import QWidget, QVBoxLayout, QGraphicsView, \
QGraphicsScene, QGraphicsPolygonItem, QApplication
from PySide2.QtGui import QPen, QColor, QBrush, QPolygonF
class Test(QWidget):
def __init__(self, parent=None):
super(Test, self).__init__(parent)
self.resize(1000, 800)
self.layout_ = QVBoxLayout()
self.view_ = GraphicsView()
self.layout_.addWidget(self.view_)
self.setLayout(self.layout_)
class GraphicsView(QGraphicsView):
def __init__(self):
super(GraphicsView, self).__init__()
self.scene_ = QGraphicsScene()
self.polygon_creation = self.PolyCreation()
self.scene_.setSceneRect(0, 0, 400, 400)
self.setScene(self.scene_)
self.polyCreator()
def polyCreator(self):
self.polygon_creation.poly()
polygon = self.polygon_creation.polygon()
new_poly = self.scene().addPolygon(polygon)
new_poly.setBrush(QBrush(QColor("gray")))
new_poly.setPen(QPen(QColor("gray")))
new_poly.setFlag(QGraphicsPolygonItem.ItemIsSelectable)
new_poly.setFlag(QGraphicsPolygonItem.ItemIsMovable)
new_poly.setFlag(QGraphicsPolygonItem.ItemIsFocusable)
new_poly.setPos(0, 0)
class PolyCreation(QGraphicsPolygonItem):
def __init__(self):
super().__init__()
self.setAcceptHoverEvents(True)
def poly(self):
self.poly_points = (QPointF(0, 0),
QPointF(0, 50),
QPointF(50, 50),
QPointF(50, 0))
self.shape = QPolygonF(self.poly_points)
self.setPolygon(self.shape)
if __name__ == '__main__':
app = QApplication([])
win = Test()
win.show()
app.exec_()
I've also found an answer in cpp, but I can't understand it very well, so if someone could "translate" it in python that'd be great too.
Here's the link restrict movable area of qgraphicsitem (Please check #Robert's answer)
The concept is to restrict the new position before it's finally applied.
To achieve so, you need to also set the ItemSendsGeometryChanges flag and check for ItemPositionChange changes, then compare the item bounding rect with that of the scene, and eventually return a different position after correcting it.
class PolyCreation(QGraphicsPolygonItem):
def __init__(self):
super().__init__(QPolygonF([
QPointF(0, 0),
QPointF(0, 50),
QPointF(50, 50),
QPointF(50, 0)
]))
self.setBrush(QBrush(QColor("gray")))
self.setPen(QPen(QColor("blue")))
self.setFlags(
self.ItemIsSelectable
| self.ItemIsMovable
| self.ItemIsFocusable
| self.ItemSendsGeometryChanges
)
self.setAcceptHoverEvents(True)
def itemChange(self, change, value):
if change == self.ItemPositionChange and self.scene():
br = self.polygon().boundingRect().translated(value)
sceneRect = self.scene().sceneRect()
if not sceneRect.contains(br):
if br.right() > sceneRect.right():
br.moveRight(sceneRect.right())
if br.x() < sceneRect.x():
br.moveLeft(sceneRect.x())
if br.bottom() > sceneRect.bottom():
br.moveBottom(sceneRect.bottom())
if br.y() < sceneRect.y():
br.moveTop(sceneRect.top())
return br.topLeft()
return super().itemChange(change, value)
class GraphicsView(QGraphicsView):
def __init__(self):
super(GraphicsView, self).__init__()
self.scene_ = QGraphicsScene()
self.scene_.setSceneRect(0, 0, 400, 400)
self.setScene(self.scene_)
self.scene_.addItem(PolyCreation())
Notes:
the above code will obviously only work properly for top level items (not children of other items);
it will work as long as the item doesn't have any transformation applied (rotation, scale, etc.); if you want to support that, you have to consider the sceneTransform() to get the actual bounding rect of the polygon;
it doesn't consider the pen width, so if the item has a thick pen, the resulting polygon may go beyond the scene boundaries; to avoid that, use the actual boundingRect() of the item and adjust it by using half the pen width;
avoid nested classes, they are rarely required and they only tend to make code unnecessarily convoluted;
you were not actually using that subclass, since you're in fact adding another polygon item based on the polygon of that instance;
all items are always positioned at (0, 0) by default, specifying it again is pointless;
shape() is an existing (and quite important) function of all items, you shall not overwrite it;
I'm new to Pyqt5 and writing applications with it in Python so forgive me if this is a very simple question but I'm having trouble drawing ellipses in my program. I want to draw one by wherever a click occurs. Here is my code:
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import *
class Window(QGraphicsView):
def __init__(self):
super().__init__()
self.scene = QGraphicsScene()
self.setScene(self.scene)
# p.setTransform(transform)
self.button = QPushButton("Draw")
self.button.setCheckable(True)
self.button.setGeometry(0, 0, 100, 30)
self.scene.addWidget(self.button)
# self.setMouseTracking(True)
width, height = 1000, 1000
self.setFixedSize(width, height);
self.setSceneRect(0, 0, width, height);
self.fitInView(0, 0, width, height, Qt.KeepAspectRatio);
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.show()
def mousePressEvent(self, event):
if self.button.isChecked():
x = event.x()
y = event.y()
print(x, y)
ellipse = QGraphicsEllipseItem(x, y, 50, 20)
self.scene.addItem(ellipse)
The issue I'm having is I think the mousePressEvent function isn't allowing me to click on my button to enable drawing but the part I'm really not sure about is what is going on in the mousePressEvent. It seems as though it's getting the (x, y) coordinates within the QGraphicsView object but my ellipses are getting drawn in strange spots far away from wherever is clicked in my application when it's open.
You should not override the mousePressEvent as you remove the default behavior such as sending the event to the button. On the other hand you have to convert the coordinates of the view to the coordinates of the scene.
self.proxy_widget = self.scene.addWidget(self.button)
def mousePressEvent(self, event):
super().mousePressEvent(event)
vp = event.pos()
if self.proxy_widget in self.items(vp):
return
if self.button.isChecked():
ellipse = QGraphicsEllipseItem(0, 0, 50, 20)
self.scene.addItem(ellipse)
sp = self.mapToScene(vp)
ellipse.setPos(sp)
I am using PyQt and I'm trying to re-implement a QGraphicsTextItem, but it seems I'm missing something.
I would like to make the NodeTag item's text editable. I have tried setting flags such as Qt.TextEditorInteraction and QGraphicsItem.ItemIsMovable , but those seem to be ignored...
Here is a Minimal Reproducible Example :
import sys
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QMainWindow, QApplication, QGraphicsItem, QGraphicsTextItem
from PyQt5.QtCore import *
from PyQt5.QtGui import QPen
class NodeTag(QGraphicsTextItem):
def __init__(self,text):
QGraphicsTextItem.__init__(self,text)
self.text = text
self.setPos(0,0)
self.setTextInteractionFlags(Qt.TextEditorInteraction)
# self.setFlag(QGraphicsItem.ItemIsFocusable, True) # All these flags are ignored...
# self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
def boundingRect(self):
return QRectF(0,0,80,25)
def paint(self,painter,option,widget):
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
painter.drawText(self.boundingRect(),self.text)
def mousePressEvent(self, event):
print("CLICK!")
# self.setTextInteractionFlags(Qt.TextEditorInteraction) # make text editable on click
# self.setFocus()
class GView(QGraphicsView):
def __init__(self, parent, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parent = parent
self.setGeometry(100, 100, 700, 450)
self.show()
class Scene(QGraphicsScene):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
tagItem = NodeTag("myText") # create a NodeTag item
self.addItem(tagItem)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__() # create default constructor for QWidget
self.setGeometry(900, 70, 1000, 800)
self.createGraphicView()
self.show()
def createGraphicView(self):
self.scene = Scene(self)
gView = GView(self)
scene = Scene(gView)
gView.setScene(scene)
# Set the main window's central widget
self.setCentralWidget(gView)
# Run program
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec_())
As you can see I have tried overriding the mousePressEvent and setting flags there too, but no luck so far.
Any help appreciated!
All QGraphicsItem subclasses have a paint method, and all items that paint some contents have that method overridden so that they can actually paint themselves.
The mechanism is the same as standard QWidgets, for which there is a paintEvent (the difference is that paint of QGraphicsItem receives an already instanciated QPainter), so if you want to do further painting other than what the class already provides, the base implementation must be called.
Consider that painting always happen from bottom to top, so everything that needs to be drawn behind the base painting has to be done before calling super().paint(), and everything that is going to be drawn in front of the default painting has to be placed after.
Depending on the situation, overriding might require that the default base implementation is called anyway, and that's important in your case for boundingRect too. QGraphicsTextItem automatically resizes itself when its contents change, so you should not always return a fixed QRect. If you need to have a minimum size, the solution is to merge a minimum rectangle with those provided by the default boundingRect() function.
Then, editing on a QGraphicsTextItem happens when the item gets focused, but since you also want to be able to move the item, things get trickier as both actions are based on mouse clicks. If you want to be able to edit the text with a single click, the solution is to make the item editable only when the mouse button has been released and has not been moved by some amount of pixels (the startDragDistance() property), otherwise the item is moved with the mouse. This obviously makes the ItemIsMovable flag useless, as we're going to take care of the movement internally.
Finally, since a minimum size is provided, we also need to override the shape() method in order to ensure that collision and clicks are correctly mapped, and return a QPainterPath that includes the whole bounding rect (for normal QGraphicsItem that should be the default behavior, but that doesn't happen with QGraphicsRectItem).
Here's a full implementation of what described above:
class NodeTag(QGraphicsTextItem):
def __init__(self, text):
QGraphicsTextItem.__init__(self, text)
self.startPos = None
self.isMoving = False
# the following is useless, not only because we are leaving the text
# painting to the base implementation, but also because the text is
# already accessible using toPlainText() or toHtml()
#self.text = text
# this is unnecessary too as all new items always have a (0, 0) position
#self.setPos(0, 0)
def boundingRect(self):
return super().boundingRect() | QRectF(0, 0, 80, 25)
def paint(self, painter, option, widget):
# draw the border *before* (as in "behind") the text
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
super().paint(painter, option, widget)
def shape(self):
shape = QPainterPath()
shape.addRect(self.boundingRect())
return shape
def focusOutEvent(self, event):
# this is required in order to allow movement using the mouse
self.setTextInteractionFlags(Qt.NoTextInteraction)
def mousePressEvent(self, event):
if (event.button() == Qt.LeftButton and
self.textInteractionFlags() != Qt.TextEditorInteraction):
self.startPos = event.pos()
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.startPos:
delta = event.pos() - self.startPos
if (self.isMoving or
delta.manhattanLength() >= QApplication.startDragDistance()):
self.setPos(self.pos() + delta)
self.isMoving = True
return
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if (not self.isMoving and
self.textInteractionFlags() != Qt.TextEditorInteraction):
self.setTextInteractionFlags(Qt.TextEditorInteraction)
self.setFocus()
# the following lines are used to correctly place the text
# cursor at the mouse cursor position
cursorPos = self.document().documentLayout().hitTest(
event.pos(), Qt.FuzzyHit)
textCursor = self.textCursor()
textCursor.setPosition(cursorPos)
self.setTextCursor(textCursor)
super().mouseReleaseEvent(event)
self.startPos = None
self.isMoving = False
As a side note, remember that QGraphicsTextItem supports rich text formatting, so even if you want more control on the text painting process you should not use QPainter.drawText(), because you'd only draw the plain text. In fact, QGraphicsTextItem draws its contents using the drawContents() function of the underlying text document.
Try it:
...
class NodeTag(QGraphicsTextItem):
def __init__(self, text, parent=None):
super(NodeTag, self).__init__(parent)
self.text = text
self.setPlainText(text)
self.setFlag(QGraphicsItem.ItemIsMovable)
self.setFlag(QGraphicsItem.ItemIsSelectable)
def focusOutEvent(self, event):
self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
super(NodeTag, self).focusOutEvent(event)
def mouseDoubleClickEvent(self, event):
if self.textInteractionFlags() == QtCore.Qt.NoTextInteraction:
self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
super(NodeTag, self).mouseDoubleClickEvent(event)
def paint(self,painter,option,widget):
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
# painter.drawText(self.boundingRect(),self.text)
super().paint(painter, option, widget)
...
Can somebody please provide an explanation, or better yet a short example, of how to use the RubberBandDrag enum value in QGraphicsView? PyQt5 would be great, but I can translate from the C++ version if that is preferred for whomever can provide a helpful exmaple.
NoDrag and ScrollHandDrag are relatively easy to understand (NoDrag makes the mouse a pointer and you can capture clicks at certain locations, ScrollHandDrag makes the mouse a hand and you can implement click and drag to scroll around), but I'm unclear on what RubberBandDrag can be used for.
Before somebody says "go read the docs", here is the information provided
https://doc.qt.io/qt-5/qgraphicsview.html
enum QGraphicsView::DragMode
QGraphicsView::RubberBandDrag
A rubber band will appear. Dragging the mouse will set the rubber band
geometry, and all items covered by the rubber band are selected. This
mode is disabled for non-interactive views.
This is clear but I'm not sure how I could actually use RubberBandDrag. Is there a way to use this to drag points of a QPolygon around after initial placement? That would be really useful.
The QGraphicsView::RubberBandDrag flag only serves to activate the internal QRubberBand:
And the QRubberBand in general only aims to visualize a selected area and in the case of QGraphicsView select the items below that area if they are selectable(enable QGraphicsItem::ItemIsSelectable flag).
According to your last question: Is there a way to use this to drag points of a QPolygon around after initial placement? That would be really useful, it seems to me that you have an XY problem since it seems that the use of drag in the terminology makes you think that it serves to drag elements, because no, that drag refers to the way of creating the rubber band.
In a few moments I will show how to implement the drag of the vertices to modify the QPolygon.
The following shows how to modify the position of the vertices by dragging the mouse:
import math
from PyQt5 import QtCore, QtGui, QtWidgets
class GripItem(QtWidgets.QGraphicsPathItem):
circle = QtGui.QPainterPath()
circle.addEllipse(QtCore.QRectF(-10, -10, 20, 20))
square = QtGui.QPainterPath()
square.addRect(QtCore.QRectF(-15, -15, 30, 30))
def __init__(self, annotation_item, index):
super(GripItem, self).__init__()
self.m_annotation_item = annotation_item
self.m_index = index
self.setPath(GripItem.circle)
self.setBrush(QtGui.QColor("green"))
self.setPen(QtGui.QPen(QtGui.QColor("green"), 2))
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True)
self.setAcceptHoverEvents(True)
self.setZValue(11)
self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
def hoverEnterEvent(self, event):
self.setPath(GripItem.square)
self.setBrush(QtGui.QColor("red"))
super(GripItem, self).hoverEnterEvent(event)
def hoverLeaveEvent(self, event):
self.setPath(GripItem.circle)
self.setBrush(QtGui.QColor("green"))
super(GripItem, self).hoverLeaveEvent(event)
def mouseReleaseEvent(self, event):
self.setSelected(False)
super(GripItem, self).mouseReleaseEvent(event)
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isEnabled():
self.m_annotation_item.movePoint(self.m_index, value)
return super(GripItem, self).itemChange(change, value)
class PolygonAnnotation(QtWidgets.QGraphicsPolygonItem):
def __init__(self, parent=None):
super(PolygonAnnotation, self).__init__(parent)
self.m_points = []
self.setZValue(10)
self.setPen(QtGui.QPen(QtGui.QColor("green"), 2))
self.setAcceptHoverEvents(True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True)
self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
self.m_items = []
def addPoint(self, p):
self.m_points.append(p)
self.setPolygon(QtGui.QPolygonF(self.m_points))
item = GripItem(self, len(self.m_points) - 1)
self.scene().addItem(item)
self.m_items.append(item)
item.setPos(p)
def movePoint(self, i, p):
if 0 <= i < len(self.m_points):
self.m_points[i] = self.mapFromScene(p)
self.setPolygon(QtGui.QPolygonF(self.m_points))
def move_item(self, index, pos):
if 0 <= index < len(self.m_items):
item = self.m_items[index]
item.setEnabled(False)
item.setPos(pos)
item.setEnabled(True)
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
for i, point in enumerate(self.m_points):
self.move_item(i, self.mapToScene(point))
return super(PolygonAnnotation, self).itemChange(change, value)
def hoverEnterEvent(self, event):
self.setBrush(QtGui.QColor(255, 0, 0, 100))
super(PolygonAnnotation, self).hoverEnterEvent(event)
def hoverLeaveEvent(self, event):
self.setBrush(QtGui.QBrush(QtCore.Qt.NoBrush))
super(PolygonAnnotation, self).hoverLeaveEvent(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
scene = QtWidgets.QGraphicsScene()
w = QtWidgets.QGraphicsView(scene)
polygon_item = PolygonAnnotation()
scene.addItem(polygon_item)
r = 100
sides = 10
for i in range(sides):
angle = 2 * math.pi * i / sides
x = r * math.cos(angle)
y = r * math.sin(angle)
p = QtCore.QPointF(x, y) + QtCore.QPointF(200, 200)
polygon_item.addPoint(p)
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
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:
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?
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.
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_())
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.
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()