【PyQt】PyQt的缺陷及意外退出的可能原因

1 背景

最近在软件项目中比较深入的用到了 Qt GraphicsView Framework。PyQt 作为 Qt 的非官方Python绑定库(Qt的官方Python绑定库是PySide)总是出现很多意外崩溃/Bug,并且很难调试和追踪。

2 问题

软件项目中需要自定义 QGraphicsItem ,该图元能在某些条件下 改变自己的几何形状,比如从三角形变成五角星。改变形状倒是没有任何难点,无非就是改变它的 path,再 drawPath 即可。

但是在利用重新实现的旋转和缩放交互(改变图元的几何形状)后,再从 scene 中删除该图元(使用scene.removeItem()函数)时,程序就会直接崩溃或者卡死,PyCharm调试窗口没有任何的输出和提示,用try...except将可疑代码包裹起来也无法调试,也是直接崩溃退出。

3 线索

最后在互联网上搜索了许久,也翻看 Qt 文档很久,才查到一些蛛丝马迹。

在 Qt 文档中,有 QGraphicsItem的 boundingRect() 函数,该函数说明中有如下一段话:

If you want to change the item's bounding rectangle, you must first call prepareGeometryChange(). This notifies the scene of the imminent change, so that it can update its item geometry index; otherwise, the scene will be unaware of the item's new geometry, and the results are undefined (typically, rendering artifacts are left within the view).

翻译过来就是:

若想改变图元的边界矩形,必须首先调用 prepareGeometryChange() 函数。该函数会通知 scene 即将来临的改变,所以 scene 能够更新它的图元几何索引(?没看到Qt 哪里讲到什么 item geometry index,看到的朋友评论里告诉我);否则,scene 将无法知道图元的新几何形状,此时结果是无法预料的(一种典型的结果就是:渲染的残影(伪影)会被遗留在视图 view 中)。

这里面提到:若想改变图元的边界矩形,必须首先调用 prepareGeometryChange() 函数。说明程序崩溃的原因,可能就是未调用 prepareGeometryChange() 函数。

该函数的用法如下(Qt官方文档代码):

 void CircleItem::setRadius(qreal newRadius)
 {
     if (radius != newRadius) {
         prepareGeometryChange();
         radius = newRadius;
     }
 }

用了此函数,并没有解决问题,最后的最后(苦苦摸索两个星期),最后发现了一个解决方案,但具体原因不明。

解决方案:

设置QGraphicsScene的属性itemIndexMethod 为 NoIndex。

案例代码如下,其中GvScene为自定义的Scene,为了修改Scene默认背景色。 

class GvScene(QGraphicsScene):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setBackgroundBrush(QColor(10,10,10))    # 背景色
        self.setSceneRect(0, 0, 1920, 1080)          # 区域大小
        self.setItemIndexMethod(QGraphicsScene.NoIndex)     # 特别关键

Qt官方文档对其描述:

itemIndexMethod : ItemIndexMethod

This property holds the item indexing method.

