pythonuser-interfacepyqt5widgetchildwindow

PyQt5 Parent Window closes when Child widget is closed


currently I am working on a python application which uses a PyQt5 GUI and ran into some trouble with the GUI. I have created a main window, which contains a button. When I click this button a second window opens in which the user inputs login data. After accepting the data by hitting another button the window changes its apperance to wait make the user verify his login. After doing so and confirming by clicking a third button the window changes one last time. Now the user is asked to specify a time period and confirming it by once again clicking a button. Afterwards the child window is supposed to close. And the application should return to the main window.

However when I close the child window after the last input the whole application closes. Which should not happen. On the other hand if I close the window in one of the prevoius steps it behaves the way I want it to. The child window is closed and the application keeps running. Therefore I expect the error to be somewhere in the last configuration of the child window, but I can't figure out where. The child window is build in a way that it is always the same window and only the widgets displayed change.

You can find a minimum working example down below.

Thanks for your help.

Main window

import sys
from random import randint
from PyQt5.QtWidgets import (
    QVBoxLayout, QPushButton, QLabel, QMainWindow, QApplication, QWidget)

from Banking_App_Login_form import LoginForm
from test_period_form import PeriodForm

class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        self.w = None  # No external window yet.
        self.button = QPushButton("Push for Window")
        self.button.clicked.connect(self.show_new_window)
        self.setCentralWidget(self.button)

    def show_new_window(self):
        if self.w is None:
            self.w = LoginForm()
            self.w.login_btn.clicked.connect(self.get_tan)            
            self.w.closed.connect(self.quit)
            self.w.show()

        else:
            self.w.close()  # Close window.
            self.w = None  # Discard reference.
            self.show_new_window()
    
    def get_tan(self):
        self.w.change_form_tan()
        self.w.ok_btn.clicked.connect(self.get_period)
        # self.w.ok_btn.clicked.connect(self.close)    

    def get_period(self):
        self.w.change_form_period()
        self.w.ok_btn_2.clicked.connect(self.close)

    def quit(self):
        """
        Function for tasks to be performed after the child window is closed.
        """
        print("Token revoked")
        self.w = None
        print(f"self.w is now {self.w}") 
    
    def close(self):
        """
        Function to close the child window.
        """
        self.w.close()

def main():
    app = QApplication(sys.argv)
    gui = MainWindow()
    gui.show()

    sys.exit(app.exec())


if __name__ == "__main__":
    main()

Child window

from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QFont, QCloseEvent
from PyQt5.QtWidgets import (QWidget, QPushButton, QLineEdit, QLabel,
                             QApplication, QComboBox, QGridLayout, QSpacerItem,
                             QSizePolicy)

import sys


