为什么PyQt有时会在退出时崩溃?

3

以下给出的代码显示一个带有4个QGraphicsViewQMainWindow,可以在其上使用鼠标进行绘制。它按预期工作,但关闭时控制台会出现以下错误消息:

Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)

代码有什么问题?


main.py

import sys

from PyQt5.QtWidgets import QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsPathItem
from PyQt5.QtGui import QPainterPath, QPen
from PyQt5.QtCore import Qt
from PyQt5.uic import loadUi

# Based on code from https://dev59.com/16Lia4cB1Zd3GeqPdRBM#44248794

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        loadUi("mainwindow.ui", self)

        self.verticalLayout_top_left.addWidget(GraphicsView())
        self.verticalLayout_top_right.addWidget(GraphicsView())
        self.verticalLayout_bottom_left.addWidget(GraphicsView())
        self.verticalLayout_bottom_right.addWidget(GraphicsView())


class GraphicsView(QGraphicsView):
    def __init__(self):
        super().__init__()
        self.start = None
        self.end = None

        self.setScene(QGraphicsScene())
        self.path = QPainterPath()
        self.item = GraphicsPathItem()
        self.scene().addItem(self.item)

        self.contents_rect = self.contentsRect()
        self.setSceneRect(0, 0, self.contents_rect.width(), self.contents_rect.height())
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

    def mousePressEvent(self, event):
        self.start = self.mapToScene(event.pos())
        self.path.moveTo(self.start)

    def mouseMoveEvent(self, event):
        self.end = self.mapToScene(event.pos())
        self.path.lineTo(self.end)
        self.start = self.end
        self.item.setPath(self.path)


class GraphicsPathItem(QGraphicsPathItem):
    def __init__(self):
        super().__init__()
        pen = QPen()
        pen.setColor(Qt.black)
        pen.setWidth(5)
        self.setPen(pen)


def main():
    app = QApplication(sys.argv)
    main_window = MainWindow()
    main_window.show()
    app.exec_()


if __name__ == "__main__":
    main()

mainwindow.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>800</width>
    <height>600</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QGridLayout" name="gridLayout">
    <item row="1" column="0">
     <widget class="QPushButton" name="pushButton_left">
      <property name="text">
       <string>PushButton</string>
      </property>
     </widget>
    </item>
    <item row="0" column="0">
     <layout class="QVBoxLayout" name="verticalLayout_top_left"/>
    </item>
    <item row="0" column="1">
     <layout class="QVBoxLayout" name="verticalLayout_top_right"/>
    </item>
    <item row="1" column="1">
     <widget class="QPushButton" name="pushButton_right">
      <property name="text">
       <string>PushButton</string>
      </property>
     </widget>
    </item>
    <item row="2" column="0">
     <layout class="QVBoxLayout" name="verticalLayout_bottom_left"/>
    </item>
    <item row="2" column="1">
     <layout class="QVBoxLayout" name="verticalLayout_bottom_right"/>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>800</width>
     <height>24</height>
    </rect>
   </property>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <resources/>
 <connections/>
</ui>

标题中带有“代码正常工作”的问题不应该包含“代码有什么问题?”这样的短语。反之亦然。 - Mad Physicist
我已经修改了标题。 - Atalanttore
2个回答

6
这是由一个长期存在的问题引起的,该问题将在即将推出的版本中得到修复(可能是PyQt-5.14)。如果您正在使用PyQt-5.13.1,您可以通过使用以下临时API来测试修复程序:
from PyQt5.QtCore import pyqt5_enable_new_onexit_scheme

pyqt5_enable_new_onexit_scheme(True)

如果没有报告问题,这将最终成为默认行为(因此无需显式启用它)。引起该问题的根本原因在此有记录:
PyQt5文档:退出时崩溃
基本上,Python的垃圾回收机制以不可预测的顺序删除对象,这有时会导致Qt尝试删除不再存在的对象(导致崩溃)。因此,问题中的示例也可以像这样修复:
def main():
    app = QApplication(sys.argv)
    main_window = MainWindow()
    main_window.show()
    app.exec_()
    # ensure correct deletion order
    del main_window, app

上述即将到来的更改意味着这种清理代码将不再必要。

很好的解释! - Atalanttore
这个问题有任何更新吗?我遇到了一个情况,添加一个调用app.processEvents()开始导致这些崩溃。@ekhumoro概述的修复方法并没有解决它。我正在Mac上运行PyQt 5.12,因为这是conda上可用的最新版本。我很快会尝试在Linux上进行测试。 - Ray Osborn
@RayOsborn 我的回答的第一部分指定了更新内容。正如我所说,第二部分中的“修复”仅适用于问题中的示例。如果您有其他问题,应该提出一个新问题(带上 [mcve]),因为没有人会在这里看到您的评论。 - ekhumoro
谢谢您的回答。我非常感激人们在这里花费时间回答问题 - 这些年来,它对我帮助很大,但如果“最小可重现示例”真的成为了一条规则,那么我认为这是一个倒退的步骤。我的应用程序有数万行代码,所以问题可能是由于它们相互交织的方式引起的。一个简单的例子并不总是可能的。人们经常来这里寻求有经验的人给他们建议,而不一定是明确解决问题。请不要阻止这样的问题。 - Ray Osborn
顺便说一下,PyQt邮件列表上的某个人向我展示了如何通过使用QApplication allWidgets函数显式关闭所有顶级窗口小部件来解决问题,因此删除主窗口然后删除QApplication并不是必要的。他曾经遇到过类似的问题,并且能够在没有MRE的情况下指导我找到解决方案。 - Ray Osborn
显示剩余2条评论

3
问题的原因是,即使在运行 app.exec_() 后,应用程序仍会释放需要访问 QApplication 实例的资源,但在您的情况下,通过使 Qt Access 的内部函数访问未保留的内存来删除“app”之前执行此操作。
考虑到上述情况,可能的解决方案是通过创建一个全局变量,延长“app”的作用域,以便在执行主函数后不会将其删除。
# ...
<b>app = None</b>


def main():
    <b>global app</b>
    app = QApplication(sys.argv)
    # ...

这是我第一次在PyQt中遇到这个错误。为什么在这段代码中"app"被过早地移除了? - Atalanttore
@Atalanttore 因为“app”是一个局部变量。 - eyllanesc
对于我大多数其他的PyQt5应用程序,“app”也是main()函数内的一个本地变量,这些应用程序可以关闭而不会在控制台中出现任何错误消息。这段代码有什么不同之处? - Atalanttore
1
@Atalanttore 或许在这些情况下,没有创建需要释放时间的资源,因此分析问题所在将是分析 Qt 的私有 API,但这需要太多时间,因此我的解决方案是我通常用作解决方法的绕过方式。 - eyllanesc
1
有趣。我在Linux上无法重现这种行为(我必须说,使用相当旧的版本,Python 3.4和PyQt 5.7.1)。sys.exit(app.exec())是否改变了结果? - musicamante
2
@musicamante 我可以在Linux(Arch Linux)上使用PyQt5 5.13.2在Python3.7/Python3.8中重现这个问题。 - eyllanesc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接