Skip to content Skip to sidebar Skip to footer

How Do I Open (and Close) A Pyqt5 Application Inside A Loop, And Get That Loop Running Multiple Times

The following is a loop that I created: import mainui import loginui from PyQt5 import QtWidgets import sys while True: print('test') app = QtWidgets.QApplication(sys.arg

Solution 1:

An important premise: usually you need only one QApplication instance.

Proposed solutions

In the following examples I'm using a single QApplication instance, and switch between windows using signals.

Since you probably need to wait for the window to be closed in some way, you might prefer to use a QDialog instead of a QMainWindow, but if for some reason you need the features provided by QMainWindow (menus, dockbars, etc) this is a possible solution:

class First(QtWidgets.QMainWindow):
    closed = QtCore.pyqtSignal()
    def __init__(self):
        super().__init__()
        central = QtWidgets.QWidget()
        self.setCentralWidget(central)
        layout = QtWidgets.QHBoxLayout(central)
        button = QtWidgets.QPushButton('Continue')
        layout.addWidget(button)
        button.clicked.connect(self.close)

    def closeEvent(self, event):
        self.closed.emit()


class Last(QtWidgets.QMainWindow):
    shouldRestart = QtCore.pyqtSignal()
    def __init__(self):
        super().__init__()
        central = QtWidgets.QWidget()
        self.setCentralWidget(central)
        layout = QtWidgets.QHBoxLayout(central)
        restartButton = QtWidgets.QPushButton('Restart')
        layout.addWidget(restartButton)
        closeButton = QtWidgets.QPushButton('Quit')
        layout.addWidget(closeButton)
        restartButton.clicked.connect(self.restart)
        closeButton.clicked.connect(self.close)

    def restart(self):
        self.exitFlag = True
        self.close()

    def showEvent(self, event):
        # ensure that the flag is always false as soon as the window is shown
        self.exitFlag = False

    def closeEvent(self, event):
        if self.exitFlag:
            self.shouldRestart.emit()


app = QtWidgets.QApplication(sys.argv)
first = First()
last = Last()
first.closed.connect(last.show)
last.shouldRestart.connect(first.show)
first.show()
sys.exit(app.exec_())

Note that you can add menubars to a QWidget too, by using setMenuBar(menuBar) on their layout.

On the other hand, QDialogs are more indicated for these cases, as they provide their exec_() method which has its own event loop and blocks everything else until the dialog is closed.

class First(QtWidgets.QDialog):
    def __init__(self):
        super().__init__()
        layout = QtWidgets.QHBoxLayout(self)
        button = QtWidgets.QPushButton('Continue')
        layout.addWidget(button)
        button.clicked.connect(self.accept)

class Last(QtWidgets.QDialog):
    def __init__(self):
        super().__init__()
        layout = QtWidgets.QHBoxLayout(self)
        restartButton = QtWidgets.QPushButton('Restart')
        layout.addWidget(restartButton)
        closeButton = QtWidgets.QPushButton('Quit')
        layout.addWidget(closeButton)
        restartButton.clicked.connect(self.accept)
        closeButton.clicked.connect(self.reject)

def start():
    QtCore.QTimer.singleShot(0, first.exec_)

app = QtWidgets.QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)
first = First()
last = Last()
first.finished.connect(last.exec_)
last.accepted.connect(start)
last.rejected.connect(app.quit)
start()
sys.exit(app.exec_())

Note that in this case I had to use a QTimer to launch the first dialog. This is due to the fact that in normal conditions signals wait for theirs slot to be completed before returning control to the emitter (the dialog). Since we're constantly recalling the same dialog, this leads to recursion:

  • First is executed
  • First is closed, emitting the finished signal, which causes the following:
    • Second is executed
    • at this point the finished signal has not returned yet
    • Second is accepted, emitting the accepted signal, which causes:
      • First hasn't returned its exec_() yet, but we're trying to exec it again
      • Qt crashes showing the error StdErr: QDialog::exec: Recursive call detected

Using a QTimer.singleShot ensures that the signal returns instantly, avoiding any recursion for exec_().

Ok, but why doesn't it work?

As said, only one Q[*]Application instance should usually exists for each process. This doesn't actually prevent to create more instances subsequently: in fact, your code works while it's in the first cycle of the loop.

The problem is related to python garbage collection and how PyQt and Qt deals with memory access to the C++ Qt objects, most importantly the application instance.

When you create the second QApplication, you're assigning it to a new variable (app2). At that point, the first one still exists, and will be finally deleted (by Qt) as soon as the process is completed with sys.exit. When the cycle restarts, instead, you're overwriting app, which would normally cause python to garbage collect the previous object as soon as possible. This represents a problem, as Python and Qt need to do "their stuff" to correctly delete an existing QApplication object and the python reference.

If you put the following line at the beginning, you'll see that the first time the instance is returned correctly, while the second returns None:

    app = QtWidgets.QApplication(sys.argv)
    print('Instance: ', QtWidgets.QApplication.instance())

There's a related question here on StackOverflow, and an important comment to its answer:

In principle, I don't see any reason why multiple instances of QApplication cannot be created, so long as no more than one exists at the same time. In fact, it may often be a requirement in unit-testing that a new application instance is created for each test. The important thing is to ensure that each instance gets deleted properly, and, perhaps more importantly, that it gets deleted at the right time.

A workaround to avoid the garbage collection is to add a persistent reference to the app:

apps = []
whileTrue:
    print('test')

    app = QtWidgets.QApplication(sys.argv)
    apps.append(app)

    # ...

    app2 = QtWidgets.QApplication(sys.argv)
    apps.append(app2)

But, as said, you should not create a new QApplication instance if you don't really need that (which is almost never the case).

As already noted in the comments to the question, you should never modify the files generated with pyuic (nor try to mimic their behavior). Read more about using Designer.

Post a Comment for "How Do I Open (and Close) A Pyqt5 Application Inside A Loop, And Get That Loop Running Multiple Times"