14. Customizing Qt Styles

Built-in Qt widgets are not drawn by hard-coded painting code inside the widget itself -they delegate drawing to the Qt Style system. When a widget receives a paint request, its paintEvent() method:

  • Prepares a style option by filling in one of the QStyleOption subclasses.

  • Uses the widget’s current style() to paint it, usually through a QStylePainter, a QPainter subclass with convenience methods that forward drawing calls to the style.

This indirection makes widgets independent of any particular look and feel, easy to theme, and visually consistent. (A widget whose paintEvent() draws directly with QPainter, like the custom widgets in Chapter 16, bypasses the style entirely.)

14.1 The Style System

Qt provides a set of prebuilt styles that you can use to easily change your application’s appearance:

Style Windows macOS Linux
fusion available available available
windows available available available
windows11 available
windowsvista available
macos available

Only Fusion and Windows (which mimics the “classic” Windows look and feel) are available on all platforms. The Windows11 style requires Qt 6.7 or later.

An icon of a clipboard-list

You want to see which visual styles are available on the current platform and switch between them at runtime.

There are several ways to apply a style to your application:

  • By setting the QT_STYLE_OVERRIDE environment variable (for example, QT_STYLE_OVERRIDE=fusion).

  • By passing the -style command-line argument when you start the application (python main.py -style fusion).

  • By changing the style in code with QApplication.setStyle().

To examine available Qt styles:

 1 import sys
 2 from PySide6.QtCore import Slot, Qt
 3 from PySide6.QtWidgets import (QApplication, QStyleFactory,
 4     QWidget, QComboBox, QVBoxLayout)
 5 from customwidgets import WidgetFactory
 6 
 7 
 8 class Window(QWidget):
 9 
10     def __init__(self, parent=None):
11 
12         super().__init__(parent)
13         self.setMinimumWidth(400)
14         self.setLayout(QVBoxLayout())
15         
16         self.style_combo = QComboBox()
17         
18         # 1. Populate a combobox with available Qt styles.
19         
20         for key in QStyleFactory.keys():
21             self.style_combo.addItem(key)
22             
23         current_style = QApplication.style().name()
24         idx = self.style_combo.findText(
25             current_style,
26             Qt.MatchFlag.MatchFixedString)
27         self.style_combo.setCurrentIndex(idx)
28         
29         # 2. Connect the currentIndexChanged() signal to a slot.
30         
31         self.style_combo.currentIndexChanged.connect(self.set_style)
32         
33         self.layout().addWidget(self.style_combo)
34         self.layout().addWidget(WidgetFactory())
35 
36     # 3. Apply selected style.
37         
38     @Slot(int)
39     def set_style(self, idx):
40 
41         style_name = self.style_combo.itemText(idx)
42         style = QStyleFactory.create(style_name)
43         QApplication.setStyle(style)
44 
45 
46 if __name__ == '__main__':
47 
48     app = QApplication(sys.argv)
49     style = QStyleFactory.create('Fusion')
50     QApplication.setStyle(style)
51     main_window = Window()
52     main_window.show()
53     sys.exit(app.exec())
  1. Populate a combobox with the available Qt styles. The QStyleFactory.keys() list contains all available style names. Use this list to add each style name to a combobox. Get the current style name with QApplication.style().name() and select the matching combobox item. Note that name() returns a lowercase name ('fusion') while keys() returns 'Fusion' so use a case-insensitive match.

  2. Connect the combobox’s currentIndexChanged() signal to a slot.

  3. In the slot, apply the selected style. Get the selected style name from the combobox. Pass the name to QStyleFactory.create() to create the corresponding QStyle object. Use QApplication.setStyle() to set the application’s style. setStyle() also accepts a style name directly which is simpler if you don’t need the style object.

Now, when you run the application and select a style from the combobox, the selected style is applied to all the widgets.

Individual widgets can override the application style with QStyle.setStyle(). This affects only the widget itself, not its children, and the widget does not take ownership of the style object.

14.2 QPalette and Color Roles

Styles consult a QPalette when painting widgets. Each palette has three color groups (plus Normal, which is an alias for Active):

Color Group
Active The widget’s window is the active window
Disabled The widget is disabled
Inactive The widget is visible but its window is not active

