如何在循环中打开(和关闭)PyQt5 应用程序,并让该循环多次运行

Posted

技术标签:

【中文标题】如何在循环中打开(和关闭)PyQt5 应用程序,并让该循环多次运行【英文标题】:How do I open (and close) a PyQt5 application inside a loop, and get that loop running multiple times 【发布时间】:2021-11-09 05:28:58 【问题描述】:

以下是我创建的循环:

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 窗口的循环,它们按顺序显示。不包含在循环中的窗口正常运行,并且在循环的第一次迭代中也运行良好。

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

任何关于为什么窗口不显示的建议,以及如何让它们显示将不胜感激。

【问题讨论】:

为什么需要多次启动应用程序?如果您需要按顺序显示窗口,那肯定不是一个好方法。另外,我的印象是您可能在 mainui 和 loginui 文件中做了其他一些非常错误的事情:如果这些脚本是由 pyuic 生成的并且您进行了修改(这是不应该永远完成)问题可能来自那里。您可以与我们分享这些文件,但我建议您首先回答我的第一个问题。 @musicamante 感谢您的回答。 mainui 文件中的一个窗口中有一个用于注销的按钮。我试图通过再次运行程序来实现它,以便它从登录页面开始。现在想想,这似乎是一种非常愚蠢的方法,但在我的辩护中,我几个月前才开始学习 Python。我很可能在其余文件中犯了很多错误。 mainui 和 loginui 文件在某些​​部分是由 pyuic5 生成的,但后来我向它添加了很多方法来真正让 ui 做一些事情。我会按您的要求添加这些文件。 【参考方案1】:

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

建议的解决方案

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

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

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

请注意,您也可以将菜单栏添加到 QWidget,方法是在其布局上使用 setMenuBar(menuBar)

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

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 来启动第一个对话框。这是因为在正常情况下,信号会在将控制权返回给发射器(对话框)之前等待其插槽完成。由于我们不断地调用同一个对话框,这会导致递归:

首先执行 First 关闭,发出finished 信号,导致以下情况: 第二次执行 此时finished 信号尚未返回 Second 被接受,发出accepted 信号,导致: First 尚未返回其exec_(),但我们正在尝试再次执行它 Qt 崩溃显示错误StdErr: QDialog::exec: Recursive call detected

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

好的,但是为什么不起作用?

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

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

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

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

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

*** 上有一个 related question,对其answer 有一个重要评论:

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

避免垃圾收集的解决方法是添加对应用程序的持久引用:

apps = []
while True:
    print('test')

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

    # ...

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

但是,如前所述,如果您不真的需要创建一个新的 QApplication 实例(这几乎是从不案例)。

正如问题的 cmets 中已经指出的那样,您应该永远不要修改使用 pyuic 生成的文件(也不要试图模仿它们的行为)。阅读更多关于using Designer的信息。

【讨论】:

感谢您花时间写出如此详细的答案。我会尝试实现这一点,但就目前而言,我的 pyuic5 基础似乎非常薄弱,需要彻底重组。

以上是关于如何在循环中打开(和关闭)PyQt5 应用程序,并让该循环多次运行的主要内容,如果未能解决你的问题,请参考以下文章

为啥在 PyQt5 中打开新窗口时我的应用程序会关闭?

在 PyQt5 中打开一个窗口和关闭一个窗口

如何要求用户在 Pyqt5 中输入图像 [关闭]

如何在 PyQt5 中同时读取和写入文件时正确执行多线程?

单击文件名时 PyQt5 QFileDialog 关闭

如何在这个 pyqt5 窗口中使用 while 循环