PyQt6 – custom widget – get default widget colors for text, background etc

Question:

I’m developing custom components using PyQt6. I now like to adapt my widgets’ colors to the default Palette colors so that they look like the default widgets of PyQt6. I think I found a way to get the default colors of a global palette:

default_palette = self.palette()
self.textColor = default_palette.text().color()
self.backgroudColor = default_palette.window().color()

My question is, how to use them in the best (practice) way. My goal is that colors also change, when I change the global stylesheets or use libraries like qt_materials.

Asked By: padmalcom

||

Answers:

One of the most important aspects to consider when dealing with widget "colors" is that the palette is just a hint.

The style (see QStyle) then decides if and how to use that palette.

For instance, the QPalette documentation says, in the role section:

There are some color roles used mostly for 3D bevel and shadow effects. All of these are normally derived from Window, and used in ways that depend on that relationship. For example, buttons depend on it to make the bevels look attractive, and Motif scroll bars depend on Mid to be slightly different from Window.

Note that the above text is also inherited from very old Qt documentation; while generally still consistent, it’s not to be taken as granted.

This is an extremely important aspect that has always to be considered, especially when dealing with system styles (specifically, macOS and Windows), that might completely ignore the palette.

That said, Qt standard UI elements normally follow the palette, meaning that you probably can safely use the palette roles.

Note that setting Qt Style Sheets (QSS) creates some inconsistencies with the palette: just like standard HTML based style sheets, QSS override the default behavior.

This implies the following aspects:

  • generic properties (like color and background) override the palette (see this related answer);
  • QSS can use palette roles (see the documentation), but they can only use the QPalette of the parent (widget or application) or any value previously set for the widget that does not override the palette itself (see the point above);
  • palette roles and groups can be cached, but you also need to ensure that changeEvent() properly checks for PaletteChange and StyleChange in order to clear that cache (and after calling the default implementation of the widget!);

This is a basic example that consider all the above aspects (except for the QSS ones):

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

class Test(QWidget):
    _background = _foreground = None
    _paletteCached = False

    def changeEvent(self, event):
        super().changeEvent(event)
        if event.type() in (event.PaletteChange, event.StyleChange):
            self._paletteCached = False

    def minimumSizeHint(self):
        return QSize(self.fontMetrics().boundingRect('Hello').width() * 2, 40)

    def paintEvent(self, event):
        if not self._paletteCached:
            self._paletteCached = True
            palette = self._paletteCached = self.palette()
            self._background = palette.color(palette.Base)
            self._foreground = palette.color(palette.Text)
        qp = QPainter(self)
        qp.setBrush(self._background)
        qp.setPen(self._foreground)
        rect = self.rect().adjusted(0, 0, -1, -1)
        qp.drawRect(rect)
        qp.drawText(rect, Qt.AlignCenter, 'Hello!')


app = QApplication([])
test = QWidget()
layout = QHBoxLayout(test)
layout.addWidget(Test(enabled=True))
layout.addWidget(Test(enabled=False))

new = test.palette()
new.setColor(QPalette.Base, Qt.darkGreen)
new.setColor(QPalette.Disabled, QPalette.Base, QColor(0, 128, 0, 128))
new.setColor(QPalette.Text, Qt.blue)
new.setColor(QPalette.Disabled, QPalette.Text, QColor(255, 0, 0, 128))
QTimer.singleShot(2000, lambda: test.setPalette(new))

test.show()
app.exec()
Answered By: musicamante
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.