Object-based paint/update in Qt/PyQt4 - python

I'd like to tag items by drawing polygons over an image in Python using PyQt4. I was able to implement the image viewer with QGraphicsScene but I don't understand the concept behind painting/updating objects.
What I'd like to do is a Polygon class, what supports adding and editing. What confuses me is the QGraphicsScene.addItem and the different paint or update methods. What I'd like to implement is to
draw a polygon as lines while not complete
draw it as a filled polygon once complete
The algorithm part is OK, what I don't understand is that how do I implement the paint or update functions.
Here is my confusion
In the original example file: graphicsview/collidingmice there is a special function def paint(self, painter, option, widget): what does the painting. There is no function calling the paint function, thus I'd think it's a special name called by QGraphicsView, but I don't understand what is a painter and what should a paint function implement.
On the other hand in numerous online tutorials I find def paintEvent(self, event): functions, what seems to follow a totally different concept compared to the graphicsview / paint.
Maybe to explain it better: for me the way OpenGL does the scene-update is clear, where you always clean everything and re-draw elements one by one. There you just take care of what items do you want to draw and draw the appropriate ones. There is no update method, because you are drawing always the most up-to-date state. This Qt GUI way is new to me. Can you tell me what happens with an item after I've added it to the scene? How do I edit something what has been added to the scene, where is the always updating 'loop'?
Here is my source in the smallest possible form, it creates the first polygon and starts printing it's points. I've arrived so far that the paint method is called once (why only once?) and there is this error NotImplementedError: QGraphicsItem.boundingRect() is abstract and must be overridden. (just copy any jpg file as big.jpg)
from __future__ import division
import sys
from PyQt4 import QtCore, QtGui
class Polygon( QtGui.QGraphicsItem ):
def __init__(self):
super(Polygon, self).__init__()
self.points = []
self.closed = False
def addpoint( self, point ):
self.points.append( point )
print self.points
def paint(self, painter, option, widget):
print "paint"
class MainWidget(QtGui.QWidget):
poly_drawing = False
def __init__(self):
super(MainWidget, self).__init__()
self.initUI()
def initUI(self):
self.scene = QtGui.QGraphicsScene()
self.img = QtGui.QPixmap( 'big.jpg' )
self.view = QtGui.QGraphicsView( self.scene )
self.view.setRenderHint(QtGui.QPainter.Antialiasing)
self.view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.pixmap_item = QtGui.QGraphicsPixmapItem( self.img, None, self.scene)
self.pixmap_item.mousePressEvent = self.pixelSelect
self.mypoly = Polygon()
layout = QtGui.QVBoxLayout()
layout.addWidget( self.view )
self.setLayout( layout )
self.resize( 900, 600 )
self.show()
def resizeEvent(self, event):
w_scale = ( self.view.width() ) / self.img.width()
h_scale = ( self.view.height() ) / self.img.height()
self.scale = min( w_scale, h_scale)
self.view.resetMatrix()
self.view.scale( self.scale, self.scale )
def pixelSelect(self, event):
if not self.poly_drawing:
self.poly_drawing = True
self.mypoly = Polygon()
self.scene.addItem( self.mypoly )
point = event.pos()
self.mypoly.addpoint( point )
def main():
app = QtGui.QApplication(sys.argv)
ex = MainWidget()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

Related

How to limit the area in QGraphicsScene where a custom QGraphicsItem can be moved?

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;

how to make an overriden QGraphicsTextItem editable & movable?

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)
...

How to create circular image using pyqt4?

