Issue
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 ashide()
(orsetVisible(False)
), even if it actually callshide()
if the close is accepted;- calling (directly or not)
hide()
orsetVisible(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 callsclose()
on its inner widget; this is important since, by default, a QMdiSubWindow automatically created withaddSubWindow()
actually has theWA_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 forClose
events call the base implementation and eventually close the subwindow too; QMdiSubWindow automatically installs its event filter withinsetWidget()
;
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 inopen_subwindow()
of your latest example didn't check for the key in the class dict, so it would have had raised aKeyError
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 asself.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
id
s 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 withpageClass = 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 ofcloseRequested
);
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.