Flana Ban
Flana Ban

Reputation: 111

Qt - Show widget or label above all widget

I want to display a loading screen every time a user presses a button (a process that takes a few seconds runs).

I want something like this

QSplashScreen does not help me because that is only used before opening the application and a QDialog is not useful for me because I want that by dragging the window the application will move along with the message Loading...

What do I have to use?

Upvotes: 2

Views: 2528

Answers (3)

musicamante
musicamante

Reputation: 48231

The only (safe) way to achieve this is to add a child widget without adding it to any layout manager.

The only things you have to care about is that the widget is always raised as soon as it's shown, and that the geometry is always updated to the parent widget (or, better, the top level window).

This is a slightly more advanced example, but it has the benefit that you can just subclass any widget adding the LoadingWidget class to the base classes in order to implement a loading mechanism.

loader example screenshot

from random import randrange
from PyQt5 import QtCore, QtGui, QtWidgets

class Loader(QtWidgets.QWidget):
    def __init__(self, parent):
        super().__init__(parent)

        self.gradient = QtGui.QConicalGradient(.5, .5, 0)
        self.gradient.setCoordinateMode(self.gradient.ObjectBoundingMode)
        self.gradient.setColorAt(.25, QtCore.Qt.transparent)
        self.gradient.setColorAt(.75, QtCore.Qt.transparent)

        self.animation = QtCore.QVariantAnimation(
            startValue=0., endValue=1., 
            duration=1000, loopCount=-1, 
            valueChanged=self.updateGradient
            )

        self.stopTimer = QtCore.QTimer(singleShot=True, timeout=self.stop)

        self.focusWidget = None
        self.hide()
        parent.installEventFilter(self)

    def start(self, timeout=None):
        self.show()
        self.raise_()
        self.focusWidget = QtWidgets.QApplication.focusWidget()
        self.setFocus()
        if timeout:
            self.stopTimer.start(timeout)
        else:
            self.stopTimer.setInterval(0)

    def stop(self):
        self.hide()
        self.stopTimer.stop()
        if self.focusWidget:
            self.focusWidget.setFocus()
            self.focusWidget = None

    def updateGradient(self, value):
        self.gradient.setAngle(-value * 360)
        self.update()

    def eventFilter(self, source, event):
        # ensure that we always cover the whole parent area
        if event.type() == QtCore.QEvent.Resize:
            self.setGeometry(source.rect())
        return super().eventFilter(source, event)

    def showEvent(self, event):
        self.setGeometry(self.parent().rect())
        self.animation.start()

    def hideEvent(self, event):
        # stop the animation when hidden, just for performance
        self.animation.stop()

    def paintEvent(self, event):
        qp = QtGui.QPainter(self)
        qp.setRenderHints(qp.Antialiasing)
        color = self.palette().window().color()
        color.setAlpha(max(color.alpha() * .5, 128))
        qp.fillRect(self.rect(), color)

        text = 'Loading...'
        interval = self.stopTimer.interval()
        if interval:
            remaining = int(max(0, interval - self.stopTimer.remainingTime()) / interval * 100)
            textWidth = self.fontMetrics().width(text + ' 000%')
            text += ' {}%'.format(remaining)
        else:
            textWidth = self.fontMetrics().width(text)
        textHeight = self.fontMetrics().height()
        # ensure that there's enough space for the text
        if textWidth > self.width() or textHeight * 3 > self.height():
            drawText = False
            size = max(0, min(self.width(), self.height()) - textHeight * 2)
        else:
            size = size = min(self.height() / 3, max(textWidth, textHeight))
            drawText = True

        circleRect = QtCore.QRect(0, 0, size, size)
        circleRect.moveCenter(self.rect().center())

        if drawText:
            # text is going to be drawn, move the circle rect higher
            circleRect.moveTop(circleRect.top() - textHeight)
            middle = circleRect.center().x()
            qp.drawText(
                middle - textWidth / 2, circleRect.bottom() + textHeight, 
                textWidth, textHeight, 
                QtCore.Qt.AlignCenter, text)

        self.gradient.setColorAt(.5, self.palette().windowText().color())
        qp.setPen(QtGui.QPen(self.gradient, textHeight))
        qp.drawEllipse(circleRect)


class LoadingExtension(object):
    # a base class to extend any QWidget subclass's top level window with a loader
    def startLoading(self, timeout=0):
        window = self.window()
        if not hasattr(window, '_loader'):
            window._loader = Loader(window)
        window._loader.start(timeout)

        # this is just for testing purposes
        if not timeout:
            QtCore.QTimer.singleShot(randrange(1000, 5000), window._loader.stop)

    def loadingFinished(self):
        if hasattr(self.window(), '_loader'):
            self.window()._loader.stop()


class Test(QtWidgets.QWidget, LoadingExtension):
    def __init__(self):
        super().__init__()
        layout = QtWidgets.QGridLayout(self)

        # just a test widget
        textEdit = QtWidgets.QTextEdit()
        layout.addWidget(textEdit, 0, 0, 1, 2)
        textEdit.setMinimumHeight(20)

        layout.addWidget(QtWidgets.QLabel('Timeout:'))
        self.timeoutSpin = QtWidgets.QSpinBox(maximum=5000, singleStep=250, specialValueText='Random')
        layout.addWidget(self.timeoutSpin, 1, 1)
        self.timeoutSpin.setValue(2000)

        btn = QtWidgets.QPushButton('Start loading...')
        layout.addWidget(btn, 2, 0, 1, 2)
        btn.clicked.connect(lambda: self.startLoading(self.timeoutSpin.value()))


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    test = Test()
    test.show()
    sys.exit(app.exec_())

Upvotes: 3

bogdyname
bogdyname

Reputation: 374

You can use this slot: void QWidget::raise(). Raises this widget to the top of the parent widget's stack. After this call the widget will be visually in front of any overlapping sibling widgets.

Upvotes: 0

JhnW
JhnW

Reputation: 155

Please check Qt::WindowFlags. The Qt::SplashScreen flag will give you splash screen experience without usage QSplashScreen (you can use it with all widget as show) or, better, use QDialog with this flag. For moving, probably fine solution is not available but you can just use parent moveEvent to emmit signal. For example: Main window: moveEvent -> signal moved Dialog: signal move -> re-center window. Its look as not hard.

By the way, I think block all GUI during application run is not the best solution. You you think use QProgressBar?

Upvotes: 0

Related Questions