When reading https://www.pythonguis.com/tutorials/creating-multiple-windows/ I may have understood the following too literally:
In Qt any widget without a parent is a window. This means, to show a new window you just need to create a new instance of a widget.
… which is why I find the behavior of the code below somewhat surprising.
In the code below, I want to be able to start/raise a QDialog window by clicking a button in the main application; and then I want to be able to start/raise yet another window from the QDialog:
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QVBoxLayout, QWidget, QDialog
from PyQt5.QtCore import Qt
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.title = QLabel("MainWindow")
self.button = QPushButton("Click for SubWindow")
self.button.clicked.connect(self.show_sub_window)
self.layout = QVBoxLayout()
self.layout.addWidget(self.title)
self.layout.addWidget(self.button)
self.central_widget = QWidget()
self.central_widget.setLayout(self.layout)
self.setCentralWidget(self.central_widget)
def show_sub_window(self):
print("show_sub_window")
self.sub_win = QDialog() # if instantiating with (self), subsubwindow ends up created behind main window, but then we cannot move main window to show it!
self.sub_win.title = QLabel("SubWindow")
self.sub_win.button = QPushButton("Click for SubSubWindow")
self.sub_win.button.clicked.connect(self.show_sub_sub_window)
self.sub_win.layout = QVBoxLayout()
self.sub_win.layout.addWidget(self.sub_win.title)
self.sub_win.layout.addWidget(self.sub_win.button)
self.sub_win.setLayout(self.sub_win.layout)
self.sub_win.exec_()
print("after sub_win.show()")
def show_sub_sub_window(self):
print("show_sub_sub_window")
self.sub_sub_win = QWidget()
self.sub_sub_win.title = QLabel("SubSubWindow")
self.sub_sub_win.button = QPushButton("(Stop clicking.)")
self.sub_sub_win.button.setEnabled(False)
self.sub_sub_win.layout = QVBoxLayout()
self.sub_sub_win.layout.addWidget(self.sub_sub_win.title)
self.sub_sub_win.layout.addWidget(self.sub_sub_win.button)
self.sub_sub_win.setLayout(self.sub_sub_win.layout)
self.sub_sub_win.show()
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()
When I execute the clicks as noted above, I get something like this:
That is, a click on “Click for SubWindow” raises SubWindow, which is modal, so it prevents interaction with MainWindow (for some reason, I always thought that modality depends on whether or not you set a parent in the QDialog constructor, but that is not the case). Then, a click on “Click for SubSubWindow” raises SubSubWindow – but interaction with this one is also disabled (actually, cannot even close it by clicking top right X)?!
So, how can I setup this code, so that SubSubWindow can be moved and interacted with independently?
It turns out, things with modality were a lot more complicated; I guess so far I’ve only had situations where I did not have to dig deeply in this. But to get a fuller picture of modality, one must consult:
- https://doc.qt.io/qtforpython-5/PySide2/QtWidgets/QDialog.html
- A modal dialog is a dialog that blocks input to other visible windows in the same application.
- Dialogs can be application modal (the default) or window modal.
- When an application modal dialog is opened, the user must finish interacting with the dialog and close it before they can access any other window in the application. Window modal dialogs only block access to the window associated with the dialog, allowing the user to continue to use other windows in an application.
- The most common way to display a modal dialog is to call its exec() function.
- An alternative is to call setModal (true) or setWindowModality(), then show(). Unlike exec(), show() returns control to the caller immediately. Calling setModal (true) is especially useful for progress dialogs, where the user must have the ability to interact with the dialog, e.g. to cancel a long running operation. If you use show() and setModal (true) together to perform a long operation, you must call processEvents() periodically during processing to enable the user to interact with the dialog.
- A modeless dialog is a dialog that operates independently of other windows in the same application. Modeless dialogs are displayed using show(), which returns control to the caller immediately.
… but also:
- https://doc.qt.io/qtforpython-5/PySide2/QtWidgets/QWidget.html
- QWidget.setWindowModality(windowModality) – This property holds which windows are blocked by the modal widget. This property only makes sense for windows. A modal widget prevents widgets in other windows from getting input. The value of this property controls which windows are blocked when the widget is visible. Changing this property while the window is visible has no effect; you must hide() the widget first, then show() it again. By default, this property is NonModal.
- Qt.WindowModality – Qt.NonModal: The window is not modal and does not block input to other windows.; Qt.WindowModal: The window is modal to a single window hierarchy and blocks input to its parent window, all grandparent windows, and all siblings of its parent and grandparent windows.; Qt.ApplicationModal – The window is modal to the application and blocks input to all windows.
So for the application I imagined with the OP example, I find WindowModal modality to be most appropriate; however do not forget “Changing this property while the window is visible has no effect; you must hide() the widget first, then show() it again.”.
So, the changes needed to make the SubSubWindow movable (and interactable), are:
# ...
def show_sub_window(self):
print("show_sub_window")
self.sub_win = QDialog() # if instantiating with (self), subsubwindow ends up created behind main window, but then we cannot move main window to show it!
self.sub_win.title = QLabel("SubWindow")
self.sub_win.button = QPushButton("Click for SubSubWindow")
self.sub_win.button.clicked.connect(self.show_sub_sub_window)
self.sub_win.layout = QVBoxLayout()
self.sub_win.layout.addWidget(self.sub_win.title)
self.sub_win.layout.addWidget(self.sub_win.button)
self.sub_win.setLayout(self.sub_win.layout)
self.setWindowModality(Qt.WindowModal) # ADDED; "Changing this property while the window is visible has no effect; you must hide() the widget first, then show() it again."
self.sub_win.show() # ADDED; ... actually, must .show() even when windows is not created yet - just to have it recognize the setWindowModality!
self.sub_win.exec_()
print("after sub_win.show()")
# ...