Olivier Cléro

Writing a custom QStyle

25/11/2021

It has been a long time that I wanted to write a total custom appearance for Qt applications. My experience with Qt’s CSS stylesheet was not very good, and I stumbled upon Phantom Style, by Andrew Richards, a custom-made Qt style that improves from Qt’s Fusion Style. Its look and performance is pretty good. So I decided to write my own, in a more modern fashion. And it’ll be the occasion to add dynamic theming. Here is a picture of what it looks like with the default colors, on Windows on a standard DPI screen:

What we’ll be handling, apart for (obviously) drawing widgets on screen:

  • Animation system
  • Dynamic Theming system, from .json files, with hot-reload.
  • Dynamic Pixmap Coloring, to ensure icons stay coherent with the theme colors.
  • Out-of-widget-bounds Focus Border, à la macOS.
  • Drop Shadows (coming soon).

QWidget and QPainter

What is QWidget?

QWidget is the class that represents a element on the Graphical User interface. It has thus a visual representation on the screen, and receives events regularly to allow it to update its state. It must be re-drawn on the screen each time it receives an event that suggests a modification of its appearance. For instance, a button will repaint itself when it is pressed by the mouse pointer.

How does QWidget draw itself on the screen?

The drawing of a QWidget is done when the method void paintEvent(QPaintEvent* evt) is called. Since this method is virtual, one’s can override it to customize the painting.

To draw, a QPainter object is needed. This class has the necessary API to draw anything: rectangles, lines, text, images, paths. The common pattern to paint a QWidget is the following:

1
2
3
4
virtual void MyWidget::paintEvent(QPaintEvent* evt) override {
  QPainter painter(this);
  painter.fillRect(rect(), Qt::red);
}

Note that the painting is made by the CPU, with QPainter’s Raster backend. This has potential implications on performance.

QStyle

What is QStyle?

The QStyle class is responsible for handling the look and feel of a Qt application. It provides methods to get sizing information, animation durations,… and last but not least: methods to paint widgets.

For each OS, QStyle has been subclassed and the look and feel of the native widgets have been reproduced. You’ll find QWindowsStyle, QMacStyle, … and so on. Unfortunately, these class are part of Qt’s private API. If we want a custom look and feel, we have to subclass it too.

Note that there is one QStyle instance running at a time, which is set on the application with the static method QApplication::setStyle(QStyle *style). Think of it like a singleton that is accessible from all the widgets.

Why not just using a CSS stylesheet?

Actually, when you use a CSS stylsheet (also known as QSS) on your Qt application, a dedicated QStyle class is automatically created by the QApplication to handle the painting based on the content of the CSS file: QStylesheetStyle. It is a quick way to customize the style of the application.

However this has some drawbacks:

  • Performance is not very good.
  • Some drawing is wrong (strokes and border-radiuses, for instance).
  • Qt uses a subset of CSS2, which is pretty old and limited. Important mising features are animations, shadows, and any kind of advanced graphical effect.

How does QStyle work?

In QWidget’s paintEvent method

When a QWidget needs to be repainted, its method paintEvent() will be called by Qt’s internal QWidgetRepaintManager. This paintEvent() method will get the QStyle that is set on the QWidget, then use it to draw the widget.

Since QStyle is a singleton-like object, and does not know the QWidget, it needs information about the widget’s state and features. This information is contained by the base class QStyleOption, which is subclassed by all the different widgets. Every widget has its corresponding QStyleOption.

For example, a QPushButton will draw itself like this (basically):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
virtual void QPushButton::paintEvent(QPaintEvent* evt) override {
  // Get the button's state (checked, enabled, pressed, etc.)
  QStyleOptionButton opt;
  initStyleOption(&opt);

  // Create a QPainter instance for this widget.
  QPainter p(this);

  // Use the QStyle currently set to draw the widget.
  // Here: background then foreground.
  const auto* style = this->style();
  style->drawControl(QStyle::ControlElement::CE_PushButtonBevel, &opt, &p, this);
  style->drawControl(QStyle::ControlElement::CE_PushButtonLabel, &opt, &p, this);
}

