Home > OS >  How to override slot from native Qt class (QStatusBar) so that the overridden implementations get ca
How to override slot from native Qt class (QStatusBar) so that the overridden implementations get ca

Time:11-03

I am trying to override the clearMessage slot/method from QStatusBar in PySide2.

I have a custom class inheriting from QStatusBar, which re-implements showMessage and clearMessage methods.

class MyStatus(QtWidgets.QStatusBar):
    def showMessage(self, *args, **kwargs):
        super(MyStatus, self).showMessage(*args, **kwargs)
        print('Showing message')

    def clearMessage(self, *args, **kwargs):
        super(MyStatus, self).clearMessage(*args, **kwargs)
        print('Clearing message')

According to the c source code I could find for the QStatusBar, when calling showMessage with a timeout argument, the clearMessage slot should be called when the timer expires:

void QStatusBar::showMessage(const QString &message, int timeout)
{
    Q_D(QStatusBar);
    if (timeout > 0) {
        if (!d->timer) {
            d->timer = new QTimer(this);
            connect(d->timer, SIGNAL(timeout()), this, SLOT(clearMessage()));
        }
        d->timer->start(timeout);
    } else if (d->timer) {
        delete d->timer;
        d->timer = nullptr;
    }
    if (d->tempItem == message)
        return;
    d->tempItem = message;
    hideOrShow();
}

While the message does clear at the end of the timer, the print line I added in the method is not being printed, which makes me believe that either:

  • it's still executing the base class's clearMessage method
  • maybe the source code I found is for a different version of Qt, and PySide2 5.12.6 doesn't call the method at all

I need to make sure my custom clearMessage is being called any time the message gets cleared, whether it's manually(currently fine) or via a timer or other reason.

I could re-implement my own Qtimer and make sure I'm catching any other method which might call clearMessage, but I'm wondering if I'm missing something obvious which is preventing me from overriding the method properly.

CodePudding user response:

clearMessage() is not listed as virtual, which means that overriding has no effect in the "internal" behavior, and it would only work when explicitly called from your code.

Since clearMessage() is actually called from Qt only from the QTimer created in showMessage(), then a possible workaround is to check for the latest QTimer child object created after calling the base implementation, disconnect its timeout signal and connect it again to your slot:

class MyStatusBar(QStatusBar):
    def showMessage(self, message, timeout=0):
        super().showMessage(message, timeout)
        if timeout:
            timer = self.findChildren(QTimer)[-1]
            timer.timeout.disconnect()
            timer.timeout.connect(self.clearMessage)

    def clearMessage(self):
        super().clearMessage()
        print('my clearMessage')


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setStatusBar(MyStatusBar(self))

Note that the above code will only work properly if you never use other timers created as children of your subclass. If you do that, you may involuntarily reconnect them instead. Take this case:

    def showMessage(self, message, timeout=0):
        super().showMessage(message, timeout)
        if timeout:
            timer = self.findChildren(QTimer)[-1]
            timer.timeout.disconnect()
            timer.timeout.connect(self.clearMessage)
            anotherTimer = QTimer(self, interval=3000, 
                timeout=self.myFunction)
            anotherTimer.start()

    def myFunction(self):
        print('my custom function')


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setStatusBar(MyStatusBar(self))
        self.statusBar().showMessage('Hello 1', 5000)
        QTimer.singleShot(2000, 
            lambda: self.statusBar().showMessage('Hello 2', 5000))

The above will result in the following sequence:

  • show a message for 5 seconds;
  • start a timer that will timeout after 3 seconds;
  • myFunction() is called;
  • 2 seconds after startup, a new showMessage() is called;
  • the override finds the last timer (which is the new anotherTimer), disconnects it, and connects it to clearMessage() instead;

While the case above obviously doesn't make a lot of sense, it's better to consider the possibility of other timers created arbitrarily; the solution is quite simple: set an object name for the internal timer and check for it before calling the base implementation.

showMessage() always creates a new timer if a previous d->timer doesn't exist, otherwise it just renews it keeping existing connections intact, so if there was no previous timer with our object name set, we set it, so that we can find it whenever showMessage() is called before its timeout and don't reconnect any timer again.

Note that I also added the Qt.FindDirectChildrenOnly flag to avoid the possibility of finding QTimers coming from other objects (like when using addWidget() or addPermanentWidget()).

    def showMessage(self, message, timeout=0):
        if timeout:
            oldTimer = self.findChild(QTimer, 'clearMessageTimer',
                Qt.FindDirectChildrenOnly)
        super().showMessage(message, timeout)
        if timeout and not oldTimer:
            # there was no previously set timer
            timer = self.findChildren(
                QTimer, Qt.FindDirectChildrenOnly)[-1]
            timer.setObjectName('clearMessageTimer')
            timer.timeout.disconnect()
            timer.timeout.connect(self.clearMessage)

For obvious reasons, this is based on the assumption that the base implementation of clearMessage() is always called in its override.

  • Related