詳解如何在PyQt5中實(shí)現(xiàn)平滑滾動(dòng)的QScrollArea
平滑滾動(dòng)的視覺效果
Qt 自帶的 QScrollArea 滾動(dòng)時(shí)只能在兩個(gè)像素節(jié)點(diǎn)之間跳變,看起來很突兀。剛開始試著用 QPropertyAnimation 來實(shí)現(xiàn)平滑滾動(dòng),但是效果不太理想。所以直接開了定時(shí)器,重寫 wheelEvent() 來實(shí)現(xiàn)平滑滾動(dòng)。效果如下:

實(shí)現(xiàn)思路
定時(shí)器溢出是需要時(shí)間的,無法立馬處理完所有的滾輪事件,所以自己復(fù)制一個(gè)滾輪事件 lastWheelEvent,然后計(jì)算每一次滾動(dòng)需要移動(dòng)的距離和步數(shù),將這兩個(gè)參數(shù)綁定在一起放入隊(duì)列中。定時(shí)器溢出時(shí)就將所有未處理完的事件對(duì)應(yīng)的距離累加得到 totalDelta,每個(gè)未處理事件的步數(shù)-1,將 totalDelta 和 lastWheelEvent 作為參數(shù)傳入 QWheelEvent的構(gòu)造函數(shù),構(gòu)建出真正需要的滾輪事件 e 并將其發(fā)送到 app 的事件處理隊(duì)列中,發(fā)生滾動(dòng)。
具體代碼
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最后
也許有人會(huì)發(fā)現(xiàn)動(dòng)圖的界面和 Groove音樂 很像,實(shí)現(xiàn)代碼放在了github。
到此這篇關(guān)于詳解如何在PyQt5中實(shí)現(xiàn)平滑滾動(dòng)的QScrollArea的文章就介紹到這了,更多相關(guān)PyQt5 QScrollArea內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用python 寫一個(gè)靜態(tài)服務(wù)(實(shí)戰(zhàn))
今天小編就為大家分享一篇使用python 寫一個(gè)靜態(tài)服務(wù)(實(shí)戰(zhàn)),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-06-06
Python中Yield的基本用法及Yield與return的區(qū)別解析
Python中有一個(gè)非常有用的語法叫做生成器,用到的關(guān)鍵字就是yield,這篇文章主要介紹了Python中Yield的基本用法及Yield與return的區(qū)別,需要的朋友可以參考下2022-10-10
Python學(xué)習(xí)_幾種存取xls/xlsx文件的方法總結(jié)
今天小編就為大家分享一篇Python學(xué)習(xí)_幾種存取xls/xlsx文件的方法總結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-05-05
Python實(shí)現(xiàn)ElGamal加密算法的示例代碼
ElGamal加密算法是一個(gè)基于迪菲-赫爾曼密鑰交換的非對(duì)稱加密算法。這篇文章通過示例代碼給大家介紹Python實(shí)現(xiàn)ElGamal加密算法的相關(guān)知識(shí),感興趣的朋友一起看看吧2020-06-06
Python?內(nèi)置模塊?argparse快速入門教程
argparse模塊是Python內(nèi)置的用于命令項(xiàng)選項(xiàng)與參數(shù)解析的模塊,argparse模塊可以讓人輕松編寫用戶友好的命令行接口,能夠幫助程序員為模型定義參數(shù),這篇文章主要介紹了快速入門Python內(nèi)置模塊argparse,需要的朋友可以參考下2023-06-06
python-tornado的接口用swagger進(jìn)行包裝的實(shí)例
今天小編就為大家分享一篇python-tornado的接口用swagger進(jìn)行包裝的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-08-08

