Search code examples
pythonpyqtpyqt5positioningqmainwindow

How can I detect when one window occludes another in PyQt5?


I'm using PyQt5 to create an app with multiple main windows. I want to be able to allow the user to save and load window sizes and window positions. That's easy with, e.g., QMainWindow.saveGeometry() and QMainWindow.loadGeometry() or the corresponding .saveState() and .loadState() variants. These work great for position and size, but if the user moves or resizes one window so that it occludes another, I want to also restore this positioning. I don't mind writing my own code to save the info for each window, but I can't see any way to detect the relative Z order of windows. Am I missing it in the docs, or is this not possible?

To see what I mean, try this:

from PyQt5.QtWidgets import QApplication, QMainWindow, QPlainTextEdit
from PyQt5.QtCore import QSettings
from PyQt5.QtGui import QCloseEvent

'''
context: Linux Mint 19.3 Tricia x86_64 
         Python 3.9
         PyQt5 5.15.1
'''


class RememberWin(QMainWindow):
    def __init__(self, win_name: str):
        super(RememberWin, self).__init__()
        self.win_name = win_name
        self.setWindowTitle(win_name)
        self.can_close = False

    def restore_window(self) -> bool:
        try:
            settings = QSettings("PyQtExamples", "RememberWinTest")
            self.restoreGeometry(settings.value(f'{self.win_name} Geometry'))
            self.restoreState(settings.value(f'{self.win_name} State'))
            return True
        except:
            return False

    def closeEvent(self, event: QCloseEvent):
        if not self.can_close:
            event.ignore()
        else:
            settings = QSettings("PyQtExamples", "RememberWinTest")
            settings.setValue(f'{self.win_name} Geometry', self.saveGeometry())
            settings.setValue(f'{self.win_name} State', self.saveState())
            QMainWindow.closeEvent(self, event)


class ControlWindow(RememberWin):
    def __init__(self, win_name: str = "ControlWindow"):
        super(ControlWindow, self).__init__(win_name=win_name)
        self.can_close = True

        self.window1 = RememberWin(win_name='WindowOne')
        self.window2 = RememberWin(win_name='WindowTwo')

        self.text = QPlainTextEdit(self)
        s = "Try making Window1 wide enough to cover Window2.\n" \
            "Then close this window (auto closes others).\n" \
            "Re-run the app and you'll notice that Window2\n" \
            "is not on top of Window1 which means that this\n" \
            "info isn't getting saved."

        self.text.setPlainText(s)
        self.setCentralWidget(self.text)

        if not self.restore_window():
            self.setGeometry(100, 390, 512, 100)
        if not self.window1.restore_window():
            self.window1.setGeometry(100, 100, 512, 384)
        if not self.window2.restore_window():
            self.window2.setGeometry(622, 100, 512, 384)

        self.window1.show()
        self.window2.show()

    def closeEvent(self, event: QCloseEvent):
        for win in (self.window1, self.window2):
            win.can_close = True
            win.close()
        super(ControlWindow, self).closeEvent(event)


if __name__ == '__main__':
    import sys

    app = QApplication(sys.argv)

    window = ControlWindow(win_name='ControlWindow (You can only close this one)')
    window.show()

    sys.exit(app.exec_())

Solution

  • The simplest way to do what you want to achieve is to keep track of the current focused widget, or, to be precise, the top level window of the last focused widget.

    You can store the focused windows in the settings as a list, using a unique objectName for each window (you are already doing this, so you just need to use setObjectName()), then restore the window by showing them in the correct order as long as the object name matches.

    class RememberWin(QMainWindow):
        def __init__(self, win_name: str):
            super(RememberWin, self).__init__()
            self.win_name = win_name
            self.setObjectName(win_name)
            self.setWindowTitle(win_name)
            self.can_close = False
    
        # ...
    
    
    class ControlWindow(RememberWin):
        def __init__(self, win_name: str = "ControlWindow"):
            # ...
            self.settings = QSettings("PyQtExamples", "RememberWinTest")
            self.zOrder = []
            QApplication.instance().focusObjectChanged.connect(self.focusChanged)
    
            windowOrder = self.settings.value('windowOrder', type='QStringList')
            topLevelWindows = QApplication.topLevelWidgets()
            if windowOrder:
                for objName in windowOrder:
                    for win in topLevelWindows:
                        if win.objectName() == objName:
                            win.show()
            else:
                self.window1.show()
                self.window2.show()
    
        def focusChanged(self, obj):
            if not obj or obj.window() == self.window():
                return
            if obj.window() in self.zOrder[:-1]:
                self.zOrder.remove(obj.window())
            self.zOrder.append(obj.window())
    
        def closeEvent(self, event: QCloseEvent):
            for win in (self.window1, self.window2):
                win.can_close = True
                win.close()
            self.settings.setValue('windowOrder', 
                [w.window().objectName() for w in self.zOrder])
            super(ControlWindow, self).closeEvent(event)