Each palette also has 21 color roles such as:

Role Typical use
Window General background
WindowText General text / foreground
Base Text entry background
Text Text on Base
Button Button background
ButtonText Text on buttons
An icon of a clipboard-list

You want to test how changing QPalette color groups and roles affect the appearance of your application across available Qt styles.

  1 import sys
  2 from PySide6.QtCore import Slot, Qt
  3 from PySide6.QtGui import QPalette, QColor
  4 from PySide6.QtWidgets import (QApplication, QStyleFactory,
  5     QWidget, QComboBox, QVBoxLayout, QColorDialog, QPushButton)
  6 from customwidgets import WidgetFactory
  7 
  8 
  9 ROLES = (QPalette.ColorRole.Window, QPalette.ColorRole.WindowText,
 10          QPalette.ColorRole.Base, QPalette.ColorRole.Text,
 11          QPalette.ColorRole.Button, QPalette.ColorRole.ButtonText,
 12          QPalette.ColorRole.Highlight, QPalette.ColorRole.HighlightedText,
 13          QPalette.ColorRole.AlternateBase,QPalette.ColorRole.Midlight)
 14 
 15 
 16 class Window(QWidget):
 17 
 18     def __init__(self, parent=None):
 19 
 20         super().__init__(parent)
 21         self.setMinimumWidth(400)
 22         self.setLayout(QVBoxLayout())
 23         
 24         self.style_combo = QComboBox()
 25         
 26         # 1. Add the styles combobox.
 27         
 28         for key in QStyleFactory.keys():
 29             self.style_combo.addItem(key)
 30             
 31         current_style = QApplication.style().name()
 32         idx = self.style_combo.findText(
 33             current_style, Qt.MatchFlag.MatchFixedString)
 34         self.style_combo.setCurrentIndex(idx)
 35         
 36         # 2. Add the color roles combobox.
 37 
 38         self.role_combo = QComboBox()        
 39         for role in ROLES:
 40             self.role_combo.addItem(role.name, role)
 41             
 42         # 3. Add the color groups combobox.
 43 
 44         self.group_combo = QComboBox()
 45         for group in QPalette.ColorGroup:
 46             if group != QPalette.ColorGroup.NColorGroups:
 47                 self.group_combo.addItem(group.name, group)
 48         
 49         # 4. Add the select color button.
 50         
 51         self.color_button = QPushButton('Select a color')
 52         
 53         # 5. Connect the signals.
 54         
 55         self.style_combo.currentIndexChanged.connect(self.set_style)
 56         self.role_combo.currentIndexChanged.connect(self.apply_palette)
 57         self.group_combo.currentIndexChanged.connect(self.apply_palette)
 58         self.color_button.clicked.connect(self.set_color)
 59         
 60         self.layout().addWidget(self.style_combo)
 61         self.layout().addWidget(self.role_combo)
 62         self.layout().addWidget(self.group_combo)
 63         self.layout().addWidget(self.color_button)
 64         
 65         self.widget_factory = WidgetFactory()
 66         
 67         self.layout().addWidget(self.widget_factory)
 68         
 69         self.original_palette = QApplication.palette()
 70         self.color = QColor('green')
 71         self.color_button.setStyleSheet('background-color: green')
 72         self.apply_palette()
 73 
 74         
 75     @Slot(int)
 76     def set_style(self, idx):
 77         style_name = self.style_combo.itemText(idx)
 78         style = QStyleFactory.create(style_name)
 79         QApplication.setStyle(style)
 80         
 81     def set_color(self):
 82         ret_color = QColorDialog.getColor(parent=self)
 83         if ret_color.isValid():
 84             self.color = ret_color
 85             self.apply_palette()
 86     
 87     # 6. Apply the palette to the selected group and role.
 88     
 89     def apply_palette(self):
 90         color_name = self.color.name(QColor.NameFormat.HexRgb)
 91         palette = QPalette(self.original_palette)
 92         palette.setColor(
 93             self.group_combo.currentData(),
 94             self.role_combo.currentData(),
 95             self.color)
 96         self.color_button.setStyleSheet(f'background-color: {color_name}')
 97         self.widget_factory.setPalette(palette)
 98 
 99 
