详解如何在PyQt5中实现平滑滚动的QScrollArea
平滑滚动的视觉效果
Qt 自带的QScrollArea滚动时只能在两个像素节点之间跳变,看起来很突兀。刚开始试着用QPropertyAnimation来实现平滑滚动,但是效果不太理想。所以直接开了定时器,重写wheelEvent()来实现平滑滚动。效果如下:
实现思路
定时器溢出是需要时间的,无法立马处理完所有的滚轮事件,所以自己复制一个滚轮事件lastWheelEvent,然后计算每一次滚动需要移动的距离和步数,将这两个参数绑定在一起放入队列中。定时器溢出时就将所有未处理完的事件对应的距离累加得到totalDelta,每个未处理事件的步数-1,将totalDelta和lastWheelEvent作为参数传入QWheelEvent的构造函数,构建出真正需要的滚轮事件e并将其发送到app的事件处理队列中,发生滚动。
具体代码
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。
关于详解如何在PyQt5中实现平滑滚动的QScrollArea的文章就介绍至此,更多相关PyQt5 QScrollArea内容请搜索编程宝库以前的文章,希望以后支持编程宝库!
tkinter改变下拉列表(Combobox)的选项值定义下拉列表:# 此处省略父容器的定义 ... # 定义下拉列表选项值集合self.Combo5List = [ ...