如何在 pyqt 中实现平滑滚动的 QScrollArea
平滑滚动的视觉效果
Qt 自带的 QScrollArea
滚动时只能在两个像素节点之间跳变,看起来很突兀。刚开始试着用 QPropertyAnimation
来实现平滑滚动,但是效果不太理想。所以直接开了定时器,重写 wheelEvent()
来实现平滑滚动。效果如下:
实现思路
定时器溢出是需要时间的,无法立马处理完所有的滚轮事件,所以自己复制一个滚轮事件 lastWheelEvent
,然后计算每一次滚动需要移动的距离和步数,将这两个参数绑定在一起放入队列中。定时器溢出时就将所有未处理完的事件对应的距离累加得到 totalDelta
,每个未处理事件的步数-1,将 totalDelta
和 lastWheelEvent
作为参数传入 QWheelEvent
的构造函数,构建出真正需要的滚轮事件 e
并将其发送到 app
的事件处理队列中,发生滚动。
具体代码
# coding:utf-8
from collections import deque
from enum import Enum
from math import cos, pi
from PyQt5.QtCore import QDateTime, Qt, QTimer, QPoint, pyqtSignal
from PyQt5.QtGui import QWheelEvent
from PyQt5.QtWidgets import QApplication, QScrollArea
class ScrollArea(QScrollArea):
""" A scroll area which can scroll smoothly """
def __init__(self, parent=None, orient=Qt.Vertical):
"""
Parameters
----------
parent: QWidget
parent widget
orient: Orientation
scroll orientation
"""
super().__init__(parent)
self.orient = orient
self.fps = 60
self.duration = 400
self.stepsTotal = 0
self.stepRatio = 1.5
self.acceleration = 1
self.lastWheelEvent = None
self.scrollStamps = deque()
self.stepsLeftQueue = deque()
self.smoothMoveTimer = QTimer(self)
self.smoothMode = SmoothMode(SmoothMode.LINEAR)
self.smoothMoveTimer.timeout.connect(self.__smoothMove)
def setSmoothMode(self, smoothMode):
""" set smooth mode """
self.smoothMode = smoothMode
def wheelEvent(self, e):
if self.smoothMode == SmoothMode.NO_SMOOTH:
super().wheelEvent(e)
return
# push current time to queque
now = QDateTime.currentDateTime().toMSecsSinceEpoch()
self.scrollStamps.append(now)
while now - self.scrollStamps[0] > 500:
self.scrollStamps.popleft()
# adjust the acceration ratio based on unprocessed events
accerationRatio = min(len(self.scrollStamps) / 15, 1)
if not self.lastWheelEvent:
self.lastWheelEvent = QWheelEvent(e)
else:
self.lastWheelEvent = e
# get the number of steps
self.stepsTotal = self.fps * self.duration / 1000
# get the moving distance corresponding to each event
delta = e.angleDelta().y() * self.stepRatio
if self.acceleration > 0:
delta += delta * self.acceleration * accerationRatio
# form a list of moving distances and steps, and insert it into the queue for processing.
self.stepsLeftQueue.append([delta, self.stepsTotal])
# overflow time of timer: 1000ms/frames
self.smoothMoveTimer.start(1000 / self.fps)
def __smoothMove(self):
""" scroll smoothly when timer time out """
totalDelta = 0
# Calculate the scrolling distance of all unprocessed events,
# the timer will reduce the number of steps by 1 each time it overflows.
for i in self.stepsLeftQueue:
totalDelta += self.__subDelta(i[0], i[1])
i[1] -= 1
# If the event has been processed, move it out of the queue
while self.stepsLeftQueue and self.stepsLeftQueue[0][1] == 0:
self.stepsLeftQueue.popleft()
# construct wheel event
if self.orient == Qt.Vertical:
p = QPoint(0, totalDelta)
bar = self.verticalScrollBar()
else:
p = QPoint(totalDelta, 0)
bar = self.horizontalScrollBar()
e = QWheelEvent(
self.lastWheelEvent.pos(),
self.lastWheelEvent.globalPos(),
QPoint(),
p,
round(totalDelta),
self.orient,
self.lastWheelEvent.buttons(),
Qt.NoModifier
)
# send wheel event to app
QApplication.sendEvent(bar, e)
# stop scrolling if the queque is empty
if not self.stepsLeftQueue:
self.smoothMoveTimer.stop()
def __subDelta(self, delta, stepsLeft):
""" get the interpolation for each step """
m = self.stepsTotal / 2
x = abs(self.stepsTotal - stepsLeft - m)
res = 0
if self.smoothMode == SmoothMode.NO_SMOOTH:
res = 0
elif self.smoothMode == SmoothMode.CONSTANT:
res = delta / self.stepsTotal
elif self.smoothMode == SmoothMode.LINEAR:
res = 2 * delta / self.stepsTotal * (m - x) / m
elif self.smoothMode == SmoothMode.QUADRATI:
res = 3 / 4 / m * (1 - x * x / m / m) * delta
elif self.smoothMode == SmoothMode.COSINE:
res = (cos(x * pi / m) + 1) / (2 * m) * delta
return res
class SmoothMode(Enum):
""" Smooth mode """
NO_SMOOTH = 0
CONSTANT = 1
LINEAR = 2
QUADRATI = 3
COSINE = 4
写在最后
也许有人会发现动图的界面和 Groove音乐 很像,实现代码放在了github。如果这篇博客或者仓库中的代码对你有启发的话就点个赞吧٩(๑>◡<๑)۶