Related
I am trying to override the paintEvent() of QMenu to make it have rounded corners.
The context menu should look something like this.
Here is the code I have tried But nothing appears:
from PyQt5 import QtWidgets, QtGui, QtCore
import sys
class Example(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Context menu')
self.show()
def contextMenuEvent(self, event):
cmenu = AddContextMenu(self)
newAct = cmenu.addAction("New")
openAct = cmenu.addAction("Open")
quitAct = cmenu.addAction("Quit")
action = cmenu.exec_(self.mapToGlobal(event.pos()))
class AddContextMenu(QtWidgets.QMenu):
def __init__(self, *args, **kwargs):
super(AddContextMenu, self).__init__()
self.painter = QtGui.QPainter(self)
self.setMinimumSize(150, 200)
self.pen = QtGui.QPen(QtCore.Qt.red)
#self.setStyleSheet('color:white; background:gray; border-radius:4px; border:2px solid white;')
def paintEvent(self, event) -> None:
self.pen.setWidth(2)
self.painter.setPen(self.pen)
self.painter.setBrush(QtGui.QBrush(QtCore.Qt.blue))
self.painter.drawRoundedRect(10, 10, 100, 100, 4.0, 4.0)
self.update()
#self.repaint()
#super(AddContextMenu, self).paintEvent(event)
def main():
app = QtWidgets.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Note: setting a style sheet doesn't work for me:
this is what I get when using the style sheet It isn't completely rounded.
This is the paintEvent after #musicamante suggestion(This is just for him/her to check)
def paintEvent(self, event) -> None:
painter = QtGui.QPainter(self)
#self.pen.setColor(QtCore.Qt.white)
#painter.setFont(QtGui.QFont("times", 22))
#painter.setPen(self.pen)
#painter.drawText(QtCore.QPointF(0, 0), 'Hello')
self.pen.setColor(QtCore.Qt.red)
painter.setPen(self.pen)
painter.setBrush(QtCore.Qt.gray)
painter.drawRoundedRect(self.rect(), 20.0, 20.0)
and in the init()
self.pen = QtGui.QPen(QtCore.Qt.red)
self.pen.setWidth(2)
I cannot comment on the paintEvent functionality, but it is possible to implement rounded corners using style-sheets. Some qmenu attributes have to be modified in order to disable the default rectangle in the background, which gave you the unwanted result.
Here is a modified version of your Example using style-sheets + custom flags (no frame + transparent background):
from PyQt5 import QtWidgets, QtCore
import sys
class Example(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Context menu')
self.show()
def contextMenuEvent(self, event):
cmenu = QtWidgets.QMenu()
# disable default frame and background
cmenu.setWindowFlags(QtCore.Qt.FramelessWindowHint)
cmenu.setAttribute(QtCore.Qt.WA_TranslucentBackground)
# set stylesheet, add some padding to avoid overlap of selection with rounded corner
cmenu.setStyleSheet("""
QMenu{
background-color: rgb(255, 255, 255);
border-radius: 20px;
}
QMenu::item {
background-color: transparent;
padding:3px 20px;
margin:5px 10px;
}
QMenu::item:selected { background-color: gray; }
""")
newAct = cmenu.addAction("New")
openAct = cmenu.addAction("Open")
quitAct = cmenu.addAction("Quit")
action = cmenu.exec_(self.mapToGlobal(event.pos()))
def main():
app = QtWidgets.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Setting the border radius in the stylesheet for a top level widget (a widget that has its own "window") is not enough.
While the solution proposed by Christian Karcher is fine, two important considerations are required:
The system must support compositing; while this is true for most modern OSes, at least on Linux there is the possibility that even an up-to-date system does not support it by choice (I disabled on my computer); if that's the case, setting the WA_TranslucentBackground attribute will not work.
The FramelessWindowHint should not be set on Linux, as it may lead to problems with the window manager, so it should be set only after ensuring that the OS requires it (Windows).
In light of that, using setMask() is the correct fix whenever compositing is not supported, and this has to happen within the resizeEvent(). Do note that masking is bitmap based, and antialiasing is not supported, so rounded borders are sometimes a bit ugly depending on the border radius.
Also, since you want custom colors, using stylesheets is mandatory, as custom painting of a QMenu is really hard to achieve.
class AddContextMenu(QtWidgets.QMenu):
def __init__(self, *args, **kwargs):
super(AddContextMenu, self).__init__()
self.setMinimumSize(150, 200)
self.radius = 4
self.setStyleSheet('''
QMenu {{
background: blue;
border: 2px solid red;
border-radius: {radius}px;
}}
QMenu::item {{
color: white;
}}
QMenu::item:selected {{
color: red;
}}
'''.format(radius=self.radius))
def resizeEvent(self, event):
path = QtGui.QPainterPath()
# the rectangle must be translated and adjusted by 1 pixel in order to
# correctly map the rounded shape
rect = QtCore.QRectF(self.rect()).adjusted(.5, .5, -1.5, -1.5)
path.addRoundedRect(rect, self.radius, self.radius)
# QRegion is bitmap based, so the returned QPolygonF (which uses float
# values must be transformed to an integer based QPolygon
region = QtGui.QRegion(path.toFillPolygon(QtGui.QTransform()).toPolygon())
self.setMask(region)
Some side notes about your paintEvent implementation, not necessary in this specific case for the above reason, but still important (some points are related to portions of code that have been commented, but the fact that you tried them makes worth mentioning those aspects):
The QPainter used for a widget must never be instanciated outside a paintEvent(): creating the instance in the __init__ as you did is a serious error and might even lead to crash. The painter can only be created when the paintEvent is received, and shall never be reused. This clearly makes useless to set it as an instance attribute (self.painter), since there's no actual reason to access it after the paint event.
If the pen width is always the same, then just set it in the constructor (self.pen = QtGui.QPen(QtCore.Qt.red, 2)), continuously setting it in the paintEvent is useless.
QPen and QBrush can directly accept Qt global colors, so there's no need to create a QBrush instance as the painter will automatically (internally and fastly) set it: self.painter.setBrush(QtCore.Qt.blue).
self.update() should never be called within a paintEvent (and not even self.repaint() should). The result in undefined and possibly dangerous.
If you do some manual painting with a QPainter and then call the super paintEvent, the result is most likely that everything painted before will be hidden; as a general rule, the base implementation should be called first, then any other custom painting should happen after (in this case it obviously won't work, as you'll be painting a filled rounded rect, making the menu items invisible).
I have implemented round corners menu using QListWidget and QWidget. You can download the code in https://github.com/zhiyiYo/PyQt-Fluent-Widgets/blob/master/examples/menu/demo.py.
I'm learning Python and thought to make a simple platform game with PyQ5t.
Currently I'm in trouble when I want to make a pixel level collision detection between shapes in my game.
I have set the QPixMap to transparent and also tried to use QGraphicsPixmapItem.HeuristicMaskShape shape mode but collision detection does not work.
If I make shape (Mouse in this case) backgroun for example gray and remove the shape mode then there happens rectangular collision detection.
What I'm missing here? I have spent few hours digging around the Internet but no solution yet...
Here is my code to show the problem, please use arrow keys to move the the red "Mouse" around :)
I hope to see collision detected text when the first pixel in the red circle touches the brown platform.
import sys
from PyQt5.QtGui import QPen, QBrush
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtWidgets import QApplication, QWidget, QGraphicsView, QGraphicsScene, QLabel, QGraphicsPixmapItem, QFrame
class Mouse(QGraphicsPixmapItem):
def __init__(self, parent):
super().__init__()
self.canvas = QtGui.QPixmap(40,40)
self.canvas.fill(Qt.transparent)
self.setPixmap(self.canvas)
self.x = 100
self.y = 100
self.setPos(self.x, self.y)
self.setFlag(QGraphicsPixmapItem.ItemIsMovable)
self.setFlag(QGraphicsPixmapItem.ItemIsFocusable)
self.setShapeMode(QGraphicsPixmapItem.HeuristicMaskShape)
self.setFocus()
parent.addItem(self)
def paint(self, painter, option, widget=None):
super().paint(painter, option, widget)
pen = QPen(Qt.black, 4, Qt.SolidLine)
brush = QBrush(Qt.red, Qt.SolidPattern)
painter.save()
painter.setPen(pen)
painter.setBrush(brush)
painter.drawEllipse(QPoint(20,20),16,16)
painter.restore()
def keyPressEvent(self, e):
if e.key() == Qt.Key_Right:
self.x += 5
if e.key() == Qt.Key_Left:
self.x -= 5
if e.key() == Qt.Key_Up:
self.y -= 5
if e.key() == Qt.Key_Down:
self.y += 5
self.setPos(self.x, self.y)
collides_with_items = self.collidingItems(mode=Qt.IntersectsItemShape)
if collides_with_items:
print("Collision detected!")
for item in collides_with_items:
print(item)
class Platform(QFrame):
PLATFORM_STYLE = "QFrame { color: rgb(153, 0, 0); \
background: rgba(0,0,0,0%); }"
def __init__(self, parent, x, y, width, height):
super().__init__()
self.setGeometry(QtCore.QRect(x, y, width, height))
self.setFrameShadow(QFrame.Plain)
self.setLineWidth(10)
self.setFrameShape(QFrame.HLine)
self.setStyleSheet(Platform.PLATFORM_STYLE)
parent.addWidget(self)
class GameScreen(QGraphicsScene):
def __init__(self):
super().__init__()
# Draw background
background = QLabel()
background.setEnabled(True)
background.setScaledContents(True)
background.setGeometry(0, 0, 1280, 720)
background.setPixmap(QtGui.QPixmap("StartScreen.png"))
background.setText("")
background.setTextFormat(QtCore.Qt.RichText)
self.addWidget(background)
self.line_5 = Platform(self, 0, 80, 431, 16)
self.mouse = Mouse(self)
class Game(QGraphicsView):
def __init__(self):
super().__init__()
self.setWindowTitle("Running Mouse")
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.gamescreen = GameScreen()
self.setScene(self.gamescreen)
self.show()
if __name__ == '__main__':
app = QApplication([])
game = Game()
sys.exit(app.exec_())
You're providing an empty pixmap (self.canvas), so no matter what shape mode you select, it will always have a null shape, unless you use BoundingRectShape, which uses the pixmap bounding rect.
The fact that you are manually painting a circle really doesn't matter, as painting is not considered for collision detection (nor it should).
You can override the shape() method to provide your own shape (using a QPainterPath):
def shape(self):
path = QtGui.QPainterPath()
path.addEllipse(12, 12, 16, 16)
return path
But, since you're only drawing an ellipse, just use QGraphicsEllipseItem instead.
Some unrequested suggestions:
I'd avoid to use the name "parent" if you're intending the scene (the parent item of a QGraphicsItem could only be another QGraphicsItem), and an item usually shouldn't "add itself" to a scene;
don't overwrite self.x and self.y, as they are QGraphicsItem existing properties;
if you have multiple items in a scene and want to control just one using the keyboard, it's usually better to overwrite the key event methods of the scene or the view (especially if you're using arrow keys: even if the scrollbars are hidden, they can still intercept those movements);
if you still want to use the keyboard events on the item, ensure that the item keeps the keyboard focus: using setFocus is not enough, as it will lose focus as soon as the user clicks elsewhere; a possible solution is to use QGraphicsItem.grabKeyboard() (which works only as soon as the item is added to a scene);
if you set a stylesheet on a QFrame, it's very likely that it will at least partially ignore any frame option set (shape, shadow, etc), as they are style/platform dependant; it's usually better to not mix stylesheets with settings of Qt widgets that are related to visualization, so in your case you'll probably only need to add a simple QFrame with the correct stylesheet parameters set;
while technically it's not a problem, it's usually preferable to be consistent with the import "styles", especially when using complex modules like Qt is: you either import the single classes (from PyQt5.QtWidgets import QApplication, ...) or the submodules (from PyQt5 import QtCore, ...), otherwise it will make the code just confusing;
Why is my image corrupt when displayed using simple QPixMap and draw commands. Every once in awhile it will display correctly.
self._pixmap = QtGui.QPixmap(128,128)
painter = QtGui.QPainter(self._pixmap)
brush = QtGui.QBrush(QtCore.Qt.SolidPattern)
brush.setColor(QtGui.QColor(240, 20, 20, 255))
painter.setPen(QtGui.QPen(brush, 1, QtCore.Qt.SolidLine,QtCore.Qt.SquareCap))
painter.drawLine(0, 0, self._pixmap.width(), self._pixmap.height())
painter.drawLine(self._pixmap.width(), 0, 0, self._pixmap.height())
painter.end()
You should call fill before painting using QPainter, look at reference here about QPixmap constructor
This will create a PySide.QtGui.QPixmap with uninitialized data. Call PySide.QtGui.QPixmap.fill() to fill the pixmap with an appropriate color before drawing onto it with PySide.QtGui.QPainter .
Is there a way to get 2 different fill colors for a single QGraphicsPathItem object?
For example:
# Try to get a white text onto a grey rectangle
itemPath = QtGui.QPainterPath()
itemPath.setFillRule(QtCore.Qt.WindingFill)
self.setBrush( QtGui.QColor(100, 100, 100) )
itemPath.addRect(-10, -60, 150, 70)
itemFont = QtGui.QFont()
itemFont.setPointSize(50)
self.setBrush( QtGui.QColor(255, 255, 255) )
itemPath.addText(0, 0, itemFont, txt)
Right now it's just using the last brush color for the rectangle and text. I'd like to color them differently though, but still as the same QGraphicsPathItem. Or better yet, a way to give the text a background so it's easier to select.
Update
Here's an example. I want a white text with a red background, but I only get the last used brush color.
from PySide import QtGui, QtCore
class TextItem(QtGui.QGraphicsPathItem):
def __init__(self):
super(TextItem, self).__init__()
itemPath = QtGui.QPainterPath()
itemPath.setFillRule(QtCore.Qt.WindingFill)
# Create rectangle with red color
self.setBrush( QtGui.QColor(255, 0, 0) )
itemPath.addRect(-10, -50, 130, 60)
self.setPath(itemPath)
# Create text with white color
itemFont = QtGui.QFont()
itemFont.setPointSize(40)
self.setBrush( QtGui.QColor(255, 255, 255) )
itemPath.addText(0, 0, itemFont, 'Test!')
self.setPath(itemPath)
self.moveBy(100, 100)
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(500, 500)
self.view = QtGui.QGraphicsView(self)
self.view.setScene( QtGui.QGraphicsScene(self) )
self.view.setSceneRect( 0, 0, 500, 500 )
self.view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
mainLayout = QtGui.QVBoxLayout()
mainLayout.addWidget(self.view)
self.setLayout(mainLayout)
newItem = TextItem()
self.view.scene().addItem( newItem )
def run(self):
self.show()
win = Window()
win.show()
The problem is that your first self.setPath and self.setBrush calls are just over-ridden by the second ones. These properties of the QGraphicsPathItem are not used until you get to the paint event. So the first settings do nothing and the values left in these properties after the initialisation are the ones used to paint the item.
It's not too difficult to do what you want but you have quite a lot of options, and you need to choose based on other aspects of your design.
The simplest option would be to just create a QGraphicsRectItem and then give it a QGraphicsSimpleTextItem child. This is particularly simple in that you wouldn't even need to create a subclass of your own: simply create the instances, set their Paths and Brushes and use them directly. I strongly recommend this approach if it does what you neeed.
rectItem = QtGui.QGraphicsRectItem(0, 0, 130, 60)
rectItem.setBrush(QtGui.QColor(255, 0, 0))
textItem = QtGui.QGraphicsSimpleTextItem("Test!",rectItem)
itemFont = QtGui.QFont()
itemFont.setPointSize(40)
textItem.setBrush( QtGui.QColor(200, 200, 200) )
textItem.setFont(itemFont)
textItem.setPos(10,0)
self.view.scene().addItem( rectItem )
rectItem.moveBy(100,100)
Another simple option would be just create the two items and then add them into a QGraphicsItemGroup. This makes sense if you might want to separate the items or work with them separately.
rectItem = QtGui.QGraphicsRectItem(0, 0, 130, 60)
rectItem.setBrush(QtGui.QColor(255, 0, 0))
self.view.scene().addItem( rectItem )
textItem = QtGui.QGraphicsSimpleTextItem("Test!")
itemFont = QtGui.QFont()
itemFont.setPointSize(40)
textItem.setBrush( QtGui.QColor(200, 200, 200) )
textItem.setFont(itemFont)
textItem.setPos(10,0)
self.view.scene().addItem( textItem )
group = QtGui.QGraphicsItemGroup()
group.addToGroup(rectItem)
group.addToGroup(textItem)
group.moveBy(100,100)
self.view.scene().addItem( group )
If you need to create a class of your own for other reasons then you have quite a few options. The most general will be to subclass QGraphicsItem. You can create a MyTextItem with, for example, two Paths and two Brushes, and use them both when you paint the Item. You will have to implement boundingRect and paint but in this case both will be quite easy. This is probably a good way to go if you foresee adding lots of additional functionality to the item.
Other options would be to subclass QRectItem and add a text path to it. Again you will need to re-implement paint, but that might be all that was needed. Or you could subclass QSimpleTextItem and add a rect to it.
I'm trying to add rounded corners to a QDialog. I'm defining my own paintEvent method to create rounded corners. It's working, but it's adding rounded borders to everything. Even the cursor is getting a border. Is there any way to disable this behavior?
Example code:
from PySide import QtCore, QtGui
class RenameDialog(QtGui.QDialog):
def __init__(self, parent=None, **kwargs):
super(RenameDialog, self).__init__(
parent=parent, f=QtCore.Qt.CustomizeWindowHint)
self.fieldA = QtGui.QLineEdit(self)
self.fieldB = QtGui.QLineEdit(self)
self.setLayout(QtGui.QHBoxLayout())
self.layout().addWidget(self.fieldA)
self.layout().addWidget(self.fieldB)
# Set background transparent. Only items drawn in paintEvent
# will be visible.
palette = QtGui.QPalette()
palette.setColor(QtGui.QPalette.Base, QtCore.Qt.transparent)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
self.setPalette(palette)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
fillColor = QtGui.QColor(75, 75, 75, 255)
lineColor = QtCore.Qt.gray
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(QtGui.QPen(QtGui.QBrush(lineColor), 2.0))
painter.setBrush(QtGui.QBrush(fillColor))
painter.drawRoundedRect(event.rect(), 15, 15)
I'm trying to do this with a paintEvent because:
QDialog stylesheets cannot use border-radius. Curved borders do show up, but corners are still visible.
QDialogs.setMask() works, but there is no way (that I know of) to anti-alias the mask.
Here is what that looks like:
Paint events are sent to a window/widget with the precise rectangle that needs updating not the whole bounding rectangle of the widget. When you call event.rect() it returns the rectangle that needs updating (As far as I know)
Try changing this line
painter.drawRoundedRect(event.rect(), 15, 15)
To this
painter.drawRoundedRect(self.rect(), 15, 15)
EDIT:
You also need to add this line anywhere in the constructor
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
Hope this helps.
I've found a work-around for now. You can hide the extra borders by using QPainter.eraseRect on the children and having the correct stylesheet set. I've also found that painting over the offending area with QPainter.fillRect works too.
def paintEvent(self, event):
painter = QtGui.QPainter(self)
fillColor = QtGui.QColor(75, 75, 75, 255)
lineColor = QtCore.Qt.gray
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(QtGui.QPen(QtGui.QBrush(lineColor), 2.0))
painter.setBrush(QtGui.QBrush(fillColor))
painter.drawRoundedRect(event.rect(), 15, 15)
# Sketchy fix:
painter.eraseRect(self.childrenRect())
# OR
painter.fillRect(self.childrenRect(), QtGui.QBrush(fillColor))
This doesn't answer my original question though. I'd like to avoid this behavior rather than masking it. So I'm not going to mark this as the answer.