Related
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 have created a custom QGraphicsWidget with the ability to resize the widget in the scene. I can also add predefined widgets such as buttons, labels, etc. to my custom widget. I now have two problems.
The first being that the widget doesn't change the size (to re-adjust) upon inserting a new label or LineEdit widget as a result newly inserted widget stays out of the custom widget border.
The second problem is encountered when I try to change the setContentMargins of the QGraphicsLayout to something other than 0. For example QGraphicsLayout.setContentMargins(1, 1, 1, 20) will delay the cursor in the LineEdit widget.
Here is the image.
(Drag the grey triangle to change size)
import sys
from PyQt5 import QtWidgets, QtCore, QtGui, Qt
from PyQt5.QtCore import Qt, QRectF, QPointF
from PyQt5.QtGui import QBrush, QPainterPath, QPainter, QColor, QPen, QPixmap
from PyQt5.QtWidgets import QGraphicsRectItem, QApplication, QGraphicsView, QGraphicsScene, QGraphicsItem
class Container(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
self.setStyleSheet('Container{background:transparent;}')
class GraphicsFrame(QtWidgets.QGraphicsWidget):
def __init__(self, *args, **kwargs):
super(GraphicsFrame, self).__init__()
x, y, h, w = args
rect = QRectF(x, y, h, w)
self.setGeometry(rect)
self.setMinimumSize(150, 150)
self.setMaximumSize(400, 800)
self.setAcceptHoverEvents(True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
self.setFlag(QGraphicsItem.ItemIsFocusable, True)
self.mousePressPos = None
self.mousePressRect = None
self.handleSelected = None
self.polygon = QtGui.QPolygon([
QtCore.QPoint(int(self.rect().width()-10), int(self.rect().height()-20)),
QtCore.QPoint(int(self.rect().width()-10), int(self.rect().height()-10)),
QtCore.QPoint(int(self.rect().width()-20), int(self.rect().height()-10))
])
graphic_layout = QtWidgets.QGraphicsLinearLayout(Qt.Vertical, self)
graphic_layout.setContentsMargins(0, 0, 0, 20) # changing this will cause the second problem
self.container = Container()
proxyWidget = QtWidgets.QGraphicsProxyWidget(self)
proxyWidget.setWidget(self.container)
graphic_layout.addItem(proxyWidget)
self.contentLayout = QtWidgets.QFormLayout()
self.contentLayout.setContentsMargins(10, 10, 20, 20)
self.contentLayout.setSpacing(5)
self.container.layout.addLayout(self.contentLayout)
self.options = []
def addOption(self, color=Qt.white, lbl=None, widget=None):
self.insertOption(-1, lbl, widget, color)
def insertOption(self, index, lbl, widget, color=Qt.white):
if index < 0:
index = self.contentLayout.count()
self.contentLayout.addRow(lbl, widget)
self.options.insert(index, (widget, color))
def update_polygon(self):
self.polygon = QtGui.QPolygon([
QtCore.QPoint(int(self.rect().width() - 10), int(self.rect().height() - 20)),
QtCore.QPoint(int(self.rect().width() - 10), int(self.rect().height() - 10)),
QtCore.QPoint(int(self.rect().width() - 20), int(self.rect().height() - 10))
])
def hoverMoveEvent(self, event):
if self.polygon.containsPoint(event.pos().toPoint(), Qt.OddEvenFill):
self.setCursor(Qt.SizeFDiagCursor)
else:
self.unsetCursor()
super(GraphicsFrame, self).hoverMoveEvent(event)
def mousePressEvent(self, event):
self.handleSelected = self.polygon.containsPoint(event.pos().toPoint(), Qt.OddEvenFill)
if self.handleSelected:
self.mousePressPos = event.pos()
self.mousePressRect = self.boundingRect()
super(GraphicsFrame, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.handleSelected:
self.Resize(event.pos())
else:
super(GraphicsFrame, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
super(GraphicsFrame, self).mouseReleaseEvent(event)
self.handleSelected = False
self.mousePressPos = None
self.mousePressRect = None
self.update()
def paint(self, painter, option, widget):
painter.save()
painter.setBrush(QBrush(QColor(37, 181, 247)))
pen = QPen(Qt.white)
pen.setWidth(2)
if self.isSelected():
pen.setColor(Qt.yellow)
painter.setPen(pen)
painter.drawRoundedRect(self.rect(), 4, 4)
painter.setPen(QtCore.Qt.white)
painter.setBrush(QtCore.Qt.gray)
painter.drawPolygon(self.polygon)
super().paint(painter, option, widget)
painter.restore()
def Resize(self, mousePos):
"""
Perform shape interactive resize.
"""
if self.handleSelected:
self.prepareGeometryChange()
width, height = self.geometry().width()+(mousePos.x()-self.mousePressPos.x()),\
self.geometry().height()+(mousePos.y()-self.mousePressPos.y())
self.setGeometry(QRectF(self.geometry().x(), self.geometry().y(), width, height))
self.contentLayout.setGeometry(QtCore.QRect(0, 30, width-10, height-20))
self.mousePressPos = mousePos
self.update_polygon()
self.updateGeometry()
def main():
app = QApplication(sys.argv)
grview = QGraphicsView()
scene = QGraphicsScene()
grview.setViewportUpdateMode(grview.FullViewportUpdate)
scene.addPixmap(QPixmap('01.png'))
grview.setScene(scene)
item = GraphicsFrame(0, 0, 300, 150)
scene.addItem(item)
item.addOption(Qt.green, lbl=QtWidgets.QLabel('I am a label'), widget=QtWidgets.QLineEdit())
item.addOption(lbl=QtWidgets.QLabel('why'), widget=QtWidgets.QLineEdit())
item.addOption(lbl=QtWidgets.QLabel('How'), widget=QtWidgets.QLineEdit())
item.addOption(lbl=QtWidgets.QLabel('Nooo.'), widget=QtWidgets.QLineEdit())
item.addOption(lbl=QtWidgets.QLabel('Nooo.'), widget=QtWidgets.QLineEdit())
item.addOption(lbl=QtWidgets.QLabel('Nooo.'), widget=QtWidgets.QLineEdit())
item2 = GraphicsFrame(50, 50, 300, 150)
scene.addItem(item2)
grview.fitInView(scene.sceneRect(), Qt.KeepAspectRatio)
grview.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
As already suggested to you more than once, using a QGraphicsWidget with a QGraphicsLayout is not a good idea if you are only using it to embed a QGraphicsProxyWidget, as you will certainly have to face unexpected behavior when changing geometries unless you really know what you're doing.
Then, prepareGeometryChange and updateGeometry are completely unnecessary for QGraphicsWidget, and resizing the widget using the item geometries is absolutely wrong for two reasons: first of all, it's up the graphics layout to manage the content size, then you're using scene coordinates, and since you're using scaling, those coordinates will not be correct as they should be transformed in widget's coordinate.
Since using a QSizeGrip is not doable due to the continuously changing scene rect (which, I have to say, is not always a good idea if done along with interactive resizing of contents), you can use a simple QGraphicsPathItem for it, and use that as a reference for the resizing, which is far more simple than continuously move the polygon and draw it.
class SizeGrip(QtWidgets.QGraphicsPathItem):
def __init__(self, parent):
super().__init__(parent)
path = QtGui.QPainterPath()
path.moveTo(0, 10)
path.lineTo(10, 10)
path.lineTo(10, 0)
path.closeSubpath()
self.setPath(path)
self.setPen(QtGui.QPen(Qt.white))
self.setBrush(QtGui.QBrush(Qt.white))
self.setCursor(Qt.SizeFDiagCursor)
class GraphicsFrame(QtWidgets.QGraphicsItem):
def __init__(self, *args, **kwargs):
super(GraphicsFrame, self).__init__()
x, y, w, h = args
self.setPos(x, y)
self.setAcceptHoverEvents(True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
self.setFlag(QGraphicsItem.ItemIsFocusable, True)
self.container = Container()
self.proxy = QtWidgets.QGraphicsProxyWidget(self)
self.proxy.setWidget(self.container)
self.proxy.setMinimumSize(150, 150)
self.proxy.setMaximumSize(400, 800)
self.proxy.resize(w, h)
self.contentLayout = QtWidgets.QFormLayout()
self.contentLayout.setContentsMargins(10, 10, 20, 20)
self.contentLayout.setSpacing(5)
self.container.layout.addLayout(self.contentLayout)
self.options = []
self.sizeGrip = SizeGrip(self)
self.mousePressPos = None
self.proxy.geometryChanged.connect(self.resized)
self.resized()
def addOption(self, color=Qt.white, lbl=None, widget=None):
self.insertOption(-1, lbl, widget, color)
def insertOption(self, index, lbl, widget, color=Qt.white):
if index < 0:
index = self.contentLayout.count()
self.contentLayout.addRow(lbl, widget)
self.options.insert(index, (widget, color))
def mousePressEvent(self, event):
gripShape = self.sizeGrip.shape().translated(self.sizeGrip.pos())
if event.button() == Qt.LeftButton and gripShape.contains(event.pos()):
self.mousePressPos = event.pos()
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.mousePressPos:
delta = event.pos() - self.mousePressPos
geo = self.proxy.geometry()
bottomRight = geo.bottomRight()
geo.setBottomRight(bottomRight + delta)
self.proxy.setGeometry(geo)
diff = self.proxy.geometry().bottomRight() - bottomRight
if diff.x():
self.mousePressPos.setX(event.pos().x())
if diff.y():
self.mousePressPos.setY(event.pos().y())
else:
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
self.mousePressPos = None
super().mouseReleaseEvent(event)
def resized(self):
rect = self.boundingRect()
self.sizeGrip.setPos(rect.bottomRight() + QtCore.QPointF(-20, -20))
def boundingRect(self):
return self.proxy.boundingRect().adjusted(-11, -11, 11, 11)
def paint(self, painter, option, widget):
painter.save()
painter.setBrush(QBrush(QColor(37, 181, 247)))
painter.drawRoundedRect(self.boundingRect().adjusted(0, 0, -.5, -.5), 4, 4)
painter.restore()
Do note that using fitInView() before showing the view is not a good idea, especially if using proxy widgets and layouts.
I am totally new to PyQt. I want to do animation using PyQt5 .This is a simple test I am doing , so I am just trying to move a rectangle from top to the bottom of the window. Here's a gist of what I am doing to achieve this.
1. I have put whatever I wanted to paint inside paintEvent() method. I have painted the rectangle using variables not constant values
2. I have also created a update() function to update all the variables
3. I have created a loop function which calls self.update() and self.repaint() every 100 milliseconds
import sys
import random
from PyQt5.QtWidgets import ( QApplication, QWidget, QToolTip, QMainWindow)
from PyQt5.QtGui import QPainter, QBrush, QPen, QColor, QFont
from PyQt5.QtCore import Qt, QDateTime
class rain_animation(QMainWindow):
def __init__(self):
super().__init__()
self.painter = QPainter()
""" Variables for the Window """
self.x = 50
self.y = 50
self.width = 500
self.height = 500
"""Variables for the rain"""
self.rain_x = self.width/2
self.rain_y = 0
self.rain_width = 5
self.rain_height = 30
self.rain_vel_x = 0
self.rain_vel_y = 5
self.start()
self.loop()
def paintEvent(self, a0):
self.painter.begin(self)
# Draw a White Background
self.painter.setPen(QPen(Qt.white, 5, Qt.SolidLine))
self.painter.setBrush(QBrush(Qt.white, Qt.SolidPattern))
self.painter.drawRect(0, 0, self.width, self.height)
#Draw the rain
self.painter.setPen(QPen(Qt.blue, 1, Qt.SolidLine))
self.painter.setBrush(QBrush(Qt.blue, Qt.SolidPattern))
self.painter.drawRect(self.rain_x, self.rain_y, self.rain_width, self.rain_height)
self.painter.end(self)
def update(self, diff):
self.rain_x += self.rain_vel_x
self.rain_y += self.rain_vel_y
def start(self):
self.setWindowTitle("Rain Animation")
self.setGeometry(self.x, self.y, self.width, self.height)
self.show()
def loop(self):
start = QDateTime.currentDateTime()
while True :
diff = start.msecsTo(QDateTime.currentDateTime())
if diff >= 100 :
print("time : {0} ms rain_x : {1} rain_y : {2}".format(diff, self.rain_x, self.rain_y))
start = QDateTime.currentDateTime()
self.update(diff)
self.repaint()
if __name__ == "__main__":
app = QApplication(sys.argv)
animation = rain_animation()
sys.exit(app.exec_())
What I should see is a rectangle moving from the top of window to the bottom of the screen but all I see is a window with a black background.
The loop() function seems working properly since the data I am printing shows that the variables are being updated every 100 milliseconds.
Though the problem seems to be something in the loop() function since after removing the self.loop() I can see a static picture of the blue box with a white background at the top of the window.
Problem:
Having a continuous loop does not allow the GUI to perform tasks such as painting, interaction with the OS, etc. Each GUI provides a way to make animations in a way that does not block the window.
Qt provides various classes that allow you to implement the animation as:
QTimer,
QTimeLine,
QVariantAnimation,
QPropertyAnimation.
On the other hand it is recommended that:
Do not create a QPainter outside of paintEvent if it is going to be responsible for the GUI painting.
Use the update() method (not your method but the one that provides Qt) instead of repaint, in case repaint will force the window to paint sometimes unnecessarily, instead update() will do it when necessary, remember that the painted is done with the refresh rate of the screen (60 Hz). For example, if you call repaint 5 times in 20 ms then paintEvent() will be called 3 times but the painting on the screen is every 16.6ms so you only need 1 paint, in the case of update() if you consider it.
Considering the above, it is best to use a QPropertyAnimation:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class RainAnimation(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Rain Animation")
self.setGeometry(50, 50, 500, 500)
self.m_rect_rain = QtCore.QRect()
animation = QtCore.QPropertyAnimation(
self,
b"rect_rain",
parent=self,
startValue=QtCore.QRect(self.width() / 2, 0, 5, 30),
endValue=QtCore.QRect(self.width() / 2, self.height() - 30, 5, 30),
duration=5 * 1000,
)
animation.start()
def paintEvent(self, a0):
painter = QtGui.QPainter(self)
# Draw a White Background
painter.setPen(QtGui.QPen(QtCore.Qt.white, 5, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.white, QtCore.Qt.SolidPattern))
painter.drawRect(self.rect())
#Draw the rain
painter.setPen(QtGui.QPen(QtCore.Qt.blue, 1, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.blue, QtCore.Qt.SolidPattern))
painter.drawRect(self.rect_rain)
#QtCore.pyqtProperty(QtCore.QRect)
def rect_rain(self):
return self.m_rect_rain
#rect_rain.setter
def rect_rain(self, r):
self.m_rect_rain = r
self.update()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = RainAnimation()
w.show()
sys.exit(app.exec_())
Another option is use QVarianAnimation:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class RainAnimation(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Rain Animation")
self.setGeometry(50, 50, 500, 500)
self.m_rect_rain = QtCore.QRect()
animation = QtCore.QVariantAnimation(
parent=self,
startValue=QtCore.QRect(self.width() / 2, 0, 5, 30),
endValue=QtCore.QRect(self.width() / 2, self.height() - 30, 5, 30),
duration=5 * 1000,
valueChanged=self.set_rect_rain,
)
animation.start()
def paintEvent(self, a0):
painter = QtGui.QPainter(self)
# Draw a White Background
painter.setPen(QtGui.QPen(QtCore.Qt.white, 5, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.white, QtCore.Qt.SolidPattern))
painter.drawRect(self.rect())
# Draw the rain
painter.setPen(QtGui.QPen(QtCore.Qt.blue, 1, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.blue, QtCore.Qt.SolidPattern))
painter.drawRect(self.m_rect_rain)
#QtCore.pyqtSlot(QtCore.QVariant)
def set_rect_rain(self, r):
self.m_rect_rain = r
self.update()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = RainAnimation()
w.show()
sys.exit(app.exec_())
The following example is using your logic but with a QTimer:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class RainAnimation(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Rain Animation")
self.setGeometry(50, 50, 500, 500)
self.m_rect_rain = QtCore.QRect(self.width() / 2, 0, 5, 30)
timer = QtCore.QTimer(self, timeout=self.update_rain, interval=100)
timer.start()
def paintEvent(self, a0):
painter = QtGui.QPainter(self)
# Draw a White Background
painter.setPen(QtGui.QPen(QtCore.Qt.white, 5, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.white, QtCore.Qt.SolidPattern))
painter.drawRect(self.rect())
# Draw the rain
painter.setPen(QtGui.QPen(QtCore.Qt.blue, 1, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.blue, QtCore.Qt.SolidPattern))
painter.drawRect(self.m_rect_rain)
#QtCore.pyqtSlot()
def update_rain(self):
self.m_rect_rain.moveTop(self.m_rect_rain.top() + 5)
self.update()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = RainAnimation()
w.show()
sys.exit(app.exec_())
I found this example by #serge_gubenko in SO.
Moving a QGraphicsItem around a central point in PyQt4
I did then some modifications to end up with:
Why is my QGraphicsItem not selectable?
If I run the example (Moving a QGraphicsItem around a central point in PyQt4) and click on the graphics item, it automatically shows up with a dashed frame which indicates that it is selected. I prepared images to show the effect, but due to my low reputation I am not yet allowed to upload those ;)
To me it looks that this "is selected indication" by the dashed frame comes automatically somehow.
In my modified example (Why is my QGraphicsItem not selectable?), this does not happen and I can't figure out why?
You use QtGui.QGraphicsItem, so you define the boundingRect and paint methods, where you used the painter drawEllipse method. In the first example you found, the class uses directly QtGui.QGraphicsEllipseItem and it does all the difference, because those methods are already defined. By the way I didn't find why the boundingRect is not drawn in your case.
Here is a working example which has its own drawFocusRect method.
Focus is indicated in two ways:
1) By clicking on the Qgraphicsitem, then the bounding rect is drawn.
2) Hovering over the item. When fireing the hoverEnterEvent the pen style is changed to DotLine converting back to SolidLine when the hoverLeaveEvent is fired.
#!d:/python27/python -u
import sys
from PyQt4 import QtGui, QtCore
class GraphicsItem(QtGui.QGraphicsItem):
"""
From the QT docs:
To write your own graphics item, you first create a subclass
of QGraphicsItem, and then start by implementing its two pure
virtual public functions: boundingRect(), which returns an estimate
of the area painted by the item, and paint(),
which implements the actual painting.
"""
# call constructor of GraphicsItem
def __init__(self, rect, pen, brush, tooltip='No tip here', parent=None):
# call constructor of QGraphicsItem
super(GraphicsItem, self).__init__()
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QtGui.QGraphicsItem.ItemIsFocusable, True)
self.setAcceptsHoverEvents(True)
self.pen = pen
pw = self.pen.widthF()
self.brush = QtGui.QBrush(QtCore.Qt.blue)
self.brush = brush
self.setToolTip(tooltip)
self.parent = parent
self.rect = QtCore.QRectF(rect[0], rect[1], rect[2], rect[3])
self.focusrect = QtCore.QRectF(rect[0]-pw/2, rect[1]-pw/2,
rect[2]+pw, rect[3]+pw)
def mouseMoveEvent(self, event):
# move object
QtGui.QGraphicsItem.mouseMoveEvent(self, event)
def mousePressEvent(self, event):
# select object
# set item as topmost in stack
self.setZValue(self.parent.scene.items()[0].zValue() + 1)
self.setSelected(True)
QtGui.QGraphicsItem.mousePressEvent(self, event)
def boundingRect(self):
return self.rect
def paint(self, painter, option, widget):
painter.setBrush(self.brush)
painter.setPen(self.pen)
painter.drawEllipse(self.rect)
if self.isSelected():
self.drawFocusRect(painter)
def drawFocusRect(self, painter):
self.focusbrush = QtGui.QBrush()
self.focuspen = QtGui.QPen(QtCore.Qt.DotLine)
self.focuspen.setColor(QtCore.Qt.black)
self.focuspen.setWidthF(1.5)
painter.setBrush(self.focusbrush)
painter.setPen(self.focuspen)
painter.drawRect(self.focusrect)
def hoverEnterEvent(self, event):
self.pen.setStyle(QtCore.Qt.DotLine)
QtGui.QGraphicsItem.hoverEnterEvent(self, event)
def hoverLeaveEvent(self, event):
self.pen.setStyle(QtCore.Qt.SolidLine)
QtGui.QGraphicsItem.hoverLeaveEvent(self, event)
class MyMainWindow(QtGui.QMainWindow):
# call constructor of MyMainWindow
def __init__(self, parent=None):
# call constructor of QMainWindow
super(MyMainWindow, self).__init__(parent)
w = 1000
h = 800
self.scene = QtGui.QGraphicsScene(-w/2, -h/2, w, h)
self.view = QtGui.QGraphicsView()
# set QGraphicsView attributes
self.view.setRenderHints(QtGui.QPainter.Antialiasing |
QtGui.QPainter.HighQualityAntialiasing)
self.view.setViewportUpdateMode(QtGui.QGraphicsView.FullViewportUpdate)
self.view.setScene(self.scene)
# set central widget for the application
self.setCentralWidget(self.view)
# add items to the scene
self.addGraphicsItem((0, 0, 250, 250), 8.0, (255, 0, 0), (0, 0, 255), 'My first item')
self.addGraphicsItem((-250, -250, 300, 200), 4.0, (0, 0, 0), (255, 0, 100), 'My 2nd item')
self.addGraphicsItem((200, -200, 200, 200), 10.0, (0, 0, 255), (0, 255, 100), 'My 3rd item')
def addGraphicsItem(self, rect, pw, pc, bc, tooltip):
pen = QtGui.QPen(QtCore.Qt.SolidLine)
pen.setColor(QtGui.QColor(pc[0], pc[1], pc[2], 255))
pen.setWidth(pw)
brush = QtGui.QBrush(QtGui.QColor(bc[0], bc[1], bc[2], 255))
item = GraphicsItem(rect, pen, brush, tooltip, self)
self.scene.addItem(item)
def mousePressEvent(self, event):
#print 'from MainWindow'
pass
def keyPressEvent(self, event):
key = event.key()
if key == QtCore.Qt.Key_Escape:
sys.exit(QtGui.qApp.quit())
else:
super(GraphicsView, self).keyPressEvent(event)
def main():
app = QtGui.QApplication(sys.argv)
form = MyMainWindow()
form.setGeometry(700, 100, 1050, 850)
form.show()
app.exec_()
if __name__ == '__main__':
main()
Updated for PyQt5
import sys
from PyQt5 import QtGui, QtCore, QtWidgets
class GraphicsItem(QtWidgets.QGraphicsItem):
"""
From the QT docs:
To write your own graphics item, you first create a subclass
of QGraphicsItem, and then start by implementing its two pure
virtual public functions: boundingRect(), which returns an estimate
of the area painted by the item, and paint(),
which implements the actual painting.
"""
# call constructor of GraphicsItem
def __init__(self, rect, pen, brush, tooltip='No tip here', parent=None):
# call constructor of QGraphicsItem
super(GraphicsItem, self).__init__()
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, True)
self.setAcceptHoverEvents(True)
self.pen = pen
pw = self.pen.widthF()
self.brush = QtGui.QBrush(QtCore.Qt.blue)
self.brush = brush
self.setToolTip(tooltip)
self.parent = parent
self.rect = QtCore.QRectF(rect[0], rect[1], rect[2], rect[3])
self.focusrect = QtCore.QRectF(rect[0]-pw/2, rect[1]-pw/2,
rect[2]+pw, rect[3]+pw)
def mouseMoveEvent(self, event):
# move object
QtWidgets.QGraphicsItem.mouseMoveEvent(self, event)
def mousePressEvent(self, event):
# select object
# set item as topmost in stack
self.setZValue(self.parent.scene.items()[0].zValue() + 1)
self.setSelected(True)
QtWidgets.QGraphicsItem.mousePressEvent(self, event)
def boundingRect(self):
return self.rect
def paint(self, painter, option, widget=None):
painter.setBrush(self.brush)
painter.setPen(self.pen)
painter.drawEllipse(self.rect)
if self.isSelected():
self.drawFocusRect(painter)
def drawFocusRect(self, painter):
self.focusbrush = QtGui.QBrush()
self.focuspen = QtGui.QPen(QtCore.Qt.DotLine)
self.focuspen.setColor(QtCore.Qt.black)
self.focuspen.setWidthF(1.5)
painter.setBrush(self.focusbrush)
painter.setPen(self.focuspen)
painter.drawRect(self.focusrect)
def hoverEnterEvent(self, event):
self.pen.setStyle(QtCore.Qt.DotLine)
QtWidgets.QGraphicsItem.hoverEnterEvent(self, event)
def hoverLeaveEvent(self, event):
self.pen.setStyle(QtCore.Qt.SolidLine)
QtWidgets.QGraphicsItem.hoverLeaveEvent(self, event)
class MyMainWindow(QtWidgets.QMainWindow):
# call constructor of MyMainWindow
def __init__(self, parent=None):
# call constructor of QMainWindow
super(MyMainWindow, self).__init__(parent)
w = 1000
h = 800
self.scene = QtWidgets.QGraphicsScene(-w/2, -h/2, w, h)
self.view = QtWidgets.QGraphicsView()
# set QGraphicsView attributes
self.view.setRenderHints(QtGui.QPainter.Antialiasing |
QtGui.QPainter.HighQualityAntialiasing)
self.view.setViewportUpdateMode(QtWidgets.QGraphicsView.FullViewportUpdate)
self.view.setScene(self.scene)
# set central widget for the application
self.setCentralWidget(self.view)
# add items to the scene
self.addGraphicsItem((0, 0, 250, 250), 8.0, (255, 0, 0), (0, 0, 255), 'My first item')
self.addGraphicsItem((-250, -250, 300, 200), 4.0, (0, 0, 0), (255, 0, 100), 'My 2nd item')
self.addGraphicsItem((200, -200, 200, 200), 10.0, (0, 0, 255), (0, 255, 100), 'My 3rd item')
def addGraphicsItem(self, rect, pw, pc, bc, tooltip):
pen = QtGui.QPen(QtCore.Qt.SolidLine)
pen.setColor(QtGui.QColor(pc[0], pc[1], pc[2], 255))
pen.setWidth(pw)
brush = QtGui.QBrush(QtGui.QColor(bc[0], bc[1], bc[2], 255))
item = GraphicsItem(rect, pen, brush, tooltip, self)
self.scene.addItem(item)
def mousePressEvent(self, event):
#print 'from MainWindow'
pass
def main():
app = QtWidgets.QApplication(sys.argv)
form = MyMainWindow()
form.setGeometry(700, 100, 1050, 850)
form.show()
app.exec_()
if __name__ == '__main__':
main()
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()