Wiki

The QStyle API in Qt 4

by Jasmin Blanchette
Qt applications are given a native look and feel on every platform, either by emulating the native style or by using the system's theming engine. Since 2.0, Qt also lets you customize the look and feel through its sophisticated style API, a feature that is especially useful for Qt/Embedded since mobile devices usually require customized user interfaces.

In this article, we will retrace the history of the style API from Qt 1 to Qt 4 and show how to get the most out of the latest API. By the end, you should understand how to write style-aware widgets and the basics of how to create custom styles with Qt 4.

Qt 1: All You Need Is GUIStyle

The first version of Qt had no real support for styles beyond the infamous GUIStyle enum:

enum GUIStyle {
    MacStyle,     // obsolete
    WindowsStyle,
    Win3Style,    // obsolete
    PMStyle,      // obsolete
    MotifStyle
};

The GUI style would be set globally for the entire application using QApplication::setStyle() and could be overridden for individual widgets using QWidget::setStyle(). Qt's built-in widgets checked the value of QWidget::style() in their paint event and tried to paint themselves correctly for the style, like this:

void QPushButton::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
 
    if (style() == MotifStyle) {
        // Motif painting code
    } else {
        // Windows painting code
    }
}

While this approach was sufficient to emulate the look and feel of the two platforms supported by Qt at the time (Unix/X11 and Windows 95), it forced users who wanted to implement custom painting styles to subclass all of Qt's built-in widgets, and even this didn't work with Qt's built-in widgets that created child widgets, such as QFileDialog, QSplitter, and QWizard.

Qt 2: QStyle Takes Over Widget Painting

The emergence of themable X11-based desktops prompted us to redesign the style API for Qt 2.0. The new API substituted the QStyle abstract base class for the GUIStyle enum. Qt 2 initially included four styles: QWindowsStyle, QMotifStyle, QCDEStyle, and QPlatinumStyle.

QStyle provided virtual functions for drawing widgets, getting sizes, and querying which part of a widget was hit when the user clicked on it. Each built-in Qt widget had its own set of functions. For example, here's the QStyle API dedicated to scroll bars:

enum ScrollControl {
    AddLine = 0x1,
    SubLine = 0x2,
    AddPage = 0x4,
    SubPage = 0x8,
    First = 0x10,
    Last = 0x20,
    Slider = 0x40,
    NoScroll = 0x80
};
 
virtual void scrollBarMetrics(const QScrollBar *bar,
        int &x, int &y, int &width, int &height);
 
virtual void drawScrollBarControls(QPainter *painter,
        const QScrollBar *bar, int sliderStart,
        uint controls, uint activeControls);
 
virtual void scrollBarPointOver(const QScrollBar *bar,
        int sliderStart, const QPoint &pos);

The paintEvent() of a class like QScrollBar would then look like this:

void QScrollBar::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    style().drawScrollBarControls(&painter, this,
         sliderStart(), m_allControls, m_pressedControls);
}

This approach made it easy for our users to create their own widget styles, by subclassing QStyle or one of the predefined styles. This made it possible for KDE, the Qt-based K Desktop Environment, to support themes in their applications.

As we released Qt versions 2.1, 2.2, and 2.3, we discovered that the main problem with QStyle was that we couldn't add new virtual functions to QStyle, even if that was necessary to support new widgets that were introduced during Qt 2's lifetime. For Qt 2, we simply had to give up making new widgets stylable through QStyle.

Qt 3: Enums Take Over QStyle

Qt 3.0 gave us the opportunity to redesign the QStyle API so that it would be possible to add support for new widgets simply by adding enum values to QStyle. Adding values to existing enum types normally doesn't compromise binary compatibility.

The Qt 3 QStyle API distinguishes between three categories of items it can draw: primitives, controls and complex controls. Each category has an enum type listing the different elements belonging to it, and a set of functions for drawing the elements or performing hit tests.

Let's take QScrollBar as our example again. A scroll bar is a complex control because it contains sub-controls that the user can click. The enum value associated with QScrollBar is called CC_ScrollBar:

