-
-
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 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.
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.
@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.
On the other hand, QMainWindow has a predefined layout:
So it is not recommended to add it directly.
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_())
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 thecenterOnParent
arg.ie. should
instead be:
If not, can you please explain why?
Thanks.