Here I wrote this code but did not work:
import sys
from PyQt4 import QtGui, QtCore
class CricleImage(QtCore.QObject):
def __init__(self):
super(CricleImage, self).__init__()
self.pix = QtGui.QGraphicsPixmapItem(QtGui.QPixmap("bird(01).jpg"))
#drawRoundCircle
rect = self.pix.boundingRect()
self.gri = QtGui.QGraphicsRectItem(rect)
self.gri.setPen(QtGui.QColor('red'))
if __name__ == '__main__':
myQApplication = QtGui.QApplication(sys.argv)
IMG = CricleImage()
#scene
scene = QtGui.QGraphicsScene(0, 0, 400, 300)
scene.addItem(IMG.pix)
#view
view = QtGui.QGraphicsView(scene)
view.show()
sys.exit(myQApplication.exec_())
One possible solution is to overwrite the paint() method of the QGraphicsPixmapItem and use setClipPath to restrict the painting region:
from PyQt4 import QtCore, QtGui
class CirclePixmapItem(QtGui.QGraphicsPixmapItem):
#property
def radius(self):
if not hasattr(self, "_radius"):
self._radius = 0
return self._radius
#radius.setter
def radius(self, value):
if value >= 0:
self._radius = value
self.update()
def paint(self, painter, option, widget=None):
painter.save()
rect = QtCore.QRectF(QtCore.QPointF(), 2 * self.radius * QtCore.QSizeF(1, 1))
rect.moveCenter(self.boundingRect().center())
path = QtGui.QPainterPath()
path.addEllipse(rect)
painter.setClipPath(path)
super().paint(painter, option, widget)
painter.restore()
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
pixmap = QtGui.QPixmap("logo.jpg")
scene = QtGui.QGraphicsScene()
view = QtGui.QGraphicsView(scene)
view.setRenderHints(
QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform
)
it = CirclePixmapItem(pixmap)
scene.addItem(it)
it.radius = pixmap.width() / 2
view.show()
sys.exit(app.exec_())
Update:
# ...
view = QtGui.QGraphicsView(
scene, alignment=QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft
)
# ...
view.show()
it.setPos(80, 80)
sys.exit(app.exec_())
Second possible solution:
import sys
#from PyQt4 import QtCore, QtGui
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class Label(QLabel):
def __init__(self, *args, antialiasing=True, **kwargs):
super(Label, self).__init__(*args, **kwargs)
self.Antialiasing = antialiasing
self.setMaximumSize(200, 200)
self.setMinimumSize(200, 200)
self.radius = 100
self.target = QPixmap(self.size())
self.target.fill(Qt.transparent) # Fill the background with transparent
# Upload image and zoom to control level
p = QPixmap("head2.jpg").scaled(
200, 200, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
painter = QPainter(self.target)
if self.Antialiasing:
# antialiasing
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setRenderHint(QPainter.HighQualityAntialiasing, True)
painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
path = QPainterPath()
path.addRoundedRect(
0, 0, self.width(), self.height(), self.radius, self.radius)
# pruning
painter.setClipPath(path)
painter.drawPixmap(0, 0, p)
self.setPixmap(self.target)
class Window(QWidget):
def __init__(self, *args, **kwargs):
super(Window, self).__init__(*args, **kwargs)
layout = QHBoxLayout(self)
layout.addWidget(Label(self))
self.setStyleSheet("background: green;")
if __name__ == "__main__":
app = QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())
Another approach, slightly different from the one provided by eyllanesc. While this might seem much more complicated than that, I believe that it offers a better implementation and interface, with the addition of better performance.
In this case, instead of overriding the paint method (that is run everytime the item is painted, which happens very often), I'm using the shape() function along with the QGraphicsItem.ItemClipsToShape flag, that allows to limit the painting only within the boundaries of the path shape.
What shape() does is to return a QPainterPath that includes only the "opaque" portions of an item that will react to mouse events and collision detection (with the scene boundaries and its other items). In the case of a QGraphicsPixmapItem this also considers the possible mask (for example, a PNG based pixmap with transparent areas, or an SVG image). By setting the ItemClipsToShape we can ensure that the painting will only cover the parts of the image that are within that shape.
The main advantage of this approach is that mouse interaction and collision detection with other items honors the actual circle shape of the item.
This means that if you click outside the circle (but still within the rectangle area of the full image), the item will not receive the event. Also, if the image supports masking (a PNG with transparent areas) which by default would not be part of the shape, this method will take that into account.
Also, by "caching" the shape we are also speeding up the painting process a bit (since Qt will take care of it, without any processing done using python).
class CircleClipPixmapItem(QtGui.QGraphicsPixmapItem):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFlag(self.ItemClipsToShape)
self.updateRect()
def updateRect(self):
baseRect = super().boundingRect()
minSize = min(baseRect.width(), baseRect.height())
self._boundingRect = QtCore.QRectF(0, 0, minSize, minSize)
self._boundingRect.moveCenter(baseRect.center())
self._shape = QtGui.QPainterPath()
self._shape.addEllipse(self._boundingRect)
# the shape might include transparent areas, using the & operator
# I'm ensuring that _shape only includes the areas that intersect
# the shape provided by the base implementation
self._shape &= super().shape()
def setPixmap(self, pm):
super().setPixmap(pm)
# update the shape to reflect the new image size
self.updateRect()
def setShapeMode(self, mode):
super().setShapeMode(mode)
# update the shape with the new mode
self.updateRect()
def boundingRect(self):
return self._boundingRect
def shape(self):
return self._shape
Keep in mind that there's a catch about both methods: if the aspect ratio of the image differs very much from 1:1, you'll always end up with some positioning issues. With my image, for example, it will always be shown 60 pixel right from the actual item position. If you want to avoid that, the updateRect function will be slightly different and, unfortunately, you'll have to override the paint() function (while still keeping it a bit faster than other options):
def updateRect(self):
baseRect = super().boundingRect()
minSize = min(baseRect.width(), baseRect.height())
self._boundingRect = QtCore.QRectF(0, 0, minSize, minSize)
# the _boundingRect is *not* centered anymore, but a new rect is created
# as a reference for both shape intersection and painting
refRect= QtCore.QRectF(self._boundingRect)
refRect.moveCenter(baseRect.center())
# note the minus sign!
self._reference = -refRect.topLeft()
self._shape = QtGui.QPainterPath()
self._shape.addEllipse(self._boundingRect)
self._shape &= super().shape().translated(self._reference)
# ...
def paint(self, painter, option, widget):
# we are going to translate the painter to the "reference" position,
# let's save its state before that
painter.save()
painter.translate(self._reference)
super().paint(painter, option, widget)
painter.restore()
This will make the boundingRect (and resulting internal shape) position the whole item at the top-left of the item position.
The following image shows the differences between the two approaches; I've used a PNG with transparent areas to better explain the whole concept.
On the top there is the source image, in the middle the paint() override approach, and finally the shape() implementation at the bottom.
While there seems to be no difference between the two methods, as shown on the examples on the left, on the right I've highlighted the actual boundaries of each item, by showing their boundingRect (in blue), shape (in red), which will be used for mouse events, collision detection and paint clipping; the green circle shows the overall circle used for both shape and painting.
The examples in the middle show the positioning based on the original image size, while on the right you can see the absolute positioning based on the effective circle size as explained above.
Drawing a circle around the image
Unfortunately, the ItemClipsToShape flag doesn't support antialiasing for clipping: if we just draw a circle after painting the image the result will be ugly. On the left you can see that the circle is very pixellated and does not overlap perfectly on the image. On the right the correct painting.
To support that, the flag must not be set, and the paint function will be a bit different.
class CircleClipPixmapItem(QtGui.QGraphicsPixmapItem):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# we don't need this anymore:
# self.setFlag(self.ItemClipsToShape)
# always set the shapeMode to the bounding rect without any masking:
# if the image has transparent areas they will be clickable anyway
self.setShapeMode(self.BoundingRectShape)
self.updateRect()
self.pen = QtGui.QPen(QtCore.Qt.red, 2)
# ...
def setPen(self, pen):
self.pen = pen
self.update()
def paint(self, painter, option, widget):
# we are going to translate the painter to the "reference" position,
# and we are also changing the pen, let's save the state before that
painter.save()
painter.translate(.5, .5)
painter.setRenderHints(painter.Antialiasing)
# another painter save "level"
painter.save()
# apply the clipping to the painter
painter.setClipPath(self._shape)
painter.translate(self._reference)
super().paint(painter, option, widget)
painter.restore()
painter.setPen(self.pen)
# adjust the rectangle to precisely match the circle to the image
painter.drawEllipse(self._boundingRect.adjusted(.5, .5, -.5, -.5))
painter.restore()
# restore the state of the painter

