PyQt5桌面应用系列

界面动画

从所受教育和时间体验而言,我对界面动画没有什么感觉、也不太喜欢。但是业界老大告诉我,能用动画就用动画,不能用动画的用图片,不能用图片的用条目文字,最后的选择才是公式。

其实要看对象是谁。如果设计的软件用户,其注意力非常需要被吸引,而且他们的注意力是非常有限的,所以动画是非常有必要的。如果是开发者,那么动画就是浪费时间,因为他们的注意力是非常集中的,而且他们的注意力是可以被控制的,所以动画是非常不必要的。

好的,人身攻击的话就到此为止。我们下面来看看动画的实现,秃子嘛,除了coding什么都不会,那就好好coding,哪些伤身体的事情(泡吧、喝酒……)就交给别人去做吧。

PyQt5的动画框架

PyQt5的动画框架是QAbstractAnimation,它是一个抽象类,不能直接使用,需要使用它的子类。它的类结构如下:

  • QAbstractAnimation:抽象动画,是所有动画的基类,不能直接使用。
    • QVariantAnimation:值动画,用于改变控件的属性,比如改变控件的位置、大小、颜色等。
      • QPropertyAnimation:属性动画,用于改变控件的属性,比如改变控件的位置、大小、颜色等。
    • QAnimationGroup:动画组,可以包含多个动画,可以包含子动画组。
      • QSequentialAnimationGroup:顺序动画组,按照添加的顺序依次执行动画。
      • QParallelAnimationGroup:并行动画组,所有动画一起执行。

我们常用的,就是三个子类。

QPropertyAnimation

这个类的作用就是在一个Qt属性上定义一段动画。比如,我们可以在一个按钮上定义一个动画,让它的位置从(0, 0)移动到(100, 100)
。那么这里的属性就是按钮的位置,动画的起始值是(0, 0),结束值是(100, 100)
。这个属性在PyQt5里面定义的方式是采用@pyqtProperty(type signiture)@property_name.setter
函数修饰语法。前者定义了这个属性的取值和名称(就是python函数的名称),后者定义了赋值函数。采用这个方法唯一的要求,就是这个对象是QObject的子类。对于哪些不是QObject的对象,我们要额外定义一个QObject的子类,然后通过某种方式在赋值函数中将这个值传递给我们的对象,去更新对象的状态。

当我们定义好了属性之后,我们就可以使用QPropertyAnimation来定义动画了。

from PyQt5.QtCore import QPropertyAnimation, QRect
from PyQt5.QtWidgets import QPushButton

but = QPushButton("Animation")
animation = QPropertyAnimation(but, b'geometry', parent=None)

animation.setStartValue(QRect(0, 0, 100, 30))
animation.setEndValue(QRect(250, 250, 100, 30))
animation.setDuration(3000)
animation.start()

这里唯一需要注意的是,在定义中,那个属性的名称必须是Union[QByteArray, bytes, bytearray]
,实际上是ASCII字符串,在Python中,可以用b'geometry'
来表示。或者把字符串转换成ASCII码,比如bytes('geometry', encoding='utf-8')'geometry'.encode('utf-8')这类。

QAnimationGroup

这个类的两个子类,一个是QSequentialAnimationGroup,一个是QParallelAnimationGroup。前者是顺序执行动画,后者是并行执行动画。这两个类的使用方法是一样的,只是执行的方式不同。这两个类本身也是QAbstractAnimation(的子类),所以可以相互组合,形成比较复杂的动画。

比如有一系列动画,用QSequentialAnimationGroup来执行,那么这些动画就是顺序执行的,所有这些动画共享一个背景动画,那么可以把每个动画和背景动画用QParallelAnimationGroup来执行,然后把并行动画加入到串行动画里。

pyqtProperty与插值

其实定义Qt Property在Python里面非常简单,只需要使用@pyqtProperty(type signiture)@property_name.setter
。其实好玩的是,这个插值的过程。

QVariantAnimation大概是这样处理插值过程的,用一个QVariant来表达各种值,然后提供一个注册插值函数的接口,每种具体的类型,调用插值函数,就可以得到插值的结果。这个插值函数的接口是这样的:

void QVariantAnimation::registerInterpolator(QVariantAnimation::Interpolator func, int interpolationType)
{
    // will override any existing interpolators
    QInterpolatorVector *interpolators = registeredInterpolators();
    // When built on solaris with GCC, the destructors can be called
    // in such an order that we get here with interpolators == NULL,
    // to continue causes the app to crash on exit with a SEGV
    if (interpolators) {
        const auto locker = qt_scoped_lock(registeredInterpolatorsMutex);
        if (interpolationType >= interpolators->size())
            interpolators->resize(interpolationType + 1);
        interpolators->replace(interpolationType, func);
    }
}