100 if __name__ == '__main__':
101 
102     app = QApplication(sys.argv)
103     style = QStyleFactory.create('Fusion')
104     QApplication.setStyle(style)
105     main_window = Window()
106     main_window.show()
107     sys.exit(app.exec())
  1. Add the styles combobox. Populate it with the names returned by QStyleFactory.keys(). Retrieve the name of the active style with QApplication.style().name() and set it as the current item.

  2. Add the color roles combobox and populate it from the ROLES list.

  3. Add the color groups combobox. Iterate over QPalette.ColorGroup and add an item to the combobox for each group.

  4. Add the Select color button.

  5. Connect the signals:

    • Connect the styles combobox currentIndexChanged() signal to a slot that creates the selected style with QStyleFactory.create() and applies it with QApplication.setStyle().
    • Connect the roles combobox currentIndexChanged() signal to a slot that applies the palette.
    • Connect the color button clicked() signal to slot.
  6. Apply the palette to the selected color group and role:

    • Create a copy of the active style’s standard palette with QApplication.style().standardPalette().
    • Call QPalette.setColor() using the currently selected group, role, and color.
    • Update the button background color.
    • Apply the modified palette.

This lets you see how changing a color role in a color group affects the appearance of different widgets under the currently active style.

If you experiment with different style-role-group combinations, you will notice that a style is free to use or ignore whichever roles it likes.

14.3 QProxyStyle: Reimplementing Metrics and Hints

There are two QStyle methods that let you customize a widget’s appearance without reimplementing its painting code:

  • def pixelMetric( self, metric: QStyle.PixelMetric, option: QStyleOption | None = None, widget: QWidget | None = None) -> int.

  • def styleHint( self, hint: QStyle.StyleHint, option: QStyleOption | None = None, widget: QWidget | None = None, returnData: QStyleHintReturn | None = None) -> int.

metric is one of the PixelMetric values like ButtonMargin or SliderLength, hint is a QStyle.StyleHint value, while option is a QStyleOption (or its subclass) that stores the information that QStyle methods need to draw the widget.

pixelMetric() returns the size in pixels of the given metric. styleHint() returns an integer whose meaning depends on the hint (a boolean flag, an alignment value, etc.).

styleHint() also has an optional returnData parameter which some hints use to return additional data. In PySide6 at its type is always QStyleHintReturn and never the actual subclass like QStyleHintReturnVariant, so hints that rely on it can not be implemented from Python.

An icon of a clipboard-list

Your application uses the Fusion style but you need buttons and indicators to have more space. You also want combo boxes to display their drop-down lists as popups, and tab bars to be center-aligned.

 1 from PySide6.QtCore import Qt
 2 from PySide6.QtWidgets import QStyle, QProxyStyle, QStyleFactory
 3 
 4 
 5 # 1. Create a QProxyStyle subclass.
 6 
 7 class MyProxyStyle(QProxyStyle):
 8 
 9     def __init__(self):
10         base = QStyleFactory.create('Fusion')
11         super().__init__(base)
12         
13     # 2. Reimplement pixelMetric().
14 
15     def pixelMetric(self, metric, option=None, widget=None):
16         
17         value = super().pixelMetric(metric, option, widget)
18 
19         if metric in (QStyle.PixelMetric.PM_ButtonMargin,
20                         QStyle.PixelMetric.PM_IndicatorHeight,
21                         QStyle.PixelMetric.PM_IndicatorWidth):
22             return value + 20
23         return value
24     
25     # 3. Reimplement styleHint().
26 
27     def styleHint(self, hint, option=None, widget=None, returnData=None):
28         if hint == QStyle.StyleHint.SH_ComboBox_Popup:
29             return 1
30         if hint == QStyle.StyleHint.SH_TabBar_Alignment:
31             return Qt.AlignmentFlag.AlignCenter
32         return super().styleHint(hint, option, widget, returnData)
  1. Create a QProxyStyle subclass. In __init__(), pass the Fusion style to the base class __init__().

  2. Reimplement pixelMetric(). Call the base implementation, then increase the returned metric value so that widgets get more space.

  3. Reimplement styleHint(). Return the new values for the hints you want to change (SH_ComboBox_Popup and SH_TabBar_Alignment), and forward other hints to the base implementation.

 1 import sys
 2 from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout
 3 from customwidgets import WidgetFactory
 4 from proxystyles import MyProxyStyle
 5 
 6 
 7 class Window(QWidget):
 8 
 9     def __init__(self, parent=None):