enum ComplexControl {
    CC_SpinWidget,
    CC_ComboBox,
    CC_ScrollBar,
    CC_Slider,
    CC_ToolButton,
    CC_TitleBar,
    CC_ListView,
    CC_CustomBase = 0xF0000000
};

The various sub-controls of a scroll bar are also identified by enum values. Here's the portion of the SubControl enum that deals with scroll bars:

enum SubControl {
    ...
    SC_ScrollBarAddLine = 0x1,
    SC_ScrollBarSubLine = 0x2,
    SC_ScrollBarAddPage = 0x4,
    SC_ScrollBarSubPage = 0x8,
    SC_ScrollBarFirst = 0x10,
    SC_ScrollBarLast = 0x20,
    SC_ScrollBarSlider = 0x40,
    SC_ScrollBarGroove = 0x80,
    ...
};

The API for drawing complex controls consists of the following four functions.

virtual void drawComplexControl(ComplexControl control,
        QPainter *painter, const QWidget *widget,
        const QRect &rect, const QColorGroup &colorGroup,
        SFlags style, SCFlags subControls,
        SCFlags activeSubControls,
        const QStyleOption &option) const;
 
virtual void drawComplexControlMask(
        ComplexControl control, QPainter *painter,
        const QWidget *widget, const QRect &rect,
        const QStyleOption &option) const;
 
virtual QRect querySubControlMetrics(
        ComplexControl control, const QWidget *widget,
        SubControl subControl,
        const QStyleOption &option) const;
 
virtual SubControl querySubControl(ComplexControl control,
        const QWidget *widget, const QPoint &pos,
        const QStyleOption &option) const;

This API proved its worth, as no fewer than 61 enum values were added to QStyle during the lifetime of Qt 3. These values were necessary to support QToolBox, which was added in Qt 3.2, and to control more attributes of existing Qt widgets.

The API also proved to be powerful enough to accommodate two new styles: QWindowsXPStyle and QMacStyle. Each of these styles uses the native theming engine on its target platform.

Styles

The main disadvantage of the Qt 3 API compared to Qt 2 is that some type safety is lost. All widgets are passed around as QWidget, and the style function has to cast it to the appropriate type based on the value of the ComplexControl parameter. There's also the risk that the caller might pass the wrong widget type. Still, this was an acceptable price to pay for the flexibility provided by enums.

Qt 4: Styles and Widgets Are Decoupled

As we started to work on Qt 4, we didn't expect to have to change QStyle significantly. The API was fairly mature and allowed both our users and ourselves to do what we wanted to do. Yet, one of our daring moves in preparation for Qt 4, the Qt library split, forced us to go back to the drawing board again.

The Library Split

In Qt 4, different widgets are provided by different libraries. In particular, compatibility widgets such as Q3ListBox and Q3IconView need to be stylable. At the same time, QStyle subclasses shouldn't have to link against the Qt3Support library---after all, the whole point of providing a compatibility class in a separate library is to avoid bloat in Qt 4 applications that don't use it.

To avoid this undesirable dependency, we had to push the ideas developed for Qt 3 to their logical conclusion. While Qt 3 eliminated the Qt widget types from the header files by using enums, Qt 4 would have to eliminate the Qt widget types from the implementation files as well. QStyle subclasses should no longer have to cast the QWidget pointer to a specific type to extract information relating to the widget to draw. Instead, all the information should be passed in the QStyleOption parameter.

This decoupling makes it possible to draw widget types without linking against them. Everything that QStyle subclasses need to know about a widget is available as function parameters.

Consequences of QStyle's API

Once again, we will take QScrollBar as an illustration of the QStyle API. Scroll bars are still treated as complex controls; the ComplexControl and SubControl enum types from Qt 3 still exist unchanged. What has changed is the name and parameter list of the QStyle functions:

virtual void drawComplexControl(ComplexControl control,
        const QStyleOptionComplex *option,
        QPainter *painter, const QWidget *widget) const;
 
virtual void drawComplexControlMask(
        ComplexControl control,
        const QStyleOptionComplex *option,
        QPainter *painter, const QWidget *widget) const;
 
