Rounded corners for QMenu in pyqt

Question:

I am trying to override the paintEvent() of QMenu to make it have rounded corners.

The context menu should look something like this.

enter image description here

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.

enter image description here

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)
Asked By: JacksonPro

||

Answers:

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

enter image description here

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()
Answered By: Christian Karcher

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:

  1. 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.
  2. 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):

  1. 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.
  2. 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.
  3. 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).
  4. self.update() should never be called within a paintEvent (and not even self.repaint() should). The result in undefined and possibly dangerous.
  5. 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

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.
enter image description here

Answered By: zhiyi
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.