In QStyle’s methods

QStyle relies on const methods: they don’t modify the input, apart from the QPainter. They use their parameters in read-only mode, and only call internal draw primitives.

Every QWidget is identified by an enum value, and its current state by a QStyleOption, so the QStyle::drawControl wouldn’t even need to have the QWidget as parameter. It is just there as a way of advanced customizing.

Also, QStyle’s code has been properly separated between const methods to draw and const methods to get geometry to draw at.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// A *very much* simplified example of how QStyle works internally.
void SomeQStyle::drawControl(ControlElement element,
                          const QStyleOption* o,
                          QPainter* p,
                          const QWidget* w) const {
  switch (element) {
  case CE_PushButtonBevel: {
    // Cast the QStyleOption to the widget-specific option.
    const auto* btnOption = qstyleoption_cast<const QStyleOptionButton *>(o);

    // Get geometry information.
    const auto bgRect = subElementRect(SE_PushButtonBevel, o, w);

    // Get color information.
    const auto colors = myButtonOption.palette.currentColorGroup();
    const auto color = btnOption.palette.color(colors, QPalette::Button);

    // Draw the button's background.
    p->setBrush(color);
    p->setPen(Qt::NoPen);
    p->drawRect(bgRect);

  } break;
  case CE_PushButtonLabel:
    // Do the same for the button's foreground.
    ...
    break;
  default:
    QCommonStyle::drawControl(element, o, p, w);
  }
}

We’ll see the internal workings of a QStyle in the next part.

A custom QStyle

Methods to implement

We’ll subclass QCommonStyle to avoid handling generic behaviors and only provide necessary code.

Qt divides widgets’ visual appearance into smaller interactive parts: subcontrols. These interactive parts are themselves divided into smaller parts: subelements. QStyle provides primitive dedicated methods to draw these elements, instead of a huge methods that would draw the complete widget. These smaller parts are identified by the following enums. It is not clear how they are classified, though.

Drawing

The enums that define the parts to draw are:

  • PrimitiveElement: Basic elements used by multiple widgets. Exemple: PE_IndicatorCheckBox is the check mark of a QCheckBox but is also used for checkable items in a QListView.
  • ControlElement: A part of a specific widget. Example: CE_PushButtonBevel is the part that you click on a QPushButton.
  • SubElement: A sub-part of a specific widget. Example: SE_PushButtonContents is the foreground (text+icon) of a QPushButton.
  • ComplexControl: Widgets composed by other interactive elements (but not widgets). Example: CC_SpinBox, which corresponds to QSpinBox has a text field and up/down buttons.
  • SubControl: A widget that is part of a ComplexControl.

We need to implement these virtual methods, to handle the possible values of the aforementioned enums (Note: the w and p mean that it takes a const QPainter* and a const QWidget* as parameters):

Method Role
void drawPrimitive(PrimitiveElement e, const QStyleOption* o, w, p) const Draws a basic, unit, element (e.g. background of a button).
void drawControl(ControlElement e, const QStyleOption* o, w, p) const Draws a QWidget, by calling drawPrimitive().
void drawComplexControl(ComplexControl c, const QStyleOptionComplex* o, w, p) const Draws a QWidget composed by one or several interactive parts.

Sizing

Not only we need to draw the widgets, but we also need to provide size and position information to draw them. Some enums help us identify which part to size.

  • PixelMetric: Used to calculate sizes for the contents of various widgets.
  • ContentsType: Used to calculate sizes for the contents of various widgets.

These enums are used by these methods:

Method Role
int pixelMetric(PixelMetric m, const QStyleOption* o, w) const Gives the size of a part of a widget like margins, padding, icon sizes, etc.
QSize sizeFromContents(ContentsType c, const QStyleOption* o, const QSize& s, w) const Gives the QWidget’s size based on its content (text, icon, etc.).
QRect subControlRect(ComplexControl c, const QStyleOptionComplex* o, SubControl sc, w) const Gives the coordinates and size where to draw an inner interactive part of a complex QWidget.
QRect subElementRect(SubElement s, const QStyleOption* o, w) const Gives the coordinates and size where to draw a part of a QWidget.

