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
Related
I am trying to make a custom animated button on PyQt. I found a website which has custom buttons: Buttons website
I already created a topic for making a 3rd button: Stackoverflow for 3rd button
#musicamante helped for the 3rd button, thank you very much again. Now I'm trying to make the 19th button.
My code for 19th button:
import sys, os, time
from math import *
from PySide6 import QtCore, QtWidgets, QtGui
from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtGui import *
class Button19(QPushButton):
Radius = 10
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.backgroundColors = (QtGui.QColor(QtCore.Qt.lightGray),QtGui.QColor(QtCore.Qt.white))
self.foregroundColors = (QtGui.QColor(QtCore.Qt.black), QtGui.QColor(QtCore.Qt.lightGray))
font = self.font()
font.setBold(True)
self.setFont(font)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.hoverAnimation = QtCore.QVariantAnimation(self)
self.hoverAnimation.setStartValue(0.)
self.hoverAnimation.setEndValue(1.)
self.hoverAnimation.setEasingCurve(QtCore.QEasingCurve.OutCubic)
self.hoverAnimation.setDuration(400)
self.hoverAnimation.valueChanged.connect(self.update)
self.setText("Button")
_m_isHover = False
def enterEvent(self, event):
super().enterEvent(event)
self._m_isHover = True
self.hoverAnimation.setDirection(self.hoverAnimation.Forward)
self.hoverAnimation.start()
def leaveEvent(self, event):
super().leaveEvent(event)
self._m_isHover = False
self.hoverAnimation.setDirection(self.hoverAnimation.Backward)
self.hoverAnimation.start()
def isHover(self):
return self._m_isHover
def paintEvent(self, event):
aniValue = self.hoverAnimation.currentValue()
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
path, path2 = QPainterPath(), QPainterPath()
painter.setBrush(QBrush(self.backgroundColors[0]))
painter.setPen(Qt.NoPen)
rect = QRectF(0, 0, self.width(), self.height())
padding = 10
rect = rect.adjusted(-padding * aniValue, -padding * aniValue, padding * aniValue, padding * aniValue)
path.addRoundedRect(rect.adjusted(padding / 2, padding, -padding / 2, -padding), self.Radius, self.Radius)
painter.drawPath(path)
painter.setBrush(QBrush(self.foregroundColors[0]))
painter.setClipPath(path)
radiusEffectSize = 75
path2.addEllipse(self.rect().center(), radiusEffectSize * aniValue, radiusEffectSize * aniValue)
painter.drawPath(path2)
if self.isHover() or self.hoverAnimation.currentValue() > 0.1: # when leaveEvent triggered, still background color black. So must wait to change textcolor (ofcourse there is probably a better way)
painter.setPen(self.foregroundColors[1])
else:
painter.setPen(self.foregroundColors[0])
painter.drawText(self.rect(), Qt.AlignCenter, self.text())
if __name__ == "__main__":
app = QApplication(sys.argv)
wind = QMainWindow()
wind.setStyleSheet("QMainWindow{background-color:rgb(247,247,250)}")
wind.resize(150, 80)
wid = QWidget()
lay = QHBoxLayout(wid)
lay.setAlignment(Qt.AlignCenter)
mycustombutton = Button19()
lay.addWidget(mycustombutton)
wind.setCentralWidget(wid)
wind.show()
sys.exit(app.exec())
Still feels different, not the same. I need help, thanks!
The main issue in your code is that the padding computation is wrong.
You are increasing the size of the padding from the current rectangle and then decrease it by half the padding size, which doesn't make a lot of sense.
You should instead consider the default padding minus the extent based on the animation value, then adjust (reduce) the rectangle based to it:
padding = 10 * (1 - aniValue)
path.addRoundedRect(
rect.adjusted(padding, padding, -padding, -padding),
self.Radius, self.Radius
)
That will not be sufficient, though: the radius has to consider the actual size of the widget, but that can be misleading: if you take the smaller dimension (between width and height) the ellipse could be smaller than the rectangle, while in the opposite case it would grow up too early, making the animation quite odd. The actual radius should actually be computed using the hypotenuse of the right triangle of the widget width and height (a "perfect" implementation should also consider the radius of the rounded rectangle, but that would be quite too much):
# using hypot() from the math module
radius = hypot(self.width(), self.height()) / 2
path2.addEllipse(self.rect().center(), radius, radius)
Not enough, though: if you closely look at the original animation, you'll see that the "leave" event will not be the same: there is no circle, the "black" rounded rectangle just fades out. We need to take care of that too:
radius = min(self.width(), self.height())
if (self.hoverAnimation.state()
and self.hoverAnimation.direction() == self.hoverAnimation.Forward):
radius *= aniValue
# always full opacity on "fade in"
opacity = 1.
else:
# "fade out"
opacity = aniValue
path2.addEllipse(self.rect().center(), radius, radius)
painter.save()
painter.setOpacity(opacity)
painter.drawPath(path2)
painter.restore()
Nearly there. But the text drawing still has issues. First of all, the "base" should always be painted, and the "hover" should be painted over with the opacity value specified above (unless you want an alpha value). Then, we should always remember that buttons could also use "mnemonics" (keyboard shortcuts that are always highlighted with an underlined character, specified with a preceding & in Qt).
For optimization reasons, it's better to "replicate" similar functions instead of using local variables. It might not be wonderful for reading purposes, but painting functions should be always try to be as fast as possible.
So, here's the final result:
def paintEvent(self, event):
aniValue = self.hoverAnimation.currentValue()
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setBrush(QBrush(self.backgroundColors[0]))
painter.setPen(Qt.NoPen)
rect = self.rect()
path = QPainterPath()
padding = 10 * (1 - aniValue)
path.addRoundedRect(
QRectF(rect).adjusted(padding, padding, -padding, -padding),
self.Radius, self.Radius
)
painter.setClipPath(path)
painter.drawPath(path)
painter.setBrush(QBrush(self.foregroundColors[0]))
if aniValue < 1:
# only draw the default text when the animation isn't finished yet
painter.setPen(self.foregroundColors[0])
painter.drawText(rect, Qt.AlignCenter|Qt.TextShowMnemonic, self.text())
if not aniValue:
# no hover, just ignore the rest
return
hoverPath = QPainterPath()
radius = hypot(self.width(), self.height()) / 2
if (aniValue and self.hoverAnimation.state()
and self.hoverAnimation.direction() == self.hoverAnimation.Forward):
hoverPath.addEllipse(rect.center(),
radius * aniValue, radius * aniValue)
painter.drawPath(hoverPath)
else:
hoverPath.addEllipse(rect.center(), radius, radius)
painter.save()
painter.setOpacity(aniValue)
painter.drawPath(hoverPath)
painter.restore()
painter.setPen(self.foregroundColors[1])
painter.drawText(rect, Qt.AlignCenter|Qt.TextShowMnemonic, self.text())
Some further notes:
isHover() is quite pointless unless you need it for something else but painting: except from extreme performance requirements (for which value caching would make sense), underMouse() is usually sufficient; for this case, it is also a bit irrelevant, as we can be quite sure that the hover state only happens when the animation value is 1 or the animation is active (animation.state()) and its direction is Forward;
the "smoothness" of the animation completely depends on its easingCurve(), so please do experiment with all available curves to find what best suits your needs;
when working with plain shapes and no borders ("pens"), Qt normally works fine, as it happens with the code above, but be aware that painting with pixel-based devices (as QWidgets) could create artifacts while using anti-aliasing; in that case you have to consider the "pen width" and translate the drawing by half its size to obtain a "perfect" shape on the screen;
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)
...
I have a QGraphicsPathItem in Qt (using the PySide bindings in Python) where there is a big rectangle and a smaller rectangle inside. Because of the default filling rule (Qt.OddEvenFill) the inner rectangle is transparent. This effectively draws a shape with a hole.
Now I want to listen to mouse events like enter, leave, click, ... My simple approach of implementing hoverEnterEvent, .. of QGraphicsItem does not create mouse events when moving over the hole because the hole is still part of the item even if it is not filled.
I want to have a QGraphicsItem derivative that displays a custom shape whose outline is defined by a QPainterPath or one or several polygons and that can have holes and when the mouse enters a hole this is regarded as outside of the shape.
Example shape with a hole (when the mouse is in the inner rectangle it should be regarded as outside of the shape and mouse leave events should be fired):
However the solution should also work for arbitrary shapes with holes.
Example code in PySide/Python 3.3
from PySide import QtCore, QtGui
class MyPathItem(QtGui.QGraphicsPathItem):
def __init__(self):
super().__init__()
self.setAcceptHoverEvents(True)
def hoverEnterEvent(self, event):
print('inside')
def hoverLeaveEvent(self, event):
print('outside')
app = QtGui.QApplication([])
scene = QtGui.QGraphicsScene()
path = QtGui.QPainterPath()
path.addRect(0, 0, 100, 100)
path.addRect(25, 25, 50, 50)
item = MyPathItem()
item.setPath(path)
item.setBrush(QtGui.QBrush(QtCore.Qt.blue))
scene.addItem(item)
view = QtGui.QGraphicsView(scene)
view.resize(200, 200)
view.show()
app.exec_()
It seems that method shape from QGraphicsItem by default returns the bounding rectangle. Its returned path is used to determine if a position is inside or outside of a complex shape. However in case of a QGraphicsPathItem we already have a path and returning this instead of the bounding rectangle could solve the problem. And to my surprise it does.
Just add these two lines to the QGraphicsPathItem derivative from the question.
def shape(self):
return self.path()
You can extend event handler to check if a given position is the specific (inner) path. Different approach - draw using move/lineTo (maybe ambiguous). For example moveTo/lineTo:
from PySide import QtCore, QtGui
class MyPathItem(QtGui.QGraphicsPathItem):
def __init__(self):
QtGui.QGraphicsPathItem.__init__(self)
self.setAcceptHoverEvents(True)
def hoverEnterEvent(self, event):
print('inside')
def hoverLeaveEvent(self, event):
print('outside')
app = QtGui.QApplication([])
scene = QtGui.QGraphicsScene()
path = QtGui.QPainterPath()
path.moveTo(0, 0)
path.lineTo(100, 0)
path.moveTo(0, 0)
path.lineTo(0, 100)
path.moveTo(100, 100)
path.lineTo(0, 100)
path.moveTo(100, 0)
path.lineTo(100, 100)
item = MyPathItem()
pen = QtGui.QPen()
pen.setWidth(25)
pen.setColor(QtCore.Qt.blue)
item.setPen(pen)
item.setPath(path)
scene.addItem(item)
view = QtGui.QGraphicsView(scene)
view.resize(200, 200)
view.show()
app.exec_()
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()
I don't see that there's any way to define a stylesheet for a specific QTreeWidgetItem, so I'm resolved to attempt to figure my solution out using either SetBackground or SetForeground. The effect I'm trying to achieve is border around a single widgetitem for a treeview, but I can't figure out how to draw one manually with a QBrush, if that's even how to go about it. Any ideas? Thanks in advance.
You can't draw a border with a QTreeWidgetItem alone, you have to use a delegate.
And you can store a border style in each item for which you want to have a border under a custom "role" to be able to retrieve it and use it inside the delegate.
Here is a complete working and commented example:
import sys
from PyQt4 import QtGui, QtCore
class BorderItemDelegate(QtGui.QStyledItemDelegate):
def __init__(self, parent, borderRole):
super(BorderItemDelegate, self).__init__(parent)
self.borderRole = borderRole
def sizeHint(self, option, index):
size = super(BorderItemDelegate, self).sizeHint(option, index)
pen = index.data(self.borderRole).toPyObject()
if pen is not None:
# Make some room for the border
# When width is 0, it is a cosmetic pen which
# will be 1 pixel anyways, so set it to 1
width = max(pen.width(), 1)
size = size + QtCore.QSize(2 * width, 2 * width)
return size
def paint(self, painter, option, index):
pen = index.data(self.borderRole).toPyObject()
# copy the rect for later...
rect = QtCore.QRect(option.rect)
if pen is not None:
width = max(pen.width(), 1)
# ...and remove the extra room we added in sizeHint...
option.rect.adjust(width, width, -width, -width)
# ...before painting with the base class method...
super(BorderItemDelegate, self).paint(painter, option, index)
# ...then paint the borders
if pen is not None:
painter.save()
# The pen is drawn centered on the rectangle lines
# with pen.width()/2 width on each side of these lines.
# So, rather than shifting the drawing of pen.width()/2
# we double the pen width and clip the part that would
# go outside the rect.
painter.setClipRect(rect, QtCore.Qt.ReplaceClip);
pen.setWidth(2 * width)
painter.setPen(pen)
painter.drawRect(rect)
painter.restore()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
mainWindow = QtGui.QMainWindow()
mainWindow.resize(300,300);
# Define the Qt.ItemDataRole we will be using
MyBorderRole = QtCore.Qt.UserRole + 1
# Create and populate the view
treeWidget = QtGui.QTreeWidget(mainWindow)
for i in range(3):
item = QtGui.QTreeWidgetItem(["Item %d"%(i)])
treeWidget.addTopLevelItem(item)
treeWidget.expandItem(item);
for j in range(10):
subItem = QtGui.QTreeWidgetItem(["SubItem %d %d"%(i,j)])
pen = QtGui.QPen(QtGui.QColor.fromHsv(j*25, 255, 255))
pen.setWidth(j)
# Store the border pen in the item as the role we defined
subItem.setData(0, MyBorderRole, pen)
item.addChild(subItem)
# Pass the role where we stored the border pen to the delegate constructor
delegate = BorderItemDelegate(treeWidget, MyBorderRole)
treeWidget.setItemDelegate(delegate)
mainWindow.setCentralWidget(treeWidget)
mainWindow.show()
sys.exit(app.exec_())