Linking actions to menu bar in PyQt from Qt Designer

Question:

I’m new to QT Designer and PyQt, but I seem to be trying to do something either too simple to be mentioned or too rare!

I created a simple window using QT Designer. In that window there is two menu options; one should close the window, the other should open another window.

I converted the ui into a python file with the pyuic5 command.

This is the code I get:

    # -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'Win1.ui'
#
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(441, 305)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 441, 22))
        self.menubar.setObjectName("menubar")
        self.menuClosing = QtWidgets.QMenu(self.menubar)
        self.menuClosing.setObjectName("menuClosing")
        self.menuOpen_Win_2 = QtWidgets.QMenu(self.menubar)
        self.menuOpen_Win_2.setObjectName("menuOpen_Win_2")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)
        self.menubar.addAction(self.menuClosing.menuAction())
        self.menubar.addAction(self.menuOpen_Win_2.menuAction())

        self.retranslateUi(MainWindow)
        self.menuClosing.triggered['QAction*'].connect(MainWindow.close) # type: ignore
        self.menuOpen_Win_2.triggered['QAction*'].connect(MainWindow.open) # type: ignore
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.menuClosing.setTitle(_translate("MainWindow", "Closing"))
        self.menuOpen_Win_2.setTitle(_translate("MainWindow", "Open Win #2"))


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

New in QT Designer I have assigned the "close()" slot to Closing and I created a new slot "open()" that I linked to Open Win #2.

Obviously, neither work.

It seems there is a different logic than there is when assigning functions to submenus (where linking close() to it does work).

Does anyone knows how I should go about to do this?

Asked By: Ad Ep

||

Answers:

Designer doesn’t allow to add basic actions to a QMenuBar.

In fact, both the "Edit Signals/Slots" mode and the "Signal/Slot Editor" only allow interacting with widgets: QActions are not widgets.

The only way to achieve this is by code, and by directly adding a real QAction to the menubar.

Remove those (probably empty) menus from the UI, rebuild the code with pyuic and then do something like this after (assuming that the output file is named win1.py):

from PyQt5 import QtWidgets
from win1 import Ui_MainWindow
from win2 import Ui_MainWindow2

class Window(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.setupUi(self)
        self.closeAction = QtWidgets.QAction('Closing', self)
        self.menuBar().addAction(self.closeAction)
        self.openWin2Action = QtWidgets.QAction('Open Win #2', self)
        self.menuBar().addAction(self.openWin2Action)

        self.closeAction.triggered.connect(self.close)
        self.openWin2Action.triggered.connect(self.openWin2)

    def openWin2(self):
        self.win2 = Window2()
        self.win2.show()


class Window2(QtWidgets.QMainWindow, Ui_MainWindow2):
    def __init__(self):
        super().__init__()
        self.setupUi(self)


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

Notes:

  • the code you provided is a pyuic output, which, in case you didn’t know, must never be directly used or edited, if not for debugging or studying purposes; the above code uses the common practice of multiple inheritance as explained in the official guidelines about using Designer;
  • I also faced the disappearing slot connection you mentioned, which is probably a Designer bug (there’s no trace of the connection in the .ui file, so it’s not a pyuic fault);
  • even ignoring the above possible bug, the connection wouldn’t have worked anyway: the triggered signal of QMenu (similarly to that of QMenuBar) is emitted only when one of its actions is triggered (not the action of the menu itself);
  • while I can understand the basic idea, using an action in a menubar is normally discouraged as counter-intuitive from the UX perspective; the convention says that users always expect a menu to be shown after clicking on a menu bar item (after all, it’s called a "menu bar": a bar of menus): even if the action text is sufficiently verbose, the user cannot know that clicking on a "menu title" would actually trigger an action as final as closing a window; for what they know, that could just be a menu that shows possible options of closing (i.e. close without saving);
  • the above code assumes that you also did create another main window in designer and also changed its object name to MainWindow2; while you could do from win2 import Ui_MainWindow as Ui_MainWindow2, it’s good practice to set unique object names of top level windows: the output of pyuic will always be Ui_<objectNameOfTopLevelWindow>;
  • in the comment of the deleted answer you mentioned that "closing like this seems to keep the Kernel running"; you’re probably using an IDE or an interactive shell, which could ignore the default behavior (quitting the QApplication as soon as the last window is closed): running the script as stand-alone won’t normally cause that, but you can always consider connecting to the QApplication.quit signal;
Answered By: musicamante