Dark-Mode für Win32
« | 15 Aug 2021 | »Etwa gleichzeitig mit der Netflix-Serie Dark
hat Microsoft in Windows 10
das Dunkle Design
als Dark-Mode
schrittweise eingeführt und mit jedem
halbjährigen Update weiter ausgebaut.
Doch leider nur für UWP Apps und das pisst mich offen gesagt sehr an. Was bitte ist so schwer, dieses Design auch in die klassische WinAPI zu übernehmen?
Seit Windows 3.1 kenne ich Windows so, dass man die Farben aller Elemente der UI einstellen kann. Und obwohl bereits Windows XP und seine Nachfolger mit “Themes” in dieses klassische Feature reingepfuscht haben, war es bis Windows 7 einigermaßen möglich, die UI-Farben nach Wunsch anzupassen und alle “guten” Programme konnten diese Einstellungen auslesen und anzeigen.
Und dann kam die unseelige Trennung von Win32 und UWP mit Windows 8 und die beiden Darstellungen drifteten auseinander. Das mag an einigen Stellen auch gut gewesen sein und die Reduktion von “Man kann alles einstellen” hin zu “man kann zwischen ein paar Designs wählen” ist eigentlich ein guter Gedanke.
Nun sind wir in Windows 10 mit zwei Designs angekommen, nämlich “Hell” (Light) und “Dunkel” (Dark). Und offen gesagt finde ich das wirklich gut. Keine selbst fabrizierten “grünen” oder “roten” Fensterrahmen, sondern nur noch zwei gut abgestimmte Farbklänge.
Und dann vergisst man diese auf Win32 zu übertragen? Warum ??
Explorer Theme ist ein “bisschen schwarz”
SetWindowTheme(hwnd, L"Explorer", NULL);
hätte eigentlich “die Lösung”
sein können. Bzw. manchmal findet man auch den Token DarkMode_Explorer
im
Netz. Denn wenn man diese Funktion für jedes Fenster und jedes Control
aufruft, dann müssten doch alle so aussehen, wie der Explorer, und der
passt sich ja neuerdings brav an den Dark-Mode an.
Leider kommt damit nur an einigen Stellen plötzlich ein bisschen schwarze Farbe zum Vorschein. So werden z.B.: die Scrollbalken in diversen Controls plötzlich dunkel, während der restliche Hintergrund hell bleibt.
Undokumentierte uxtheme.dll APIs
Im GitHub Project “win32-darkmode”
werden für die Verdunkelung einige nicht benannte Funktionen aus uxtheme.dll
über ihre fixe ID importiert. Man nutzt also
FARPROC proc = GetProcAddress(hmodule, MAKEINTRESOURCEA(ordinal));
zum Laden.
Ordinal | Prototype |
---|---|
49 | HTHEME OpenNcThemeData(HWND hWnd, LPCWSTR pszClassList) |
94 | DWORD GetImmersiveColorSetCount(void) |
95 | DWORD GetImmersiveColorFromColorSetEx(UINT dwImmersiveColorSet, UINT dwImmersiveColorType, bool bIgnoreHighContrast, UINT dwHighContrastCacheMode) |
96 | DWORD GetImmersiveColorTypeFromName(const WCHAR *name) |
98 | DWORD GetImmersiveUserColorSetPreference(bool bForceCheckRegistry, bool bSkipCheckOnFail) |
100 | WCHAR** GetImmersiveColorNamedTypeByIndex(UINT dwImmersiveColorType) |
104 | void RefreshImmersiveColorPolicyState(void) |
106 | BOOL GetIsImmersiveColorUsingHighContrast(IMMERSIVE_HC_CACHE_MODE mode) |
132 | BYTE ShouldAppsUseDarkMode(void) |
133 | BOOL AllowDarkModeForWindow(HWND hWnd, BOOL allow) |
135 | BOOL AllowDarkModeForApp(BOOL allow) |
135 | PREFERREDAPPMODE SetPreferredAppMode(PREFERREDAPPMODE appMode) |
136 | VOID FlushMenuThemes(void) |
137 | BOOL IsDarkModeAllowedForWindow(HWND hWnd) |
138 | BOOL ShouldSystemUseDarkMode(void) |
139 | BOOL IsDarkModeAllowedForApp(void) |
Und dann schalten APIs wie SetPreferredAppMode()
und AllowDarkModeForWindow()
(theoretisch) die App oder das Fenster in den Dark-Mode um, falls
ShouldSystemUseDarkMode()
diesen auch als aktiviert meldet.
Ordinal 135 hat dabei schon einen Wandel erlebt, weil es vor Windows 10/19H1
(Build 18362) als AllowDarkModeForApp()
nur einen Schalter umlegte und ab
19H1 mit SetPreferredAppMode()
per Enumeration mehr Optionen anbot.
Das findet man aber alles unter github.com/ysc3839/win32-darkmode.
Doch leider reicht das immer noch nicht. Denn nun hat man zwar dunkle Buttons und dunkle Menüeinträge, doch die meisten Controls und die Menüzeile selbst bleiben weiß bzw. klassisch.
Eigene Farbanpassungen tun den Rest
Meine vorläufige Lösung sieht daher so aus (wie auch oft im Netz beschrieben),
dass man per IsDarkModeAllowedForWindow()
fragt, ob das Fenster in den
Dark-Mode versetzt wurde und dann wird in der WNDPROC
beim Zeichnen eine andere Text- und Hintergrundfarbe genutzt.
Windows hat das schon seit seinen Anfängen vorgesehen und mit Messages wie
WM_CTLCOLORBTN
, WM_CTLCOLOREDIT
, WM_CTLCOLORLISTBOX
, WM_CTLCOLORSCROLLBAR
WM_CTLCOLORMSGBOX
, WM_CTLCOLORDLG
und WM_CTLCOLORSTATIC
kann man
die Standardfarben von Controls in seinem Elternfenster abändern.
Man setzt also quasi im Fensterrahmen-Code die Farben der Kindelemente.
Wenn man das nun auch noch mit den “richtigen” Farben des Dark-Mode tut
(siehe GetImmersiveColorFromColorSetEx()
), dann kommt man den Farben
des dunklen Windows Explorers schon recht nahe. Nur die fu**ing
Menüzeile bleibt leider weiter weiß.
Welche Farben jetzt aber die “richtigen” sind, habe ich noch nicht
klar herausbekommen, denn die Text-Token sind alles andere als eindeutig.
In einem Forum-Artikel
liegt zwar ein schöner Demo-Code um alle aufzulisten, aber was daraus
jetzt eine “richtige” Listbox darstellt ist nicht dokumentiert.
Andere Programme definieren das offenbar auch selbst und arbeiten generell
nur mit der öffentlichen Theme-API.
So können sie den Dark-Mode auch in Windows 7 selbst implementieren und per
Programm-Option ein und ausschalten.
Das ist eigentlich auch ein interessanter Ansatz, aber es sieht dann
zwangsläufig auch etwas anders aus im Vergleich zum System-Standard.
Fazit
Wie schon gesagt … ich bin angepisst.
Denn eben wegen solchen fehlenden APIs bringt man Entwickler dazu, ihre eigene Variante nachzubauen … und die wird dann immer schlecht gewartet und produziert im Nachgang Fehler.
Es wäre für MS leicht gewesen, den Dark-Mode auch auf die Standard-Controls anzuwenden, am besten mit dem bekannten “Explorer” Theme. Aber nein.
Und warum sind APIs wie AllowDarkModeForWindow()
undokumentiert und nicht
Teil der offiziellen API?
Ich verstehe es einfach nicht.