Desktop styling with Qt Quick Controls

Qt Quick Controls 1 was our first UI framework for QML. The controls were mostly written in QML without much C++. At the time, QML was still a new technology, which meant that we didn’t have enough experience with designing for performance to know what to expect. So the styling API ended up inefficient by design, with many fat delegates that used extensive amounts of JavaScript, bindings, introspection, Loaders, and QObjects for both control logic and styling. It also had a linking dependency to Qt Widgets to get native styling and Widget-based dialogs. And without the QML compiler that we have today, this all ended up as a rather slow and messy approach. When we realized that it also didn’t perform well on embedded hardware, it was time to rethink the solution. And the result was Qt Quick Controls 2.

Qt Quick Controls 2

With Controls 2 the basic idea was to shift most of the implementation from QML to C++. By doing so, we aimed at slimming down the delegates as much as possible, and instead do the heavy lifting from C++. This included all the control logic, mouse, touch, event handling, API, and more. Only the visuals were left to be defined in QML. This resulted in thin delegates, less run-time overhead, less memory usage, clearer code flow, and faster execution.

But one thing that has been missing so far, is support for native desktop styling. The main motivation for Controls 2 was to achieve good performance on embedded devices, and since we had Qt Widgets for desktop, we sort of accepted that some essential desktop features were missing. We shipped styles that have open documentation (Universal, Material), and made it easy to create custom ones. But now that Controls 1 has been deprecated since Qt 5.12, and will also be excluded from the Qt 6 binary package, it’s time to work on desktop-oriented styles again.

Native desktop styles

In Qt 6.0, we are adding two new desktop styles to Controls 2: Windows and macOS. Unlike the existing Fusion style, which uses Qt Quick primitives, these new styles use QStyle for drawing. The reason is that it still does a good job of interfacing with “low-level” platform APIs to get true native styling. But instead of sharing QStyle with Widgets, we make an exclusive copy just for Controls. The latter might sound odd, since it’s natural to assume that sharing one codebase would be an easier approach. But the reasons are several: first of all, we don’t want to break Widgets or any 3rd party styles out there. And we cannot factor QStyle out of Widgets without breaking source compatibility. Not only does QStyle have references to Widgets all over its API, but it also uses enums and constants directly from the Widgets themselves. At the same time, we don’t want Controls to have a dependency on Widgets long into the future either.

Another reason is that we want the freedom to change the code wherever we see fit. There are many ways we can optimize the QStyle-copy going forward, to better fit the Qt Quick scene graph and the Controls styling API. One such change is that we only use QStyle for drawing the background of a control. The same background texture will be cached and reused for all controls of the same type, and scaled to the correct size with the help of a QSGNinePatchNode. The text and foreground will be rendered on top using normal QML. And for all such differences, we want to be able to implement them without being constrained or worried about keeping compatibility with Widgets. For the same reasons, the fact that we use QStyle for drawing is a private implementation detail. Controls already has a great styling API, and we don’t want to introduce and maintain a second one.

Lastly, we don’t necessarily want the native styles to look exactly like Widgets. For some controls, we use a hybrid approach, with QML to add animations and fading effects on top of the textures painted by QStyle. In the end, what we really want is for the native styles to have fluid performance, and to look as “native” as possible, and not necessarily like Widgets.

What is ready for Qt 6.0?

For Qt 6.0, most controls will be supported by the new desktop styles. But not all. Those that will be missing are the ones that currently don't have an implementation in QStyle from before, like Switch or RangeSlider. They are still available, but will be drawn with the Fusion style. Missing controls will gradually be supported in upcoming releases of Qt, the same with native dialogs and menus. And if we look even further, our plans include offering more desktop-centric controls that you might find in Widgets or native toolkits today.

So, still lots to do. But we see the new desktop styles as the first step of many to bring Controls on par with Widgets for desktop development over the next few years.

Below are screenshots of a Qt Quick Controls 2 application running with the new desktop styles. Note that this is still ongoing work, and any glitches will be ironed out before the final release.

 

macOS style (dark mode):



Windows style:

 

Fusion style (for comparison):


Blog Topics:

Comments