Background
I would like to draw a simple shape on the screen, and I have selected PyQt as the package to use, as it seems to be the most established. I am not locked to it in any way.
Problem
It seems to be over complicated to just draw a simple shape like for example a polygon on the screen. All examples I find try to do a lot of extra things and I am not sure what is actually relevant.
Question
What is the absolute minimal way in PyQt to draw a polygon on the screen?
I use version 5 of PyQt and version 3 of Python if it makes any difference.
i am not sure, what you mean with
on the screen
you can use QPainter, to paint a lot of shapes on any subclass of QPaintDevice e.g. QWidget and all subclasses.
the minimum is to set a pen for lines and text and a brush for fills. Then create a polygon, set all points of polygon and paint in the paintEvent():
import sys, math
from PyQt5 import QtCore, QtGui, QtWidgets
class MyWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent)
self.pen = QtGui.QPen(QtGui.QColor(0,0,0)) # set lineColor
self.pen.setWidth(3) # set lineWidth
self.brush = QtGui.QBrush(QtGui.QColor(255,255,255,255)) # set fillColor
self.polygon = self.createPoly(8,150,0) # polygon with n points, radius, angle of the first point
def createPoly(self, n, r, s):
polygon = QtGui.QPolygonF()
w = 360/n # angle per step
for i in range(n): # add the points of polygon
t = w*i + s
x = r*math.cos(math.radians(t))
y = r*math.sin(math.radians(t))
polygon.append(QtCore.QPointF(self.width()/2 +x, self.height()/2 + y))
return polygon
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setPen(self.pen)
painter.setBrush(self.brush)
painter.drawPolygon(self.polygon)
app = QtWidgets.QApplication(sys.argv)
widget = MyWidget()
widget.show()
sys.exit(app.exec_())
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;
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
I'm writing a simulation GUI, in which QtPainter is draws a Pixmap instantly when the windows is opening.
I use different buttons, that are supposed to result in painting on top of the pixmap (lines and other geometric forms)
I've tried writing functions that take the QtPainter object as an argument and use button.clicked.connect() to call the functions but the drawings never appear on screen.
As I am new to PyQt I am not sure with how it works, but I guess I can only paint by calling the paintEvent() function, but if I write all geometric forms in paintEvent(), how do I make sure they only appear when the button is pressed?
To draw directly onto a widget you can override it's paintEvent. The thing to remember when doing so is that paintEvent fires every time the Widget deems it necessary to redraw itself e.g. when it has been resized or moved. This means that when implementing your own version of paintEvent you need to draw all the shapes you want drawn on your widget. Note that paintEvent is rarely called directly. If you want the widget to redraw itself, you should call update() which will schedule a paintEvent for you.
Here is a simple example where arbitrary rectangles are added to a canvas when clicking on a button. The rectangles are stored in an array in the Canvas object. In Canvas.paintEvent, we create an instance of QPainter and use this object to draw all the rectangles in the array.
from PyQt5 import QtWidgets, QtGui, QtCore
from random import randrange
class Canvas(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.rectangles = []
def add_rectangle(self, rect, color):
self.rectangles.append((rect, color))
self.update()
def paintEvent(self, event):
painter = QtGui.QPainter(self)
brush = QtGui.QBrush(QtCore.Qt.white)
painter.setBrush(brush)
painter.drawRect(event.rect())
pen = QtGui.QPen()
pen.setWidth(3)
for rect, color in self.rectangles:
pen.setColor(color)
painter.setPen(pen)
brush.setColor(color)
painter.setBrush(brush)
painter.drawRect(rect)
class MyWindow(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.canvas = Canvas(self)
self.button = QtWidgets.QPushButton('Add rectangle')
self.button.clicked.connect(self.add_rectangle)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.addWidget(self.canvas)
self.layout.addWidget(self.button)
self.resize(500,500)
def add_rectangle(self):
w = self.canvas.width()
h = self.canvas.height()
x0, y0 = randrange(w), randrange(h)
x1, y1 = randrange(w), randrange(h)
shape = QtCore.QRect(min(x0,x1), min(y0,y1), abs(x0-x1), abs(y0-y1))
color = QtGui.QColor.fromRgb(*(randrange(256) for i in range(3)), 180)
self.canvas.add_rectangle(shape, color)
if __name__ == "__main__":
app = QtWidgets.QApplication([])
window = MyWindow()
window.show()
app.exec()
In Java, you can draw 3-d rectangles using (see https://way2java.com/awt-graphics/4891/):
void fill3DRect(int x, int y, int width, int height, boolean raised)
Here, the last parameter "raised" is used to lower/elevate the 3d rectangle with respect to the drawing surface.
How can I achieve this effect in PyQt?
It depends on what level of paint you want to use:
There are 2 options:
Using QPainter:
This effect can be achieved by drawing 2 displaced rectangles where the color of the background rectangle is darker than the color of the front:
from PyQt5 import QtCore, QtGui, QtWidgets
def draw3DRect(painter, rect, color, raised=False, offset=QtCore.QPoint(4, 4)):
if raised:
painter.fillRect(rect.translated(offset), color.darker())
painter.fillRect(rect, color)
class Widget(QtWidgets.QWidget):
def paintEvent(self, event):
painter = QtGui.QPainter(self)
r = QtCore.QRect(
self.width() / 4,
self.height() / 4,
self.width() / 2,
self.height() / 2,
)
draw3DRect(painter, r, QtGui.QColor("green"), raised=True)
def sizeHint(self):
return QtCore.QSize(320, 240)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
Using QGraphicsDropShadowEffect:
In this case the QWidget and QGraphicsItem support this effect:
from PyQt5 import QtCore, QtGui, QtWidgets
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QWidget()
lay = QtWidgets.QHBoxLayout(w)
scene = QtWidgets.QGraphicsScene()
view = QtWidgets.QGraphicsView(scene)
rect_item = QtWidgets.QGraphicsRectItem(QtCore.QRectF(0, 0, 200, 100))
rect_item.setBrush(QtGui.QColor("green"))
effect_item = QtWidgets.QGraphicsDropShadowEffect(
offset=QtCore.QPointF(3, 3), blurRadius=5
)
rect_item.setGraphicsEffect(effect_item)
scene.addItem(rect_item)
rect_widget = QtWidgets.QWidget()
rect_widget.setFixedSize(320, 240)
rect_widget.setStyleSheet("background-color:green;")
effect_widget = QtWidgets.QGraphicsDropShadowEffect(
offset=QtCore.QPointF(3, 3), blurRadius=5
)
rect_widget.setGraphicsEffect(effect_widget)
lay.addWidget(view)
lay.addWidget(rect_widget)
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
To my knowledge there is no built in PyQt 3D paint widget/function as you can only paint 2D polygons. But we can create a custom class to emulate 3D painting. From your Java linked reference:
Java supports 3D rectangles but the effect of third dimension is not very visible. As the elevation is less, the effect is negligible. Java designers gave the effect of 3D by drawing lighter and darker lines along the rectangle border.
We can emulate the effect of Java's 3D paint function:
void fill3DRect(int x, int y, int width, int height, boolean raised)
This method draws a solid 3D rectangle with the above specified parameters. The last boolean parameter true indicates elevation above the drawing surface and false indicates etching into the surface.
To obtain a 3D effect in Python we can essentially do the same thing by having two shades of a color then darkening and lighting some sides.
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
class Rectangle3D(QtWidgets.QWidget):
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent)
# Elevated 3D rectangle color settings
self.elevated_border_color = QtGui.QColor(111,211,111)
self.elevated_fill_color = QtGui.QColor(0,255,0)
self.elevated_pen_width = 2.5
# Lowered 3D rectangle color settings
self.lowered_border_color = QtGui.QColor(0,235,0)
self.lowered_fill_color = QtGui.QColor(0,178,0)
self.lowered_pen_width = 2.5
def draw3DRectangle(self, x, y, w, h, raised=True):
# Specify the border/fill colors depending on raised or lowered
if raised:
# Line color (border)
self.pen = QtGui.QPen(self.elevated_border_color, self.elevated_pen_width)
# Fill color
self.fill = QtGui.QBrush(self.elevated_fill_color)
else:
# Line color (border)
self.pen = QtGui.QPen(self.lowered_border_color, self.lowered_pen_width)
# Fill color
self.fill = QtGui.QBrush(self.lowered_fill_color)
painter = QtGui.QPainter(self)
# Draw border color of rectangle
painter.setPen(self.pen)
painter.setBrush(self.fill)
painter.drawRect(x, y, w, h)
# Cover up the top and left sides with filled color using lines
if raised:
painter.setPen(QtGui.QPen(self.elevated_fill_color, self.elevated_pen_width))
else:
painter.setPen(QtGui.QPen(self.lowered_fill_color, self.lowered_pen_width))
painter.drawLine(x, y, x + w, y)
painter.drawLine(x, y, x, y + h)
def paintEvent(self, event):
self.draw3DRectangle(50,50,300,150,True)
self.draw3DRectangle(50,250,300,150,False)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
widget = Rectangle3D()
widget.show()
sys.exit(app.exec_())
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_()