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

相关文章

  • python rsa-oaep加密的示例代码

    python rsa-oaep加密的示例代码

    这篇文章主要介绍了python rsa-oaep加密示例的示例代码,帮助大家更好的利用python加解密,感兴趣的朋友可以了解下
    2020-09-09
  • 使用python 写一个静态服务(实战)

    使用python 写一个静态服务(实战)

    今天小编就为大家分享一篇使用python 写一个静态服务(实战),具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-06-06
  • Python中Yield的基本用法及Yield与return的区别解析

    Python中Yield的基本用法及Yield与return的区别解析

    Python中有一个非常有用的语法叫做生成器,用到的关键字就是yield,这篇文章主要介绍了Python中Yield的基本用法及Yield与return的区别,需要的朋友可以参考下
    2022-10-10
  • Python图像处理Pillow库的安装使用

    Python图像处理Pillow库的安装使用

    本文详细介绍了Python第三方库Pillow的使用,通过导入Pillow库、打开和保存图像、基本图像操作以及图像处理高级功能的代码示例,我们了解了Pillow库的强大功能和灵活性,感兴趣的朋友跟随小编一起看看吧
    2023-07-07
  • Python学习_几种存取xls/xlsx文件的方法总结

    Python学习_几种存取xls/xlsx文件的方法总结

    今天小编就为大家分享一篇Python学习_几种存取xls/xlsx文件的方法总结,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-05-05
  • python升级pip及失败处理方式

    python升级pip及失败处理方式

    这篇文章主要介绍了python升级pip及失败处理方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-02-02
  • Python实现ElGamal加密算法的示例代码

    Python实现ElGamal加密算法的示例代码

    ElGamal加密算法是一个基于迪菲-赫尔曼密钥交换的非对称加密算法。这篇文章通过示例代码给大家介绍Python实现ElGamal加密算法的相关知识,感兴趣的朋友一起看看吧
    2020-06-06
  • Python 内置模块 argparse快速入门教程

    Python 内置模块 argparse快速入门教程

    argparse模块是Python内置的用于命令项选项与参数解析的模块,argparse模块可以让人轻松编写用户友好的命令行接口,能够帮助程序员为模型定义参数,这篇文章主要介绍了快速入门Python内置模块argparse,需要的朋友可以参考下
    2023-06-06
  • 浅谈Python 函数式编程

    浅谈Python 函数式编程

    这篇文章主要介绍了Python 函数式编程的相关资料,文中讲解非常细致,代码帮助大家更好的理解和学习,感兴趣的朋友可以了解下
    2020-06-06
  • python-tornado的接口用swagger进行包装的实例

    python-tornado的接口用swagger进行包装的实例

    今天小编就为大家分享一篇python-tornado的接口用swagger进行包装的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-08-08

最新评论