General-purpose

More general-purpose enums and methods:

  • StandardPixmap: General-purpose icons (success, warning, error, etc.) and images.
  • StyleHint: Identifies a size, a mode or anything else, that is used to customize the behavior and does not fit anywhere else.
Method Role
SubControl hitTestComplexControl(ComplexControl cc, const QStyleOptionComplex* o, const QPoint& p, w) const Returns which sub-widget has been hit by the mouse.
int styleHint(StyleHint s, const QStyleOption* o, w, QStyleHintReturn *hret = nullptr) const Provides specific values (such as border radius). hret is used if value is more complicated than just an int.
QPixmap standardPixmap(StandardPixmap sp, const QStyleOption* o, w) const Used to give custom icons to widgets, dialogs, etc.
void polish(QWidget* w) Called before w is painted for the first time by the QStyle.
void unpolish(QWidget* w) Called before w is destroyed or when its QStyle has changed.

Theming support

Qt has QPalette, its built-in theming object. Yet, this class is not powerful enough to handle proper modern theming, like we see with current CSS frameworks.

We can create a Theme class to replace QPalette, that would contain all basic values like colors, sizes, animation durations, etc.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Theme {
  // Colors.
  QColor  primaryColor{ 0x0CA277 };
  QColor  primaryColorHovered{ 0x3DB592 };
  QColor  primaryColorPressed{ 0x6DC7AD };
  QColor  primaryColorDisabled{ 0x91D5C1 };
  ...

  // Sizes.
  int     animationDuration{ 192 }; // ms
  double  borderRadius{ 6. }; // px
  QSize   iconSize{ 16, 16 }; // px
  int     spacing{ 8 }; // px
  ...
};

A Theme object can be created from JSON (or any data-oriented text files). JSON is conveniant because Qt has everything we need to parse it (QJsonDocument, QJsonObject…). Here is an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "primaryColor": "#009167",
  "primaryColorDisabled": "#00916733",
  "primaryColorHovered": "#067a59",
  "primaryColorPressed": "#038660",
  ...
  "animationDuration": 192,
  "borderRadius": 6.0,
  "iconSize": 16,
  "spacing": 8
  ...
}

Note that you have to write parsers to create Qt objects like QColor, QSize, etc. They won’t be all shown here, as they’re pretty straightforward to write, and will depend on your own design and data specifications. I had to write functions like std::optional<QColor> tryGetColorFromHexaString(QString const& str), as instance:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
std::optional<QColor> tryGetColorFromHexaString(QString const& str) {
  constexpr auto HEX_BASE = 16;
  constexpr auto RGB_LENGTH = 3 * 2 + 1;
  constexpr auto RGBA_LENGTH = 4 * 2 + 1;

  const auto length = str.length();
  if (str.startsWith('#') && (length == RGB_LENGTH || length == RGBA_LENGTH)) {
    auto success{ false };

    const auto r_str = str.midRef(1, 2);
    const auto r = r_str.toInt(&success, HEX_BASE);
    if (success) {
      const auto g_str = str.midRef(3, 2);
      const auto g = g_str.toInt(&success, HEX_BASE);
      if (success) {
        const auto b_str = str.midRef(5, 2);
        const auto b = b_str.toInt(&success, HEX_BASE);
        if (success) {
          QColor result{ r, g, b };

          const auto a_str = str.midRef(7, 2);
          const auto a = a_str.toInt(&success, HEX_BASE);
          if (success) {
            result.setAlpha(a);
          }

          return result;
        }
      }
    }
  }

  return {};
}

Our QStyle can have a current Theme that it will use to paint. It even allows to change theme at run time, and having everything re-draw.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void CustomQStyle::setTheme(Theme const& theme) {
  if (_currentTheme != theme) {
    _currentTheme = theme;
    emit themeChanged();
    triggerCompleteRepaint();
  }
}

