如何在循环内打开(和关闭)PyQt5 应用程序,并使该循环多次运行 [英] How do I open (and close) a PyQt5 application inside a loop, and get that loop running multiple times

查看:134
本文介绍了如何在循环内打开(和关闭)PyQt5 应用程序,并使该循环多次运行的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

以下是我创建的循环:

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()

这是一个包含几个按顺序显示的 PyQt5 窗口的循环.当窗口不包含在循环中时,它们会正常运行,并且它们在循环的第一次迭代中也运行得很好.

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.

但是,当满足重复条件时,即使循环确实进行了迭代(再次打印 'test') - ui 和 ui2 窗口不会再次显示,随后程序遇到退出条件并停止.

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.

推荐答案

一个重要的前提:通常你只需要一个 QApplication 实例.

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

在以下示例中,我使用单个 QApplication 实例,并使用信号在窗口之间切换.

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

由于您可能需要以某种方式等待窗口关闭,因此您可能更喜欢使用 QDialog 而不是 QMainWindow,但如果出于某种原因您需要 QMainWindow 提供的功能(菜单、停靠栏等)这是一个可能的解决方案:

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_())

请注意,您也可以使用 setMenuBar(menuBar) 在他们的布局上.

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

另一方面,QDialogs 更适用于这些情况,因为它们提供了 exec_() 方法,该方法具有自己的事件循环并阻止其他所有内容,直到对话框关闭.

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_())

请注意,在这种情况下,我必须使用 QTimer 来启动第一个对话框.这是因为在正常情况下,信号会在将控制权返回给发射器(对话)之前等待其插槽完成.由于我们不断地回忆同一个对话,这会导致递归:

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 关闭,发出 finished 信号,这会导致以下情况:
    • 第二个被执行
    • 此时finished信号还没有返回
    • 第二个被接受,发出 accepted 信号,这会导致:
      • First 还没有返回它的 exec_(),但我们正在尝试再次执行它
      • Qt 崩溃显示错误 StdErr: QDialog::exec: Recursive call detected
      • 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

          使用 QTimer.singleShot 确保信号立即返回,避免 exec_() 的任何递归.

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

          如前所述,通常每个进程应该只存在一个 Q[*]Application 实例.这实际上并不会阻止随后创建更多实例:事实上,您的代码在循环的第一个循环中工作.

          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.

          问题与 python 垃圾收集以及 PyQt 和 Qt 如何处理对 C++ Qt 对象(最重要的是应用程序实例)的内存访问有关.

          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.

          当您创建第二个 QApplication 时,您将其分配给一个 new 变量 (app2).那时,第一个仍然存在,并且一旦该过程完成并使用 sys.exit 最终将被删除(由 Qt).相反,当循环重新开始时,您正在覆盖 app,这通常会导致 python 尽快垃圾收集前一个对象.
          这代表了一个问题,因为 Python 和 Qt 需要做他们的事情"正确删除现有的 QApplication 对象和 python 引用.

          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.

          如果你把下面这行放在开头,你会看到第一次正确返回实例,而第二次返回None:

          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())
          

          StackOverflow 上有一个相关问题,以及对其答案:

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

          原则上,我看不出为什么不能创建多个 QApplication 实例,只要同时存在的实例不超过一个即可.事实上,单元测试中的要求通常可能是为每个测试创建一个新的应用程序实例.重要的是确保每个实例都被正确删除,也许更重要的是,它会在正确的时间被删除.

          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)
          

          但是,如上所述,如果您不需要真的(几乎从不),您不应该创建一个新的 QApplication 实例情况).

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

          正如在问题的评论中已经指出的,您应该永远修改用 pyuic 生成的文件(也不应该尝试模仿它们的行为).详细了解使用 Designer.

          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.

          这篇关于如何在循环内打开(和关闭)PyQt5 应用程序,并使该循环多次运行的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