qtpyqtqstatusbar

How to add a permanent icon before statusBar in QMainWindow?


I want to add a permanent icon before the statusBar in QMianWindow, and I know that there is an addWidget method that can be used, but the widgets added by this method will only be displayed when there are messages.

I also know that in other widgets, this can be achieved by using a custom layout to wrap the statusBar. But in MainWindow, I can't modify the layout of the statusBar.

Additionally, I know that this may be achieved by rewriting the paintEvent method, but I am not very clear on how to implement it. Here is the rendering I want to achieve:

enter image description here


Solution

  • QStatusBar has its own internal layout system, which is a bit complex, and unfortunately cannot be used as a standard Qt layout manager.

    With some ingenuity, though, we can achieve the wanted result.

    The trick is to use addWidget() with two widgets:

    1. an "icon widget" that will display the icon;
    2. a QLabel that will display the status tip;

    Note that while we could use a QLabel for the icon, that will force us to explicitly set the icon size and get the QPixmap of the QIcon. If we use a QToolButton, instead, we can just use QIcon objects, and Qt will automatically set an appropriate icon size (based on the PM_SmallIconSize pixel metric of the current style).

    We can just use a style sheet for the button in order to completely hide the button border (so that it will not look like a button) and still get its features, such as the clicked signal, or even a custom menu.

    Then, we can update the label by connecting the messageChanged signal to the standard QLabel setText() slot. Note that the label should have an explicit minimum width (1) so that, in case the status tip is too long, it won't force the resizing of the main window due to the label's size hint.

    Unfortunately, just doing the above will not be enough, as the addWidget() documentation explains:

    The widget [...] may be obscured by temporary messages.

    Luckily, QStatusBar provides the reformat() function:

    Changes the status bar's appearance to account for item changes.

    Special subclasses may need this function, but geometry management will usually take care of any necessary rearrangements.

    And, yes, we need this function, as what it does is to "reset" the internal layout, ensuring that all widgets (permanent or not) will be visible, no matter the currently shown message.

    Now, the problem is that even if the widgets would become visible again after calling reformat(), the original status message would be painted anyway from the paintEvent() function, under our widgets; by default, basic widgets that don't autofill their background (such as labels, and buttons with some visual properties set, like in our case), will show everything that is behind them:

    enter image description here

    Gosh, that's bad...

    Well, the trick here is to "hide" the text by setting a further style sheet rule, using a transparent color property for the status bar: it will be "painted" anyway, but it won't be visible.

    The following code shows how the above was implemented, and includes an example that shows a list widget with QStyle standard icons: hovering them will show a proper status tip in our own label, and clicking them will actually change the status bar icon, since I've "patched" the icon() and setIcon() functions of the status bar with those of the button.

    The result will be something like this:

    Screenshot of the example code

    class IconStatusBar(QStatusBar):
        iconClicked = pyqtSignal(bool)
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.setStyleSheet('''
                QStatusBar { color: transparent; }
                QToolButton#statusBarIconWidget { border: none; }
            ''')
    
            self._iconWidget = QToolButton(objectName='statusBarIconWidget')
            self.addWidget(self._iconWidget)
            # add direct references to the icon functions
            self.icon = self._iconWidget.icon
            self.setIcon = self._iconWidget.setIcon
            # force the button to always show the icon, even if the 
            # current style default is different
            self._iconWidget.setToolButtonStyle(Qt.ToolButtonIconOnly)
    
            # just set an arbitrary icon
            icon = self.style().standardIcon(QStyle.SP_MessageBoxInformation)
            self.setIcon(icon)
    
            self._statusLabel = QLabel()
            self._statusLabel.setMinimumWidth(1) # allow ignoring the size hint
            self.addWidget(self._statusLabel)
    
            self.messageChanged.connect(self._updateStatus)
            self._iconWidget.clicked.connect(self.iconClicked)
    
        def _updateStatus(self, text):
            self.reformat()
            self._statusLabel.setText(text)
    
    
    if __name__ == '__main__':
        import sys
        app = QApplication(sys.argv)
    
        test = QMainWindow()
        statusBar = IconStatusBar()
        test.setStatusBar(statusBar)
    
        listWidget = QListWidget()
        test.setCentralWidget(listWidget)
    
        listWidget.setMouseTracking(True)
        style = app.style()
        for sp in range(80):
            icon = style.standardIcon(sp)
            if icon.isNull():
                continue
            item = QListWidgetItem(icon, 'Standard Icon #{}'.format(sp))
            item.setStatusTip('Click to set #{} as status bar icon'.format(sp))
            listWidget.addItem(item)
    
        def setIcon(item):
            statusBar.setIcon(item.icon())
        listWidget.itemClicked.connect(setIcon)
    
        test.show()
        sys.exit(app.exec())