Issue
I want to show a spinner while a task is being performed in my pyqt5 application. I found this nice implementation of a spinner, so I tried it: https://github.com/z3ntu/QtWaitingSpinner
The demo works ok, but in the demo the spinner is shown into an empty area of the dialog. I would like it to be an overlay which covers the whole dialog.
The author of QtWaitingSpinner suggests that "As an alternative example, the code below will create a spinner that (1) blocks all user input to the main application for as long as the spinner is active, (2) automatically centers itself on its parent widget every time "start" is called and (3) makes use of the default shape, size and color settings." with the following code:
spinner = QtWaitingSpinner(self, True, True, Qt.ApplicationModal)
spinner.start() # starts spinning
But I tried this implementation as an example, and it didn't work:
from PyQt5.QtWidgets import QApplication, QDialog, QTabWidget, QWidget, QGroupBox, QPushButton, QVBoxLayout
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon
import requests
import urllib
from waitingspinnerwidget import QtWaitingSpinner
class DownloadDataDialog(QDialog):
def __init__(self, parent=None):
super(DownloadDataDialog, self).__init__(parent)
self.spinner = QtWaitingSpinner(self, True, True, Qt.ApplicationModal)
tabWidget = QTabWidget(self)
tabWidget.addTab(MyTab(tabWidget), "MyTab")
mainLayout = QVBoxLayout()
mainLayout.addWidget(tabWidget)
self.setLayout(mainLayout)
self.setWindowTitle("Download option chain data from web")
class MyTab(QWidget):
def __init__(self, parent=None):
super(MyTab, self).__init__(parent)
dataGroup = QGroupBox('Data')
getButton = QPushButton('Download')
getButton.clicked.connect(self.download_data)
dataLayout = QVBoxLayout()
dataLayout.addWidget(getButton)
dataGroup.setLayout(dataLayout)
mainLayout = QVBoxLayout()
mainLayout.addWidget(dataGroup)
mainLayout.addStretch(1)
self.setLayout(mainLayout)
def download_data(self):
self.parent().parent().parent().spinner.start()
url = 'http://www.meff.es/docs/Ficheros/Descarga/dRV/RV180912.zip'
filepath = None
try:
filepath = self.download_data_file(url)
except Exception as e:
print(e)
self.parent().parent().parent().spinner.stop()
if filepath:
#TODO doing stuff here
self.parent().parent().parent().close()
else:
pass #TODO show error dialog
def download_data_file(self, download_url):
# Build request URL and download the file
destination = 'test.zip'
urllib.request.urlretrieve(download_url, destination)
return destination
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
tabdialog = DownloadDataDialog()
tabdialog.show()
sys.exit(app.exec_())
So my intention is creating an invisible layer, setting the spinner as its only widget, and showing the translucid layer over the whole dialog window.
Any idea of how I should do that?
Solution
Once I also had that problem so I modified the library, first activate the flags: QtCore.Qt.Dialog | QtCore.Qt.FramelessWindowHint
, and the other change must be done in updatePosition() method:
def updatePosition(self):
if self.parentWidget() and self._centerOnParent:
parentRect = QtCore.QRect(self.parentWidget().mapToGlobal(QtCore.QPoint(0, 0)), self.parentWidget().size())
self.move(QtWidgets.QStyle.alignedRect(QtCore.Qt.LeftToRight, QtCore.Qt.AlignCenter, self.size(), parentRect).topLeft())
The result is as follows:
waitingspinnerwidget.py
import math
from PyQt5 import QtCore, QtGui, QtWidgets
class QtWaitingSpinner(QtWidgets.QWidget):
def __init__(self, parent=None, centerOnParent=True, disableParentWhenSpinning=False, modality=QtCore.Qt.NonModal):
super().__init__(parent, flags=QtCore.Qt.Dialog | QtCore.Qt.FramelessWindowHint)
self._centerOnParent = centerOnParent
self._disableParentWhenSpinning = disableParentWhenSpinning
# WAS IN initialize()
self._color = QtGui.QColor(QtCore.Qt.black)
self._roundness = 100.0
self._minimumTrailOpacity = 3.14159265358979323846
self._trailFadePercentage = 80.0
self._revolutionsPerSecond = 1.57079632679489661923
self._numberOfLines = 20
self._lineLength = 10
self._lineWidth = 2
self._innerRadius = 10
self._currentCounter = 0
self._isSpinning = False
self._timer = QtCore.QTimer(self)
self._timer.timeout.connect(self.rotate)
self.updateSize()
self.updateTimer()
self.hide()
# END initialize()
self.setWindowModality(modality)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
def paintEvent(self, QPaintEvent):
self.updatePosition()
painter = QtGui.QPainter(self)
painter.fillRect(self.rect(), QtCore.Qt.transparent)
painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
if self._currentCounter >= self._numberOfLines:
self._currentCounter = 0
painter.setPen(QtCore.Qt.NoPen)
for i in range(0, self._numberOfLines):
painter.save()
painter.translate(self._innerRadius + self._lineLength, self._innerRadius + self._lineLength)
rotateAngle = float(360 * i) / float(self._numberOfLines)
painter.rotate(rotateAngle)
painter.translate(self._innerRadius, 0)
distance = self.lineCountDistanceFromPrimary(i, self._currentCounter, self._numberOfLines)
color = self.currentLineColor(distance, self._numberOfLines, self._trailFadePercentage,
self._minimumTrailOpacity, self._color)
painter.setBrush(color)
painter.drawRoundedRect(QtCore.QRect(0, -self._lineWidth / 2, self._lineLength, self._lineWidth), self._roundness,
self._roundness, QtCore.Qt.RelativeSize)
painter.restore()
def start(self):
self.updatePosition()
self._isSpinning = True
self.show()
if self.parentWidget and self._disableParentWhenSpinning:
self.parentWidget().setEnabled(False)
if not self._timer.isActive():
self._timer.start()
self._currentCounter = 0
def stop(self):
self._isSpinning = False
self.hide()
if self.parentWidget() and self._disableParentWhenSpinning:
self.parentWidget().setEnabled(True)
if self._timer.isActive():
self._timer.stop()
self._currentCounter = 0
def setNumberOfLines(self, lines):
self._numberOfLines = lines
self._currentCounter = 0
self.updateTimer()
def setLineLength(self, length):
self._lineLength = length
self.updateSize()
def setLineWidth(self, width):
self._lineWidth = width
self.updateSize()
def setInnerRadius(self, radius):
self._innerRadius = radius
self.updateSize()
def color(self):
return self._color
def roundness(self):
return self._roundness
def minimumTrailOpacity(self):
return self._minimumTrailOpacity
def trailFadePercentage(self):
return self._trailFadePercentage
def revolutionsPersSecond(self):
return self._revolutionsPerSecond
def numberOfLines(self):
return self._numberOfLines
def lineLength(self):
return self._lineLength
def lineWidth(self):
return self._lineWidth
def innerRadius(self):
return self._innerRadius
def isSpinning(self):
return self._isSpinning
def setRoundness(self, roundness):
self._roundness = max(0.0, min(100.0, roundness))
def setColor(self, color=QtCore.Qt.black):
self._color = QColor(color)
def setRevolutionsPerSecond(self, revolutionsPerSecond):
self._revolutionsPerSecond = revolutionsPerSecond
self.updateTimer()
def setTrailFadePercentage(self, trail):
self._trailFadePercentage = trail
def setMinimumTrailOpacity(self, minimumTrailOpacity):
self._minimumTrailOpacity = minimumTrailOpacity
def rotate(self):
self._currentCounter += 1
if self._currentCounter >= self._numberOfLines:
self._currentCounter = 0
self.update()
def updateSize(self):
size = (self._innerRadius + self._lineLength) * 2
self.setFixedSize(size, size)
def updateTimer(self):
self._timer.setInterval(1000 / (self._numberOfLines * self._revolutionsPerSecond))
def updatePosition(self):
if self.parentWidget() and self._centerOnParent:
parentRect = QtCore.QRect(self.parentWidget().mapToGlobal(QtCore.QPoint(0, 0)), self.parentWidget().size())
self.move(QtWidgets.QStyle.alignedRect(QtCore.Qt.LeftToRight, QtCore.Qt.AlignCenter, self.size(), parentRect).topLeft())
def lineCountDistanceFromPrimary(self, current, primary, totalNrOfLines):
distance = primary - current
if distance < 0:
distance += totalNrOfLines
return distance
def currentLineColor(self, countDistance, totalNrOfLines, trailFadePerc, minOpacity, colorinput):
color = QtGui.QColor(colorinput)
if countDistance == 0:
return color
minAlphaF = minOpacity / 100.0
distanceThreshold = int(math.ceil((totalNrOfLines - 1) * trailFadePerc / 100.0))
if countDistance > distanceThreshold:
color.setAlphaF(minAlphaF)
else:
alphaDiff = color.alphaF() - minAlphaF
gradient = alphaDiff / float(distanceThreshold + 1)
resultAlpha = color.alphaF() - gradient * countDistance
# If alpha is out of bounds, clip it.
resultAlpha = min(1.0, max(0.0, resultAlpha))
color.setAlphaF(resultAlpha)
return color
With the above we solve one of those problems, the other problem is that urllib.request.urlretrieve()
is blocking so it will cause the GUI to freeze, so the solution is to move it to another thread, using a previous response we can do it in the following way:
from PyQt5 import QtCore, QtGui, QtWidgets
import urllib.request
from waitingspinnerwidget import QtWaitingSpinner
class RequestRunnable(QtCore.QRunnable):
def __init__(self, url, destination, dialog):
super(RequestRunnable, self).__init__()
self._url = url
self._destination = destination
self.w = dialog
def run(self):
urllib.request.urlretrieve(self._url, self._destination)
QMetaObject.invokeMethod(self.w, "FinishedDownload", QtCore.Qt.QueuedConnection)
class DownloadDataDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(DownloadDataDialog, self).__init__(parent)
self.spinner = QtWaitingSpinner(self, True, True, QtCore.Qt.ApplicationModal)
tabWidget = QtWidgets.QTabWidget(self)
tabWidget.addTab(MyTab(), "MyTab")
mainLayout = QtWidgets.QVBoxLayout(self)
mainLayout.addWidget(tabWidget)
self.setWindowTitle("Download option chain data from web")
class MyTab(QtWidgets.QWidget):
def __init__(self, parent=None):
super(MyTab, self).__init__(parent)
dataGroup = QtWidgets.QGroupBox('Data')
getButton = QtWidgets.QPushButton('Download')
getButton.clicked.connect(self.download_data)
dataLayout = QtWidgets.QVBoxLayout(self)
dataLayout.addWidget(getButton)
mainLayout = QtWidgets.QVBoxLayout(self)
mainLayout.addWidget(dataGroup)
mainLayout.addStretch(1)
def download_data(self):
self.parentWidget().window().spinner.start()
url = 'http://www.meff.es/docs/Ficheros/Descarga/dRV/RV180912.zip'
destination = 'test.zip'
runnable = RequestRunnable(url, destination, self)
QtCore.QThreadPool.globalInstance().start(runnable)
@QtCore.pyqtSlot()
def FinishedDownload(self):
self.parentWidget().window().spinner.stop()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
tabdialog = DownloadDataDialog()
tabdialog.show()
sys.exit(app.exec_())
Answered By - eyllanesc
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.