Search code examples
pythonpyqtpyqt5

Making an invisible layer in pyQt which covers the whole dialog


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_())
    

    enter image description here