Search code examples
pythonpyqtpyqt5

How to Closing Subwindow in PyQt5 when Close Button is Pressed?


I am currently working on a PyQt5 application that includes an MdiSubWindow with multiple subwindows. Each subwindow contains buttons to open different pages such as "Personal," "Contacts," and "Educational."

The problem I am facing is with closing the subwindows. When I click on a button to open a subwindow, it opens successfully. However, when I try to close the subwindow by clicking the close button, the contents of the window will deleted/hide/close, but the subwindow remains open, and I cannot close it. ( The content of window will be deleted once, very first time only, after that nothing will happen)

I have implemented the close functionality using the closeRequested signal in the Educational class. I connected this signal to a lambda function to call the close() method on the page widget. However, it doesn't seem to work as expected.

I would appreciate any insights or suggestions on how to correctly implement the functionality to close the subwindows when clicking the close button.

Thank you for your assistance!

Main Block

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


from sample_tabwidget_base import Contact, Personal, Educational

dict_link_itemwise = {
    "Personal": Personal,
    "Contacts": Contact,
    "Educational": Educational

}


class MdiSubWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Mdi and SubWindow Examples")
        self.open_sub_windows = {}  # Dictionary to store open subwindows
        self.ui()

    def ui(self):
        self.stack_page = QStackedWidget()

        self.Hbox_page = QHBoxLayout()
        self.btn_page1 = QPushButton("Page 1")
        self.btn_page1.clicked.connect(self.page1)


        self.Hbox_page.addWidget(self.btn_page1)
        self.Hbox_page.addWidget(self.btn_page2)
        self.Hbox_page.addStretch()

        self.btn_Personal = QPushButton("Personal")
        self.btn_Contacts = QPushButton("Contacts")
        self.btn_Educational = QPushButton("Educational")
        self.btn_Personal.clicked.connect(self.get_btn_name)
        self.btn_Contacts.clicked.connect(self.get_btn_name)
        self.btn_Educational.clicked.connect(self.get_btn_name)



        self.page1_container = QWidget()
        self.page1_container.setLayout(QHBoxLayout())
        self.page1_container.layout().addWidget(self.btn_Personal)
        self.page1_container.layout().addWidget(self.btn_Contacts)
        self.page1_container.layout().addWidget(self.btn_Educational)
        self.page1_container.layout().addStretch()

        self.mdi = QMdiArea()
        self.mdi.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.mdi.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.mdi.cascadeSubWindows()

        self.main_layout = QVBoxLayout()
        self.main_layout.addLayout(self.Hbox_page)
        self.main_layout.addWidget(self.stack_page)
        self.main_layout.addWidget(self.mdi)
        self.main_layout.addStretch(10)
        self.setLayout(self.main_layout)

    def page1(self):
        self.stack_page.addWidget(self.page1_container)
        self.stack_page.setCurrentWidget(self.page1_container)


    def get_btn_name(self):
        clicked_btn_name = self.sender().text()
        self.open_subwindow(clicked_btn_name)

    def createPageInstance(self, page_name):
        if page_name in dict_link_itemwise:
            page_class = dict_link_itemwise[page_name]
            return page_class()
        return None


    def open_subwindow(self, page_name):

        if page_name in self.open_sub_windows:
            sub_window = self.open_sub_windows[page_name]
            sub_window.widget().setParent(None)
            sub_window.setWidget(self.createPageInstance(page_name))
            sub_window.show()
            self.page_widget.closeRequsted.connect(self.createCloseEvent)
        else:
            self.page_widget = self.createPageInstance(page_name)
            if self.page_widget is not None:
                sub_window = QMdiSubWindow()
                sub_window.setObjectName(page_name + "_obj")
                sub_window.setWidget(self.page_widget)
                self.open_sub_windows[page_name] = sub_window
                self.mdi.addSubWindow(sub_window)
                sub_window.show()
                self.page_widget.closeRequsted.connect(lambda :self.page_widget.close())
            else:
                print("Invalid page class:", page_name)

    def createCloseEvent(self):
        self.page_widget.close()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    mainwindow = MdiSubWindow()
    app.setStyle("Windows")
    mainwindow.show()
    sys.exit(app.exec_())

Imported Block

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import  pyqtSignal