class LoginForm(QWidget):
    """
    login form for banking application
    ...

    Attributes
    ----------
    None

    Methods
    -------
    _create_layouts
    _create_fonts
    _create_tbs
    _create_tbns

    """
    # creates new signal to be emitted when window is closed
    # signal must be instanciated before __init__
    closed = pyqtSignal()

    def __init__(self) -> None:
        """
        Init Method of the Login form
        ...

        Parameters
        ----------
        None

        Returns
        -------
        None

        """
        super().__init__()

        self._cursor_status = "standard"

        self.setWindowTitle("LogIn")
        self.resize(300, 120)
        self.setMinimumSize(150, 100)

        self._create_layouts()
        self._create_fonts()
        self._create_tbs()
        self._create_btns()
        self._create_lbls()
        self._create_cmbs()

        self.setLayout(self._main_layout)

        self._main_layout.addWidget(self._lbl_user, 0, 0)
        self._main_layout.addWidget(self._lbl_pwd, 1, 0)

        self._main_layout.addWidget(self.tb_user, 0, 1)
        self._main_layout.addWidget(self.tb_pwd, 1, 1)
        self._main_layout.addWidget(self.login_btn, 2, 1)

    def _create_layouts(self) -> None:
        """
        Create Main layout for login form
        ...

        Parameters
        ----------
        Nonen

        Return
        ------
        None
        """
        self._main_layout = QGridLayout()

    def _create_fonts(self) -> None:
        """
        Create the fonts used in the Login form
        ...

        Parameters
        ----------
        None

        Returns
        -------
        None
        """
        self._font = QFont()
        self._font.setFamily("Calibri")
        self._font.setPointSize(12)

    def _create_lbls(self) -> None:
        """
        Create label
        ...

        Parameters
        ----------
        None 

        Return
        ------
        None
        """
        self._lbl_user = QLabel("Benutzername")
        self._lbl_user.setFixedSize(100, 25)
        self._lbl_user.setFont(self._font)

        self._lbl_pwd = QLabel("Passwort")
        self._lbl_pwd.setFixedSize(100, 25)
        self._lbl_pwd.setFont(self._font)

        self._lbl_tan = QLabel("TAN bestätigen und Ok drücken")
        self._lbl_tan.setFixedSize(220, 25)
        self._lbl_tan.setFont(self._font)

        self._lbl_zeitraum = QLabel("Zeitraum eingeben:")
        self._lbl_zeitraum.setFixedSize(220, 25)
        self._lbl_zeitraum.setFont(self._font)

        self._lbl_start = QLabel("Startdatum")
        self._lbl_start.setFixedSize(100, 25)
        self._lbl_start.setFont(self._font)

        self._lbl_ende = QLabel("Endatum")
        self._lbl_ende.setFixedSize(110, 25)
        self._lbl_ende.setFont(self._font)

    def _create_cmbs(self) -> None:
        self.cmb_von_monat = QComboBox()
        self.cmb_von_monat.setFixedSize(70, 25)
        self.cmb_von_monat.setFont(self._font)

        self.cmb_von_jahr = QComboBox()
        self.cmb_von_jahr.setFixedSize(70, 25)
        self.cmb_von_jahr.setFont(self._font)

        self.cmb_bis_monat = QComboBox()
        self.cmb_bis_monat.setFixedSize(70, 25)
        self.cmb_bis_monat.setFont(self._font)

        self.cmb_bis_jahr = QComboBox()
        self.cmb_bis_jahr.setFixedSize(70, 25)
        self.cmb_bis_jahr.setFont(self._font)

    def _create_tbs(self) -> None:
        """
        Create textboxes
        ...

        Parameters
        ----------
        None

        Returns
        -------
        None
        """

        self.tb_user = QLineEdit()
        self.tb_user.setFixedSize(175, 25)
        self.tb_user.setFont(self._font)

        self.tb_pwd = QLineEdit()
        self.tb_pwd.setFixedSize(175, 25)
        self.tb_pwd.setFont(self._font)
        self.tb_pwd.setEchoMode(QLineEdit.EchoMode.Password)

    def _create_btns(self) -> None:
        """
        create buttons
        ...

        Parameters
        ----------
        None

        Returns
        -------
        None
        """

        self.login_btn = QPushButton("Login")
        self.login_btn.setFont(self._font)
        self.login_btn.setFixedSize(175, 25)

        self.ok_btn = QPushButton("Ok")
        self.ok_btn.setFont(self._font)
        self.ok_btn.setFixedSize(100, 25)

        self.ok_btn_2 = QPushButton("Ok")
        self.ok_btn_2.setFont(self._font)
        self.ok_btn_2.setFixedSize(100, 25)

    def _create_spacer(self) -> None:
        """
        create Spacers
        ...

        Parameters
        ----------
        None

        Returns
        -------
        None
        """
        self._fixed_spacer = QSpacerItem(
            10, 20, QSizePolicy.Fixed, QSizePolicy.Minimum)

    def change_form_tan(self) -> None:
        """
        Change form apperance for TAN verification.
        ...

        Parameters
        ----------
        None

        Returns
        -------
        None
        """
        self.setWindowTitle("TAN bestätigen")
        self.resize(250, 100)

        # deletes widget from Layout but references inside the layout are
        # still kept and the widget is shown
        self._main_layout.removeWidget(self._lbl_user)
        # deletes all references to the widget and allows python to purge it
        self._lbl_user.deleteLater()
        self._main_layout.removeWidget(self._lbl_pwd)
        self._lbl_pwd.deleteLater()

        self._main_layout.removeWidget(self.tb_user)
        self.tb_user.deleteLater()
        self._main_layout.removeWidget(self.tb_pwd)
        self.tb_pwd.deleteLater()
        self._main_layout.removeWidget(self.login_btn)
        self.login_btn.deleteLater()

        self._main_layout.addWidget(self._lbl_tan, 0, 0)
        self._main_layout.addWidget(self.ok_btn, 1, 0)

    def change_form_period(self) -> None:
        """
        Change form apperance for period selection.
        ...

        Parameters
        ----------
        None

        Returns
        -------
        None
        """

        self.setWindowTitle("Zeitraum auswählen")
        self.resize(300, 180)
        self.setMinimumSize(300, 180)

        self._create_spacer()

        self._main_layout.removeWidget(self._lbl_tan)
        self._lbl_tan.deleteLater()
        self._main_layout.removeWidget(self.ok_btn)
        self.ok_btn.deleteLater()

        self._main_layout.addWidget(self._lbl_zeitraum, 0, 0)
        self._main_layout.addItem(self._fixed_spacer, 1, 1)
        self._main_layout.addWidget(self._lbl_start, 2, 0)
        self._main_layout.addItem(self._fixed_spacer, 3, 1)
        self._main_layout.addWidget(self._lbl_ende, 4, 0)

        self._main_layout.addWidget(self.cmb_von_monat, 2, 1)
        self._main_layout.addWidget(self.cmb_von_jahr, 2, 2)
        self._main_layout.addWidget(self.cmb_bis_monat, 4, 1)
        self._main_layout.addWidget(self.cmb_bis_jahr, 4, 2)

        self._main_layout.addItem(self._fixed_spacer, 5, 1)
        self._main_layout.addWidget(self.ok_btn_2, 6, 1)

        self.cmb_von_monat.addItems(
            ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"])
        self.cmb_bis_monat.addItems(
            ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"])
        self.cmb_von_jahr.addItems([str(2021+i) for i in range(50)])
        self.cmb_bis_jahr.addItems([str(2021+i) for i in range(50)])

    def change_cursor(self) -> None:
        """
        Change cursor to wait cursor
        ...

        Parameters
        ----------
        None

        Return
        ------
        None
        """
        if self._cursor_status == "standard":
            self._cursor_status = "wait"
            QApplication.setOverrideCursor(Qt.WaitCursor)
        else:
            self._cursor_status = "standard"
            QApplication.restoreOverrideCursor()

    def closeEvent(self, event: QCloseEvent) -> None:
        self.closed.emit()
        super().closeEvent(event)

Solution

  • Not a real answer, but also more than a comment...

    I tried to reproduce this by simplifying it even more, but the error does not persist here, i.e. the main window stays open when the child is closed:

    from PyQt5 import QtWidgets, QtCore
    
    class LoginForm(QtWidgets.QWidget):
        closed = QtCore.pyqtSignal()    
        
        def __init__(self):
            super().__init__()
            self._main_layout = QtWidgets.QGridLayout()
            self.setLayout(self._main_layout)
            self.ok_btn_2 = QtWidgets.QPushButton("Ok")
            self._main_layout.addWidget(self.ok_btn_2, 1, 1)        
    
        def closeEvent(self, event):
            self.closed.emit()
            super().closeEvent(event)
            
    
    class MainWindow(QtWidgets.QMainWindow):
        def __init__(self):
            super().__init__()
            self.w = None  # No external window yet.
            self.button = QtWidgets.QPushButton("Push for Window")
            self.button.clicked.connect(self.show_new_window)
            self.setCentralWidget(self.button)
    
        def show_new_window(self):
            if self.w is None:
                self.w = LoginForm()
                self.w.ok_btn_2.clicked.connect(self.close)      
                self.w.closed.connect(self.quit)
                self.w.show()
            else:
                self.w.close()  # Close window.
                self.w = None  # Discard reference.
                self.show_new_window()
    
        def quit(self):
            """
            Function for tasks to be performed after the child window is closed.
            """
            print("Token revoked")
            self.w = None
            print(f"self.w is now {self.w}") 
    
        def close(self):
            """
            Function to close the child window.
            """
            self.w.close()
    
    def main():
        app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
        gui = MainWindow()
        gui.show()
        app.exec()
    
    main()   # I was running this in IPython, so I called it directly
    

    Maybe you can figure out where the difference to your code is?

    Some other ideas: