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:
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:
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:
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:
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())