平滑滾動的視覺效果
Qt 自帶的 QScrollArea 滾動時只能在兩個像素節點之間跳變,看起來很突兀。剛開始試着用 QPropertyAnimation 來實現平滑滾動,但是效果不太理想。所以直接開了定時器,重寫 wheelEvent() 來實現平滑滾動。效果如下:

實現思路
定時器溢出是需要時間的,無法立馬處理完所有的滾輪事件,所以自己復制一個滾輪事件 lastWheelEvent,然后計算每一次滾動需要移動的距離和步數,將這兩個參數綁定在一起放入隊列中。定時器溢出時就將所有未處理完的事件對應的距離累加得到 totalDelta,每個未處理事件的步數-1,將 totalDelta 和 lastWheelEvent 作為參數傳入 QWheelEvent的構造函數,構建出真正需要的滾輪事件 e 並將其發送到 app 的事件處理隊列中,發生滾動。
具體代碼
import sys
from collections import deque
from enum import Enum
from math import cos, pi
from PyQt5.QtCore import QDateTime, Qt, QTimer, QPoint
from PyQt5.QtGui import QWheelEvent
from PyQt5.QtWidgets import QApplication, QScrollArea
class ScrollArea(QScrollArea):
""" 一個可以平滑滾動的區域 """
def __init__(self, parent=None):
super().__init__(parent)
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.COSINE)
self.smoothMoveTimer.timeout.connect(self.smoothMove)
self.setVerticalScrollMode(self.ScrollPerPixel)
def wheelEvent(self, e: QWheelEvent):
""" 實現平滑滾動效果 """
if self.smoothMode == SmoothMode.NO_SMOOTH:
super().wheelEvent(e)
return
# 將當前時間點插入隊尾
now = QDateTime.currentDateTime().toMSecsSinceEpoch()
self.scrollStamps.append(now)
while now - self.scrollStamps[0] > 500:
self.scrollStamps.popleft()
# 根據未處理完的事件調整移動速率增益
accerationRatio = min(len(self.scrollStamps) / 15, 1)
if not self.lastWheelEvent:
self.lastWheelEvent = QWheelEvent(e)
else:
self.lastWheelEvent = e
# 計算步數
self.stepsTotal = self.fps * self.duration / 1000
# 計算每一個事件對應的移動距離
delta = e.angleDelta().y() * self.stepRatio
if self.acceleration > 0:
delta += delta * self.acceleration * accerationRatio
# 將移動距離和步數組成列表,插入隊列等待處理
self.stepsLeftQueue.append([delta, self.stepsTotal])
# 定時器的溢出時間t=1000ms/幀數
self.smoothMoveTimer.start(1000 / self.fps)
def smoothMove(self):
""" 計時器溢出時進行平滑滾動 """
totalDelta = 0
# 計算所有未處理完事件的滾動距離,定時器每溢出一次就將步數-1
for i in self.stepsLeftQueue:
totalDelta += self.subDelta(i[0], i[1])
i[1] -= 1
# 如果事件已處理完,就將其移出隊列
while self.stepsLeftQueue and self.stepsLeftQueue[0][1] == 0:
self.stepsLeftQueue.popleft()
# 構造滾輪事件
e = QWheelEvent(self.lastWheelEvent.pos(),
self.lastWheelEvent.globalPos(),
QPoint(),
QPoint(0, totalDelta),
round(totalDelta),
Qt.Vertical,
self.lastWheelEvent.buttons(),
Qt.NoModifier)
# 將構造出來的滾輪事件發送給app處理
QApplication.sendEvent(self.verticalScrollBar(), e)
# 如果隊列已空,停止滾動
if not self.stepsLeftQueue:
self.smoothMoveTimer.stop()
def subDelta(self, delta, stepsLeft):
""" 計算每一步的插值 """
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):
""" 滾動模式 """
NO_SMOOTH = 0
CONSTANT = 1
LINEAR = 2
QUADRATI = 3
COSINE = 4
寫在最后
也許有人會發現動圖的界面和 Groove音樂 很像,實現代碼放在了github。如果這篇博客或者倉庫中的代碼對你有啟發的話就點個贊吧٩(๑>◡<๑)۶