QGraphicsScene applies an indexing algorithm to the scene, to speed up item discovery functions like items() and itemAt(). Indexing is most efficient for static scenes (i.e., where items don't move around). For dynamic scenes, or scenes with many animated items, the index bookkeeping can outweight the fast lookup speeds.

For the common case, the default index method BspTreeIndex works fine. If your scene uses many animations and you are experiencing slowness, you can disable indexing by calling setItemIndexMethod(NoIndex).

Access functions:

QGraphicsScene::ItemIndexMethod

itemIndexMethod() const

void

setItemIndexMethod(QGraphicsScene::ItemIndexMethod method)

See also bspTreeDepth.

ItemIndexMethod 的说明:

enum QGraphicsScene::ItemIndexMethod

This enum describes the indexing algorithms QGraphicsScene provides for managing positional information about items on the scene.

Constant

Value

Description

QGraphicsScene::BspTreeIndex

0

A Binary Space Partitioning tree is applied. All QGraphicsScene's item location algorithms are of an order close to logarithmic complexity, by making use of binary search. Adding, moving and removing items is logarithmic. This approach is best for static scenes (i.e., scenes where most items do not move).

QGraphicsScene::NoIndex

-1

No index is applied. Item location is of linear complexity, as all items on the scene are searched. Adding, moving and removing items, however, is done in constant time. This approach is ideal for dynamic scenes, where many items are added, moved or removed continuously.

翻译如下:

此枚举变量描述的是 QGraphicsScene 为管理Scene中的图元的位置信息而提供的索引算法。

BspTreeIndex:使用二叉空间剖分树(BSP树)作为索引。所有的QGraphicsScene的图元的定位算法通过使用二分查找可接近对数级复杂度(即O(logN))。增加、移动、删除图元也是对数级。这个方法最适合静态的scene(scene内的图元不移动)。

NoIndex:不使用索引。图元定位是线性复杂度,因为要检索scene中的所有图元。但是增加、移动和删除图元立刻就可完成。这个方法是动态的scene的理想方法,动态的scene经常需要连续的增加、移动和删除大量的图元。

看上面的官方文档说明,确实是应该使用 NoIndex,但是为何默认使用 BspTreeIndex 时,程序会直接崩溃呢?原因不明。

4 教训

在找到 解决方案:

设置QGraphicsScene的属性itemIndexMethod 为 NoIndex

之前,我搜索了很多文章资料,有很多可能导致程序崩溃的原因,于是我逐一做了修改,但都没有起到关键性的作用(没有阻止程序崩溃),但也可能有作用,记录如下。

4.1 自定义图元的boundingRect

在官方文档中,官方特意强调 boundingRect  要包含 Pen 的宽度的一半。

Make sure to constrain all painting inside the boundaries of boundingRect() to avoid rendering artifacts (as QGraphicsView does not clip the painter for you). In particular, when QPainter renders the outline of a shape using an assigned QPen, half of the outline will be drawn outside, and half inside, the shape you're rendering (e.g., with a pen width of 2 units, you must draw outlines 1 unit inside boundingRect()). QGraphicsItem does not support use of cosmetic pens with a non-zero width.

必须确保将所有的绘制的东西都包含在boundingRect() 内,这样可以避免出现残影(因为 QGraphicsView 并不会自动替你裁剪图形)。

特别要强调的是,当 QPainter 使用被设置的QPen来渲染几何形状(shape)的轮廓(outline)时,轮廓的一半将被画到几何形状(shape)的里面,一半会在几何形状(shape)的外面(举例来说,如果设置了2单位宽度的画笔,那么你必须在图元的 boudingRect()函数中画出1像素的轮廓来)。

QGraphicsItem不支持使用非零宽度的修饰画笔。

引自Qt5官方文档

其含义见图即明:

如图,红色矩形框  即为  shape ,即图元的逻辑几何形状;灰色矩形中空框 即为 outline ,也就是图元的 shape 的轮廓。

 我之前偷懒直接用 自定义图元的path.boundingRect作为 图元的 boundingRect。后来改成了:

    
def boundingRect(self):
    path = self.Path        # 此处path为图元的path
    pathRect = path.boundingRect()
    itemRect = pathRect.adjusted(-2, -2, 2, 2)    # 这里的2根据你的画笔宽度而定
                                     # 至少为画笔宽度的1/2,也可偷懒直接用画笔宽度
    return itemRect

# adjusted()的4个参数依次加到 QRect的四个坐标值上。
# adjusted(-2, -2, 2, 2)表示矩形的左上角点向左和向上各移动2像素,
# 右下角点向右和向下各移动2像素,
# 矩形长度增长4,高度增长4

注意: adjusted() 函数暗藏风险,切勿错误理解 adjusted()函数,adjusted()函数详细解释见此文。

4.2 自定义图元几何结构变化前调用prepareGeometryChange()

前文提到prepareGeometryChange()函数的作用,此处不再赘述。

在这里说明,prepareGeometryChange()函数最好放置在 改变图元几何结构的函数的内部第一行最妥当。

4.3 自定义图元的paint()函数

自定义图元在paint()函数中绘制自身。paint()函数的三个参数:painter, qstyleopt, widget,根据我实操得知,painter 是同一个自定义图元类的所有对象 共用的 ! widget 也是 同一个自定义图元类的所有对象 共用的 !

比如,你创建了一个自定义图元:苹果图元类 AppleItem,然后在Scene上添加10个苹果(对象)。此时,这10个苹果(对象)的 paint()函数中的 painter 都是同一个(可以在paint函数中print(painter)查看),这10个苹果(对象)的widget也是同一个。

既然是大家共用同一个painter,那不得乱了套。所以最好在 paint 函数中,设置 painter 前,先调用 painter.save() ,将当前的 painter 状态暂存起来,等着此次用完再恢复 painter.restore()。

You can at any time save the QPainter's state by calling the save() function which saves all the available settings on an internal stack. The restore() function pops them back.

如果 paint 中出现 for 循环, for循环中不断的setPen和setBrush(),那就更加需要调用 save()和 restore()。

虽然 Qt5 文档中,不需要显式的调用 save和restore,但是非要直接调用它,肯定不会错。

这样,大家各不耽误,就像借别人电脑,先把别人正在处理的文件保存好,再使用电脑做自己的事情,用完电脑,再把别人的文件都重新恢复,别人可以继续处理他的文件。

    def paint(self, painter, qstyleopt, widget):
        painter.save()                # 暂存当前状态
        painter.setPen(YourPen)       # 处理自己的事情
        painter.setBrush(YourBrush)
        painter.drawPath(path)
        painter.restore()             # 恢复开始的状态

5 其它收获

在解决该问题的过程中,收获到一篇文章,讲到 PyQt 的缺陷和避免BUG的经验,是这篇文章启发了我,特转载如下。

这篇文章可能是外网文章,由机器翻译过来的,无所谓,可以看懂其意思。适合研究较深入的人了解。

以下为原文,转载自:

关于python:避免PyQt崩溃/挂起的好的做法是什么?https://www.codenong.com/11945183/


<转载文章开始>

关于python:避免PyQt崩溃/挂起的好的做法是什么?


What are good practices for avoiding crashes / hangs in PyQt?

我同时喜欢python和Qt,但是对我来说很明显Qt并不是在设计时就考虑到python的。 有很多方法可以使PyQt / PySide应用程序崩溃,即使使用适当的工具,也很难调试很多方法。

我想知道:使用PyQt和PySide时避免崩溃和锁定的良好实践是什么? 从常规的编程技巧和支持模块到高度特定的解决方法和应避免的错误,这些都可以。

 相关讨论

  • 嘿,我已经看到您发布了很多有关pyqt的信息,我想知道您是否可以解决这个问题:/ stackoverflow.com/questions/57298291/


通用编程实践

  • 如果必须使用多线程代码,则永远不要从非GUI线程访问GUI。总是通过发出信号或其他线程安全机制将消息发送到GUI线程。
  • 注意"模型/查看任何内容"。 TableView,TreeView等。它们很难正确编程,并且任何错误都会导致无法跟踪的崩溃。使用模型测试可帮助确保模型内部一致。
  • 了解Qt对象管理与Python对象管理交互的方式以及可能出错的情况。参见http://python-camelot.s3.amazonaws.com/gpl/release/pyqt/doc/advanced/development.html

    • 没有父级的Qt对象由Python拥有;只有Python可以删除它们。
    • 带有父对象的Qt对象归Qt所有,如果删除了它们的父对象,则Qt将删除它们。
    • 示例:带有PyQt4的核心转储
  • QObject通常不应引用其父代或其任何祖先(可以使用弱引用)。这将最多导致内存泄漏,并偶尔导致崩溃。
  • 请注意Qt自动删除对象的情况。如果未通知python包装器删除了C ++对象,则对其进行访问将导致崩溃。由于PyQt和PySide在跟踪Qt对象方面遇到困难,因此可以通过许多不同的方式发生这种情况。

    • 复合控件,例如QScrollArea及其滚动条,QSpinBox及其QLineEdit等。(Pyside没有此问题)
    • 删除QObject会自动删除其所有子级(不过PyQt通常会正确处理此子级)。
    • 从QTreeWidget中删除项目将导致所有关联的小部件(通过QTreeWidget.setItemWidget设置)被删除。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27

      # Example:
      from PyQt4 import QtGui, QtCore
      app = QtGui.QApplication([])

      # Create a QScrollArea, get a reference to one of its scroll bars.
      w = QtGui.QWidget()
      sa = QtGui.QScrollArea(w)
      sb = sa.horizontalScrollBar()

      # Later on, we delete the top-level widget because it was removed from the
      # GUI and is no longer needed
      del w

      # At this point, Qt has automatically deleted all three widgets.
      # PyQt knows that the QScrollArea is gone and will raise an exception if
      # you try to access it:
      sa.parent()
      Traceback (most recent call last):
        File"<stdin>", line 1, in <module>
      RuntimeError: underlying C/C++ object has been deleted

      # However, PyQt does not know that the scroll bar has also been deleted.
      # Since any attempt to access the deleted object will probably cause a
      # crash, this object is 'toxic'; remove all references to it to avoid
      # any accidents
      sb.parent()
      # Segmentation fault (core dumped)

具体的解决方法/错误

  • 在不先调用prepareGeometryChange()的情况下更改QGraphicsItems的边界可能会导致崩溃
  • 在QGraphicsItem.paint()内部引发异常可能导致崩溃。始终在paint()中捕获异常并显示一条消息,而不是让异常继续进行。
  • QGraphicsItems永远不要保留对其生活的QGraphicsView的引用。(可以使用弱引用)。
  • 重复使用QTimer.singleShot可能会导致锁定。
  • 避免将QGraphicsView与QGLWidget一起使用。

避免出口崩溃的实践

  • 不属于QGraphicsScene的QGraphicsItem可能导致退出时崩溃。
  • 引用其父级或任何祖先的QObject可能导致退出崩溃。
  • 没有父级的QGraphicsScene可能导致退出崩溃。
  • 避免退出崩溃的最简单方法是在python开始收集Qt对象之前调用os._exit()。但是,这可能很危险,因为程序的某些部分可能依赖于正确的退出处理才能正确运行(例如,终止日志文件或正确关闭设备句柄)。至少,应该在调用os._exit()之前手动调用atexit回调。

 相关讨论

  • 您对QObject层次结构提出的几项主张值得高度质疑。与PyQt或PySide本身的问题相比,它们听起来更像是程序错误的程序-但是很难看到真实的例子。您给出的唯一特定示例完全是人工的-为什么创建QScrollArea然后立即将其丢弃?您是否向PyQt和/或PySide的维护者提出了这些问题?如果您没有,在这里记录它们似乎毫无意义。
  • 您是正确的,其中许多问题是程序设计错误的结果,而不是pyqt / pyside错误(我无意暗示)。但是,我在此列表中提出的每个问题都至少来自我在自己的应用程序中遇到的一个实际错误。他们中的许多人花费了数小时的调试才能解决,因此需要像这样的综合清单。我举的例子是人为的,是的,但这仅仅是因为我简化了它。不难想象会导致崩溃的实际情况。
  • 我为您的部分添加了一些有关删除C ++对象的额外示例...但是,我仍然觉得总体而言,使用ScrollWidget来完成它是一个糟糕的示例,因为这永远不会发生。我认为应该完全重新编辑它,以简单地显示python和c ++所有权ob对象之间的区别。
  • 该示例并非旨在涉及对象所有权-删除Qt对象有很多合理的理由。关键是PyQt和PySide在跟踪其Qt对象方面并不完美。如果出现问题,将导致崩溃。更大的一点是,如果我们了解导致这些崩溃的原因,则可以通过更仔细的编程实践完全避免它们。


仅供参考,我将PyQt的作者对路加的回答发表评论:"这是垃圾。"

我认为这很重要,因为有人可以跳到这篇文章,并对所有这些(不存在的)"问题"感到困惑。

 相关讨论

  • 作为一般建议,很多建议都具有误导性。它的确突出了一些重要的问题,但是许多建议的"解决方案"实际上比他们试图解决的所谓问题更糟。
  • 其可编辑;欢迎您修复它。这些是我多年来修复与PyQt相关的分段错误的观察结果。我肯定那里列出的许多项目已经修复,或者有更复杂的原因,而我从未完全意识到。


补充一点:

如果必须在基于qt的程序中使用线程,则实际上必须禁用自动垃圾收集器并在主线程上进行手动收集(如http://pydev.blogspot.com.br/2014/03/should- python-garbage-collector-be.html)-请注意,即使确保对象没有循环也应该这样做(通过循环,基本上可以使对象处于活动状态,直到python循环垃圾收集器崩溃为止,但是有时,如果您有例外情况,则可能会使某个帧保持活动状态,因此,在这种情况下,您的对象可能仍保持活动状态的时间比您预期的长)。在这种情况下,垃圾收集器可能会撞到在辅助线程中,这可能导致qt发生段错误(必须始终将qt小部件收集在主线程中)。

 相关讨论

  • 啊,我什至从未考虑过G??C!
  • 这里唯一要注意的是,如果您在带有qt小部件的python中创建循环,则gc.collect()仍然有可能崩溃(因为删除顺序可能会出错-可能会出现段错误-这可以认为是PySide中的错误,不幸的是并不罕见)。


<转载文章结束> 

参考资料:

1. QGraphicsItem删除item崩溃_QtHalcon的博客-CSDN博客_qgraphicsitem delete

 使用QGraphicsScene的setItemIndexMethod(QGraphicsScene::NoIndex);解决残影问题。

2. Qt工作笔记-QGraphics框架场景中图元的移除与析构_IT1995的博客-CSDN博客

 clear与removeItem的区别。

3. QGraphicsItem在组删除时从场景中消失

如何从QGraphicsItemGroup中解散图元并添加到场景中。

4. prepareGeometryChange()引发的崩溃_KATA~的博客-CSDN博客_preparegeometrychange

从程序崩溃推导出需要调用prepareGeometryChange()函数。文章结论:由于在自定义图元移动或者boundingrect变化时,没有提前调用prepareGeometryChange(),导致场景内可能存在无效的item索引,在绘图过程中引发崩溃。

5. Qt 解决报错:QBackingStore::endPaint() called with active painter_0ices的博客-CSDN博客

该文章的的解决方案123经我实操在PyQt5中无效,可以参考尝试。另外可以试试本文提到的解决方案就是,成对出现的painter.save()和painter.restore()。

<全文结束>

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qilei2010

送我一张彩票中了平分

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值