class Contact(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("Contact Details")
        self.layout = QFormLayout()
        name_line_edit = QLineEdit()
        # name_line_edit.setFocus()
        self.layout.addRow("Name", name_line_edit)
        self.layout.addRow("Address", QLineEdit())
        self.setLayout(self.layout)

class Personal(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Personal Details")
        self.layout = QFormLayout()
        self.sex = QHBoxLayout()
        self.sex.addWidget(QRadioButton("Male"))
        self.sex.addWidget(QRadioButton("Female"))
        self.layout.addRow(QLabel("Sex"), self.sex)
        self.layout.addRow("Date of Birth", QLineEdit())
        self.setLayout(self.layout)

class Educational(QWidget):
    closeRequsted = pyqtSignal()
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Educational Details")
        self.btn_close = QPushButton("Close")
        self.layout = QHBoxLayout()
        self.layout.addWidget(QLabel("subjects"))
        self.layout.addWidget(QCheckBox("Physics"))
        self.layout.addWidget(QCheckBox("Maths"))
        self.layout.addWidget(self.btn_close)
        self.btn_close.clicked.connect(self.closeRequsted)
        self.setLayout(self.layout)

    def on_close_clicked(self):
        self.close()

Reduced minimal reproducible example : As sugessted By @musicamante. Reduce the unnecessary portion of the code

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *

class Educational(QWidget):
    closeRequsted = pyqtSignal()
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Educational Details")
        self.btn_close = QPushButton("Close")
        self.layout = QHBoxLayout()
        self.layout.addWidget(QLabel("subjects"))
        self.layout.addWidget(QCheckBox("Physics"))
        self.layout.addWidget(QCheckBox("Maths"))
        self.layout.addWidget(self.btn_close)
        self.btn_close.clicked.connect(self.close)
        self.setLayout(self.layout)
    
dict_link_itemwise = {"Educational": Educational}

class MdiSubWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Mdi and SubWindow Examples")
        self.open_sub_windows = {}  # Dictionary to store open subwindows
        self.ui()

    def ui(self):
        self.Hbox_page = QHBoxLayout()
        self.btn_Educational = QPushButton("Educational")
        self.Hbox_page.addWidget(self.btn_Educational)
        self.btn_Educational.clicked.connect(self.open_subwindow)

        self.mdi = QMdiArea()

        self.main_layout = QVBoxLayout()
        self.main_layout.addLayout(self.Hbox_page)
        self.main_layout.addWidget(self.mdi)
        self.setLayout(self.main_layout)

    def open_subwindow(self):
        page_name = self.sender().text()
        if page_name in self.open_sub_windows:
            sub_window = self.open_sub_windows[page_name]
            sub_window.widget().setParent(None)
            sub_window.setWidget(dict_link_itemwise[page_name]())
            sub_window.show()
        else:
            self.page_widget = dict_link_itemwise[page_name]()
            if self.page_widget is not None:
                sub_window = QMdiSubWindow()
                sub_window.setObjectName(page_name + "_obj")
                sub_window.setWidget(self.page_widget)
                self.open_sub_windows[page_name] = sub_window
                self.mdi.addSubWindow(sub_window)
                sub_window.show()
                self.page_widget.closeRequsted.connect(lambda :self.page_widget.close())
            else:
                print("Invalid page class:", page_name)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    mainwindow = MdiSubWindow()
    app.setStyle("Windows")
    mainwindow.show()
    sys.exit(app.exec_())

Solution

  • There are many important aspects that have to be kept in mind here.

    • closing/hiding a child does not hide its parent;
    • close() is not the same as hide() (or setVisible(False)), even if it actually calls hide() if the close is accepted;
    • calling (directly or not) hide() or setVisible(False) makes a widget explicitly hidden;
    • if a child has been explicitly hidden, showing its parent after it's being hidden doesn't make the child visible again;
    • close() on a QMdiSubWindow actually calls close() on its inner widget; this is important since, by default, a QMdiSubWindow automatically created with addSubWindow() actually has the WA_DeleteOnClose flag, so the window must be sure that it can actually close before destroying itself and all its descendants;

    Then, considering the above:

    • if you want to close a widget that has been added to a QMdiArea, you have to close its QMdiSubWindow, not the widget;
    • if you want to show the widget again, it's not enough to show the subwindow, but you should also check that the contained widget is actually visible, or, to be precise, it's not hidden (which is not the same thing);

    All this may seem a bit counter-intuitive, but it makes sense and is also consistent with the general Qt behavior: as written above, a child widget that has been closed/hidden will (and must) not be automatically shown whenever the parent is shown again.

    A simpler and clearer approach would be to use a custom subclass for QMdiSubWindow. Two functions need to be reimplemented:

    • showEvent() should also show the child that was previously hidden by a close event;
    • eventFilter() should check for Close events call the base implementation and eventually close the subwindow too; QMdiSubWindow automatically installs its event filter within setWidget();
    class ClosableMdiSubwindow(QMdiSubWindow):
        def eventFilter(self, obj, event):
            if (
                obj == self.widget() 
                and event.type() == event.Close
                and obj.close()
            ):
                self.close()
                return True
            return super().eventFilter(obj, event)
    
        def showEvent(self, event):
            super().showEvent(event)
            if self.widget() and self.widget().isHidden():
                self.widget().show()
    

    With this class, you don't need any closeRequested signal and show/close/hide events and calls are automatically handled.

    class Educational(QWidget):
        def __init__(self):
            ...
            self.btn_close.clicked.connect(self.close)
    
    
    class MdiSubWindow(QWidget):
        ...
        def open_subwindow(self):
            page_name = self.sender().text()
            if page_name in self.open_sub_windows:
                sub_window = self.open_sub_windows[page_name]
                sub_window.show()
            elif page_name in dict_link_itemwise:
                page_widget = dict_link_itemwise[page_name]()
                sub_window = ClosableMdiSubwindow()
                sub_window.setObjectName(page_name + "_obj")
                sub_window.setWidget(page_widget)
                self.open_sub_windows[page_name] = sub_window
                self.mdi.addSubWindow(sub_window)
                sub_window.show()
            else:
                print("Invalid page class:", page_name)
    

    Note that:

    • the main else block in open_subwindow() of your latest example didn't check for the key in the class dict, so it would have had raised a KeyError for invalid names;
    • creating instance attributes in dynamic functions is almost always discouraged and rarely makes sense: it is unlikely that you will ever need the "latest" reference to page_widget, so creating it as self.page_widget is pointless;
    • while it's not strictly wrong to use a button text for functional reasons, it may be problematic considering other aspects (for instance, localization); another solution for your case would be to use QButtonGroup and assign proper ids to each button, corresponding to the related classes; alternatively, use dynamic properties: for instance, self.btn_Educational.setProperty('widgetClass', Educational), then use a dictionary that has classes as keys and instances as values, get the class with pageClass = self.sender().property('widgetClass'), check if the key/class exists in the dictionary and show it, otherwise create the new instance and add it to the dictionary;
    • you had a typo in the custom signal (closeRequsted instead of closeRequested);