如何在pyqt中实现平滑滚动的QScrollArea
平滑滚动的视觉效果
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。如果这篇博客或者仓库中的代码对你有启发的话就点个赞吧٩(๑>◡<๑)۶
如何在pyqt中实现平滑滚动的QScrollArea的更多相关文章
- 页面中的平滑滚动——smooth-scroll.js的使用
正常的本页面锚链接跳转的时候跟PPT似的,特别生硬,用户体验非常差. 这时候我们就可以借助smooth-scroll.js这个插件,来实现本页面的平滑的跳转. 1首先,导入必须的JS文件 <sc ...
- 如何在pyqt中自定义无边框窗口
前言 之前写过很多关于无边框窗口并给窗口添加特效的博客,按照时间线罗列如下: 如何在pyqt中实现窗口磨砂效果 如何在pyqt中实现win10亚克力效果 如何在pyqt中通过调用SetWindowCo ...
- 如何在pyqt中实现窗口磨砂效果
磨砂效果的实现思路 这两周一直在思考怎么在pyqt上实现窗口磨砂效果,网上搜了一圈,全都是 C++ 的实现方法.正好今天查python的官方文档的时候看到了 ctypes 里面的 HWND,想想倒不如 ...
- 如何在pyqt中实现win10亚克力效果
亚克力效果的实现思路 上一篇博客<如何在pyqt中实现窗口磨砂效果> 中实现了win7中的Aero效果,但是和win10的亚克力效果相比,Aero还是差了点内味.所以今天早上又在网上搜了一 ...
- 如何在pyqt中通过调用 SetWindowCompositionAttribute 实现Win10亚克力效果
亚克力效果 在<如何在pyqt中实现窗口磨砂效果>和<如何在pyqt中实现win10亚克力效果>中,我们调用C++ dll来实现窗口效果,这种方法要求电脑上必须装有MSVC.V ...
- 如何在pyqt中在实现无边框窗口的同时保留Windows窗口动画效果(一)
无边框窗体的实现思路 在pyqt中只要 self.setWindowFlags(Qt.FramelessWindowHint) 就可以实现边框的去除,但是没了标题栏也意味着窗口大小无法改变.窗口无法拖 ...
- 如何在pyqt中给无边框窗口添加DWM环绕阴影
前言 在之前的博客<如何在pyqt中通过调用SetWindowCompositionAttribute实现Win10亚克力效果>中,我们实现了窗口的亚克力效果,同时也用SetWindowC ...
- 如何在pyqt中实现带动画的动态QMenu
弹出菜单的视觉效果 QLineEdit 原生的菜单弹出效果十分生硬,而且样式很丑.所以照着Groove中单行输入框弹出菜单的样式和动画效果写了一个可以实现动态变化Item的弹出菜单,根据剪贴板的内容是 ...
- 如何在 pyqt 中捕获并处理 Alt+F4 快捷键
前言 如果在 Windows 系统的任意一个窗口中按下 Alt+F4,默认行为是关闭窗口(或者最小化到托盘).对于使用了亚克力效果的窗口,使用 Alt+F4 最小化到托盘,再次弹出窗口的时候可能出现亚 ...
随机推荐
- Mysterious For(hdu4373)
Mysterious For Time Limit: 4000/2000 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Others)To ...
- 用户线程&&守护线程
守护线程是为用户线程服务的,当一个程序中的所有用户线程都执行完成之后程序就会结束运行,程序结束运行时不会管守护线程是否正在运行,由此我们可以看出守护线程在 Java 体系中权重是比较低的.当 ...
- Differential Evolution: A Survey of the State-of-the-Art
@ 目录 概 主要内容 DE/rand/1/bin DE/?/?/? DE/rand/1/exp DE/best/1 DE/best/2 DE/rand/2 超参数的选择 的选择 的选择 的选择 一些 ...
- MySQL数据操作与查询笔记 • 【第3章 DDL 和 DML】
全部章节 >>>> 本章目录 3.1 使用 DDL 定义数据库表结构 3.1.1 SQL 简介 3.1.2 维护数据库和创建数据表 3.2 使用 DDL 维护数据库表结构 ...
- js tab栏切换
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" ...
- 在安装pdfplumber时报错 Microsoft Visual C++ 14.0 is required.
在安装pdfplumber时报下列错误: 解决方法: 更新pip ,因为pip 版本太旧 来自为知笔记(Wiz)
- 在 CentOS 7 上安装和配置 Puppet
1 准备 2台 centos7 (master/server:192.168.1.103 agent/client:192.168.1.106) 分别添加puppet自定义仓库 https://yum ...
- 聊聊docker那些端口问题
今天来系统聊一聊docker的端口,常见的有容器内程序端口.容器端口.主机端口.Dockerfile中EXPOSE端口.docker-compose和docker run中的port等. 貌似很多端口 ...
- Python常用功能函数系列总结(二)
本节目录 常用函数一:sel文件转换 常用函数二:refwork文件转换 常用函数三:xml文档解析 常用函数四:文本分词 常用函数一:sel文件转换 sel是种特殊的文件格式,具体应用场景的话可以 ...
- SYCOJ304末尾0的个数
https://oj.shiyancang.cn/Problem/304.html 首先数据范围不可能算出来的,那么就要看数的性质. 0是怎么来的首先我们知道,有一个0,就必然会有一个5和2. n!在 ...