pythonmenupyqt5mdi

Where is the correct place to create/destroy and show/hide a QMdiSubWindow menu?


I am learning Python by creating an MDI application in PyQt5. This application contains a class derived from QMdiSubWindow. These sub-windows need their own menu to be added to the main menu-bar. Where is the 'correct' place to create and show/hide that part of the menu which is only relevant to the sub-window when it's in focus? And where should the menu be destroyed (if it doesn't happen automatically because ownership is taken by the parent)? My attempt at detecting when the sub-window gains/loses focus causes infinite recursion, presumably because the newly visible menu steals the focus back from the sub-window.

This is probably such a common requirement that it's not mentioned in the tutorials, but the only reference in the docs to sub-window menus seems to just refer to the system menu, and not the main menu-bar. Most other Q&A's just refer to activating other sub-windows from the main menu. Several hours of searching haven't quite got what I need, so thank you for your help in either pointing me to the right place in the docs, or by improving my code ... or even both!

A minimal app to illustrate the problem:

#!/usr/bin/python3
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

class TestSubWin(QMdiSubWindow):
    def __init__(self, parent=None):
        super().__init__()
        self.setWidget(QLabel("Hello world"))
        self.own_menu = QMenu("Sub win menu")
        parent.menuBar().addMenu(self.own_menu)
        # Add sub-window actions to the menu here

## Causes infinite recursion
#    def focusOutEvent(self,  event):
#        self.own_menu.setVisible(False)
#        
#    def focusInEvent(self,  event):
#        self.own_menu.setVisible(True)

class MainWindow(QMainWindow):
    def __init__(self, parent = None):
        super(MainWindow, self).__init__(parent)
        self.mdi = QMdiArea()
        self.setCentralWidget(self.mdi)
        bar = self.menuBar()

        file = bar.addMenu("File")
        file.addAction("New")
        file.triggered[QAction].connect(self.windowaction)
        self.setWindowTitle("MDI demo")

    def windowaction(self, q):
        if q.text() == "New":
            sub = TestSubWin(self)
            self.mdi.addSubWindow(sub)
            sub.show()

def main():
    app = QApplication(sys.argv)
    ex = MainWindow()
    ex.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

Solution

  • QMenu and QMenuBar don't take ownership of QActions (and QMenus), unless when created with the functions that accepts icon/title arguments.

    This also means that you shall not need to destroy the menus, but only remove them from the menu bar.

    The solution is to connect to the subWindowActivated signal, remove the previously added menu, retrieve the menu for the newly active sub window (if any) and add it.

    Note that in order to remove a menu from QMenuBar you have to use removeAction() along with the menuAction(), which is the action associated with the menu and shown as menubar title for the menu (or item in a menu for sub menus).

    In the following example I'm creating a base subclass for any mdi subwindows that will support menubar menus, and further subclasses for different window types.

    import sys
    from PyQt5.QtCore import *
    from PyQt5.QtGui import *
    from PyQt5.QtWidgets import *
    
    class MenuSubWin(QMdiSubWindow):
        own_menu = None
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.setAttribute(Qt.WA_DeleteOnClose)
    
        def menu(self):
            return self.own_menu
    
    
    class TestSubWin1(MenuSubWin):
        def __init__(self):
            super().__init__()
            self.setWidget(QLabel("Hello world"))
            self.own_menu = QMenu("Sub win menu 1")
            self.own_menu.addAction('Test 1')
    
    
    class TestSubWin2(MenuSubWin):
        def __init__(self):
            super().__init__()
            self.setWidget(QLabel("How are you?"))
            self.own_menu = QMenu("Sub win menu 2")
            self.own_menu.addAction('Test 2')
    
    
    class MainWindow(QMainWindow):
        def __init__(self, parent = None):
            super(MainWindow, self).__init__(parent)
            self.mdi = QMdiArea()
            self.setCentralWidget(self.mdi)
            bar = self.menuBar()
    
            fileMenu = bar.addMenu("File")
            new1Action = fileMenu.addAction("New 1")
            new1Action.setData(TestSubWin1)
            new2Action = fileMenu.addAction("New 2")
            new2Action.setData(TestSubWin2)
            fileMenu.triggered.connect(self.newWindow)
            self.setWindowTitle("MDI demo")
    
            self.subWinMenu = None
            self.mdi.subWindowActivated.connect(self.subWindowActivated)
    
        def subWindowActivated(self, subWindow):
            if self.subWinMenu:
                self.menuBar().removeAction(self.subWinMenu.menuAction())
                self.subWinMenu = None
            if subWindow is None or not hasattr(subWindow, 'menu'):
                return
            self.subWinMenu = subWindow.menu()
            if self.subWinMenu:
                self.menuBar().addMenu(self.subWinMenu)
    
        def newWindow(self, action):
            cls = action.data()
            if not cls:
                return
            sub = cls()
            self.mdi.addSubWindow(sub)
            sub.show()
    
    
    if __name__ == '__main__':
        app = QApplication(sys.argv)
        ex = MainWindow()
        ex.show()
        sys.exit(app.exec_())
    

    Notes: