Search code examples
pythonpyqt5pyside2qwidgetaction

QMenu.setTearOffEnabled window doesn't correctly size itself to display a QWidgetAction


QMenu correctly displays a QWidgetAction that contains a QWidget with its own dimensions, but when the menu is torn off (and becomes an independent window) things go wrong. The window is sized to properly display any actions that were added with addAction(str), but the QWidgetAction's QWidget is almost completely hidden.

This code reproduces the problem:

#!/usr/bin/env python

from PySide2 import QtCore, QtWidgets


def main():
    app = QtWidgets.QApplication()
    window = QtWidgets.QWidget()
    layout = QtWidgets.QHBoxLayout()
    window.setLayout(layout)
    button = QtWidgets.QPushButton("Push Me")

    menu = QtWidgets.QMenu()
    menu.setTearOffEnabled(True)
    menu.addAction("Ok")

    qwa = QtWidgets.QWidgetAction(menu)
    big_button = QtWidgets.QPushButton("Big Button")
    big_button.setMinimumWidth(400)
    big_button.setMinimumHeight(400)
    qwa.setDefaultWidget(big_button)

    menu.addAction(qwa)
    button.setMenu(menu)
    layout.addWidget(button)
    window.show()
    app.exec_()

main()

When we display the menu

When we tear off the menu


Solution

  • This is caused by the fact that when a QMenu is teared off, you don't actually see the same QMenu object, but a new QMenu based from it.

    The QMenu documentation actually explains this, but I admit that more emphasis on this important aspect could have been used:

    A QMenu can also provide a tear-off menu. A tear-off menu is a top-level window that contains a copy of the menu.

    Then, note this section from the QWidgetAction description:

    If you have only one single custom widget then you can set it as default widget using setDefaultWidget(). That widget will then be used if the action is added to a QToolBar, or in general to an action container that supports QWidgetAction. If a QWidgetAction with only a default widget is added to two toolbars at the same time then the default widget is shown only in the first toolbar the action was added to.

    The above is not only valid for toolbars, but for menus ("an action container that supports QWidgetAction"), including tear off menus: if you add the same QWidgetAction to more than one menu and that action only implements setDefaultWidget(), the widget will only be visible in the first menu, which is exactly what happens when using tearing off.

    The solution is then to subclass QWidgetAction and use createWidget() instead:

    class ButtonAction(QtWidgets.QWidgetAction):
        def createWidget(self, parent):
            big_button = QtWidgets.QPushButton("Big Button", parent)
            big_button.setMinimumWidth(400)
            big_button.setMinimumHeight(400)
            return big_button
    
    
    ...
    menu = QtWidgets.QMenu()
    qwa = ButtonAction(menu)
    menu.addAction(qwa)
    

    Note that you should probably connect the buttons' clicked signal to the action's triggered one.