Search code examples
pythonpyqt5qt5pyside2

PySide2 main window not working after pop-up


I am building a graphical interface for an application using PySide2. My main window is a QMainWindow and I am trying to open a pop-up window, which is a QDialog, whenever a specific action is performed on the main window.

The pop-up opens perfectly fine. However, after it is open, the main window is no longer responsive. I believe the problem is that my application is overwriting the main window with the popup window. The error message whenever I try to change the main window's stackedWidget index is:

AttributeError: 'Ui_popupHideSuccess' object has no attribute 'stackedWidget'

The code I am using to open the main window is the following:

if __name__ == '__main__':
    app = QApplication(sys.argv)
    myWindow = MainWindow()
    myWindow.show()
    sys.exit(app.exec_())

And the code I am using to open the pop-up window is the following:

def showPopupSuccessHide(self):
        self.window = QDialog()
        self.ui = Ui_popupHideSuccess()
        self.ui.setupUi(self.window)
        self.window.show()

The code for the windows themselves are on other files (as I am using QtDesigner for developing them). I believe it to be unnecessary for resolving this issue, but I can provide it if needed. What am I doing wrong here? I need to open pop-ups and still interact with the main window after.

I have no idea how to actually resolve this. I believe my error to be in the code I am using to open the pop-up window, but I'm not sure how to tweak it for it to work properly.


Solution

  • TL;DR

    Do not overwrite self.ui.

    Explanation

    How uic composition works

    One of the common ways of properly using pyuic generated files is to use composition (as opposed to multiple inheritance):

    from PyQt5.QtWidgets import QApplication, QMainWindow, QDialog
    from ui_mainWindow import Ui_MainWindow
    
    class MyWindow(QMainWindow):
        def __init__(self):
            super().__init__()
            self.ui = Ui_MainWindow()
            self.ui.setupUi(self)
            self.ui.myLineEdit.setText('some text')
    

    This is perfectly fine, and makes sense: the concept is that an instance of the pyuic class (sometimes called "form class") is created and then the actual window is "set up" using that instance, with the self.ui object containing references to all widgets.

    Note that making the ui persistent (using an instance attribute) is actually not a strict requirement, but it is usually necessary in order to be able to directly access the widgets, which is normally important to create signal connections or read properties.
    But, if that's not required, it will work anyway: the widgets are automatically "reparented" to the main window (or their direct parents), and the garbage collection is not an issue as Qt will keep its own references internally (in Qt terms, "the window takes ownership").

    Technically speaking, this is completely valid:

    class MyWindow(QMainWindow):
        def __init__(self):
            super().__init__()
            Ui_MainWindow().setupUi(self)
    

    Then, we can still access the widgets using findChild and their object names (those set in Designer):

            self.findChild(QLineEdit, 'myLineEdit').setText('some text')
    

    Obviously, it is not very practical.

    Creating "child" windows

    When there is the need to create a child window (usually, a dialog), it's normally suggested to use an instance attribute to avoid garbage collection:

        def createWindow(self):
            self.window = QDialog()
            self.window.show()
    

    If that dialog also has a Designer file, we need to do something similar to what done at the beginning. Unfortunately, a very common mistake is to create the ui instance by using the same name:

        def createWindow(self):
            self.window = QDialog()
            self.ui = Ui_Dialog()
            self.ui.setupUi(self.window)
            self.ui.anotherLineEdit.setText('another text')
            self.window.show()
    

    This is theoretically fine: all works as expected. But there's a huge problem: the above overwrites self.ui, meaning that we lose all references to the widgets of the main window.

    Suppose that you want to set the text of the line edit in the dialog based on the text written in that of the main window; the following will probably crash:

        def createWindow(self):
            self.window = QDialog()
            self.ui = Ui_Dialog()
            self.ui.setupUi(self.window)
            self.ui.anotherLineEdit.setText(self.ui.myLineEdit.text())
            self.window.show()
    

    This clearly shows an important aspect: it's mandatory to always think before assigning attributes that may already exist.

    In the code here above, this was actually done twice: not only we overwrote the self.ui we created before, but we also did it for window(), which is an existing function of all Qt widgets (it returns the top level ancestor window of the widget on which it was called).

    As a rule of thumb, always take your time to decide how to name objects, use smart names, and consider that most common names are probably already taken: remember to check the "List of all members, including inherited members" link in the documentation of the widget type you're using, until you're experienced enough to remember them.

    Solutions

    The obvious solution is to use a different name for the ui of the dialog:

        def createWindow(self):
            self.dialog = QDialog()
            self.dialog_ui = Ui_Dialog()
            self.dialog_ui.setupUi(self.dialog)
            self.dialog_ui.anotherLineEdit.setText(self.ui.myLineEdit.text())
            self.dialog.show()
    

    A better solution is to create a subclass for your dialog instead:

    class MyDialog(QDialog):
        def __init__(self, parent=None)
            super().__init__(parent)
            self.ui = Ui_Dialog()
            self.ui.setupUi(self)
    
    
    class MyWindow(QMainWindow):
        # ...
        def createWindow(self):
            self.dialog = MyDialog()
            self.dialog.ui.anotherLineEdit.setText(self.ui.myLineEdit.text())
            self.dialog.show()
    

    Also remember that another common (and, to my experience, simpler and more intuitive) method is to use multiple inheritance instead of composition:

    class MyDialog(QDialog, Ui_Dialog):
        def __init__(self, parent=None)
            super().__init__(parent)
            self.setupUi(self)
    
    
    class MyWindow(QMainWindow, Ui_MainWindow):
        def __init__(self):
            super().__init__()
            self.setupUi(self)
            self.myLineEdit.setText('some text')
    
        def createWindow(self):
            self.dialog = MyDialog()
            self.dialog.anotherLineEdit.setText(self.myLineEdit.text())
            self.dialog.show()
    

    The only issue of this approach is that it may inadvertently overwrite names of functions of the "main" widget: for instance, if you created a child widget in Designer and renamed it "window". As said above, if you always think thoroughly about the names you assign to objects, this will probably never happen (it doesn't make a lot of sense to name a widget "window").