Styles and Style Sheets
Qt's styling has two layers: the style (the widget rendering engine - the Fusion, the Windows, the macOS - that draws the widgets) and the style sheet (the CSS-like setStyleSheet that overrides the style's rendering for the selected widgets). The style is the platform's or the app-chosen rendering (the QApplication.setStyle), and the style sheet is the app's override (the setStyleSheet on the app, on a widget, or loaded from a file) - the colors, the borders, the fonts, the states (the :hover, the :pressed, the :disabled), and the properties (the background-color, the border, the padding). This chapter covers the style (the setStyle, the available styles), the style sheet's syntax (the selector, the property, the state pseudo-class), the application-wide versus the per-widget sheet, the properties the Qt style sheet supports (the CSS subset), and the rules that keep a style sheet maintainable.
The Style: The Rendering Engine
from PyQt5.QtWidgets import QApplication, QStyleFactory
import sys
# the STYLE: the widget rendering engine (the look the
# widgets have WITHOUT any style sheet)
# the available styles (the QStyleFactory.keys):
print(QStyleFactory.keys())
# the common: the "Fusion" (the cross-platform, the flat),
# the "Windows" (the win's native), the "macOS" (the mac's)
app = QApplication(sys.argv)
# the app's style: the setStyle (the all-widgets change)
app.setStyle("Fusion")
# the Fusion is the cross-platform choice (the consistent
# look on the three OS, the base the style sheet's
# overrides sit on) - the app that wants a controlled,
# reproducible look sets the Fusion explicitly instead of
# trusting the platform default
The style is the rendering engine that draws the widgets: the QStyleFactory.keys() lists the available ones (the Fusion, the Windows, the macOS), and the app.setStyle("Fusion") changes the rendering for every widget in the process. The Fusion is the cross-platform choice - the flat, consistent look that renders the same on the three OS, which is the base an app that wants a controlled, reproducible appearance sets explicitly instead of trusting the platform default (the platform style gives the native look but varies between OS). The style is the layer under the style sheet: the sheet's properties override the style's rendering for the widgets it selects, and the widgets the sheet does not reach keep the style's rendering. The two layers are the style (the engine, the platform or the chosen look) and the sheet (the override, the app's specific colors and borders), and understanding which layer a change belongs to is the first decision in styling an app.
The Style Sheet: The CSS Subset
from PyQt5.QtWidgets import (QApplication, QPushButton, QLineEdit)
import sys
app = QApplication(sys.argv)
# the setStyleSheet: on the app (the global) or on a widget
# (the local, the scoped to that widget and its children)
# the GLOBAL sheet (the app-wide):
app.setStyleSheet("""
QPushButton {
background-color: #2563eb;
color: white;
border: none;
border-radius: 4px;
padding: 6px 12px;
font-weight: bold;
}
QPushButton:hover { background-color: #1d4ed8; }
QPushButton:pressed { background-color: #1e40af; }
QPushButton:disabled { background-color: #93c5fd; }
""")
# the SELECTORS: the widget class (the QPushButton), the
# object name (the #name, the setObjectName), the
# property (the [property=value]), the descendant (the
# "QDialog QPushButton" - the button inside a dialog),
# the child (the "QDialog > QPushButton" - the direct)
# the STATES (the pseudo-classes): the :hover (the pointer
# over), the :pressed (the click held), the :disabled
# (the insensitive), the :checked (the checkable on),
# the :focus (the keyboard focus)
# the PROPERTIES: the background-color / the background
# (the color, the gradient, the image), the border (the
# width + the style + the color), the border-radius, the
# padding, the margin, the color, the font (the family,
# the size, the weight), the min-width / the max-height,
# the outline (the focus ring)
btn = QPushButton("Save")
btn.setObjectName("saveButton")
# the object-name selector: the #saveButton in the sheet
# targets ONLY that button (the specific override)
The style sheet is the CSS subset, and its syntax is the familiar one: the selector (the widget class, the #objectName, the [property=value], the descendant and the child combinator) opens a block of the property declarations. The states are the pseudo-classes - the :hover (the pointer over), the :pressed (the click held), the :disabled (the insensitive), the :checked (the checkable on), the :focus (the keyboard focus) - and the properties are the CSS subset Qt supports: the background-color and the background (the color, the gradient, the image), the border, the border-radius, the padding, the margin, the color, the font properties, the min-width/max-height bounds, and the outline (the focus ring). The setStyleSheet goes on the app (the global, the all-widgets) or on a widget (the local, scoped to that widget and its children - the child selectors resolve within the widget's subtree). The setObjectName gives a widget the #name selector, the precise target for the override that is one widget, not the whole class. The sheet is the app's visual identity layer: the style draws, and the sheet overrides where the app says so.
The Scope: Global Versus the Widget
from PyQt5.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
# the scope is the DECISION: the sheet on the app reaches
# EVERY widget; the sheet on a widget reaches that widget
# and its CHILDREN (the descendant selectors resolve in
# the widget's subtree)
# the pattern: the global sheet for the app-wide identity
# (the palette, the fonts, the common widget look), and
# the per-widget sheet for the specific (the one button's
# color, the one panel's border) - the specific is MORE
# specific and wins over the global for the widget it
# targets (the CSS-like specificity)
window.setStyleSheet("""
#statusBar {
background-color: #f1f5f9;
border-top: 1px solid #e2e8f0;
}
""")
# the sheet from a FILE: the read + the set (the
# externalized sheet, the versionable asset)
with open("style.qss", encoding="utf-8") as f:
app.setStyleSheet(f.read())
# the .qss file is the plain text of the same syntax -
# the sheet as the asset (the resource, the next
# chapter's subject)
# the RESET: the empty string clears the sheet
# (the app.setStyleSheet("") - the back to the style's
# rendering)
The scope is the decision: the sheet on the app reaches every widget (the app-wide identity), and the sheet on a widget reaches that widget and its children, with the descendant selectors resolving in the widget's subtree. The pattern is the split: the global sheet carries the app-wide identity (the palette, the fonts, the common widget look), and the per-widget sheet carries the specific (the one button, the one panel), the more specific selector winning over the global for the widget it targets - the CSS-like specificity the Qt style sheet implements. The externalized form is the .qss file: the same syntax as plain text, read and set with the setStyleSheet, the sheet as the asset (the versionable file, the resource chapter's subject for bundling it into the app), and the reset is the empty string (the back to the style's rendering). The two scopes plus the file form are the styling architecture: the style as the base, the global sheet as the identity, the widget sheet as the specific, and the file as the asset.
The Working Rules
- The style is the engine, the sheet is the override. The
setStylechooses the rendering (theFusionfor the cross-platform controlled look); thesetStyleSheetoverrides it where the app says so. The change that belongs to the whole look is the style; the change that is the app's specific is the sheet. - The global sheet for the identity, the widget sheet for the specific. The app-wide palette and fonts on the app; the one widget's color on the widget (the
#objectNamethe precise target). The specificity resolves the overlap the CSS-like way. - The states are the pseudo-classes, not the code. The
:hover, the:pressed, the:disabled, the:focusin the sheet - the state styling is a declaration, not the event handler that changes the color. - Externalize the sheet as the
.qssasset. The file (the read, thesetStyleSheet) is the versionable form, the resource chapter bundles it, and the empty string is the reset - the sheet as the asset, not the inline string in the code. - The sheet is a CSS subset, not the full CSS. The properties the Qt style sheet supports are the documented set (the
background, theborder, thefont, the bounds, theoutline); the property it does not support is the silent no-op, and the Qt style sheet reference is the authority for what it does.
Summary
Qt's styling is two layers: the style (the rendering engine - the QStyleFactory.keys() the available, the app.setStyle("Fusion") the cross-platform choice, the platform style the native look) and the style sheet (the CSS subset - the setStyleSheet on the app for the global or on a widget for the scoped, the selector the class / the #objectName / the descendant, the states the :hover / :pressed / :disabled / :focus, the properties the background / border / font / the bounds / the outline). The scope is the decision: the global sheet carries the app's identity, the widget sheet carries the specific (the specificity resolves the overlap), and the .qss file is the externalized asset the resource chapter bundles, with the empty string the reset. The rules keep the styling maintainable: the style for the whole look, the sheet for the specific, the states as the declarations not the code, the sheet as the file not the inline string, and the subset documented - the app's visual identity is the style plus the sheet, layered the CSS-like way.