详解如何在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 = [ ...