void CustomQStyle::triggerCompleteRepaint() {
  // Clear cached QPixmaps if they depend on the colors or your theme.
  clearCaches();

  // Update the QPalette.
  updatePalette();
  QApplication::setPalette(standardPalette());

  // Repaint all top-level widgets.
  for (auto* widget : QApplication::topLevelWidgets()) {
    widget->update();
  }
}

Animations

Since QStyle only gets information about the widget’s state with QStyleOption, it does not have any notion of time, which is necessary for animations. We need a way to store this state.

Qt’s own private implementation

Qt’s sources show us that the workaround they used is to keeps a HashMap in the QStyle, associating a QObject* to a dedicated object used for handling its animation: QStyleAnimation. It can animates any QObject and not only its subclass QWidget.

1
QHash<const QObject*, QStyleAnimation*> animations;

When the animation is running and a value (e.g. a QColor) has been updated, it needs a way to trigger a visual update of the QWidget. This is done by sending an event synchronously to the QWidget

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Qt's implementation.

// Create an event of type QEvent::StyleAnimationUpdate.
QEvent event(QEvent::StyleAnimationUpdate);
event.setAccepted(false);

// Send the event to the widget.
QCoreApplication::sendEvent(widget, &event);

// Stop the animation if the widget set Accepted to false.
if (!event.isAccepted()) {
  stop();
}

Unfortunately, QStyleAnimation is not part of Qt’s public API. We’ll follow the same mechanism by re-making this system from scratch for our own QStyle.

Custom implementation

This is what our system needs to do:

  1. Registering all newly created QWidget and their associated WidgetAnimator in the HashMap. We’ll do this lazily, only as needed, when an animation is requested.

  2. Triggering animations when the QWidget’s state changes. We’ll do this lazily, directly in the paint methods.

  3. Providing animated (i.e. interpolated) values that our QStyle will use to paint the QWidget.

WidgetAnimator is a class that stores all the animations set on a QWidget. For the animations, we have two options:

  1. Create our own implementation.
  2. Rely on the already-existing Qt implementation that is QVariantAnimation.

The second option might be not the best, performance-wise, but it’s sufficient. Consequently, an animation is handled by the class WidgetAnimation<T> that is a typed wrapper over QVariantAnimation. Typing is useful so we won’t have to write the conversion code lines every time. WidgetAnimation<T> will trigger a new repaint for the QWidget every time the animation value has changed.

1
2
3
4
5
6
7
8
9
// Our own implementation.
QObject::connect(&_qVariantAnimation,
                 &QVariantAnimation::valueChanged,
                 parentWidget,
  [parentWidget](const QVariant&) {
    // Force widget repaint.
    parentWidget->update();
  },
  Qt::ConnectionType::QueuedConnection);

We need a class that creates WidgetAnimators when needed: WidgetAnimationManager.

To animate a property, we have two options:

  1. Rely on an enum called StyleProperty that contains BackgroundColor, ForegroundColor, etc. and we would store corresponding animations into an hash map.
  2. Directly add the methods in the class.

We chose the second option as it allow us to have typed methods. A MACRO (not shown here) may help, because the code is the same for all properties.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
QColor animateBackgroundColor(const QWidget* w, const QColor& target, int duration) {
  if (_animationsEnabled) {
    // Create the animator lazily.
    auto* animator = getOrCreateAnimator(w);

    // Configure animation.
    animator->setBackgroundColorDuration(w->isEnabled() ? duration : 0);
    animator->setBackgroundColorEasing(easing);
    animator->setBackgroundColor(target);

    // Return current value.
    return animator->getBackgroundColor();
  } else {
    return target;
  }
}

This method is called at every paint, i.e. for every frame that needs to be drawn.

  • If the target value changes, it restarts the animation, with the current one as starting value.
  • If the target value doesn’t change, it returns the current interpolated value.

Out-of-widget-bounds focus border

