PyQt5桌面应用开发(15):界面动画
PyQt5桌面应用系列
- PyQt5桌面应用开发(1):需求分析
- PyQt5桌面应用开发(2):事件循环
- PyQt5桌面应用开发(3):并行设计
- PyQt5桌面应用开发(4):界面设计
- PyQt5桌面应用开发(5):对话框
- PyQt5桌面应用开发(6):文件对话框
- PyQt5桌面应用开发(7):文本编辑+语法高亮与行号
- PyQt5桌面应用开发(8):从QInputDialog转进到函数参数传递
- PyQt5桌面应用开发(9):经典布局QMainWindow
- PyQt5桌面应用开发(10):界面布局基本支持
- PyQt5桌面应用开发(11):摸鱼也要讲基本法,两个字,16
- PyQt5桌面应用开发(12):QFile与线程安全
- PyQt5桌面应用开发(13):QGraphicsView框架
- PyQt5桌面应用开发(14):数据库+ModelView+QCharts
- PyQt5桌面应用开发(15):界面动画
界面动画
从所受教育和时间体验而言,我对界面动画没有什么感觉、也不太喜欢。但是业界老大告诉我,能用动画就用动画,不能用动画的用图片,不能用图片的用条目文字,最后的选择才是公式。
其实要看对象是谁。如果设计的软件用户,其注意力非常需要被吸引,而且他们的注意力是非常有限的,所以动画是非常有必要的。如果是开发者,那么动画就是浪费时间,因为他们的注意力是非常集中的,而且他们的注意力是可以被控制的,所以动画是非常不必要的。
好的,人身攻击的话就到此为止。我们下面来看看动画的实现,秃子嘛,除了coding什么都不会,那就好好coding,哪些伤身体的事情(泡吧、喝酒……)就交给别人去做吧。
PyQt5的动画框架
PyQt5的动画框架是QAbstractAnimation,它是一个抽象类,不能直接使用,需要使用它的子类。它的类结构如下:
- QAbstractAnimation:抽象动画,是所有动画的基类,不能直接使用。
- QVariantAnimation:值动画,用于改变控件的属性,比如改变控件的位置、大小、颜色等。
- QPropertyAnimation:属性动画,用于改变控件的属性,比如改变控件的位置、大小、颜色等。
- QAnimationGroup:动画组,可以包含多个动画,可以包含子动画组。
- QSequentialAnimationGroup:顺序动画组,按照添加的顺序依次执行动画。
- QParallelAnimationGroup:并行动画组,所有动画一起执行。
- QVariantAnimation:值动画,用于改变控件的属性,比如改变控件的位置、大小、颜色等。
我们常用的,就是三个子类。
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中。
一个例子
我们来看一个例子,这个例子是一个动画的组合,包括一个顺序动画组和一个并行动画组。这个例子的代码如下。
代码
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来设置的。
然后是一个函数,用来创建一个按钮和一个动画,最后是将这个动画添加到动画组里面。
结论
- QPropertyAnimation是一个用来改变属性的动画类。
- QPropertyAnimation的属性必须是QObject的子类。
- QPropertyAnimation的属性必须是QVariant类型。
- QPropertyAnimation的属性必须有读写的函数。
- 动画组同样也是动画,并行和串行可以相互嵌套,构成复杂的动画。