10 
11         super().__init__(parent)
12 
13         self.setMinimumWidth(400)
14         self.setLayout(QVBoxLayout())
15         self.layout().addWidget(WidgetFactory())
16 
17 
18 if __name__ == '__main__':
19     
20     # 4. Apply your custom style.
21 
22     app = QApplication(sys.argv)
23     app.setStyle(MyProxyStyle())
24     main_window = Window()
25     main_window.show()
26     sys.exit(app.exec())
  1. Apply your custom style. In main window, create an instance of your proxy style and set it with QApplication.setStyle().

Note that if you want to change metrics or hints at runtime you need to create a new proxy style instance and pass it to QApplication.setStyle() - changing the attributes of the installed style does not refresh pixel metrics.

14.4 Primitives, Controls, and Complex Controls

Beyond simple changes like widget metrics and style hints, QStyle lets you draw widgets or their elements yourself using methods like drawPrimitive(), drawControl(), or drawComplexControl():

 1 def drawPrimitive(
 2     self,
 3     element: QStyle.PrimitiveElement,
 4     option: QStyleOption,
 5     painter: QPainter,
 6     widget: QWidget | None = None
 7 ) -> None
 8 
 9 def drawControl(
10     self,
11     element: QStyle.ControlElement,
12     option: QStyleOption,
13     painter: QPainter,
14     widget: QWidget | None = None
15 ) -> None
16 
17 def drawComplexControl(
18     self,
19     control: QStyle.ComplexControl,
20     option: QStyleOptionComplex,
21     painter: QPainter,
22     widget: QWidget | None = None
23 ) -> None

drawPrimitive() draws primitive elements, like checkbox and radio button indicators, or focus rectangles. drawControl() draws individual control elements, like labels, push buttons, checkboxes, or headers. drawComplexControl() draws controls that are composed of multiple subcontrols, such as scroll bars, sliders, and spin boxes.

An icon of a clipboard-list

You want to understand how a style paints a widget before you start overriding it. You write a small QProxyStyle that logs widget painting at one of the three levels (primitives, controls or complex controls) so you can see which calls are involved in painting different widgets.

To log drawPrimitive(), drawControl(), and drawComplexControl():

 1 from PySide6.QtWidgets import QStyle, QProxyStyle, QStyleFactory
 2 
 3 # 1 Create a custom style.
 4 
 5 class MyProxyStyle(QProxyStyle):
 6 
 7     def __init__(self, level=None):
 8         super().__init__(QStyleFactory.create('Fusion'))
 9         self.logging_enabled = True
10         self.last_widget = None
11         
12     # 2. Create the log method.
13 
14     def log(self, method, name, widget):
15         
16         if not self.logging_enabled:
17             return
18 
19         widget_name = widget.metaObject().className()
20         if widget_name != self.last_widget:
21             print(f'\n{widget_name}')
22             self.last_widget = widget_name
23 
24         print(f'    {method:<22}{name}')
25         
26     # 3. Reimplement the three methods.
27 
28     def drawPrimitive(self, element, option, painter, widget=None):
29         name = QStyle.PrimitiveElement(element).name
30         self.log('drawPrimitive()', name, widget)
31         super().drawPrimitive(element, option, painter, widget)
32 
33     def drawControl(self, element, option, painter, widget=None):
34         name = QStyle.ControlElement(element).name
35         self.log('drawControl()', name, widget)
36         super().drawControl(element, option, painter, widget)
37 
38     def drawComplexControl(self, control, option, painter, widget=None):
39         name = QStyle.ComplexControl(control).name
40         self.log('drawComplexControl()', name, widget)
41         super().drawComplexControl(control, option, painter, widget)
  1. Create a custom style class. Add a flag to enable or disable logging and a variable to hold the last widget that was painted.

  2. Add a log method to the class. Print the widget’s class name whenever it changes, followed by the drawing method name and the element/control name.

  3. Reimplement the three drawing methods. Reimplement drawPrimitive(), drawControl(), and drawComplexControl() to call the log method and forward the call to the base implementation.

