SOUTHWORKS Dev Team
January 8, 2021
It is becoming more common for applications to have a “Dark” and a “Light” mode. Many websites have also started to offer these alternatives, and even web browsers have either built-in support, or addons to make them switch between dark and light mode.
But back at the time when Windows Presentation Foundation (WPF) was created, this wasn’t so common. But there was a similar concept: the “themes”. WPF supports even Windows XP, but in reality it was created for Windows Vista, and later Windows 7. These versions of windows came with different themes, that basically changed the look and feel of the operative system’s graphic interface.
Before starting, let me mention the team that work on this, contributing as well as giving feedback: Ignacio Boada, Matías Nicolás Gesualdi, Gabriela Gutierrez, Nicolas Bello Camilletti, Mauro Krikorian and Juan Pablo Tomasi.
As such, WPF was created with these concepts in mind, so applications that use WPF controls will automatically use the theme that is currently set on the system to draw them. It is also possible to force the application to use a certain theme, regardless of what the OS user has chosen.
But if we see the original themes included in Windows Vista and Windows 7, we will find there are only 6 different themes included: Aero, Architecture, Characters, Landscapes, Nature, Scenes. Those are all the themes existing in Windows 7. None of them sound like “Light” or “Dark”. Of course, some of them are darker than others, but they don’t exactly fit what one would think of as a dark theme. So, how can we offer a “Dark” theme for our application? Fortunately, there are some alternatives for doing it and we will cover them in this post.
WPF included ways to customize the look and feel of any component. The group of resources that allow this customization is called “skin”. These skins were often used for adding branding colors. In this way, we can create a “Dark” skin, and then apply that skin to the application, thus offering dark mode support. There are three ways of loading resources in WPF: Static, Dynamic and Loose.
Dynamic: This approach also involves changing the ResourceDictionary of the application, but in this case doing it dynamically from code.
Where packUri can be something like pack://application:,,,/<AssemblyName>;component/<ThemeName>.xaml.
Loose: This approach is the same as the dynamic one, but it allows to load XAML resource files that haven’t been compiled in any assembly. To do this, we first load the XAML files by either using the XamlReader.Load method, or dynamically creating a ResourceDictionary with its source pointed to the not compiled XAML file. After that, we merge the dictionary just like in the dynamic approach.
We can now have a skin for the dark theme and change it whenever we like. But we are still missing something. We need a way to detect if a user has changed their OS theme, and skin our application accordingly. A way to do this in WPF, is by using the system’s event SystemEvents.UserPreferenceChanging. We just have to assign to this event, a handler that dynamically changes the skin accordingly. Sadly, WPF doesn’t offer a way to know what theme is being used by the OS, so we have to use a Win32 way. One such way is using the unmanaged method called GetCurrentThemeName.
There is another way to get the current theme by querying the register. We then store the name of the previous theme, if when the UserPreferenceChanging event is called, we check if the theme changed.
So far, so good, but this still has the problem that we have to use a third party skin that may or may not support all the controls we need, and it also has a custom look. Fortunately, there is a way to use the standard Windows 10 controls in dark mode using XAML Islands.
XAML Islands is basically a way to add UWP controls to a WPF or Win32 application. It mainly consists of adding an UWP project with the desired controls, which is then wrapped by the Microsoft.Toolkit.Win32.UI.XamlHost.XamlApplication class, and included in the WPF project using the Microsoft.Toolkit.Win32.UI.XamlApplication class (note the little difference on the namespace of these two different classes).
UWP controls have built-in support for dark mode, so we don’t have to worry about skins or anything like that. The problem is that currently XAML Islands has some limitations. One of these is that XAML Islands doesn’t detect theme changes in runtime.
Taken directly from official documentation, “UWP XAML content in XAML Islands doesn’t respond to Windows theme changes from dark to light or vice versa at run time. Content does respond to high contrast changes at run time”.
To solve this, we can use a workaround which is similar to what we do in WPF, using the SystemEvents.UserPreferenceChanging event. Since we have an UWP project, we can use the following function to know if the color changed, instead of using GetCurrentThemeName.
If you don’t like the limitations and work arounds that XAML Islands presents, there may be soon another option available. This option is called WinUI 3, and it will allow a WPF application to use the same native modern UI elements as UWP and XAML Islands, including the dark/light mode support. WinUI is currently in its 2.0 version, that can’t be used in WPF, but the 3.0 version (that is compatible with WPF) is already in preview, so you can start trying it by following the official documentation. Of course, building production applications with a preview library may not be a good idea. So we will have to wait some time to have this feature fully functional. Be sure to check the WinUI 3 roadmap. Its release date for production is expected for sometime in early 2021.
We have seen that there are several ways to implement dark/light themes in WPF. The choice will be based on how important this feature is in a particular situation. Many developers will deem having a modern UI crucial, so they will either try XAML Islands or wait for WinUI 3. While others won’t care too much about it (or they already have a custom skin that they must use anyway) so they will chose to go with WPF’s native support.
Thanks to Nicolás Bello Camilletti
Originally published by Sebastian Rial for SOUTHWORKS on Medium 08 January 2021