一个重要的前提:通常你只需要一个 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())
StackOverflow 上有一个 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的信息。