0

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.argv)
    ui = loginui.Ui_MainWindow()
    ui.setupUi()
    ui.MainWindow.show()
    app.exec_()

    username=ui.username

    app2 = QtWidgets.QApplication(sys.argv)
    ui2 = mainui.Ui_MainWindow(username)
    ui2.setupUi()
    ui2.MainWindow.show()
    app2.exec_()

    if ui2.exitFlag=='repeat':#Repeat Condition  
        continue
    else:                     #Exit Condition
        sys.exit()

This is a loop containing a couple of PyQt5 windows, which are displayed in order. The windows run normally when they are not contained within a loop, and they also run pretty well in the first iteration of the loop.

But, when the repeat condition is satisfied, even though the loop does iterate (prints 'test' again) - the ui and ui2 windows do not get displayed again, and subsequently the program hits the exit condition and stops.

Any suggestions about why the windows do not get displayed, and how I can get them displayed would be very much appreciated.

elementalneil
  • 73
  • 1
  • 8
  • 2
    Why do you need to start the application more than once? If you need to display windows in sequence, that's certainly not a good approach. Also, I'm under the impression that you've probably done something else very wrong in the mainui and loginui files: if those scripts were generated by pyuic and you modified (which is something that should **never** be done) it's possible that the issue comes from there. You could share those files with us, but I suggest you to answer my first question to begin with. – musicamante Oct 18 '20 at 11:57
  • @musicamante Thanks for the answer. There is a button in one of the windows in the mainui file to logout. I tried to implement that by running the program over again, so that it starts from the login page. It does seem like a very silly approach now that I think about it, but in my defence, I started learning Python mere months ago. It's very possible that I made a lot of mistakes in the rest of the files. The mainui and loginui files were in some part, generated by pyuic5, but then I added a lot of methods to it to actually get the ui to do stuff. I will add those files as you requested. – elementalneil Oct 21 '20 at 10:58

1 Answers1

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 = []
while True:
    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.

musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thanks for taking the effort write such a detailed answer. I would try to implement this, but as it stands, it seems that my pyuic5 basics is seriously weak, and would need a thorough restructuring. – elementalneil Oct 21 '20 at 11:05