As you may already known, it is only possible to drawn within the QWidget’s bounds. The QPainter is clipped to its limit, thus can’t drawn outside of the QWidget coordinates.

If we want a focus border that draws outside the limits of the QWidget, we can simply use Qt’s QFocusBorder. Before a QWidget draws itself, the method QStyle::polish(QWidget* w)is called, allowing us to install any kind of behavior on the widget. We use this to instanciate aQFocusBorder` on the widgets we want to have this macOS-like focus style.

1
2
3
4
5
6
7
8
void CustomStyle::polish(QWidget* w) {
  QCommonStyle::polish(w);

  if (shouldHaveExternalFocusFrame(w)) {
    auto* focusframe = new QFocusFrame(w);
    focusframe->setWidget(w);
  }
}

This class will call QStyle::drawControl(CE_FocusFrame, styleOpption, widget) to draw itself. We can then check its parent QWidget’s type to determine the QRect of the focus border. We need to do this for every type that has a QFocusBorder.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void CustomStyle::drawControl(ControlElement ce,
                              const QStyleOption* opt,
                              QPainter* p,
                              const QWidget* w) const {
  switch (ce) {
  ...
  case CE_FocusFrame:
    if (const auto* focusFrame = qobject_cast<const QFocusFrame*>(w)) {
      const auto* monitoredWidget = focusFrame->widget();
      if (const auto* button = qobject_cast<const QPushButton*>(monitoredWidget)) {
        // Prepare monitored widget QStyleOption.
        QStyleOptionButton optButton;
        optButton.QStyleOption::operator=(*opt);
        optButton.initFrom(button);

        // Call QStyle's method to get the focus rect.
        // It's up to you to give the correct out-of-bounds QRect there.
        rect = subElementRect(SE_PushButtonFocusRect, &optButton, button);
      }
    }
    return;
  ...
  }
}

Cheatsheet for QWidgets and their QStyle enum values

Each QWidget has its dedicated enum values. Here is a guide to help you with this mess.

Basic Controls

QPushButton

  • PM_ButtonMargin, PM_ButtonDefaultIndicator, PM_MenuButtonIndicator, PM_ButtonShiftHorizontal, PM_ButtonShiftVertical, PM_ButtonIconSize
  • CT_PushButton
  • SE_PushButtonContents, SE_PushButtonBevel, SE_PushButtonFocusRect
  • PE_PanelButtonBevel, PE_IndicatorButtonDropDown, PE_FrameButtonBevel
  • CE_PushButton, CE_PushButtonBevel, CE_PushButtonLabel

QCheckBox

  • PM_CheckBoxLabelSpacing, PM_IndicatorWidth, PM_IndicatorHeight
  • CT_CheckBox
  • SE_CheckBoxContents, SE_CheckBoxClickRect, SE_CheckBoxIndicator, SE_CheckBoxFocusRect
  • PE_IndicatorCheckBox
  • CE_CheckBox, CE_CheckBoxLabel

QRadioButton

  • PM_RadioButtonLabelSpacing, PM_ExclusiveIndicatorWidth, PM_ExclusiveIndicatorHeight
  • CT_RadioButton
  • SE_RadioButtonContents, SE_RadioButtonClickRect, SE_RadioButtonIndicator, SE_RadioButtonFocusRect
  • PE_IndicatorRadioButton
  • CE_RadioButton, CE_RadioButtonLabel

QProgressBar

  • PM_ProgressBarChunkWidth
  • SE_ProgressBarGroove, SE_ProgressBarContents, SE_ProgressBarLabel
  • CE_ProgressBar, CE_ProgressBarGroove, CE_ProgressBarContents, CE_ProgressBarLabel
  • PE_IndicatorProgressChunk
  • CT_ProgressBar

QMenuBar

  • PE_PanelMenuBar
  • CE_MenuBarItem, CE_MenuBarEmptyArea
  • PM_MenuBarPanelWidth, PM_MenuBarItemSpacing, PM_MenuBarHMargin, PM_MenuBarVMargin
  • CT_MenuBar, CT_MenuBarItem

QMenu

  • PE_IndicatorMenuCheckMark, PE_PanelMenu
  • CE_MenuItem, CE_MenuScroller, CE_MenuTearoff, CE_MenuEmptyArea, CE_MenuHMargin, CE_MenuVMargin
  • PM_SubMenuOverlap, PM_MenuScrollerHeight, PM_MenuTearoffHeight, PM_MenuDesktopFrameWidth, PM_MenuPanelWidth, PM_MenuHMargin, PM_MenuVMargin
  • CT_Menu, CT_MenuItem

QToolBar

  • PE_PanelToolBar, PE_IndicatorToolBarHandle, PE_IndicatorToolBarSeparator
  • CE_ToolBar
  • PM_ToolBarFrameWidth, PM_ToolBarHandleExtent, PM_ToolBarItemMargin, PM_ToolBarItemSpacing, PM_ToolBarSeparatorExtent, PM_ToolBarExtensionExtent
  • SE_ToolBarHandle

QLineEdit

  • PE_PanelLineEdit
  • PM_TextCursorWidth
  • SE_LineEditContents
  • CT_LineEdit

QTreeView, QListView, QTableView

  • PE_IndicatorBranch, PE_IndicatorItemViewItemCheck, PE_IndicatorHeaderArrow, PE_IndicatorColumnViewArrow, PE_PanelItemViewItem, PE_PanelItemViewRow
  • CE_Header, CE_HeaderSection, CE_HeaderLabel, CE_ItemViewItem, CE_HeaderEmptyArea
  • PM_TreeViewIndentation, PM_HeaderDefaultSectionSizeHorizontal, PM_HeaderDefaultSectionSizeVertical, PM_HeaderMarkSize, PM_HeaderGripMargin, PM_HeaderMargin
  • SE_HeaderArrow, SE_HeaderLabel, SE_ItemViewItemCheckIndicator, SE_TreeViewDisclosureItem, SE_ItemViewItemDecoration, SE_ItemViewItemText, SE_ItemViewItemFocusRect
  • CT_HeaderSection, CT_ItemViewItem

QToolTip

  • PE_PanelTipLabel

QTabBar

  • PE_FrameTabBarBase, PE_IndicatorTabTearLeft, PE_IndicatorTabTearRight, PE_IndicatorTabClose
  • CE_TabBarTab, CE_TabBarTabShape, CE_TabBarTabLabel
  • PM_TabBarTabOverlap, PM_TabBarTabHSpace, PM_TabBarTabVSpace, PM_TabBarBaseHeight, PM_TabBarBaseOverlap, PM_TabBarScrollButtonWidth, PM_TabBarTabShiftHorizontal, PM_TabBarTabShiftVertical, PM_TabBar_ScrollButtonOverlap, PM_TabCloseIndicatorWidth, PM_TabCloseIndicatorHeight
  • SE_TabBarTearIndicatorLeft, SE_TabBarTearIndicatorRight, SE_TabBarScrollLeftButton, SE_TabBarScrollRightButton, SE_TabBarTabLeftButton, SE_TabBarTabRightButton, SE_TabBarTabText
  • CT_TabBarTab

QStatusBar

  • PE_PanelStatusBar, PE_FrameStatusBar,

QSplitter

  • CE_Splitter
  • PM_SplitterWidth
  • CT_Splitter

QFocusFrame

  • CE_FocusFrame

QMessagaBox

  • PM_MessageBoxIconSize

Complex Controls

QSlider

  • PM_SliderThickness, PM_SliderControlThickness, PM_SliderLength, PM_SliderTickmarkOffset, PM_SliderSpaceAvailable
  • SC_SliderGroove, SC_SliderHandle, SC_SliderTickmarks
  • SE_SliderFocusRect
  • CC_Slider
  • CT_Slider

QDial

  • SC_DialHandle, SC_DialGroove, SC_DialTickmarks
  • CC_Dial

QScrollBar

  • CE_ScrollBarAddLine, CE_ScrollBarSubLine, CE_ScrollBarAddPage, CE_ScrollBarSubPage, CE_ScrollBarSlider, CE_ScrollBarFirst, CE_ScrollBarLast
  • PM_ScrollBarExtent, PM_ScrollBarSliderMin, PM_ScrollView_ScrollBarSpacing, PM_ScrollView_ScrollBarOverlap
  • SC_ScrollBarAddLine, SC_ScrollBarSubLine, SC_ScrollBarAddPage, SC_ScrollBarSubPage, SC_ScrollBarFirst, SC_ScrollBarLast, SC_ScrollBarSlider, SC_ScrollBarGroove
  • CC_ScrollBar
  • CT_ScrollBar

QComboBox

  • PM_ComboBoxFrameWidth
  • CT_ComboBox
  • SE_ComboBoxFocusRect
  • CE_ComboBoxLabel
  • SC_ComboBoxArrow, SC_ComboBoxEditField, SC_ComboBoxFrame, SC_ComboBoxListBoxPopup
  • CC_ComboBox

QDockWidget

  • PE_IndicatorDockWidgetResizeHandle
  • CE_DockWidgetTitle
  • PM_DockWidgetTitleBarButtonMargin, PM_DockWidgetSeparatorExtent, PM_DockWidgetHandleExtent, PM_DockWidgetFrameWidth, PM_DockWidgetTitleMargin
  • SE_DockWidgetFloatButton, SE_DockWidgetTitleBarText, SE_DockWidgetCloseButton, SE_DockWidgetIcon

QToolButton

  • PE_PanelButtonTool, PE_FrameButtonTool
  • CE_ToolButtonLabel
  • SC_ToolButton, SC_ToolButtonMenu
  • SE_ToolBoxTabContents
  • CC_ToolButton
  • CT_ToolButton

QSpinBox

  • PE_IndicatorSpinDown, PE_IndicatorSpinUp, PE_IndicatorSpinMinus, PE_IndicatorSpinPlus
  • PM_SpinBoxFrameWidth, PM_SpinBoxSliderHeight
  • SC_SpinBoxUp, SC_SpinBoxDown, SC_SpinBoxFrame, SC_SpinBoxEditField
  • CC_SpinBox
  • CT_SpinBox

QGroupBox

  • SC_GroupBoxFrame, SC_GroupBoxLabel, SC_GroupBoxCheckBox, SC_GroupBoxContents
  • CC_GroupBox
  • CT_GroupBox

QProxyStyle

A QProxyStyle is a way to modify the behavior of a QStyle by dynamically overriding painting or other specific style behavior. Instead of subclassing a QStyle-derived class, you need a class that inherits QProxyStyle.

The QStyle checks if it has a proxy before each method call, and calls it if so. However, as said in the documentation, there is no guarantee that QStyle::proxy() will be called by the QStyle you override. User-defined or system-controlled styles may ignore their proxy.

Currently, my QStyle doesn’t support the use of a proxy style.

Coming soon…

Usage

To use a QStyle on a QApplication, it can be done like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int main(int argc, char* argv[]) {
  QApplication qApplication(argc, argv);

  // Create your custom style.
  auto* style = new CustomStyle(&qApplication);

  // Configure your custom style.
  style->setAnimationsEnabled(true);
  style->setThemeJsonPath(QStringLiteral(":/dark-theme.json"));

  // Applies the custom style to the whole application.
  qApplication.setStyle(style);
  ...
  return qApplication.exec();
}

Final Thoughts

It was a long and tedious task, but now I have more control over what is displayed by the Qt application. I also see the limits of Qt styling system. Some widgets were particularly hard to get right, such as QTabBar, because Qt’s private code calls the QStyle methods in a specific order that we can’t modify. For the QTabBar, as instance, the tabs are drawn in a specific order and it will impact the shape you may give them.

I made sure that my QStyle library does not contain any company-specific code, so the lib is totally generic and can be used on any Qt application.

I plan to improve it progressively and open-source it, so the Qt community is able to benefit it.