详解如何在PyQt5中实现平滑滚动的QScrollArea

 更新时间:2023年01月28日 10:07:14   作者:之一Yo  
Qt 自带的 QScrollArea 滚动时只能在两个像素节点之间跳变,看起来很突兀。所以本文将通过定时器,重写 wheelEvent() 来实现平滑滚动,需要的可以参考一下

平滑滚动的视觉效果

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内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 创建虚拟环境打包py文件的实现步骤

    创建虚拟环境打包py文件的实现步骤

    使用虚拟环境,可以为每个项目创建一个独立的Python环境,每个环境都有自己的库和版本,从而避免了依赖冲突,本文主要介绍了创建虚拟环境打包py文件的实现步骤,感兴趣的可以了解一下
    2024-04-04
  • 用Python实现的等差数列方式

    用Python实现的等差数列方式

    这篇文章主要介绍了用Python实现的等差数列方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-12-12
  • python爬取网易云音乐评论

    python爬取网易云音乐评论

    这篇文章主要为大家详细介绍了python爬取网易云音乐评论,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-11-11
  • 解决PyCharm中光标变粗的问题

    解决PyCharm中光标变粗的问题

    下面小编就为大家带来一篇解决PyCharm中光标变粗的问题。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-08-08
  • Django CBV与FBV原理及实例详解

    Django CBV与FBV原理及实例详解

    这篇文章主要介绍了Django CBV与FBV原理及实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-08-08
  • python中print的不换行即时输出的快速解决方法

    python中print的不换行即时输出的快速解决方法

    下面小编就为大家带来一篇python中print的不换行即时输出的快速解决方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考
    2016-07-07
  • Python变量和数据类型详解

    Python变量和数据类型详解

    这篇文章主要介绍了Python变量和数据类型,是Python学习当中的基础知识,需要的朋友可以参考下,希望能够给你带来帮助
    2021-10-10
  • 如何使用 Python 读取文件和照片的创建日期

    如何使用 Python 读取文件和照片的创建日期

    这篇文章主要介绍了如何使用 Python 读取文件和照片的创建日期,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-09-09
  • Python3.7 读取 mp3 音频文件生成波形图效果

    Python3.7 读取 mp3 音频文件生成波形图效果

    这篇文章主要介绍了Python3.7 读取 mp3 音频文件生成波形图小编,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-11-11
  • python获取指定网页上所有超链接的方法

    python获取指定网页上所有超链接的方法

    这篇文章主要介绍了python获取指定网页上所有超链接的方法,涉及Python使用urllib2模块操作网页抓取的技巧,非常具有实用价值,需要的朋友可以参考下
    2015-04-04

最新评论