virtual SubControl hitTestComplexControl(
        ComplexControl control,
        const QStyleOptionComplex *option,
        const QPoint &pos, const QWidget *widget) const;
 
virtual QRect subControlRect(ComplexControl control,
        const QStyleOptionComplex *option,
        SubControl subControl,
        const QWidget *widget) const;

The widget's state is entirely defined by the option parameter. The widget parameter is optional (its default value is 0); it is provided to make porting custom styles to Qt 4 easier and to allow occasional hacks.

The new design has one unforeseen advantage: It is now easier to make custom widgets stylable. If you want to design a custom widget that resembles a QTabBar, you can now invoke QStyle::{|1s}drawControl() with CE_TabBarTab without having to pass a QTabBar object to QStyle.

The QStyleOption Class Hierarchy

The option parameter is a pointer to a QStyleOptionComplex. For most complex control type, there exists a QStyleOptionComplex subclass that provides additional information for the specific type. For QScrollBar (and QSlider), the subclass is QStyleOptionSlider.

The QStyleOption classes are simple C++ structs with public data members. For example, here's how to draw a scroll bar:

void QScrollBar::paintEvent(QPaintEvent *)
{
    QStyleOptionSlider option;
    option.init(this);
    option.subControls = QStyle::SC_All;
    option.activeSubControls = m_pressedControl;
    option.orientation = orientation();
    option.minimum = minimum();
    ...
    option.upsideDown = invertedAppearance();
 
    QPainter painter(this);
    style().drawComplexControl(QStyle::CC_ScrollBar,
                               &option, &painter, this);
}

The last three lines can be made slightly shorter by using the QStylePainter convenience class:

QStylePainter painter(this);
painter.drawComplexControl(QStyle::CC_ScrollBar, option);

The code inside the drawComplexControl() reimplementation looks like this:

void XyzStyle::drawComplexControl(ComplexControl control,
        const QStyleOptionComplex *option,
        QPainter *painter, const QWidget *widget) const
{
    switch (control) {
    case CC_ScrollBar:
        if (const QStyleOptionSlider *sliderOpt =
            qstyleoption_cast<const QStyleOptionSlider *>(option)) {
            // draw a scroll bar
        }
        break;
    ...
    }
}

The qstyleoption_cast() function is new in Qt 4. Like standard C++'s dynamic_cast() construct, it attempts to cast its argument down the inheritance hierarchy, and returns a null pointer if the actual object isn't of the right type. For example:

QStyleOption *slider = new QStyleOptionSlider;
QStyleOption *frame = new QStyleOptionFrame;
 
qstyleoption_cast<QStyleOptionSlider *>(slider);  // returns slider
qstyleoption_cast<QStyleOptionSlider *>(frame);   // returns 0

qstyleoption_cast() works on subclasses of QStyleOption. Unlike dynamic_cast(), it works across dynamic library boundaries and doesn't require the compiler to support RTTI (run-time type identification).

Binary Compatibility and Extensibility

Since QStyleOption and its subclasses provide direct access to their data members for efficiency reasons, the problem of binary compatibility arises again. What happens if Trolltech wants to add members to these structs?

Fortunately, there's a solution. The day we need to add a data member to a QStyleOption subclass, we will solve the issue by introducing a new subclass with a V2 suffix, standing for version 2. Existing styles will still work as before, because the version 2 class will inherit the version 1 class; but new styles will use qstyleoption_cast() to find out whether the option object is V2 or not. For example:

if (const QStyleOptionSlider *sliderOpt =
       qstyleoption_cast<const QStyleOptionSlider *>(option)) {
    int extraMember = 0;
    if (const QStyleOptionSlider *sliderOptV2 =
           qstyleoption_cast<const QStyleOptionSliderV2 *>(option))
        extraMember = sliderOptV2->extraMember;
 
    // draw a scroll bar
}

With luck, Qt 5 will be released before we need to implement this solution, auspiciously delivering us from the subjugation of binary compatibility.


Copyright © 2005 Trolltech Trademarks