Initially moved scroll bar inside QGraphics

I am trying to set the vertical and horizontal scroll bars initially moved inside a QGraphicsScene widget. The following code should move the bars and set them in the middle, but they are not moved:
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
class Diedrico(QtWidgets.QWidget):
def paintEvent(self, event):
qp = QtGui.QPainter(self)
pen = QtGui.QPen(QtGui.QColor(QtCore.Qt.black), 5)
qp.setPen(pen)
qp.drawRect(500, 500, 1000, 1000)
class UiVentana(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(UiVentana, self).__init__(parent)
self.resize(1000, 1000)
self.setFixedSize(1000, 1000)
self.scene = QtWidgets.QGraphicsScene(self)
self.view = QtWidgets.QGraphicsView(self.scene)
# This two lines should move the scroll bar
self.view.verticalScrollBar().setValue(500)
self.view.horizontalScrollBar().setValue(500)
self.diedrico = Diedrico()
self.diedrico.setFixedSize(2000, 2000)
self.scene.addWidget(self.diedrico)
self.setCentralWidget(self.view)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_R:
self.view.setTransform(QtGui.QTransform())
elif event.key() == QtCore.Qt.Key_Plus:
scale_tr = QtGui.QTransform()
scale_tr.scale(1.5, 1.5)
tr = self.view.transform() * scale_tr
self.view.setTransform(tr)
elif event.key() == QtCore.Qt.Key_Minus:
scale_tr = QtGui.QTransform()
scale_tr.scale(1.5, 1.5)
scale_inverted, invertible = scale_tr.inverted()
if invertible:
tr = self.view.transform() * scale_inverted
self.view.setTransform(tr)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
ui = UiVentana()
ui.show()
sys.exit(app.exec_())
I could move the bars when I used a scroll area such as in this question
The answer given by #S.Nick works fine, but I'd like to add some insight about why you are facing this issue and what's happening "under the hood".
First of all, in your code you try to set the values of the scroll bars before adding any object to the scene.
At that point, you just created the view and the scene. The view widget has not been shown (so it doesn't "know" its actual size yet), and the scene is empty, meaning that the sceneRect is null, as in 0 width and 0 height: in this scenario, the scroll bars have a maximum value of 0, and setting any value won't give any result.
NOTE: There is a very important aspect to keep in mind: unless
explicitly declared or requested, the sceneRect of a
QGraphicsScene is always null until a view shows it. And by
"requested" I mean that even just calling scene.sceneRect() is
enough to ensure that the scene actually and finally "knows" its
extent.
After trying to set the scroll bars (with no results), you added the widget to the scene. The problem is that a view (which is a QAbstractScrollArea descendant) only updates its scrollbars as soon as it's actually mapped on the screen.
This is a complex "path" that starts from showing the main parent window (if any), which, according to its contents resizes itself and, again, resizes its contents if they require it, eventually based on their [nested widget] size policies. Only then, the view "decides" if scrollbars are needed, and eventually sets their maximum. And, only then you can actuall set a value for those scroll bars, and that's because only then the view "asks" the scene about its sceneRect.
This also (partially) explains why the view behaves in different way than a standard scroll area: widgets have a sizeHint that is used by the QWidget that contains them inside the scroll area, and, theoretically, their size is mapped as soon as they're created. But. this depends on their size hints and policies, so you cannot guarantee the actual scroll area contents size until it's finally mapped/shown; long story short: it "works", but not perfectly - at least not until everything has finally been shown.
A test example
There are different ways to solve your problem, according to your needs and implementation.
Set the sceneRect independently, even before adding any object to the scene (but if those objects boundaries go outside the scene, you'll face some inconsistency)
Call scene.sceneRect() as explained above, after adding all objects
Set the scoll bars only after the view has been shown and resized
I've prepared an example that shows the three situations explained above. It will create a new view and update its scrollbars at different points according to the checkboxes, to show how differently they behave. Note that when setting the sceneRect I used a rectangle smaller than the widget size to better display its behavior: you can see that the visual result of "Set scene rect" and "Check scene rect" is similar, but the scroll bar positions are different.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Diedrico(QtWidgets.QWidget):
def paintEvent(self, event):
qp = QtGui.QPainter(self)
pen = QtGui.QPen(QtGui.QColor(QtCore.Qt.black), 5)
qp.setPen(pen)
qp.drawRect(500, 500, 1000, 1000)
class TestView(QtWidgets.QGraphicsView):
def __init__(self, setRect=False, checkScene=False, showEventCheck=False):
super(TestView, self).__init__()
self.setFixedSize(800, 800)
scene = QtWidgets.QGraphicsScene()
self.setScene(scene)
self.diedrico = Diedrico()
self.diedrico.setFixedSize(2000, 2000)
scene.addWidget(self.diedrico)
if setRect:
scene.setSceneRect(0, 0, 1500, 1500)
elif checkScene:
scene.sceneRect()
self.showEventCheck = showEventCheck
if not showEventCheck:
self.scroll()
def scroll(self):
self.verticalScrollBar().setValue(500)
self.horizontalScrollBar().setValue(500)
def showEvent(self, event):
super(TestView, self).showEvent(event)
if not event.spontaneous() and self.showEventCheck:
self.scroll()
class ViewTester(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
self.setRectCheck = QtWidgets.QCheckBox('Set scene rect')
layout.addWidget(self.setRectCheck)
self.checkSceneCheck = QtWidgets.QCheckBox('Check scene rect')
layout.addWidget(self.checkSceneCheck)
self.showEventCheck = QtWidgets.QCheckBox('Scroll when shown')
layout.addWidget(self.showEventCheck)
showViewButton = QtWidgets.QPushButton('Show view')
layout.addWidget(showViewButton)
showViewButton.clicked.connect(self.showView)
self.view = None
def showView(self):
if self.view:
self.view.close()
self.view.deleteLater()
self.view = TestView(
setRect = self.setRectCheck.isChecked(),
checkScene = self.checkSceneCheck.isChecked(),
showEventCheck = self.showEventCheck.isChecked()
)
self.view.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
viewTester = ViewTester()
viewTester.show()
sys.exit(app.exec_())
Finally, remember that using absolute values for scrollbars is not a good idea. If you want to "center" the view, consider using centerOn (and its item based overload), or set values based on scrollBar.maximum()/2.
You want to set the value when the widget is not yet formed, make it a moment.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Diedrico(QtWidgets.QWidget):
def paintEvent(self, event):
qp = QtGui.QPainter(self)
pen = QtGui.QPen(QtGui.QColor(QtCore.Qt.black), 5)
qp.setPen(pen)
qp.drawRect(500, 500, 1000, 1000)
class UiVentana(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(UiVentana, self).__init__(parent)
self.resize(1000, 1000)
self.setFixedSize(1000, 1000)
self.scene = QtWidgets.QGraphicsScene(self)
self.view = QtWidgets.QGraphicsView(self.scene)
# This two lines should move the scroll bar
QtCore.QTimer.singleShot(0, self.set_Value) # +++
self.diedrico = Diedrico()
self.diedrico.setFixedSize(2000, 2000)
self.scene.addWidget(self.diedrico)
self.setCentralWidget(self.view)
def set_Value(self): # +++
self.view.verticalScrollBar().setValue(500)
self.view.horizontalScrollBar().setValue(500)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_R:
self.view.setTransform(QtGui.QTransform())
elif event.key() == QtCore.Qt.Key_Plus:
scale_tr = QtGui.QTransform()
scale_tr.scale(1.5, 1.5)
tr = self.view.transform() * scale_tr
self.view.setTransform(tr)
elif event.key() == QtCore.Qt.Key_Minus:
scale_tr = QtGui.QTransform()
scale_tr.scale(1.5, 1.5)
scale_inverted, invertible = scale_tr.inverted()
if invertible:
tr = self.view.transform() * scale_inverted
self.view.setTransform(tr)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
ui = UiVentana()
ui.show()
sys.exit(app.exec_())

How to add widgets dynamically after parent is created

I'm trying to build an interface using custom widgets, and have run into the following problem.
I have a widget Rectangle which I want to use as an interactive element in my interface. To define a rectangle I just need to give it a parent, so it knows what window to draw itself in, and a position [x,y, width, height] defining its position and size. (I know that some of you will say "You should be using layouts as opposed to absolute positioning" but I am 100% sure that I need absolute positioning for this particular application).
from PySide.QtCore import *
from PySide.QtGui import *
import sys
class Rectangle(QWidget):
def __init__(self, parent, *args):
super(self.__class__,self).__init__(parent)
print parent, args
#expect args[0] is a list in the form [x,y,width,height]
self.setGeometry(*args[0])
def enterEvent(self, e):
print 'Enter'
def leaveEvent(self, e):
print 'Leave'
def paintEvent(self, e):
print 'Painted: ',self.pos
painter = QPainter(self)
painter.setPen(Qt.NoPen)
painter.setBrush(QColor(200,100,100))
painter.drawRect(0,0,self.width()-1, self.height()-1)
painter.end()
I also have a Window widget which is the canvas on which my visualization is to be drawn. In the Window's __init__() definition I create a rectangle A at 20,40.
class Window(QWidget):
def __init__(self):
super(self.__class__, self).__init__()
self.widgets = [Rectangle(self,[20,40,100,80])]
self.setMouseTracking(True)
self.setGeometry(300,300,800,600)
self.setWindowTitle('Window')
self.show()
def addWidget(self,Widget, *args):
self.widgets += [Widget(self, *args)]
self.update()
def mousePressEvent(self, e):
for widget in self.widgets:
print widget.geometry()
Since I am building a visualization, I want to create my Window and then add widgets to it afterwords, so I create an instance mWindow, which should already have rectangle A defined. I then use my window's addWidget() method to add a second rectangle at 200,200 - call it rectangle B.
if __name__ == "__main__":
app= QApplication(sys.argv)
mWindow = Window()
mWindow.addWidget(Rectangle, [200,200,200,80])
sys.exit(app.exec_())
The issue I have is that only rectangle A actually gets drawn.
I know that both rectangle A and **rectangle B are getting instantiated and both have myWindow as their parent widgets, because of the output of print parent in the constructor for Rectangle.
However, when I resize the window to force it to repaint itself, the paintEvent() method is only called on rectangle A, not rectangle B. What am I missing?
You just forgot to show the rectangle. In addWidget, add this before self.update():
self.widgets[-1].show()
The reason why you don't need show for the first rectangle object is because it is
created in the Window constructor. Then, Qt itself is making sure objects are properly
shown (which is misleading, I agree...).

Categories