When you run the application, the log shows which calls the Fusion style makes to pain each widget. Three widgets show the typical patterns:

 1 QPushButton
 2     drawControl()         CE_PushButton
 3     drawControl()         CE_PushButtonBevel
 4     drawPrimitive()       PE_PanelButtonCommand
 5     drawControl()         CE_PushButtonLabel
 6 
 7 QCheckBox
 8     drawControl()         CE_CheckBox
 9     drawPrimitive()       PE_IndicatorCheckBox
10     drawControl()         CE_CheckBoxLabel
11 
12 QComboBox
13     drawComplexControl()  CC_ComboBox
14     drawPrimitive()       PE_PanelButtonCommand
15     drawControl()         CE_ComboBoxLabel
QLabel CE_ShapedFrame only. The text is drawn with drawItemText(), which we don’t log.
QLineEdit Two primitives: PE_PanelLineEdit and PE_FrameLineEdit.
QSpinBox One complex control: CC_SpinBox. The spin box embedded QLineEdit is painted separately (PE_PanelLineEdit).
QTabBar PE_FrameTabBarBase, then for each tab CE_TabBarTab (which draws CE_TabBarTabShape and CE_TabBarTabLabel).
QHeaderView For each section, CE_Header (which draws CE_HeaderSection and CE_HeaderLabel), then CE_HeaderEmptyArea.
QTableWidget CE_ShapedFrame and PE_Frame for the frame, then for each cell PE_PanelItemViewRow and CE_ItemViewItem (which draws PE_PanelItemViewItem).

14.5 Reimplementing drawControl()

All three draw* methods require you to draw widget elements with a QPainter. This lower-level approach lets you set pen and brush and use the painter to draw shapes like lines, rectangles, arcs, and ellipses, but also paths, pictures and text. We will see more examples of QPainter use in Chapters 16 and 35, but for now let’s examine a very simple example.

An icon of a clipboard-list

Your application groups its settings into panels, and you want every panel to get a distinctive border without changing the code that builds the panels.

To use custom painting to draw a frame:

 1 from PySide6.QtWidgets import QStyle, QProxyStyle, QStyleFactory
 2 
 3 # 1 Create a custom style.
 4 
 5 class MyProxyStyle(QProxyStyle):
 6 
 7     def __init__(self, level=None):
 8         super().__init__(QStyleFactory.create('Fusion'))
 9         self.logging_enabled = True
10         self.last_widget = None
11         
12     # 2. Create the log method.
13 
14     def log(self, method, name, widget):
15         
16         if not self.logging_enabled:
17             return
18 
19         widget_name = widget.metaObject().className()
20         if widget_name != self.last_widget:
21             print(f'\n{widget_name}')
22             self.last_widget = widget_name
23 
24         print(f'    {method:<22}{name}')
25         
26     # 3. Reimplement the three methods.
27 
28     def drawPrimitive(self, element, option, painter, widget=None):
29         name = QStyle.PrimitiveElement(element).name
30         self.log('drawPrimitive()', name, widget)
31         super().drawPrimitive(element, option, painter, widget)
32 
33     def drawControl(self, element, option, painter, widget=None):
34         name = QStyle.ControlElement(element).name
35         self.log('drawControl()', name, widget)
36         super().drawControl(element, option, painter, widget)
37 
38     def drawComplexControl(self, control, option, painter, widget=None):
39         name = QStyle.ComplexControl(control).name
40         self.log('drawComplexControl()', name, widget)
41         super().drawComplexControl(control, option, painter, widget)
  1. Create a custom style. Subclass QproxyStyle and initialize it with the Fusion style.

  2. Check if the element is a frame. In drawControl(), test whether the element is CE_ShapedFrame and the frame shape is StyledPanel.

  3. Draw the rectangles. When the conditions match, save the painter state, enable antialiasing, and draw three nested rectangles (a soft outer glow, a main border, and a thinner inner border). After that, restore the painter. For every other element forward the call to the base class.