Skip to content

Instantly share code, notes, and snippets.

@eyllanesc
Created January 25, 2018 13:16
Show Gist options
  • Save eyllanesc/1a09157d17ba13d223c312b28a81c320 to your computer and use it in GitHub Desktop.
Save eyllanesc/1a09157d17ba13d223c312b28a81c320 to your computer and use it in GitHub Desktop.
import requests
"""from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *"""
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from WaitingSpinnerWidget import QtWaitingSpinner
class RequestRunnable(QRunnable):
def __init__(self, dialog):
QRunnable.__init__(self)
self.w = dialog
def run(self):
QThread.msleep(10000)
QMetaObject.invokeMethod(self.w, "setData",
Qt.QueuedConnection,
Q_ARG(str, "finish"))
class Dialog(QDialog):
def __init__(self, *args, **kwargs):
QDialog.__init__(self, *args, **kwargs)
self.setLayout(QVBoxLayout())
btn = QPushButton("Submit", self)
btn.clicked.connect(self.submit)
self.spinner = QtWaitingSpinner(self)
self.layout().addWidget(btn)
self.layout().addWidget(self.spinner)
def submit(self):
self.spinner.start()
runnable = RequestRunnable(self)
QThreadPool.globalInstance().start(runnable)
@pyqtSlot(str)
def setData(self, data):
print(data)
self.spinner.stop()
self.adjustSize()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
dial = Dialog()
dial.show()
sys.exit(app.exec_())
from math import ceil
"""from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *"""
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class QtWaitingSpinner(QWidget):
mColor = QColor(Qt.gray)
mRoundness = 100.0
mMinimumTrailOpacity = 31.4159265358979323846
mTrailFadePercentage = 50.0
mRevolutionsPerSecond = 1.57079632679489661923
mNumberOfLines = 20
mLineLength = 10
mLineWidth = 2
mInnerRadius = 20
mCurrentCounter = 0
mIsSpinning = False
def __init__(self, centerOnParent=True, disableParentWhenSpinning=True, *args, **kwargs):
QWidget.__init__(self, *args, **kwargs)
self.mCenterOnParent = centerOnParent
self.mDisableParentWhenSpinning = disableParentWhenSpinning
self.initialize()
def initialize(self):
self.timer = QTimer(self)
self.timer.timeout.connect(self.rotate)
self.updateSize()
self.updateTimer()
self.hide()
@pyqtSlot()
def rotate(self):
self.mCurrentCounter += 1
if self.mCurrentCounter > self.numberOfLines():
self.mCurrentCounter = 0
self.update()
def updateSize(self):
size = (self.mInnerRadius + self.mLineLength) * 2
self.setFixedSize(size, size)
def updateTimer(self):
self.timer.setInterval(1000 / (self.mNumberOfLines * self.mRevolutionsPerSecond))
def updatePosition(self):
if self.parentWidget() and self.mCenterOnParent:
self.move(self.parentWidget().width() / 2 - self.width() / 2,
self.parentWidget().height() / 2 - self.height() / 2)
def lineCountDistanceFromPrimary(self, current, primary, totalNrOfLines):
distance = primary - current
if distance < 0:
distance += totalNrOfLines
return distance
def currentLineColor(self, countDistance, totalNrOfLines, trailFadePerc, minOpacity, color):
if countDistance == 0:
return color
minAlphaF = minOpacity / 100.0
distanceThreshold = ceil((totalNrOfLines - 1) * trailFadePerc / 100.0)
if countDistance > distanceThreshold:
color.setAlphaF(minAlphaF)
else:
alphaDiff = self.mColor.alphaF() - minAlphaF
gradient = alphaDiff / distanceThreshold + 1.0
resultAlpha = color.alphaF() - gradient * countDistance
resultAlpha = min(1.0, max(0.0, resultAlpha))
color.setAlphaF(resultAlpha)
return color
def paintEvent(self, event):
self.updatePosition()
painter = QPainter(self)
painter.fillRect(self.rect(), Qt.transparent)
painter.setRenderHint(QPainter.Antialiasing, True)
if self.mCurrentCounter > self.mNumberOfLines:
self.mCurrentCounter = 0
painter.setPen(Qt.NoPen)
for i in range(self.mNumberOfLines):
painter.save()
painter.translate(self.mInnerRadius + self.mLineLength,
self.mInnerRadius + self.mLineLength)
rotateAngle = 360.0 * i / self.mNumberOfLines
painter.rotate(rotateAngle)
painter.translate(self.mInnerRadius, 0)
distance = self.lineCountDistanceFromPrimary(i, self.mCurrentCounter,
self.mNumberOfLines)
color = self.currentLineColor(distance, self.mNumberOfLines,
self.mTrailFadePercentage, self.mMinimumTrailOpacity, self.mColor)
painter.setBrush(color)
painter.drawRoundedRect(QRect(0, -self.mLineWidth // 2, self.mLineLength, self.mLineLength),
self.mRoundness, Qt.RelativeSize)
painter.restore()
def start(self):
self.updatePosition()
self.mIsSpinning = True
self.show()
if self.parentWidget() and self.mDisableParentWhenSpinning:
self.parentWidget().setEnabled(False)
if not self.timer.isActive():
self.timer.start()
self.mCurrentCounter = 0
def stop(self):
self.mIsSpinning = False
self.hide()
if self.parentWidget() and self.mDisableParentWhenSpinning:
self.parentWidget().setEnabled(True)
if self.timer.isActive():
self.timer.stop()
self.mCurrentCounter = 0
def setNumberOfLines(self, lines):
self.mNumberOfLines = lines
self.updateTimer()
def setLineLength(self, length):
self.mLineLength = length
self.updateSize()
def setLineWidth(self, width):
self.mLineWidth = width
self.updateSize()
def setInnerRadius(self, radius):
self.mInnerRadius = radius
self.updateSize()
def color(self):
return self.mColor
def roundness(self):
return self.mRoundness
def minimumTrailOpacity(self):
return self.mMinimumTrailOpacity
def trailFadePercentage(self):
return self.mTrailFadePercentage
def revolutionsPersSecond(self):
return self.mRevolutionsPerSecond
def numberOfLines(self):
return self.mNumberOfLines
def lineLength(self):
return self.mLineLength
def lineWidth(self):
return self.mLineWidth
def innerRadius(self):
return self.mInnerRadius
def isSpinning(self):
return self.mIsSpinning
def setRoundness(self, roundness):
self.mRoundness = min(0.0, max(100, roundness))
def setColor(self, color):
self.mColor = color
def setRevolutionsPerSecond(self, revolutionsPerSecond):
self.mRevolutionsPerSecond = revolutionsPerSecond
self.updateTimer()
def setTrailFadePercentage(self, trail):
self.mTrailFadePercentage = trail
def setMinimumTrailOpacity(self, minimumTrailOpacity):
self.mMinimumTrailOpacity = minimumTrailOpacity
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
dial = QDialog()
w = QtWaitingSpinner(dial)
dial.show()
w.start()
QTimer.singleShot(1000, w.stop)
sys.exit(app.exec_())
@cranialhum
Copy link

cranialhum commented Oct 26, 2021

Firstly, thanks for your code.
But it looks like on Line 30 of main.py, you're passing the self arg in the position of the centerOnParent arg.

ie. should

self.spinner = QtWaitingSpinner(self)

instead be:

self.spinner = QtWaitingSpinner(parent=self)

If not, can you please explain why?

Thanks.

@eyllanesc
Copy link
Author

eyllanesc commented Oct 26, 2021

@cranialhum Both ways are valid, by not using a keyword then self is passed through *args:

def __init__(self, centerOnParent=True, disableParentWhenSpinning=True, *args, **kwargs):

and the first argument of QWidget constructor is the parent.

@cranialhum
Copy link

cranialhum commented Oct 26, 2021

Of course, thanks for taking the time to respond.
Reason I'm looking into this is I've been using your class in a QMainWindow (PyQt5), and running into a scenario where if I add it to the QMainWindow.centralWidget().layout(), it doesn't seem to draw after calling start(), whereas if I add it directly to the QMainWindow.layout(), it does. Any recommendations on why the QtWaitingSpinner would behave differently in these two scenarios?
Of course this may be out of the scope - if so, let me know and I'll pull this down.

Working code.

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.widget_main = QtWidgets.QWidget(self)
        self.setCentralWidget(self.widget_main)
        self.layout_main = QtWidgets.QVBoxLayout()
        self.widget_main.setLayout(self.layout_main)
        self.busy_spinner = QtWaitingSpinner(self)
        self.layout().addWidget(self.busy_spinner)

Non-working code.

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.widget_main = QtWidgets.QWidget(self)
        self.setCentralWidget(self.widget_main)
        self.layout_main = QtWidgets.QVBoxLayout()
        self.widget_main.setLayout(self.layout_main)
        self.busy_spinner = QtWaitingSpinner(self.centralWidget())
        self.centralWidget().layout().addWidget(self.busy_spinner)

Thanks in advance.

@eyllanesc
Copy link
Author

@cranialhum I just tested my code with:

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.widget_main = QWidget(self)
        self.setCentralWidget(self.widget_main)
        self.layout_main = QVBoxLayout()
        self.widget_main.setLayout(self.layout_main)
        self.busy_spinner = QtWaitingSpinner(self.centralWidget())
        self.centralWidget().layout().addWidget(self.busy_spinner)

if __name__ == '__main__':
    import sys

    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    w.busy_spinner.start()
    sys.exit(app.exec_())

And works.

Screenshot_20211025_235030


On the other hand, QMainWindow has a predefined layout:

So it is not recommended to add it directly.

@cranialhum
Copy link

cranialhum commented Oct 26, 2021

Thanks @eyllanesc. This works with me also, so I'm not sure what's going on in my application at this stage - will just need to dig in.
I'm not sure if you may find this useful or not, but I built a function decorator to executelong execution functions in a separate thread, which additionally manages the QtWaitingSpinner interface.
The following code works nicely (based on your example) - i'm sure there's room for improvement, but it's been stable for me across various applications.

Thanks for the help again.

import sys
import time
from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QLabel, QApplication
from PyQt5.QtCore import QThread

def busy_spinner_decorator(busy_spinner_attr_name=None, parent_callback_func_name=None):
    """
    This decorator causes the decorated function to be executed within it's own thread, operates the QtWaitingSpinner,
    and calls back the desired function on completion (whether success or fail).
    Callback is called from main thread and will be served the return value of the decorated function.

    How to use
    @busy_spinner_decorator("my_busy_spinner_instance", "function_i_want_callback")
    def long_execution_function(self):

    @param busy_spinner_attr_name: QtWaitingSpinner instance variable name
    @type busy_spinner_attr_name: str
    @param parent_callback_func_name: Call back function name
    @type parent_callback_func_name: str
    """

    # Reference
    # https://www.techblog.moebius.space/posts/2019-06-22-third-time-lucky-with-python-decorators/#2-decorator-classes
    # https://www.programmersought.com/article/6421908527/
    # https://realpython.com/primer-on-python-decorators/#classes-as-decorators
    # https://www.datacamp.com/community/tutorials/decorators-python
    # https://www.py4u.net/discuss/205775
    def deco(function):
        def inner(self, *args, **kwargs):
            # Get the function 'parent' (ie. 'self'), and get access to the attributes used by this decorator
            parent = self
            busy_spinner = getattr(self, busy_spinner_attr_name) if busy_spinner_attr_name is not None else None
            parent_callback = getattr(self, parent_callback_func_name) if parent_callback_func_name is not None else None

            def func_threaded_start(self, *args, **kwargs):
                nonlocal busy_spinner

                def _callback(result):
                    nonlocal parent_callback
                    nonlocal busy_spinner
                    if parent_callback:
                        LOGGER.debug(f"calling parent_callback with result = {result}")
                        parent_callback(result)
                    if busy_spinner:
                        busy_spinner.stop()

                class Worker(QtCore.QThread):
                    def __init__(self):
                        nonlocal parent
                        super(QtCore.QThread, self).__init__(parent=parent)
                        self.finished.connect(self._complete)
                        self.result = None

                    def _complete(self):
                        nonlocal _callback
                        LOGGER.debug("calling self._callback")
                        _callback(self.result)

                    def run(self):
                        nonlocal parent
                        try:
                            LOGGER.debug("calling function")
                            self.result = function(parent, *args, **kwargs)
                        except Exception as e:
                            raise e
                            #self.result = traceback.format_exc()

                _my_thread_instance = Worker()
                if busy_spinner: busy_spinner.start()
                _my_thread_instance.start()
                LOGGER.debug("_my_thread_instance started")

            return func_threaded_start(self, *args, **kwargs)
        return inner
    return deco


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.widget_main = QWidget(self)
        self.setCentralWidget(self.widget_main)
        self.layout_main = QVBoxLayout()
        self.widget_main.setLayout(self.layout_main)
        self.busy_spinner = QtWaitingSpinner(self.centralWidget())
        self.centralWidget().layout().addWidget(self.busy_spinner)
        self.label = QLabel("")
        self.centralWidget().layout().addWidget(self.label)

    @busy_spinner_decorator("busy_spinner", "callback")
    def long_operation(self):
        self.label.setText("running")
        time.sleep(3)
        return True

    def callback(self, retval):
        self.label.setText(f"complete: {str(retval)}")


if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    w.long_operation()
    sys.exit(app.exec_())

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment