Dark Mode on Windows 11 with Qt 6.5

With recent Windows 10 builds, and with Windows 11 even more so, dark color schemes have at last landed as a mainstream personalisation option on the Windows desktop. Qt has supported the dark appearance setting on macOS for many years, and with Qt 6.5 we are bringing better support for dark themes to Windows as well.

QStyle in Windows XP

On Windows, theming and personalisation have a long history. Windows 3.1 already had custom mouse cursor and icon packages, and I filled several floppies and later DVDs with background images and sound effects inspired by the latest movies.

Proper UI theming was introduced with Windows XP through the Win32 APIs from the UxTheme system library. I went to the Microsoft's TechEd conference in Barcelona in 2001 to learn what we can expect from that framework over time. The possibilities seemed both endless and scary for people in the audience that had developed custom controls with MFC or Win32 APIs. I remember talks about round UI elements, buttons with holes, gradients, semi-transparency, even animations!

We had already been working on Qt's Windows XP style implementation based on the Windows XP preview builds, and used the new native APIs to draw the various elements of user interface controls. That work influenced a lot of the QStyle API design, just in time for the then-upcoming Qt 3 release. And the QStyle API is still largely the same in Qt 6's QtWidgets. The implementation of the Windows XP style later became the basis for the Windows Vista style, and for Qt 6, we use that implementation in the native Windows desktop style for Qt Quick Controls.

But fashion changes, and not all technologies evolve as originally intended. The expectation was perhaps that a community of creators would build lots of custom UI themes for the UxTheme library, similar to the many cursor and icon libraries. That did not really happen, and while buttons are nowadays just a flat frame with rounded corners, they - fortunately, I suppose - still have no holes.

The UxTheme API is still present on Windows 11, and Qt still uses it to render the elements of user interface controls in the native Windows Vista style. However, the control assets we get are still based on the Aero design system of Windows Vista. And in that design system, there is no "dark mode", and there is no documented way to get the dark control assets through that API. So while Qt can read a dark palette from the system, we can't really use it when the UxTheme based Vista style is used - we'd get dark theme colors for some UI elements, but light UI control assets from the system, leaving the user with an unusable user interface.

In recent years, Microsoft has moved towards the Fluent design system, which the Windows 11 system UI is based on. However, that system is not available through UxTheme, and so it is perhaps not a surprise that on a Windows 11 system, even the operating system UI comes in a mix of styles. Applications and system dialogs not written with the relatively new WinUI library don't look "fluent", often don't support the dark color scheme, and some control panels deeper down in the tree still look like they used to with Windows 2000.

Before Qt 6.5

Since version 5.15, Qt has provided an opt-in way to use the dark system palette, or to respect the dark system theme at least for the window decoration. That opt-in is a parameter to the QPA platform, and could be set through a command line option when starting the application:

> gallery.exe -platform windows:darkmode=1
> gallery.exe -platform windows:darkmode=2

or through an environment variable (preferably set in the main function via qsetenv("QT_QPA_PLATFORM", "windows:darkmode=[1|2]")).

The darkmode value of '1' enables window decoration theming, allowing applications that implement a custom palette and corresponding style to have a consistent window frame and title bar. The value '2' will in addition make Qt read and use the dark system palette. The default up to Qt 6.4 was to support neither.

In Qt 6.4 we changed the default behavior depending on the application default palette: if the application palette is dark, then windows automatically use the dark window decoration. To determine whether the palette is dark or light, we compare the window color with the text color:

static bool shouldApplyDarkFrame()
{
// ...
const QPalette defaultPalette;
return defaultPalette.color(QPalette::WindowText).lightness()
> defaultPalette.color(QPalette::Window).lightness();
}

Applications can then use a style that looks good with a dark palette, set a dark palette, and automatically get a dark window title and frame.

int main(int argc, char **argv)
{
QApplication app(argc, argv);
    app.setStyle("fusion"); // looks good with dark color scheme
    app.setPalette(myDarkPalette());

    MainWindow mainWindow;
    mainWindow.show();
    return app.exec();
}

The problem is still that the dark palette is not read from the system and needs to be hand-crafted, unless darkmode=2 is specified. And if darkmode=2 is specified, then a dark palette is used even if the style cannot possible work with a dark color scheme. Also, there is no Qt API to find out whether the system is running with a dark or light color scheme.

The Qt 6.5 Way

In Qt 6.5 we are taking the next step, while trying to keep things unchanged for existing applications.

