1

语境

我有一个自定义小部件,它应该制作点移动的动画,以制作一种加载小部件。为了实现这个目标,我开始使用 QPainter 和 QVariantAnimation 对象,这似乎是完成这项工作的不错工具。问题是我认为我在绘图时初始化的 QPainter 相互冲突。

技术

为了实现这一点,我初始化了多个 QVariantAnimation,它表示 .valueChanged() 我连接到一个函数 update(),它应该启动 painEvent(),例如文档中所写

绘制事件是重新绘制全部或部分小部件的请求。它可能由于以下原因之一发生:调用了 repaint() 或 update(),小部件被遮挡并且现在已被发现,或许多其他原因。

由于我在不同的时间启动不同的动画,我认为 update() 被调用了很多次,从而干扰了另一个已经在工作的 QPainter。但是,正如我在文档中所读到的,

当 update() 被多次调用或窗口系统发送多个绘制事件时,Qt 将这些事件合并为一个具有更大区域的事件。

但它没有指定 QPainter 具有相同的区域,这就是我认为它崩溃的原因。它记录以下消息:

QBackingStore::endPaint() called with active painter on backingstore paint device

最小的工作示例

from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QDialog, QPushButton
from PyQt5.QtCore import Qt, pyqtSlot, QVariantAnimation, QVariant, QTimer
from PyQt5.QtGui import QColor, QPainter, QBrush
import time

class Dialog(QDialog):
    def __init__(self, *args, **kwargs):
        QDialog.__init__(self, *args, **kwargs)
        self.resize(500, 500)
        self.setLayout(QVBoxLayout())
        self.button = QPushButton()
        self.layout().addWidget(self.button)
        self.paintWidget = PaintWidget()
        self.layout().addWidget(self.paintWidget)
        self.button.clicked.connect(self.paintWidget.startPainting)
        self.button.clicked.connect(self.reverse)

    def reverse(self):
        if self.paintWidget.isMoving:
            self.paintWidget.stopPainting()

class PaintWidget(QWidget):
    def __init__(self):
        super(PaintWidget, self).__init__()
        self.dotRadius = 10
        self.dotColor = QColor(255, 100, 100)
        self.numberOfDots = 3

        self.isMoving = False

        self.animation = []
        self.createAnimation()

        self.dotPosition = [[0, 0], [0, 0], [0, 0]]

    def startPainting(self):
        for i in range(self.numberOfDots):
            self.animation[i].start()
            time.sleep(200)
        self.isActive = True

    def createAnimation(self):
        for i in range(self.numberOfDots):
            self.animation.append(QVariantAnimation(self, startValue=0, endValue=500, duration=3000))
            self.animation[i].valueChanged.connect(self.updatePosition)

    @pyqtSlot(QVariant)
    def updatePosition(self, position):
        self.dotPosition = [position, 0]
        self.update()

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.fillRect(self.rect(), Qt.transparent)
        painter.setRenderHint(QPainter.Antialiasing, True)
        painter.setPen(Qt.NoPen)

        for i in range(self.numberOfDots):
            painter.save()
            painter.translate(0, 0)
            position = (self.dotPosition[i][0], self.dotPosition[i][1])
            color = self.dotColor
            painter.setBrush(QBrush(color, Qt.SolidPattern))
            painter.drawEllipse(position[0], position[1], self.dotRadius, self.dotRadius)
            painter.restore()


if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    dial = Dialog()
    dial.show()
    sys.exit(app.exec_())

结果

我知道现在这段代码不起作用,因为我无法检索更新了哪个点的动画,但我相信这里的主要问题是画家之间的干扰。因此,谁能告诉我为什么会发生这种情况并指出一个潜在的解决方案?另外,为了知道更新的点并选择了好的位置,我真的不确定如何做到这一点。

4

1 回答 1

2

您的代码有以下错误:

  • 切勿在主 GUI 线程中使用 time.sleep(),因为它会阻塞导致应用程序冻结的事件循环。

  • 变量 dotPosition 必须存储您要替换它的所有位置,在 updatePosition 方法中只有一个位置。

  • 如果要存储位置而不是列表,则应该使用 QPoint,使用列表还不错,但是使用 QPoint 会使代码更具可读性。

  • 不要不必要地使用painter.save() 和painter.restore(),也不要使用painter.translate()。

考虑到上述情况,解决方案如下:

from functools import partial
from PyQt5 import QtCore, QtGui, QtWidgets


class Dialog(QtWidgets.QDialog):
    def __init__(self, *args, **kwargs):
        super(Dialog, self).__init__(*args, **kwargs)
        self.resize(500, 500)

        self.button = QtWidgets.QPushButton()
        self.paintWidget = PaintWidget()

        self.button.clicked.connect(self.paintWidget.startPainting)

        lay = QtWidgets.QVBoxLayout(self)
        lay.addWidget(self.button)
        lay.addWidget(self.paintWidget)


class PaintWidget(QtWidgets.QWidget):
    def __init__(self):
        super(PaintWidget, self).__init__()
        self.dotRadius = 10
        self.dotColor = QtGui.QColor(255, 100, 100)

        self.animations = []

        self.dotPosition = [
            QtCore.QPoint(0, 0),
            QtCore.QPoint(0, 0),
            QtCore.QPoint(0, 0),
        ]

        self.createAnimation()

    def startPainting(self):
        for i, animation in enumerate(self.animations):
            QtCore.QTimer.singleShot(i * 200, animation.start)

    def createAnimation(self):
        for i, _ in enumerate(self.dotPosition):
            wrapper = partial(self.updatePosition, i)
            animation = QtCore.QVariantAnimation(
                self,
                startValue=0,
                endValue=500,
                duration=3000,
                valueChanged=wrapper,
            )
            self.animations.append(animation)

    @QtCore.pyqtSlot(int, QtCore.QVariant)
    def updatePosition(self, i, position):
        self.dotPosition[i] = QtCore.QPoint(position, 0)
        self.update()

    def paintEvent(self, event):
        painter = QtGui.QPainter(self)
        painter.fillRect(self.rect(), QtCore.Qt.transparent)
        painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
        painter.setPen(QtCore.Qt.NoPen)
        painter.setBrush(
            QtGui.QBrush(self.dotColor, QtCore.Qt.SolidPattern)
        )

        for position in self.dotPosition:    
            painter.drawEllipse(position, self.dotRadius, self.dotRadius)


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    dial = Dialog()
    dial.show()
    sys.exit(app.exec_())
于 2019-06-09T17:44:57.553 回答