Issue
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)
Solution
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 evenself.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).
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.