当前,Qt内置了一些类的插值函数:

  • Int
  • UInt
  • Double
  • Float
  • QLine
  • QLineF
  • QPoint
  • QPointF
  • QSize
  • QSizeF
  • QColor
  • QRectF
  • QRect

如果你需要插值其他的类型,包括自定义类型,你必须自己实现插值。你可以注册一个插值函数,这个函数有三个参数:起始值,结束值,当前的进度。

最后就是一个动画的播放速度的,这个在Qt中称为QEasyCurve,它定义了在动画的播放过程中,时间和进度的关系。Qt内置了一些常用的曲线,比如:

  • Linear:线性
  • InQuad:初始点附近二次方
  • OutQuad:结束点二次方
  • InOutQuad:二次方

当然,还可以自己定义曲线,并注册到Qt中。

一个例子

我们来看一个例子,这个例子是一个动画的组合,包括一个顺序动画组和一个并行动画组。这个例子的代码如下。

PyQt5桌面应用开发(15):界面动画-LMLPHP

代码

import sys

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *


class ParentBackgroundAnimation(QObject):

    def __init__(self, parent: QWidget = None):
        super(ParentBackgroundAnimation, self).__init__(parent)
        self._color = QColor(Qt.transparent)

    @pyqtProperty(QColor)
    def color(self):
        return self._color

    @color.setter
    def color(self, color):
        self._color = color
        self.update()

    def update(self):
        if self.parent is not None:
            win: QWidget = self.parent()
            win.setPalette(QPalette(self._color))


def make_animation_button(win: QWidget, layout: QLayout, animation_group: QAnimationGroup, params):
    x0, y0, w0, h0, x1, y1, w1, h1, curve, label, time_ms = params
    button = QPushButton(label, win)
    if layout is not None:
        layout.addWidget(button)

    animation = QPropertyAnimation(button, b"geometry")
    animation.setDuration(time_ms)
    animation.setStartValue(QRect(x0, y0, w0, h0))
    animation.setEndValue(QRect(x1, y1, w1, h1))

    animation.setEasingCurve(curve)

    button.clicked.connect(animation_group.start)

    animation_group.addAnimation(animation)

    return button, animation


def color_animation(win, animation_group):
    ti = ParentBackgroundAnimation(win)

    animation = QPropertyAnimation(ti, b"color")
    animation.setDuration(2000)
    animation.setStartValue(QColor(255, 0, 0, 100))
    animation.setEndValue(QColor(0, 255, 0, 100))

    animation_group.addAnimation(animation)


if __name__ == "__main__":
    app = QApplication([])

    win = QWidget()

    layout = None

    animation_group = QSequentialAnimationGroup()

    curves = [QEasingCurve.Linear, QEasingCurve.OutQuint, QEasingCurve.OutBounce, QEasingCurve.OutElastic,
              QEasingCurve.OutBack, QEasingCurve.OutExpo]
    curve_labels = ["Linear", "OutQuint", "OutBounce", "OutElastic", "OutBack", "OutExpo"]

    for i, (c, l) in enumerate(zip(curves, curve_labels)):
        sub_animation_group = QParallelAnimationGroup()
        make_animation_button(win, layout, sub_animation_group, (
            100 + i * 200, 10, 150, 80, 100 + i * 200, 600, 150, 80, c, l, 2000
        ))
        color_animation(win, sub_animation_group)
        animation_group.addAnimation(sub_animation_group)

    animation_group.start()

    win.setWindowTitle("Animate buttons.")

    win.resize(1400, 700)

    win.show()

    sys.exit(app.exec_())

代码解析

首先是一个自定义的动画类,这个类的作用是改变父窗口的背景颜色。

class ParentBackgroundAnimation(QObject):

    def __init__(self, parent: QWidget = None):
        super(ParentBackgroundAnimation, self).__init__(parent)
        self._color = QColor(Qt.transparent)

    @pyqtProperty(QColor)
    def color(self):
        return self._color

    @color.setter
    def color(self, color):
        self._color = color
        self.update()

    def update(self):
        if self.parent is not None:
            win: QWidget = self.parent()
            win.setPalette(QPalette(self._color))

这里面有一个update函数,这个函数的作用是更新父窗口的背景颜色。注意QWdiget的背景颜色是通过QPalette来设置的。

然后是一个函数,用来创建一个按钮和一个动画,最后是将这个动画添加到动画组里面。

结论

  1. QPropertyAnimation是一个用来改变属性的动画类。
  2. QPropertyAnimation的属性必须是QObject的子类。
  3. QPropertyAnimation的属性必须是QVariant类型。
  4. QPropertyAnimation的属性必须有读写的函数。
  5. 动画组同样也是动画,并行和串行可以相互嵌套,构成复杂的动画。
05-18 19:56