QStyleHints has a new property, colorScheme, that holds a Qt::ColorScheme enum value, either Qt::ColorScheme::Light or Qt::ColorScheme::Dark (or Qt::ColorScheme::Unknown on systems where there is no such system color scheme). This allows applications to determine whether a hand-crafted application palette should be dark or light, respecting the user's system preference. And like all good properties, it comes with a change notification signal QStyleHints::colorSchemeChanged that allows applications to respond to changes in the system color scheme.

But ideally you don't have to hand-craft a palette anymore anyway - it's frankly a slippery slope to override user preferences, especially when considering users with a need for e.g. high-contrast user interfaces.

Qt now always loads the system palette, which is based on the user's preference - in Qt 6.5 also on the Gnome desktop, by the way. However, as explained above, not all styles can look good with any palette, and the Windows Vista style definitely can't. So a style can decide to ignore that system palette, and overwrite it with a different palette, using existing polishing infrastructure in QStyle (and equivalent theme overrides in Qt Quick styles). The Windows Vista style will always replace the system palette with the light system palette. Styles that work well with any palette, such as the "Fusion" or classic Windows style, will leave the application palette unmodified.

The widget gallery example running on Windows 11, left side with default Windows Vista style, right side with the Fusion style.

This screenshot shows the widget "gallery" example (from my local dev branch, but you'll get the same results with the Qt 6.5 beta packages). On the left side it is running the default Windows style, and on the right side it was started with the -style fusion command line option. We are not changing the default style in Qt on Windows, so by default, your application will use the Windows Vista style, which will use the light system palette even on a Windows 11 system running with a dark theme. The window framing will be consistent with the application palette.

But now applications can easily enable dark mode support, while respecting the user's preferences for their personal color scheme: you only have to set a style that works. In a Qt Quick application that would be:

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickStyle>

using namespace Qt::StringLiterals;

int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);

     QQuickStyle::setStyle("fusion");
     QQmlApplicationEngine engine;

     engine.load(QUrl(u"qrc:/main.qml"_s));

     return app.exec();
}

The Fusion style is our preferred style for Windows 11. It looks good with dark and light palette, and while it does not speak the Fluent design language, it blends rather well into the Windows 11 desktop.

What else to think about

There are few things to check in your application before opting into the support of a dark color scheme.

If you have written your own style or use a style sheet, then you might perhaps hardcode certain colors. For instance, it's not unusual to make the text of a disabled button a bit lighter so that it looks "grayed out". However, if the color scheme is dark, then you will typically want to make the light text a bit darker instead.

And if your application defines custom colors for certain visual elements, such as graphs, then you will also have to make sure that the colors you choose make sense when the background is dark and the foreground is light. You might even consider giving the user the option to flip between dark and light palettes for certain UI elements, e.g. when displaying rich text that might contain hard coded colors.

Beyond Qt 6.5

There are a few simple (but not necessarily easy) things we might want to add in future versions of Qt.

We might want to add a QPalette::colorScheme member that evaluates the palette's colors to determine whether the palette is dark or light. An application's choice of color for custom visual elements should be based on the actual palette used, rather than on the QStyleHints::colorScheme value. A member function like the simple shouldApplyDarkFrame() helper above would make that easy.

A common request is to make the QStyleHints::colorScheme a writable property, so that applications can force a certain color scheme, no matter the system setting, and including the window frame. This will have limitations on some platforms - on Windows, we don't think it's possible to force a dark window decoration on an otherwise light system, while macOS allows us to control the appearance separately for each window.

When we read the dark system palette on Windows, then we use the theme's accent color for the QPalette::Highlight color role. This is however not quite correct, as the Accent is used in other places as well, and some controls use different colors (i.e. on Windows 11's "red" dark theme, the highlight is bright red, while the focus frames are in a paler orange). QPalette doesn't have an Accent color role right now, and adding that is a bit complex as the internal QPalette data structures need to change quite a bit once we go above 21 color roles - and we are at 21 already.

Fluent Design with Figma

And we do want to bring a style that implements the Fluent design system to Qt as well. The Fluent design system is available as an open source Figma package, and Brook has recently blogged about using Figma designs with Qt Quick Controls. That is a path we are following further right now: we would like to make the visual assets in a Figma design available to Qt Quick user interfaces, and maybe even to QtWidgets UIs. This can probably be achieved with a mix of import/export bridges, Qt Design Studio integration, and build system tools. A convincing native look of Qt applications on Windows based on the Fluent design provided by Microsoft will be a good benchmark for this work.


Blog Topics:

Comments