Make an animated wave with drawPolyline in PySide/

2019-03-16 19:21发布

问题:

I'm trying to animate a polyline (it have to act like a wave). I've tried this way:

from PySide.QtCore import *
from PySide.QtGui import *
import sys, time

class Test(QMainWindow):
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)

    def poly(self, pts):
        return QPolygonF(map(lambda p: QPointF(*p), pts))

    def paintEvent(self, event):
        painter = QPainter(self)

        pts = [[80, 490], [180, 0], [280, 0], [430, 0], [580, 0], [680, 0], [780, 0]]

        for i in pts:
            while i[1] < 600:

                painter.setPen(QPen(QColor(Qt.darkGreen), 3))

                painter.drawPolyline(self.poly(pts))

                painter.setBrush(QBrush(QColor(255, 0, 0)))
                painter.setPen(QPen(QColor(Qt.black), 1))

                for x, y in pts:
                    painter.drawEllipse(QRectF(x - 4, y - 4, 8, 8))

                i[1] += 1
                print pts
                time.sleep(0.0025)
                self.update()

if __name__ == '__main__':
    example = QApplication(sys.argv)
    test2 = Test()
    test2.resize(800, 600)
    test2.show()
    sys.exit(example.exec_())

But, it's not working! There is a mess on the screen, when the program runs. It seems, that self.update() doesn't update window. Please, help.

回答1:

There are a few issues with this code, obviously. I will list everything I notice, and then go through the explanations:

  1. doing far too much processing in a paintEvent
  2. doing a sleep inside of that paintEvent (bad)
  3. calling self.update() while inside of a paintEvent

Alright. A paint event is where the widget wants to redraw and should be as fast as possible. You should not be doing anything recursive in this event, or taking too much time as it will slow down your draw. Also, calling update() while inside your event is potentially recursive. The goal of the paint event should be to respond to the current state of the widget, paint, and get out.

Here is a modified version of your code that works. Its not the most ideal approach, but I will explain that more below...

from PySide.QtCore import *
from PySide.QtGui import *
import sys, time

class Test(QMainWindow):
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        self.pts = [[80, 490], [180, 0], [280, 0], [430, 0], [580, 0], [680, 0], [780, 0]] 

    def poly(self, pts):
        return QPolygonF(map(lambda p: QPointF(*p), pts))

    def paintEvent(self, event):
        painter = QPainter(self)

        pts = self.pts[:]

        painter.setPen(QPen(QColor(Qt.darkGreen), 3))
        painter.drawPolyline(self.poly(pts))

        painter.setBrush(QBrush(QColor(255, 0, 0)))
        painter.setPen(QPen(QColor(Qt.black), 1))

        for x, y in pts:
            painter.drawEllipse(QRectF(x - 4, y - 4, 8, 8))

        # print pts

    def wave(self):

        for point in self.pts:
            while point[1] < 600:
                point[1] += 1
                self.update()               
                QApplication.processEvents()
                time.sleep(0.0025)


if __name__ == '__main__':
    example = QApplication(sys.argv)
    test2 = Test()
    test2.resize(800, 600)
    test2.show()
    test2.raise_()
    test2.wave()
    sys.exit(example.exec_())

Notice that the points have been moved to a member attribute, self.pts, and the paintEvent() now only paints the current state of the points. Then, the animation logic is moved to another method called wave(). In this method, it loops and modifies each point and calls update() to trigger the redraw. Note we are calling update() outside of the paintEvent. This is important because should any other events occur in your application that cause the window to redraw (resizing, etc), you paintEvent could have looped forever.

So we modify this point list, sleep, and an important addition it to call QApplication.processEvents(). Normally, events are processed when the application becomes idle (leaves the current call). Because you are calling a repaint constantly, and stacking these events up, you need to tell the event loop to go ahead and flush everything through. Try commenting out that processEvents() command and see what happens. Your app would simply spin doing nothing until the loop is complete, and the resulting line will pop into place.

Now for the part where I was suggesting this isn't really the most ideal approach, though it works as an example. This current example blocks the main thread while it is performing a wave. You should always avoid blocking the main thread as its meant purely to respond to GUI events. So here are some possible suggestions:

  1. You could create a QTimer using the 0.0025 animation speed as a timeout. Connect the timeout() signal to a version of the wave() method that performs a single step and calls update. No sleep needed here anymore. Once your wave calculations have reached the end, you would check for that in wave() and call stop() on the timer.

  2. Move the entire wave() loop and initial dataset from the example above into a QThread. This QThread would emit a custom signal like waveUpdate(QPolygonF). When you start this thread it would do the loop, and handle creating the QPolygonF and on each step it would emit the signal and sleep. You could connect this signal to a method on your main window that would receive the new Polygon, assign it to self._polygon, and call update(), which would then just grab self._polygon and paint it. The idea here is to move as much of the heavy lifting as possible into the thread, and only tell your main GUI thread to repaint with new values.