Mode sombre et thèmes de contraste dans les applications Windows — barres de titre DWM sombres, suivi du thème système dans WinForms/WPF et dessin en contraste élevé

· Mis à jour le: · · Mode sombre, Thèmes de contraste, Contraste élevé, DWM, WinForms, WPF, Windows 11, Accessibilité, Applications métier, Windows

Historique des révisions (première version, publiée le 2 Sep 2026)
Première publication

« Nous avons remplacé les PC du bureau par Windows 11, et les salariés qui utilisent le mode sombre se plaignent que seule notre application métier a une barre de titre d’un blanc éblouissant. » « Un salarié malvoyant a activé un thème de contraste, et l’affichage d’état de l’écran de saisie de commande a disparu. » Ce sont deux plaintes que nous entendons plus souvent depuis un an ou deux.

La première vient du paramètre Couleurs de Windows 11, la seconde des paramètres d’accessibilité, mais pour un développeur elles ressemblent au même problème : l’application ne suit pas le thème. Et de fait, le fondement de la correction est commun. Ne pas coder les couleurs en dur ; lire le paramètre système, remarquer les changements, et repeindre. Ces trois points.

L’article précédent, « Introduction à l’accessibilité des applications Windows », traitait de la façon dont les lecteurs d’écran lisent une application (UI Automation) et des bases du nommage, du fonctionnement au clavier et de la couleur. Il abordait le suivi des thèmes de contraste, mais pas le mode de couleur clair/sombre lui-même. Cet article en est le compagnon. Il relie les deux axes du mode de couleur (clair/sombre) et des thèmes de contraste au niveau de l’implémentation, dans cet ordre : dessin de la barre de titre par DWM (Gestionnaire de fenêtres du Bureau), suivi du thème système dans WinForms/WPF, et dessin sous un thème de contraste. Le public visé est les développeurs qui construisent et maintiennent des applications métier en WinForms, WPF ou Win32. Les prérequis sont Windows 11 (les barres de titre sombres exigent la build 22000 ou une version ultérieure) et WinForms/WPF sous .NET 9/10 (sous .NET Framework 4.8 et .NET 8, certaines parties doivent être implémentées à la main). Le niveau de difficulté est intermédiaire.

Le déroulement de cet articleLa structure de cet article, qui relie dans l'ordre les deux axes de thème, pourquoi les fenêtres sont claires par défaut, les barres de titre DWM sombres, le mécanisme de détection et de suivi, les implémentations WinForms et WPF, le dessin sous un thème de contraste, le choix d'une politique, et la vérificationClarifier les deux axes de thèmePourquoi le défaut est clairBarre de titre DWM sombreDétection et suiviImplémentation WinForms/WPFDessin sous un thème de contrasteChoisir une politique et vérifier

Figure 1 : Cet article va en une ligne du tri des thèmes jusqu’aux mécanismes, à l’implémentation, aux thèmes de contraste et à la vérification.

1. D’abord la conclusion

  • Windows a deux axes de thème. Clair/sombre (le mode de couleur) sous Paramètres > Personnalisation > Couleurs, et Paramètres > Accessibilité > Thèmes de contraste. Le second est une palette contrainte à un ratio de contraste d’environ 7:1 ou plus, et c’est autre chose que clair/sombre. Le mode sombre n’est pas disponible tant qu’un thème de contraste est actif. L’ordre de priorité de la détection est « thème de contraste, puis clair/sombre ».12
  • La barre de titre d’une application existante reste blanche parce que c’est le défaut de compatibilité. Windows n’a aucun moyen de savoir si une application prend en charge le mode sombre, donc il traite chaque fenêtre comme claire par défaut.3
  • Ce qui rend la barre de titre sombre, c’est DWMWA_USE_IMMERSIVE_DARK_MODE (valeur 20) via DwmSetWindowAttribute. Passez un BOOL TRUE et le cadre est dessiné en sombre lorsque le système est sombre. La prise en charge documentée est Windows 11 build 22000 ou une version ultérieure.43
  • Lisez le mode actuel avec UISettings.GetColorValue et recevez les changements via ColorValuesChanged. La procédure officielle de Microsoft est : si le premier plan (la couleur de texte par défaut) est clair, le mode est sombre. L’événement n’est pas garanti d’arriver sur le thread UI, donc remontez vers l’UI avant de repeindre.35
  • WinForms a reçu Application.SetColorMode dans .NET 9, et cela a cessé d’être expérimental dans .NET 10. Appelez-le avec SystemColorMode.System avant Application.Run. Il a trois contraintes : Windows 11 uniquement, désactivé tant qu’un thème de contraste est actif, et pas de suivi des changements de paramètre pendant l’exécution.672
  • WPF a reçu le thème Fluent et ThemeMode dans .NET 9. ThemeMode="System" suit le système et contrôle aussi l’assombrissement de la fenêtre. Cependant, la manipulation depuis le code reste expérimentale dans .NET 10 (WPF0001), et les styles Fluent sont « in progress ». Si vous restez sur le thème classique, échangez des ResourceDictionary clairs et sombres référencés via DynamicResource.8910
  • Sous un thème de contraste, mappez les couleurs vers les paires de couleurs système correctes, omettez les images derrière le texte, et dessinez les graphismes multicolores dans les deux couleurs de premier plan et d’arrière-plan. Détectez avec SPI_GETHIGHCONTRAST (SystemInformation.HighContrast dans WinForms, SystemParameters.HighContrast dans WPF) ; les notifications sont WM_SYSCOLORCHANGE / WM_THEMECHANGED (SystemEvents.UserPreferenceChanged dans .NET).111213
  • La prise en charge du mode sombre n’est pas un substitut à la prise en charge de l’accessibilité. Une palette sombre a toujours besoin d’un ratio de contraste de 4,5:1, et transmettre l’information par plus que la couleur seule est requis sous tout thème.1415

En une phrase : la prise en charge des thèmes, c’est rassembler les couleurs en un seul endroit, lire le paramètre système, remarquer les changements et repeindre ; mais sous un thème de contraste, se remettre entièrement aux paires de couleurs système.

Dans le diagramme, un trait continu marque une relation qui vaut toujours et un trait pointillé une relation conditionnelle (les conditions figurent dans l’explication de chaque relation sur la page de détail). La liste complète des relations (28 au total, avec preuve et niveau de certitude) et les définitions des concepts principaux sont rassemblées sur la page de détail de la carte des connaissances (en japonais). Données : JSON-LD / Turtle

2. Le « thème » a deux axes — clair/sombre et thèmes de contraste

2.1. Clair/sombre (mode de couleur)

Le mode de couleur sous Paramètres > Personnalisation > Couleurs dans Windows est le paramètre qui décide de la luminosité du premier plan et de l’arrière-plan à travers le système d’exploitation et toutes les applications. La documentation Microsoft définit le clair comme « un premier plan sombre sur un arrière-plan clair » et le sombre comme « un premier plan clair sur un arrière-plan sombre », en ajoutant que premier plan signifie ici « la couleur de texte par défaut ». En mode sombre, le premier plan (le texte) est clair et l’arrière-plan est sombre.3

Le paramètre est stocké dans le registre sous HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize comme les valeurs DWORD AppsUseLightTheme (le mode de l’application) et SystemUsesLightTheme (le mode de Windows lui-même), et il figure dans la référence des paramètres de Microsoft.16 Comme décrit plus bas, cependant, le moyen canonique de le lire depuis une application est la classe WinRT UISettings.

2.2. Thèmes de contraste (contraste élevé)

Un thème de contraste, choisi sous Paramètres > Accessibilité > Thèmes de contraste, utilise une palette contrainte à un ratio de contraste d’environ 7:1 ou plus, et existe pour les utilisateurs qui ont besoin d’une séparation visuelle forte du premier plan et de l’arrière-plan. Windows 11 en a quatre intégrés, Aquatic, Desert, Dusk et Night sky, et l’utilisateur peut non seulement en choisir un, mais aussi éditer individuellement les couleurs d’arrière-plan, de texte, d’hyperlien, de texte désactivé, de texte sélectionné et de bouton. Alt gauche + Maj gauche + Impr. écran bascule rapidement un thème de contraste, et Aquatic est appliqué si aucun n’a été sélectionné.1

La documentation Microsoft dit clairement : « ne confondez pas les thèmes de contraste avec les thèmes clairs et sombres ». Clair/sombre utilise une palette large et n’est pas optimisé pour un contraste maximal.1 Et le point important est que le mode sombre n’est pas disponible tant qu’un thème de contraste est actif. Application.SetColorMode de WinForms ne fournit pas le mode sombre pendant un thème de contraste, et RequestedTheme de XAML est remplacé par le système.217

2.3. L’ordre de priorité

L’implémentation dans une application suit donc cet ordre. Décidez d’abord si un thème de contraste est actif ; si c’est le cas, remettez-vous entièrement aux couleurs système. Sinon, choisissez soit la palette claire, soit la palette sombre.

Les deux axes de thème et l'ordre de prioritéL'ordre de décision dans lequel l'application se remet entièrement aux paires de couleurs système si un thème de contraste est actif, et sinon lit le mode de couleur clair/sombre et choisit la palette de l'applicationOuiNonClairSombreThème de contraste actif ?Se remettre aux paires de couleurs systèmeMode de couleur ?Palette clairePalette sombreLe mode sombre n'est pas disponible

Figure 2 : Placez la décision de thème de contraste en premier, et ne choisissez la palette claire ou sombre que lorsqu’aucun thème de contraste n’est actif.

3. Pourquoi les applications existantes restent blanches en mode sombre

Une fenêtre se compose de deux zones : la zone non cliente, formée de la barre de titre, du cadre et des boutons de légende, et la zone cliente, que l’application dessine. Depuis Windows Vista, la zone non cliente est composée et dessinée par DWM (Gestionnaire de fenêtres du Bureau), et l’application spécifie des attributs de la façon dont elle est dessinée via DwmSetWindowAttribute.18

La documentation Microsoft explique franchement pourquoi les applications existantes restent blanches. « Windows ne sait pas si une application peut prendre en charge le mode sombre, donc elle suppose qu’elle ne le peut pas, pour des raisons de compatibilité descendante. » Des frameworks comme WinUI et le Windows App SDK gèrent le mode sombre nativement, mais les applications Win32 ne prennent généralement pas en charge le mode sombre, donc Windows leur donne une barre de titre claire par défaut.3

Les deux zones d'une fenêtre et qui les dessineLa zone non cliente formée de la barre de titre et du cadre est dessinée par DWM, et la zone cliente est dessinée par l'application ou le framework UI, donc la prise en charge du mode sombre est nécessaire dans les deuxFenêtre de premier niveauZone non cliente (barre de titre, cadre)Zone cliente (le contenu de la fenêtre)Composée et dessinée par DWMDessinée par l'application ou le frameworkIndiquée via DwmSetWindowAttributeLa palette propre de l'application

Figure 3 : DWM dessine la barre de titre et l’application dessine le contenu, donc la prise en charge du mode sombre a besoin à la fois d’une instruction à DWM et de la palette de l’application.

Deux conséquences s’ensuivent. Premièrement, pour rendre la barre de titre sombre, l’application doit le demander explicitement à DWM. Deuxièmement, ce qui devient sombre du fait de la demande n’est que la barre de titre ; la zone cliente doit être repeinte par l’application elle-même. La documentation dit aussi que « pour prendre pleinement en charge le mode sombre, toute la surface de l’application doit suivre le thème sombre », et note que le guide officiel ne couvre que la détection et la barre de titre, pas comment repeindre la zone cliente.3 Une application avec une barre de titre noire et un contenu blanc paraît moins naturelle qu’une qui reste blanche de bout en bout.

Comment le défaut devient clairWindows ne sait pas si une application prend en charge le mode sombre, donc pour la compatibilité le défaut est clair, et seulement lorsque l'application passe TRUE via l'attribut DWM il dessine le cadre selon le paramètre sombre du systèmeNonOuiWindows ne peut pas dire si l'application le prend en chargeLe défaut est clair, pour la compatibilitéL'application a-t-elle passé TRUE ?Toujours un cadre clairDessiné selon le paramètre système

Figure 4 : Ne sachant pas si l’application le prend en charge, Windows met le défaut au clair et ne suit le système que lorsque l’application le dit explicitement.

4. La barre de titre DWM sombre — DwmSetWindowAttribute

4.1. DWMWA_USE_IMMERSIVE_DARK_MODE

L’attribut qui rend la barre de titre sombre est DWMWA_USE_IMMERSIVE_DARK_MODE. L’énumération DWMWINDOWATTRIBUTE le décrit ainsi : « Permet au cadre de cette fenêtre d’être dessiné dans les couleurs du mode sombre lorsque le paramètre système du mode sombre est activé. Pour des raisons de compatibilité, toutes les fenêtres sont par défaut en mode clair indépendamment du paramètre système. Le paramètre pvAttribute pointe vers une valeur de type BOOL. TRUE pour honorer le mode sombre pour la fenêtre, FALSE pour toujours utiliser le mode clair. Cette valeur est prise en charge à partir de Windows 11 Build 22000. »4

Autrement dit, TRUE ne signifie pas « rends-le sombre » ; c’est une permission qui dit « tu peux le dessiner sombre si le système est sombre ». Si l’application est prête à peindre sa zone cliente en sombre, passer TRUE suffit pour que la barre de titre suive le paramètre système. Inversement, si l’application est conçue pour toujours s’afficher en mode clair (la politique « clair fixe » décrite plus loin), laisser le défaut FALSE convient.

Le code C++ du guide officiel a la forme suivante. Il inclut même l’étape de définir soi-même la valeur 20 pour les SDK plus anciens dont les en-têtes n’ont pas la constante.3

#include <dwmapi.h>
#pragma comment(lib, "dwmapi.lib")

#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif

// Windows 11 (build 22000) ou version ultérieure ? Suppose un manifeste qui déclare supportedOS
// pour Windows 10 ou une version ultérieure (sans un, la version est arrondie vers le bas à Windows 8)
bool IsWindows11OrGreater()
{
    OSVERSIONINFOEXW osvi{ sizeof(osvi) };
    osvi.dwMajorVersion = 10;
    osvi.dwMinorVersion = 0;
    osvi.dwBuildNumber = 22000;
    DWORDLONG mask = 0;
    VER_SET_CONDITION(mask, VER_MAJORVERSION, VER_GREATER_EQUAL);
    VER_SET_CONDITION(mask, VER_MINORVERSION, VER_GREATER_EQUAL);
    VER_SET_CONDITION(mask, VER_BUILDNUMBER, VER_GREATER_EQUAL);
    return ::VerifyVersionInfoW(
        &osvi, VER_MAJORVERSION | VER_MINORVERSION | VER_BUILDNUMBER, mask) != FALSE;
}

// honorDarkMode = true : la barre de titre peut être dessinée sombre lorsque le système est sombre
void ApplyTitleBarTheme(HWND hwnd, bool honorDarkMode)
{
    if (!IsWindows11OrGreater())
    {
        // La prise en charge documentée est la build 22000 ou une version ultérieure. En dessous, ne pas l'appeler et suivre le défaut (clair)
        LogInfo(L"DWMWA_USE_IMMERSIVE_DARK_MODE is not documented for this OS build; keeping the default light frame");
        return;
    }
    BOOL value = honorDarkMode ? TRUE : FALSE;
    HRESULT hr = ::DwmSetWindowAttribute(
        hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &value, sizeof(value));
    if (FAILED(hr))
    {
        // Un échec sur un OS pris en charge est anormal. Ne pas l'avaler en silence ; enregistrer le HRESULT et le faire apparaître
        LogWarning(L"DwmSetWindowAttribute(DWMWA_USE_IMMERSIVE_DARK_MODE) failed: 0x%08X", hr);
    }
}

Un mot sur la raison pour laquelle l’appel est conditionné à la version de l’OS. La prise en charge documentée est Windows 11 build 22000 ou une version ultérieure.4 Les rapports selon lesquels la même valeur fonctionne sous Windows 10 ne sont pas rares, mais l’affichage d’une application métier ne devrait pas dépendre d’un comportement non documenté. Si la conception est « essayer l’appel et abandonner en cas d’échec », alors lorsque l’appel réussit par hasard sous Windows 10, la barre de titre devient sombre par-dessus un comportement non documenté. En dessous de la build 22000, ne pas l’appeler et suivre le défaut documenté d’une barre de titre claire ; sur un OS pris en charge, consigner le HRESULT d’un échec et le faire apparaître. C’est tout. Internet fait aussi circuler une procédure plus ancienne qui utilise la valeur 19, et une technique qui appelle des exportations ordinales de uxtheme.dll pour assombrir les contrôles communs, mais les deux sont des API non documentées, et personne ne les garantit lorsqu’une mise à jour change leur comportement.

4.2. Quand l’appeler — tant que le HWND est vivant, et chaque fois qu’il est recréé

DwmSetWindowAttribute s’appelle sur un HWND, donc il doit s’exécuter après que le handle de fenêtre a été créé. Et un formulaire WinForms peut avoir son handle recréé, par exemple lorsque ShowInTaskbar change. Le nouveau HWND après recréation ne porte aucun attribut, donc l’endroit où l’appeler n’est pas le constructeur mais l’endroit qui s’exécute chaque fois qu’un handle est créé : OnHandleCreated dans WinForms, SourceInitialized dans WPF.

Quand appeler DwmSetWindowAttributeDéfinir l'attribut DWM après la création du handle de fenêtre, le définir à nouveau sur le nouveau handle lorsque le handle est recréé, et le définir à nouveau lorsqu'une notification de changement de thème arriveHandle recrééNotification de changement de thèmeHWND crééDéfinir l'attribut DWMAffiché

Figure 5 : L’attribut DWM est lié au HWND, donc définissez-le à nouveau à chaque création et à chaque recréation.

Le P/Invoke dans WinForms ressemble à ceci (pour la façon sûre d’écrire DllImport, voir « Appeler les API Win32 en toute sécurité depuis C# »).

using System.Runtime.InteropServices;

public partial class MainForm : Form
{
    private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;

    [DllImport("dwmapi.dll")]
    private static extern int DwmSetWindowAttribute(
        IntPtr hwnd, int attribute, ref int value, int size);

    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);
        ApplyTitleBarTheme();
    }

    private void ApplyTitleBarTheme()
    {
        // La prise en charge documentée est la build 22000 ou une version ultérieure. En dessous, ne pas l'appeler et suivre le défaut (clair).
        // Cette vérification fonctionne aussi sous .NET Framework. Sous .NET Framework, cependant, sans un manifeste qui
        // déclare supportedOS pour Windows 10 ou une version ultérieure, la version est arrondie vers le bas à Windows 8
        // (sous .NET 5 ou une version ultérieure, OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000) fonctionne aussi)
        if (Environment.OSVersion.Version < new Version(10, 0, 22000))
        {
            _logger.LogInformation("Dark title bar is not documented for this OS build; keeping the default light frame");
            return;
        }
        // 1 (TRUE) = peut être dessiné sombre lorsque le système est sombre. 0 (FALSE) = toujours clair
        int honorDarkMode = 1;
        int hr = DwmSetWindowAttribute(
            Handle, DWMWA_USE_IMMERSIVE_DARK_MODE, ref honorDarkMode, sizeof(int));
        if (hr < 0)
        {
            _logger.LogWarning("DwmSetWindowAttribute failed: 0x{Hr:X8}", hr);
        }
    }
}

Ce code est nécessaire aux applications sous .NET 8 ou antérieur, .NET Framework, et Win32/MFC. Lorsque vous utilisez Application.SetColorMode dans WinForms sous .NET 9 ou une version ultérieure, ou ThemeMode dans WPF sous .NET 9 ou une version ultérieure, le framework prend en charge l’assombrissement de la fenêtre (la documentation de ThemeMode indique qu’il « contrôle aussi l’application du matériau d’arrière-plan et du mode sombre à la fenêtre »).10 L’appeler deux fois ne nuit pas, mais cela brouille qui est responsable, donc choisissez l’un ou l’autre.

Dans WPF, le HWND est fixé à SourceInitialized. Obtenez le handle depuis WindowInteropHelper.19

using System.Windows.Interop;

public partial class MainWindow : Window
{
    protected override void OnSourceInitialized(EventArgs e)
    {
        base.OnSourceInitialized(e);
        var hwnd = new WindowInteropHelper(this).Handle;
        TitleBarTheme.Apply(hwnd, honorDarkMode: true); // le corps est le P/Invoke montré plus haut
    }
}

4.3. Couleur de barre de titre, couleur de texte, couleur de bordure et matériau d’arrière-plan

Windows 11 a ajouté des attributs qui spécifient la couleur de la barre de titre elle-même, au-delà du choix binaire sombre ou clair.

Attribut Valeur Signification Build prise en charge
DWMWA_USE_IMMERSIVE_DARK_MODE 20 Dessiner le cadre en sombre lorsque le système est sombre (BOOL) 22000
DWMWA_BORDER_COLOR 34 Couleur de la bordure de fenêtre (COLORREF). DWMWA_COLOR_NONE retire la bordure 22000
DWMWA_CAPTION_COLOR 35 Couleur de la barre de titre (COLORREF) 22000
DWMWA_TEXT_COLOR 36 Couleur du texte de titre (COLORREF) 22000
DWMWA_SYSTEMBACKDROP_TYPE 38 Matériau d’arrière-plan dessiné par le système (Mica ou Acrylic) 22621

Pour les trois attributs de couleur, passer DWMWA_COLOR_DEFAULT (0xFFFFFFFF) rétablit le défaut système. Notez que pour la couleur de bordure, « il incombe à l’application de changer la couleur en réponse aux changements d’état tels que l’activation de la fenêtre ».4 Le matériau d’arrière-plan se spécifie avec l’énumération DWM_SYSTEMBACKDROP_TYPE ; sous Windows 11, DWMSBT_MAINWINDOW correspond à Mica et DWMSBT_TRANSIENTWINDOW à Acrylic, mais la documentation indique que « l’effet du matériau peut changer dans les versions futures de Windows ».20

Réfléchissez soigneusement à la place que cela occupe dans une application métier. Une fois que vous peignez la barre de titre dans une couleur de marque, vous devenez responsable de garantir le contraste du texte de titre et des boutons de légende sur cette couleur. Aux deux états sombre et clair s’ajoutent les combinaisons avec actif et inactif. Pour la plupart des applications métier, la bonne réponse est « suivre le défaut système (se contenter de mettre la valeur 20 à TRUE) », et une couleur de marque est une option pour lorsqu’elle est vraiment nécessaire.

Comment décider la couleur de la barre de titreSuivre le défaut système n'exige que de mettre la valeur 20 à TRUE, mais peindre une couleur de marque rend l'application responsable de garantir le contraste du texte et des boutons de légende et de gérer les couleurs actives et inactives, et rétablir passe DWMWA_COLOR_DEFAULTSuivre le défaut systèmePeindre une couleur de marqueCouleur de la barre de titre ?Se contenter de mettre la valeur 20 à TRUESpécifier les attributs de couleur (34 à 36)Garantir soi-même le contraste du texte et des boutonsGérer soi-même actif/inactifRétablir avec COLOR_DEFAULT

Figure 6 : Choisir une couleur de marque déplace la responsabilité du contraste et de la gestion d’état vers l’application, donc pour la plupart des applications métier suivre le défaut est la bonne réponse.

5. Détecter et suivre le thème système — lire, remarquer, repeindre

Le travail de faire suivre le thème à la zone cliente se décompose en trois parties : lire le paramètre actuel, remarquer les changements, et repeindre.

Les trois étapes de la détection et du suiviLa boucle de lecture du mode de couleur actuel avec UISettings au démarrage, de remarque des changements via des notifications telles que ColorValuesChanged, de remontée vers le thread UI, et de repeinture de la palette de l'applicationLire : UISettings.GetColorValueRepeindre : réappliquer la paletteRemarquer : ColorValuesChangedRemonter vers le thread UIRedéfinir aussi l'attribut DWM

Figure 7 : Lire au démarrage, remarquer via les notifications, remonter vers le thread UI, et repeindre : cette boucle est le squelette du suivi de thème.

5.1. Lire — UISettings et « si le premier plan est clair, c’est sombre »

La procédure officielle de Microsoft utilise la classe WinRT Windows.UI.ViewManagement.UISettings. Obtenez la couleur de premier plan (la couleur de texte par défaut) avec GetColorValue(UIColorType::Foreground), estimez sa luminance perçue par une arithmétique entière pour décider si elle est « claire », et concluez mode sombre si le premier plan est clair. La documentation note que la formule n’est pas un modèle de luminance rigoureux, seulement une approximation suffisante pour classer clair et sombre.321

UISettings est une classe WinRT, mais les applications C# WPF et WinForms peuvent l’appeler directement si le TargetFramework porte une version de SDK Windows, telle que net8.0-windows10.0.19041.0 (pour le fonctionnement, voir « WinRT est COM »). Vous pourriez aussi lire AppsUseLightTheme directement dans le registre, mais le registre est l’endroit où le paramètre est stocké, pas un contrat d’API ; si vous le lisez, traitez UISettings comme la source de vérité et gardez le registre pour le diagnostic.

Moyens de lire le mode de couleurLe chemin canonique est d'obtenir la couleur de premier plan via WinRT UISettings et de la classer comme claire ou sombre ; la valeur de registre AppsUseLightTheme est l'emplacement de stockage et devrait être gardée pour le diagnosticClair ou sombre en ce moment ?UISettings.GetColorValueRegistre AppsUseLightThemeJuger la luminance perçue du premier planClair signifie mode sombreEmplacement de stockage. Garder pour le diagnostic

Figure 8 : Le chemin de lecture canonique est UISettings ; le registre n’est que l’endroit où le paramètre est stocké.

5.2. Remarquer — ColorValuesChanged n’arrive pas sur le thread UI

UISettings sert aussi à détecter les changements. L’événement ColorValuesChanged se déclenche lorsqu’une valeur de couleur change, et le guide officiel utilise cet événement pour suivre les changements de paramètre.53 Une précaution pratique s’applique ici. Cet événement n’est pas garanti d’arriver sur le thread UI. Remontez vers le thread UI avec le Dispatcher de WPF, Control.Invoke de WinForms, ou le SynchronizationContext qui fonctionne dans les deux, avant de toucher un contrôle. Le travail avec le thread UI est résumé dans « WPF/WinForms : async et le thread UI récapitulés en une fiche ».

L’événement se déclenche aussi lorsque la couleur d’accent change. Si vous voulez ne repeindre que lorsque clair/sombre change, réévaluez à chaque événement et notifiez seulement lorsque le résultat diffère de la dernière fois. Et, selon l’ordre de priorité du chapitre 2, ne regardez pas la luminosité du premier plan tant qu’un thème de contraste est actif. Un thème de contraste avec un arrière-plan sombre tel qu’Aquatic a un premier plan clair, donc la luminosité seule le classerait à tort comme « sombre ». Traitez le thème de contraste comme un état indépendant et décidez-le d’abord.

La classe suivante rassemble cet ordre de décision en un seul endroit. Le SynchronizationContext utilisé pour remonter vers le thread UI et la vérification de thème de contraste (SystemInformation.HighContrast dans WinForms, SystemParameters.HighContrast dans WPF ; voir le chapitre 8) sont passés par l’appelant. Attention au moment de la création. Dans WinForms, au point de Program.Main il n’y a pas encore de boucle de messages ni de Control, donc SynchronizationContext.Current est null. Passez SynchronizationContext.Current après que les contrôles existent, par exemple depuis le constructeur du formulaire ou OnLoad. Dans WPF vous pouvez passer new DispatcherSynchronizationContext(Application.Current.Dispatcher).

using Windows.UI.ViewManagement; // TargetFramework : net8.0-windows10.0.19041.0 ou version ultérieure

public enum ThemeState { Light, Dark, HighContrast }

public sealed class SystemThemeWatcher : IDisposable
{
    private readonly UISettings _settings = new();
    private readonly SynchronizationContext _ui;
    private readonly Func<bool> _isHighContrast;

    private bool _disposed;

    public ThemeState Current { get; private set; }
    public event EventHandler? Changed;

    // ui : le SynchronizationContext du thread UI. Passez SynchronizationContext.Current
    //     après que les contrôles existent, ou un DispatcherSynchronizationContext dans WPF
    // isHighContrast : () => SystemInformation.HighContrast (WinForms)
    //                  () => SystemParameters.HighContrast (WPF)
    public SystemThemeWatcher(SynchronizationContext ui, Func<bool> isHighContrast)
    {
        _ui = ui ?? throw new ArgumentNullException(nameof(ui));
        _isHighContrast = isHighContrast ?? throw new ArgumentNullException(nameof(isHighContrast));
        Current = Read();
        _settings.ColorValuesChanged += OnColorValuesChanged;
    }

    private ThemeState Read()
    {
        // Ordre de décision comme au chapitre 2 : thème de contraste d'abord. Un thème de contraste avec un
        // arrière-plan sombre a un premier plan clair, donc la luminosité seule le classerait à tort comme « sombre »
        if (_isHighContrast()) return ThemeState.HighContrast;
        // Même test que le guide officiel : sombre si le premier plan (couleur de texte par défaut) est clair
        var fg = _settings.GetColorValue(UIColorType.Foreground);
        bool isDark = (5 * fg.G + 2 * fg.R + fg.B) > 8 * 128;
        return isDark ? ThemeState.Dark : ThemeState.Light;
    }

    // Appeler depuis le thread UI. Peut aussi être appelé depuis d'autres chemins de notification tels que UserPreferenceChanged
    public void Refresh()
    {
        if (_disposed) return;
        var next = Read();
        // Entre clair et sombre, ne pas notifier lorsque l'état est inchangé, pour ignorer les changements de seule couleur d'accent.
        // Pendant un thème de contraste c'est l'exception : l'état reste HighContrast même lorsque l'utilisateur édite
        // les couleurs du thème, donc notifier même pour le même état afin que les couleurs système soient relues
        if (next == Current && next != ThemeState.HighContrast) return;
        Current = next;
        Changed?.Invoke(this, EventArgs.Empty);
    }

    private void OnColorValuesChanged(UISettings sender, object args)
    {
        // Non garanti d'arriver sur le thread UI, donc remonter vers l'UI avant de décider et de notifier.
        // Un appel qui arrive après Dispose (déjà en file) est ignoré par le drapeau dans Refresh
        _ui.Post(_ => Refresh(), null);
    }

    public void Dispose()
    {
        // Se désabonner n'arrête que les livraisons futures ; les appels déjà postés au thread UI restent.
        // Mettre le drapeau pour que les appels restants s'ignorent eux-mêmes (appeler ceci sur le thread UI)
        _disposed = true;
        _settings.ColorValuesChanged -= OnColorValuesChanged;
    }
}

Au niveau Win32, WM_SETTINGCHANGE est envoyé à chaque fenêtre de premier niveau lorsqu’un paramètre change,22 et dans .NET il arrive comme SystemEvents.UserPreferenceChanged.23 Un basculement de style visuel (y compris l’activation d’un thème de contraste) apporte WM_THEMECHANGED,24 et un changement de couleur système apporte WM_SYSCOLORCHANGE.25 Plutôt que d’écrire un traitement distinct pour chaque type de notification, appelez la même routine « lire et repeindre » quelle que soit la notification qui arrive ; c’est plus difficile à casser, et cela a la même forme que le suivi de thème de contraste présenté dans l’article précédent. Du point de vue du SystemThemeWatcher ci-dessus, cela signifie d’appeler aussi Refresh() depuis les gestionnaires UserPreferenceChanged et StaticPropertyChanged.

Chemins et threads de notification de changement de thèmeColorValuesChanged de UISettings peut arriver hors du thread UI et est remonté, WM_SETTINGCHANGE arrive comme SystemEvents.UserPreferenceChanged, et WM_THEMECHANGED et WM_SYSCOLORCHANGE arrivent à la procédure de fenêtre. Tous convergent vers la même routine de réapplicationColorValuesChangedRemonter vers le thread UIUserPreferenceChangedLire et repeindreWM_THEMECHANGED etc.

Figure 9 : Il y a plusieurs chemins de notification, mais tous convergent vers la même routine « lire et repeindre ».

5.3. Repeindre — rassembler les couleurs en un seul endroit

La condition préalable qui rend la repeinture possible est que les couleurs sont rassemblées en un seul endroit. Si Color.White et #FFFFFF sont dispersés dans les formulaires et le XAML, vous ne pouvez pas énumérer les endroits à repeindre. Dans WinForms, créez une classe « palette » (deux instances, une claire et une sombre) et faites prendre aux contrôles leurs couleurs depuis elle au démarrage et à chaque notification. Dans WPF, rassemblez les couleurs dans un ResourceDictionary et référencez-les depuis XAML avec DynamicResource. Dans WinUI, cette structure est fournie dès le départ comme ThemeDictionaries.

La structure qui rassemble les couleurs en un seul endroitL'application tient une palette claire et une palette sombre, et chaque écran prend ses couleurs dans la palette choisie pour le mode actuel, donc les cibles d'une repeinture peuvent être énuméréesClairSombreMode actuelLaquelle ?Palette clairePalette sombreChaque écran et chaque contrôleColor.White disperséImpossible d'énumérer ce qu'il faut repeindre

Figure 10 : Avec la palette en un seul endroit, une repeinture peut être énumérée ; avec des couleurs codées en dur dispersées, non.

Ce travail de « rassembler les couleurs » se paie directement dans la prise en charge des thèmes de contraste et dans les vérifications de ratio de contraste décrites plus loin. Le plus gros coût de la prise en charge du mode sombre n’est pas les appels d’API, mais ce nettoyage.

6. Implémentation dans WinForms

6.1. .NET 9/10 — Application.SetColorMode

WinForms a reçu une prise en charge préliminaire du mode sombre dans .NET 9, et elle a été « pleinement intégrée » dans .NET 10. Application.SetColorMode accepte trois valeurs.67

  • SystemColorMode.Classic — le défaut. Clair, comme avant.
  • SystemColorMode.System — suivre le paramètre clair/sombre de Windows.
  • SystemColorMode.Dark — sombre.

Appelez-le avant Application.Run, avant qu’un quelconque élément d’interface soit créé. Sous .NET 9 c’était une fonctionnalité expérimentale, donc c’était une erreur de compilation sauf si WFO5001 était masqué dans le fichier projet ; à partir de .NET 10 l’erreur n’apparaît plus.26

static class Program
{
    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();
        Application.SetColorMode(SystemColorMode.System); // appeler avant de créer toute UI
        Application.Run(new MainForm());
    }
}

Lorsque le mode de couleur change, System.Drawing.SystemColors bascule vers les couleurs correspondantes, et les contrôles standard sont dessinés en conséquence.6 Sous le capot, la propriété expérimentale SystemColors.UseAlternativeColorSet (SYSLIB5002) « fait que les valeurs KnownColor système renvoient un jeu de couleurs alternatif (actuellement la version du mode sombre) » ; parce que les couleurs système Win32 elles-mêmes ne changent pas avec le paramètre clair/sombre, .NET porte le jeu alternatif de son côté. La même documentation dit aussi que lorsqu’un thème de contraste est actif, les couleurs Windows actuelles sont toujours renvoyées.27

Gardez à l’esprit les trois contraintes écrites dans la documentation de SetColorMode.2

  1. Le mode de couleur sombre n’est disponible que sous Windows 11 ou une version ultérieure.
  2. Le mode sombre n’est pas disponible lorsqu’un thème de contraste est actif.
  3. Même avec SystemColorMode.System, l’application ne suit pas automatiquement un changement du paramètre Windows pendant qu’elle s’exécute.

La troisième tend à générer des questions de support dans les applications métier. Écrivez dans la documentation destinée à l’utilisateur que le mode est décidé par le paramètre Windows au démarrage et prend effet au lancement suivant. Si vous devez absolument suivre un basculement à l’exécution, vous avez besoin d’une conception qui recrée les formulaires, et cela n’en vaut presque jamais la peine.

Le flux et les contraintes de SetColorModeSetColorMode est appelé avant Application.Run, SystemColors bascule vers le jeu alternatif, et les contrôles standard suivent. Il a trois contraintes : Windows 11 uniquement, désactivé pendant un thème de contraste, et pas de suivi des changements de paramètre à l'exécutionSetColorMode (System)Avant Application.RunSystemColors bascule vers le jeu alternatifLes contrôles standard suiventWindows 11 uniquementDésactivé pendant un thème de contrasteNe suit pas les changements à l'exécution

Figure 11 : SetColorMode prend effet une fois avant le démarrage et a trois contraintes documentées.

6.2. Contrôles dessinés par le propriétaire et ApplyThemingImplicitly

Les contrôles standard suivent le mode de couleur de l’application, mais la documentation de .NET 10 liste deux cas exceptionnels. Si un contrôle que vous composez et dessinez vous-même utilise des contrôles communs Win32 tels que les barres de défilement, ceux-ci restent clairs s’ils n’optent pas explicitement. Inversement, si vous héritez d’un contrôle existant qui suit le thème et voulez le contrôle total de son dessin vous-même, vous optez pour la sortie.7

Dans les deux cas, remplacez Control.CreateParams et appelez SetStyle(ControlStyles.ApplyThemingImplicitly, true/false) avant de lire base.CreateParams. Le piège de cette API est que le constructeur de la classe de base lit CreateParams, donc votre propre constructeur arrive trop tard.7

public partial class GanttChartControl : Control
{
    protected override CreateParams CreateParams
    {
        get
        {
            // Le définir avant de lire base.CreateParams. Le constructeur arrive trop tard
            SetStyle(ControlStyles.ApplyThemingImplicitly, true);
            return base.CreateParams;
        }
    }
}
Quand ApplyThemingImplicitly peut être définiApplyThemingImplicitly se décide au point où le constructeur de la classe de base lit CreateParams, donc SetStyle doit être appelé avant base.CreateParams à l'intérieur du remplacement de CreateParams, et le constructeur de la classe dérivée arrive trop tardConstructeur de la classe de baseLit CreateParamsSetStyle doit se produire d'iciConstructeur de la classe dérivéeL'appeler ici arrive trop tard

Figure 12 : ApplyThemingImplicitly doit être décidé avant que le constructeur de la classe de base lise CreateParams.

Le dessin par le propriétaire lui-même (dessin GDI+ dans OnPaint) suit le jeu alternatif tant qu’il utilise SystemColors / SystemBrushes / SystemPens. Tout endroit qui peint avec Color.White est remplacé par la palette décrite plus haut, ici aussi.

6.3. .NET Framework 4.8 et .NET 8 ou antérieur — déclarer « clair fixe »

Dans les environnements sans SetColorMode, il n’y a pas de prise en charge standard du mode sombre. Il y a deux options.

  1. Déclarer le clair fixe. Laissez l’attribut DWM à son défaut FALSE (toujours une barre de titre claire) et gardez la zone cliente telle quelle. La prise en charge des thèmes de contraste (chapitre 8) reste obligatoire.
  2. Implémenter la prise en charge complète vous-même. Consolidez la palette, lisez et suivez avec UISettings, mettez l’attribut DWM à TRUE, et repeignez tout y compris l’apparence des contrôles communs.

L’option 2 tend à finir « surtout sombre, mais clair par endroits », parce que l’application ne peut pas pleinement contrôler le dessin des contrôles communs Win32 (barres de défilement, en-têtes, boutons d’expansion d’arbre, et ainsi de suite). Comme le dit le guide officiel, « toute la surface doit suivre » ;3 un mode sombre à moitié fait est une pire expérience que le clair fixe. Pour les actifs existants, choisissez l’option 1, déclarez « cette application s’affiche en mode clair », et passez à SetColorMode lorsque vous migrez vers .NET 10. C’est la politique réaliste, et la plus facile à expliquer.

Options WinForms selon l'environnement d'exécutionSous .NET 10 ou une version ultérieure utiliser SetColorMode, sous .NET 9 la même API avec WFO5001 masqué, et sous .NET 8 ou antérieur ou .NET Framework choisir entre déclarer le clair fixe et implémenter la prise en charge complète vous-même jusqu'aux contrôles communs.NET 10 ou version ultérieure.NET 9.NET 8 ou antérieur / .NET FrameworkRecommandéSi vous y êtes préparéEnvironnement d'exécution ?SetColorMode (System)SetColorMode + masquer WFO5001Que faire ?Déclarer le clair fixePrise en charge complète vous-mêmeLes contrôles communs restent

Figure 13 : Sur les environnements d’exécution sans SetColorMode, déclarer le clair fixe est le défaut réaliste.

7. Implémentation dans WPF

7.1. .NET 9/10 — Le thème Fluent et ThemeMode

WPF sous .NET 9 livre un nouveau thème qui suit le design Fluent de Windows 11, avec prise en charge du clair/sombre et de la couleur d’accent. Il y a deux façons de l’appliquer : définir la propriété ThemeMode, ou ajouter le dictionnaire de ressources PresentationFramework.Fluent à MergedDictionaries.8

ThemeMode prend quatre valeurs, Light / Dark / System / None (le défaut ; le thème Aero2 classique). Défini sur Application il affecte toute l’application ; défini sur une Window il n’affecte que cette fenêtre.8

<Application x:Class="OrderEntry.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml"
             ThemeMode="System">
</Application>

ThemeMode ne charge pas seulement les dictionnaires de thème Fluent dans les ressources ; la documentation indique qu’il « contrôle aussi l’application du matériau d’arrière-plan et du mode sombre à la fenêtre ». Autrement dit, WPF s’occupe de l’attribut DWM du chapitre 4. ThemeMode et Resources sont aussi conçus pour rester en synchronisation, ce que la documentation explique pour éviter une incohérence où la fenêtre est sombre mais les contrôles à l’intérieur sont clairs.10

Ce que ThemeMode fait dans WPFDéfinir ThemeMode à System charge les dictionnaires de thème Fluent correspondant au paramètre Windows dans les ressources et contrôle aussi l'application du mode sombre et du matériau d'arrière-plan à la fenêtreThemeMode=SystemLire le paramètre WindowsCharger les dictionnaires Fluent dans les ressourcesAssombrir la fenêtre et appliquer l'arrière-planRester en synchronisation avec Resources pour éviter l'incohérence

Figure 14 : ThemeMode contrôle ensemble le chargement des dictionnaires Fluent et l’assombrissement de la fenêtre.

Il y a deux précautions à connaître avant de l’adopter. Premièrement, la lecture et l’écriture de ThemeMode depuis le code sont une fonctionnalité expérimentale, et y accéder produit l’erreur WPF0001. Si vous la masquez vous pouvez écrire Application.Current.ThemeMode = ThemeMode.Dark, mais la référence d’API porte encore [Experimental("WPF0001")] dans .NET 10, avec la note qu’il « peut être retiré à l’avenir ».810 Deuxièmement, la couverture des styles Fluent est encore « in progress » dans .NET 10. .NET 10 a ajouté des styles pour DatePicker, GridSplitter, GroupBox, TextBox et d’autres, et a corrigé des plantages liés à HighContrast,9 ce qui, lu à l’envers, signifie que le thème Fluent de .NET 9 en manquait. Avant de décider de l’adopter, confirmez sur une machine réelle que les contrôles que votre application métier utilise (DataGrid et les contrôles tiers en particulier) ne se cassent pas sous Fluent.

7.2. Suivre le thème sur le thème classique — échanger des ResourceDictionary

Dans WPF sans Fluent (ou sous .NET 8 ou antérieur, ou .NET Framework), il n’y a pas de prise en charge standard du mode sombre. Les couleurs système Win32 ne changent pas avec le paramètre clair/sombre, donc référencer les SystemColors de WPF ne rendra rien sombre. La structure pour suivre le thème vous-même est la suivante.

  1. Définissez les couleurs et les pinceaux avec les mêmes clés dans Themes/Light.xaml pour le clair et Themes/Dark.xaml pour le sombre.
  2. Référencez-les depuis XAML avec DynamicResource, comme dans {DynamicResource App.WindowBackgroundBrush} (StaticResource est fixé au chargement et ne suit pas un échange).
  3. Sur une notification du SystemThemeWatcher du chapitre 5, échangez le dictionnaire correspondant dans MergedDictionaries. Pendant un thème de contraste, quel que soit le dictionnaire que vous mettez, le déclencheur de la section 8.4 remplace les couleurs par des couleurs système, donc mettez le clair.
public static class AppTheme
{
    private static readonly Uri Light = new("pack://application:,,,/Themes/Light.xaml");
    private static readonly Uri Dark = new("pack://application:,,,/Themes/Dark.xaml");

    public static void Apply(ThemeState state)
    {
        var merged = Application.Current.Resources.MergedDictionaries;
        var current = merged.FirstOrDefault(d => d.Source == Light || d.Source == Dark);
        // Le dictionnaire sombre seulement lorsque sombre. Pendant un thème de contraste, se remettre aux couleurs système (section 8.4)
        var next = new ResourceDictionary { Source = state == ThemeState.Dark ? Dark : Light };
        if (current is null)
        {
            merged.Add(next);
        }
        else
        {
            merged[merged.IndexOf(current)] = next; // échanger sur place, à la même position
        }
    }
}
Échanger des dictionnaires de ressources dans WPFDéfinir les couleurs avec les mêmes clés dans un dictionnaire clair et un sombre, les référencer depuis XAML avec DynamicResource, et échanger le dictionnaire dans MergedDictionaries sur une notification de changement de thème pour que les références se mettent à jourNotification de changement de thèmeÉchanger le dictionnaire dans MergedDictionariesLight.xaml (mêmes clés)Dark.xaml (mêmes clés)Les références DynamicResource se mettent à jourRéférences StaticResourceGardent la valeur du chargement

Figure 15 : Échangez des dictionnaires avec les mêmes clés, et seules les références DynamicResource suivent.

Les modèles des contrôles standard (arrière-plans de boutons, couleurs de barres de défilement) portent les couleurs du thème classique, donc ici aussi il y aura des endroits où « les surfaces propres de l’application sont sombres mais les contrôles standard sont clairs ». Estimez le travail de remplacer le style de chaque contrôle dont vous avez besoin, puis comparez-le à l’adoption de Fluent ou au clair fixe.

7.3. La barre de titre

Si vous utilisez ThemeMode, WPF s’en occupe. Si vous suivez le thème vous-même sur le thème classique, utilisez la définition de l’attribut DWM depuis OnSourceInitialized montrée à la section 4.2, et redéfinissez-le sur les notifications de SystemThemeWatcher.

8. Dessiner sous un thème de contraste — garder les paires de couleurs système

8.1. Détection et notification

Dans Win32, passez SPI_GETHIGHCONTRAST à SystemParametersInfo pour recevoir une structure HIGHCONTRAST, et testez le bit HCF_HIGHCONTRASTON de dwFlags. cbSize doit être défini avant l’appel.1128 Microsoft positionne cela comme « le seul moyen pris en charge de vérifier si le contraste élevé est activé ».12

bool IsContrastThemeActive()
{
    HIGHCONTRASTW hc{};
    hc.cbSize = sizeof(hc);
    if (!::SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(hc), &hc, 0))
    {
        // Ne pas cacher un échec derrière une valeur par défaut. Le faire apparaître avec le code d'erreur pour que la cause puisse être trouvée
        throw std::system_error(::GetLastError(), std::system_category(),
                                "SystemParametersInfo(SPI_GETHIGHCONTRAST)");
    }
    return (hc.dwFlags & HCF_HIGHCONTRASTON) != 0;
}

Chaque framework a une propriété qui enveloppe cet appel.

Environnement Détection Notification de changement
Win32 / MFC SPI_GETHIGHCONTRAST + HCF_HIGHCONTRASTON WM_SYSCOLORCHANGE, WM_THEMECHANGED
WinForms SystemInformation.HighContrast SystemEvents.UserPreferenceChanged
WPF SystemParameters.HighContrast (correspond à SPI_GETHIGHCONTRAST) SystemParameters.StaticPropertyChanged
WinUI 3 ThemeSettings.HighContrast (Microsoft.UI.System) ThemeSettings.Changed

Le didacticiel d’accessibilité WinForms vous demande de vérifier HighContrast au démarrage et de répondre aux changements via UserPreferenceChanged.13 SystemParameters.HighContrast de WPF correspond à SPI_GETHIGHCONTRAST et HCF_HIGHCONTRASTON,29 et les changements des propriétés statiques sont annoncés via StaticPropertyChanged.30 ThemeSettings de WinUI 3 se crée lié à une fenêtre avec CreateForWindowId et vous vous abonnez à son événement Changed, mais notez que les événements s’arrêtent si vous ne continuez pas à tenir une référence à l’objet.31

Chemins de détection des thèmes de contrasteWin32 SPI_GETHIGHCONTRAST est la seule méthode de détection prise en charge, et SystemInformation.HighContrast dans WinForms, SystemParameters.HighContrast dans WPF et ThemeSettings.HighContrast dans WinUI 3 sont fournis par chaque framework comme enveloppes autourSPI_GETHIGHCONTRAST (la seule méthode de détection)WinForms SystemInformationWPF SystemParametersWinUI 3 ThemeSettingsWin32 : l'appeler directement

Figure 16 : La racine de la détection est une seule API Win32, et chaque framework a une propriété qui l’enveloppe.

8.2. Principes de dessin — paires de premier plan et d’arrière-plan

Le « High contrast parameter » de Microsoft liste trois choses qu’une application devrait faire lorsque le contraste élevé est activé.11

  1. Mapper chaque couleur vers une paire de couleurs de premier plan et d’arrière-plan. Utilisez GetSysColor avec la paire COLOR_WINDOWTEXT et COLOR_WINDOW, ou la paire COLOR_BTNTEXT et COLOR_BTNFACE.
  2. Omettre les images bitmap affichées derrière le texte. Elles sont un obstacle visuel pour les utilisateurs qui ont besoin d’un contraste élevé.
  3. Dessiner les images multicolores dans les couleurs de premier plan et d’arrière-plan utilisées pour le texte.

La « paire » est le nœud. Le guide de Windows 8 et versions ultérieures explique que COLOR_HIGHLIGHTTEXT est conçu pour être combiné avec l’arrière-plan COLOR_HIGHLIGHT et COLOR_WINDOWTEXT avec l’arrière-plan COLOR_WINDOW, et vous demande de ne pas coder en dur les couleurs de texte et, parce que les utilisateurs personnalisent les couleurs, de construire une interface qui ne dépend pas du thème en vigueur.12 L’exemple du même guide, « dans Aero, le texte est toujours noir et la couleur de sélection est bleu clair, mais dans High Contrast Black la couleur de sélection est noire. Si vous supposez un texte noir et utilisez la couleur de sélection système, vous obtenez du texte noir sur noir », est exactement la plainte « l’affichage d’état a disparu » de l’ouverture.

Le guide des thèmes de contraste de Windows 11 tabule les appariements.1

Usage Premier plan Arrière-plan
Titres, corps de texte, listes, bordures, interface non interactive SystemColorWindowText SystemColorWindow
Hyperliens SystemColorHotlight SystemColorWindow
Interface désactivée ou inactive SystemColorGrayText SystemColorWindow
Sélectionné, survol, enfoncé, en cours SystemColorHighlightText SystemColorHighlight
Interface interactive telle que les boutons SystemColorButtonText SystemColorButtonFace

Ce qu’il ne faut pas faire est aussi énoncé. N’utilisez pas GrayText pour du texte supplémentaire ou d’indication (c’est pour l’état désactivé uniquement) ; n’utilisez pas Hotlight pour autre chose que les hyperliens ; ne mélangez pas des premiers plans et des arrière-plans incompatibles ; ne choisissez pas les couleurs d’après l’apparence (les utilisateurs les changent vraiment). Il y a aussi une ligne directrice de conception selon laquelle les arrière-plans des pages, des volets et des popups sont basés sur SystemColorWindow, donc les surfaces adjacentes finissent avec la même couleur d’arrière-plan, et seules les frontières qui comptent sont séparées par une bordure utilisée uniquement sous les thèmes de contraste (2 px est recommandé pour les menus volants et les boîtes de dialogue).1

Comment rompre la paire rend le texte illisibleSupposer que le texte est noir et n'utiliser la couleur de sélection système que pour l'arrière-plan de sélection donne du noir sur noir dans High Contrast Black, où la couleur de sélection est noire. Prendre premier plan et arrière-plan comme une paire garde le texte lisible même lorsque l'utilisateur édite les couleursSupposer que le texte est noirSeul l'arrière-plan de sélection utilise la couleur de sélection systèmeDans High Contrast Black la couleur de sélection est noireTexte noir sur noirPrendre premier plan et arrière-plan comme une paireLisible même lorsque l'utilisateur édite les couleursL'utilisateur édite les couleurs

Figure 17 : Utiliser une couleur système pour un seul côté peut donner du noir sur noir, mais prendre la paire reste lisible même lorsque les couleurs sont éditées.

Décisions de dessin sous un thème de contrasteLorsqu'un thème de contraste est actif, mapper les couleurs vers les paires de couleurs système, omettre les images derrière le texte, dessiner les images multicolores dans les deux couleurs de premier plan et d'arrière-plan, et ne pas utiliser de couleurs codées en durThème de contraste actifMapper les couleurs vers des pairesOmettre les images derrière le texteDessiner les graphismes multicolores en deux couleursPas de couleurs codées en dur

Figure 18 : Dessiner sous un thème de contraste actif se résume à quatre points : mapper, omettre, deux couleurs, et pas de codage en dur.

8.3. Implémentation dans WinForms

Les contrôles WinForms standard suivent les couleurs système tant que ForeColor / BackColor sont laissés à leurs défauts. Seuls les endroits avec des couleurs personnalisées et le dessin par le propriétaire sont basculés selon la vérification. L’exemple du didacticiel prend une étiquette jaune sur bleu en temps normal et la rétablit à SystemColors.Window / SystemColors.WindowText sous contraste élevé.13 Cela revient à ajouter une branche de thème de contraste à la structure de palette vue plus haut.

using Microsoft.Win32;

public partial class OrderForm : Form
{
    private readonly SynchronizationContext _ui;

    public OrderForm()
    {
        InitializeComponent();
        // Les contrôles existent maintenant, donc un WindowsFormsSynchronizationContext est en place
        _ui = SynchronizationContext.Current
              ?? throw new InvalidOperationException("Create this form on the UI thread.");
        ApplyColorScheme();
        SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged;
    }

    private void ApplyColorScheme()
    {
        if (SystemInformation.HighContrast)
        {
            // Garder la paire, se remettre entièrement aux couleurs système, et retirer l'image derrière le texte
            statusLabel.BackColor = SystemColors.Window;
            statusLabel.ForeColor = SystemColors.WindowText;
            headerPanel.BackgroundImage = null;
        }
        else
        {
            var p = AppPalette.Current; // la palette clair/sombre (chapitre 5)
            statusLabel.BackColor = p.PanelBackground;
            statusLabel.ForeColor = p.PanelForeground;
            headerPanel.BackgroundImage = Properties.Resources.HeaderPattern;
        }
    }

    private void OnUserPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e)
    {
        // Cet événement n'est pas non plus garanti d'arriver sur le thread UI. Remonter vers l'UI, puis réévaluer sans filtrer par catégorie
        _ui.Post(_ =>
        {
            if (IsDisposed) return;
            ApplyColorScheme();
        }, null);
    }

    // Un événement statique, donc le formulaire fuit s'il n'est pas détaché. Détacher dans Dispose(bool), qui s'exécute aussi
    // sur les chemins où le formulaire est disposé sans être fermé (si le Dispose(bool) généré par le concepteur existe, le mettre là)
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged;
        }
        base.Dispose(disposing);
    }
}

Le dessin par le propriétaire dans OnPaint utilise des pinceaux système qui gardent la paire, tels que SystemBrushes.Window / SystemPens.WindowText, et les graphismes multicolores tels qu’un point coloré qui représente un état sont remplacés par une bordure et du texte (« Running », « Stopped ») dans la couleur de premier plan. Transmettre l’information par plus que la couleur seule est le même point que le critère de succès 1.4.1 traité dans l’article précédent.

Branchement du schéma de couleurs WinFormsApplyColorScheme est appelé au démarrage et à chaque UserPreferenceChanged ; si SystemInformation.HighContrast est vrai il se remet aux paires de couleurs système et retire l'image d'arrière-plan, et s'il est faux il prend les couleurs dans la palette clair/sombreVraiFauxDémarrage / UserPreferenceChangedApplyColorSchemeSystemInformation.HighContrast ?Se remettre aux paires SystemColorsRetirer l'image d'arrière-planPrendre les couleurs dans la palette clair/sombre

Figure 19 : Dans WinForms la même routine est appelée au démarrage et à chaque notification, et sous un thème de contraste elle se remet aux couleurs système.

8.4. Implémentation dans WPF

Les SystemColors de WPF se mettent à jour automatiquement lorsqu’un pinceau change si vous référencez une clé de ressource telle que WindowBrushKey via DynamicResource (une référence statique qui utilise WindowBrush directement ne se met pas à jour).32 Pour changer l’apparence seulement pendant un thème de contraste, référencez la valeur de SystemParameters.HighContrast depuis un DataTrigger. Cependant, SystemParameters.HighContrast est une propriété statique, donc à elle seule ce n’est pas une source de liaison vivante. Préparez un petit proxy qui s’abonne à StaticPropertyChanged, tient la valeur, et notifie via INotifyPropertyChanged, et liez avec cette instance comme Source.30

public sealed class ThemeSettings : INotifyPropertyChanged
{
    public static ThemeSettings Instance { get; } = new();

    public bool IsHighContrast { get; private set; } = SystemParameters.HighContrast;
    public event PropertyChangedEventHandler? PropertyChanged;

    private ThemeSettings()
    {
        // Déclenché lorsqu'une propriété statique de SystemParameters change (SPI_GETHIGHCONTRAST est relu)
        SystemParameters.StaticPropertyChanged += (_, e) =>
        {
            if (!string.IsNullOrEmpty(e.PropertyName)
                && e.PropertyName != nameof(SystemParameters.HighContrast)) return;
            IsHighContrast = SystemParameters.HighContrast;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsHighContrast)));
        };
    }
}
<!-- Déclarer xmlns:local="clr-namespace:OrderEntry" au préalable -->
<Style x:Key="CardStyle" TargetType="Border">
    <Setter Property="Background" Value="{DynamicResource App.CardBackgroundBrush}"/>
    <Setter Property="BorderBrush" Value="{DynamicResource App.CardBorderBrush}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Style.Triggers>
        <DataTrigger Binding="{Binding Source={x:Static local:ThemeSettings.Instance}, Path=IsHighContrast}"
                     Value="True">
            <!-- Garder la paire : l'arrière-plan est Window, la bordure et le texte sont WindowText. Épaissir la frontière -->
            <Setter Property="Background"
                    Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
            <Setter Property="BorderBrush"
                    Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"/>
            <!-- Laisser le texte à l'intérieur l'hériter aussi. Notez qu'un enfant qui définit Foreground explicitement coupe l'héritage -->
            <Setter Property="TextElement.Foreground"
                    Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"/>
            <Setter Property="BorderThickness" Value="2"/>
        </DataTrigger>
    </Style.Triggers>
</Style>
Références de couleurs système WPF et le déclencheur de thème de contrasteRéférencer les clés de ressource SystemColors via DynamicResource suit les changements de pinceau automatiquement, et un déclencheur lié à IsHighContrast sur un proxy qui s'abonne à StaticPropertyChanged réagit à un basculement à l'exécution et bascule vers des couleurs qui gardent la paire. Une référence directe à WindowBrush ne se met pas à jourRéférencer WindowBrushKey via DynamicResourceSuit les changements de pinceau automatiquementStaticPropertyChangedIsHighContrast sur le proxyDataTrigger réagitBasculer vers des couleurs qui gardent la paireRéférencer WindowBrush directementNe se met pas à jour

Figure 20 : WPF suit un basculement à l’exécution via des références dynamiques aux clés de ressource et une liaison à un proxy qui relaie les changements de la propriété statique.

Si vous utilisez le thème Fluent, gardez à l’esprit que .NET 10 incluait des corrections de plantage liées à HighContrast,9 et faites de la vérification sous un thème de contraste une condition d’adoption.

8.5. Implémentation dans WinUI 3

Dans WinUI 3, les contrôles standard suivent les thèmes clairs, sombres et de contraste dès le départ, et les couleurs propres de l’application se définissent dans ResourceDictionary.ThemeDictionaries sous les clés Default (sombre), Light et HighContrast. Sous HighContrast, ne codez pas les couleurs en dur ; référencez des couleurs système dynamiques telles que SystemColorWindowColor via ThemeResource. Un contrôle personnalisé qui a Light/Dark doit toujours avoir aussi HighContrast, et HighContrast est la clé de repli utilisée lorsqu’aucun autre thème de contraste nommé n’est trouvé.331

<ResourceDictionary.ThemeDictionaries>
    <ResourceDictionary x:Key="Default">
        <SolidColorBrush x:Key="App.CardBackgroundBrush" Color="#2B2B2B"/>
    </ResourceDictionary>
    <ResourceDictionary x:Key="Light">
        <SolidColorBrush x:Key="App.CardBackgroundBrush" Color="#F3F3F3"/>
    </ResourceDictionary>
    <ResourceDictionary x:Key="HighContrast">
        <SolidColorBrush x:Key="App.CardBackgroundBrush"
                         Color="{ThemeResource SystemColorWindowColor}"/>
    </ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>

Encore une chose : WinUI a un mécanisme appelé HighContrastAdjustment, activé par défaut. Il force un texte blanc et un arrière-plan de surbrillance noir pour préserver le contraste, et le guide recommande qu’une fois que vous avez préparé des dictionnaires de thème qui utilisent correctement les couleurs système, vous le mettiez à None pour que vos propres styles s’appliquent.1

Comment WinUI résout ThemeDictionariesSelon le thème actuel, l'un des dictionnaires Default (sombre), Light et HighContrast est choisi, et sous HighContrast des couleurs système dynamiques sont référencées via ThemeResource. HighContrast est la clé de repli lorsqu'il n'y a pas de thème de contraste nomméSombreClairThème de contrasteThème actuel ?Dictionnaire DefaultDictionnaire LightDictionnaire HighContrastRéférencer les ressources SystemColor via ThemeResourceRepli lorsqu'il n'y a pas de thème nommé

Figure 21 : Dans WinUI le dictionnaire de chaque thème est choisi automatiquement, et le dictionnaire HighContrast référence les couleurs système.

9. La prise en charge du mode sombre n’est pas un substitut à la prise en charge de l’accessibilité

Rapporter la prise en charge du mode sombre comme « nous avons fait le travail d’accessibilité » est une erreur. La relation entre les deux peut se poser ainsi.

  • La norme de ratio de contraste s’applique à la palette sombre tout autant. Le critère de succès WCAG 1.4.3 exige 4,5:1 pour le texte et 3:1 pour le grand texte, et cela ne change pas lorsque l’arrière-plan est sombre.14 Un design sombre qui pose du texte gris moyen sur un arrière-plan gris foncé a le même problème que « gris clair sur blanc » en mode clair.
  • Éviter le noir pur et le blanc pur est le design de Windows 11. Les bonnes pratiques de Microsoft expliquent que Windows 11 s’est éloigné du blanc pur et du noir pur vers des tons plus faciles pour les yeux.34 Inversement, un arrière-plan #000000 en mode sombre conduit certaines personnes à se plaindre d’halation, un effet de blooming causé par un contraste excessif contre un texte clair.
  • L’accommodement de la diversité de la vision des couleurs est nécessaire quel que soit le thème. Le guide des couleurs de Microsoft demande d’utiliser la couleur comme renforcement visuel plutôt que comme moyen premier de communication, et de ne jamais faire de la combinaison du rouge et du vert la seule distinction.3515
  • Les thèmes de contraste sont une exigence indépendante du mode sombre. Comme le chapitre 2 l’a expliqué, le mode sombre n’est pas disponible tant qu’un thème de contraste est actif, donc aussi parfaite que soit votre prise en charge du mode sombre, elle n’atteint jamais les utilisateurs de thèmes de contraste.
La relation entre le mode sombre et l'accessibilitéLa prise en charge du mode sombre est une affaire de préférence visuelle et d'environnement, tandis que les exigences d'accessibilité de ratio de contraste, de transmission de l'information par plus que la couleur seule, et de prise en charge des thèmes de contraste doivent être satisfaites séparément quel que soit le thèmePas un substitut àPrise en charge du mode sombrePréférence et environnementAccessibilitéRatio de contraste 4,5:1Pas la couleur seulePrise en charge des thèmes de contraste

Figure 22 : La prise en charge du mode sombre adresse la préférence et l’environnement ; les exigences d’accessibilité doivent être satisfaites séparément.

D’un autre côté, le travail de « rassembler les couleurs en un seul endroit » du chapitre 5 est le fondement des deux. Avec la palette en un seul endroit, vous pouvez énumérer ce qu’il faut mesurer pour le ratio de contraste en clair et en sombre, et la branche de thème de contraste peut s’écrire au même endroit. Servez-vous de la prise en charge du mode sombre comme occasion de consolider la palette, et inspectez les ratios de contraste et les thèmes de contraste en même temps. C’est l’ordre avec le meilleur retour sur l’investissement.

10. Choisir une politique — recommandations par type d’application

Type d’application Politique recommandée
Nouveau WinForms (.NET 10) Utiliser SetColorMode(System). Fonder le dessin par le propriétaire sur SystemColors, et faire opter les contrôles personnalisés qui contiennent des contrôles communs avec ApplyThemingImplicitly
Nouveau WPF (.NET 9/10) Adopter ThemeMode="System" après avoir vérifié comment les contrôles que vous utilisez s’affichent et comment l’application se comporte sous les thèmes de contraste. Si c’est difficile, thème classique plus échange de dictionnaires
WinForms/WPF existant (.NET Framework 4.8, .NET 8 ou antérieur) Déclarer « clair fixe » et laisser l’attribut DWM à son défaut (FALSE). La prise en charge des thèmes de contraste est obligatoire ; passer à la prise en charge du mode sombre lors de la migration vers .NET 10
WinUI 3 Suit le système par défaut. Définir les couleurs propres de l’application dans ThemeDictionaries y compris HighContrast, et mettre HighContrastAdjustment à None
Win32 / MFC Attribut DWM + votre propre palette + recalcul sur WM_THEMECHANGED / WM_SYSCOLORCHANGE. Le guide officiel ne couvre que la détection et la barre de titre ; repeindre les contrôles communs est hors de sa portée
Le chemin des actifs existants vers la prise en charge du mode sombreLes actifs existants déclarent d'abord le clair fixe, achèvent sans faute la prise en charge des thèmes de contraste, consolident la palette comme préparation, puis migrent vers .NET 10 et passent à SetColorMode ou ThemeMode. Un mode sombre à moitié fait est une pire expérience que le clair fixe, donc ce chemin n'est pas prisNon prisDéclarer le clair fixe (maintenant)Prise en charge des thèmes de contraste (obligatoire)Consolider la palette (préparation)Migrer vers .NET 10Passer à SetColorMode / ThemeModeMode sombre à moitié faitPire expérience que le clair fixe

Figure 23 : Les actifs existants partent du clair fixe, passent par la prise en charge des thèmes de contraste et la consolidation de la palette, et vont vers la prise en charge du mode sombre lors de la migration vers .NET 10.

Le « clair fixe » n’est pas une défaite. C’est le comportement par défaut de Windows lui-même, et un comportement documenté. Une application qui est constamment claire est bien meilleure pour les utilisateurs qu’un mode sombre à moitié fait livré avec « seulement la barre de titre noire » ou « seulement les barres de défilement blanches ». Cependant, la prise en charge des thèmes de contraste est la seule chose qui ne peut pas être « fixée ». Elle relève de l’aménagement raisonnable traité dans l’article précédent : c’est une question de savoir si l’application est utilisable, pas une question de préférence de thème.

11. Liste de vérification

Une fois le travail fait, vérifiez sur une machine réelle dans l’ordre suivant. Chaque étape peut se basculer depuis l’application Paramètres en quelques secondes.

  1. Basculer clair/sombre pendant que l’application s’exécute. Changez le mode sous Paramètres > Personnalisation > Couleurs, et confirmez que la barre de titre et la zone cliente suivent, ou que l’application se comporte comme spécifié avec « prend effet au lancement suivant » (SetColorMode dans WinForms ne suit pas).
  2. Déclencher une recréation de handle. Dans WinForms, basculez ShowInTaskbar à l’exécution et confirmez que l’attribut de barre de titre est conservé.
  3. Essayer les quatre thèmes de contraste. Basculez avec Alt gauche + Maj gauche + Impr. écran, et confirmez dans chacun d’Aquatic, Desert, Dusk et Night sky que le texte, les bordures, les lignes sélectionnées, les éléments désactivés et les liens sont lisibles.1
  4. Éditer les couleurs d’un thème de contraste. Les utilisateurs changent vraiment les couleurs. Éditez l’arrière-plan vers une couleur extrême pour faire sortir les couleurs encore codées en dur.
  5. Vérifier les journaux. Confirmez qu’un échec de DwmSetWindowAttribute ou SystemParametersInfo sur un OS pris en charge est enregistré, et que sous Windows 10 en dessous de la build 22000 l’application démarre claire sans appeler l’attribut DWM.
  6. Mesurer les ratios de contraste. En clair et en sombre, vérifiez chaque combinaison de couleur de texte et d’arrière-plan dans la palette contre 4,5:1.14
Étapes de vérification de la prise en charge des thèmesVérifier sur une machine réelle dans cet ordre : basculer clair/sombre à l'exécution, recréation de handle, les quatre thèmes de contraste, éditer les couleurs du thème, vérifier les journaux d'échec, et mesurer les ratios de contrasteBasculer clair/sombre à l'exécutionRecréation de handleLes quatre thèmes de contrasteÉditer les couleurs du thèmeVérifier les journaux d'échecMesurer les ratios de contraste

Figure 24 : La vérification commence par le basculement des paramètres et se clôt par la vérification des journaux et des ratios de contraste.

12. Synthèse

  • Les thèmes Windows ont deux axes, clair/sombre et thèmes de contraste, et le mode sombre n’est pas disponible tant que le second est actif. Détectez d’abord le thème de contraste.
  • La barre de titre d’une application existante est blanche parce que c’est le défaut de compatibilité ; passez TRUE pour DWMWA_USE_IMMERSIVE_DARK_MODE (valeur 20, Windows 11 build 22000 ou une version ultérieure) via DwmSetWindowAttribute, et elle est dessinée sombre lorsque le système est sombre. Définissez-le chaque fois que le HWND est créé, et consignez les échecs.
  • Décidez le mode actuel d’après la luminosité de la couleur de premier plan de UISettings.GetColorValue, remarquez les changements avec ColorValuesChanged, remontez vers le thread UI, et repeignez. Gardez les couleurs rassemblées en un seul endroit.
  • WinForms : Application.SetColorMode(SystemColorMode.System) sous .NET 9/10. Connaissez les trois contraintes (Windows 11 uniquement, désactivé pendant un thème de contraste, pas de suivi des changements à l’exécution) et ApplyThemingImplicitly pour les contrôles personnalisés.
  • WPF : ThemeMode="System" sous .NET 9/10. La manipulation depuis le code est expérimentale et Fluent est en cours, donc évaluez avant d’adopter. Sur le thème classique, échange de dictionnaires plus DynamicResource.
  • Sous un thème de contraste, détectez avec la famille SPI_GETHIGHCONTRAST, mappez les couleurs vers les paires de couleurs système, omettez les images derrière le texte, et dessinez les graphismes multicolores en deux couleurs. GrayText est pour l’état désactivé, Hotlight pour les liens uniquement.
  • La prise en charge du mode sombre n’est pas un substitut à la prise en charge de l’accessibilité. 4,5:1 s’applique encore en mode sombre, l’information ne doit pas reposer sur la couleur seule, et la prise en charge des thèmes de contraste est requise séparément.
  • Pour les actifs existants, déclarer « clair fixe » est la réponse réaliste, et la prise en charge des thèmes de contraste est la seule chose qui ne peut pas être fixée.

Comme premier pas recommandé, prenez un écran principal, activez d’abord un thème de contraste avec Alt gauche + Maj gauche + Impr. écran et regardez, revenez avec les mêmes touches, puis basculez le paramètre de couleur Windows vers sombre (le mode sombre n’est pas disponible tant qu’un thème de contraste est actif, donc essayez les deux séparément). En quelques minutes vous verrez « où votre application tient ses couleurs ».

Articles associés

Domaines de conseil associés

KomuraSoft LLC prend en charge le mode sombre des applications métier WinForms/WPF (consolider la palette, évaluer une migration vers SetColorMode / ThemeMode sous .NET 9/10, intégrer l’attribut DWM), le diagnostic et la correction des cassures d’affichage sous les thèmes de contraste, et les consultations sur le suivi de thème dans les actifs Win32/MFC. Commencer dès le stade de « les salariés se sont plaints une fois passés en mode sombre » convient.

Références

  1. Microsoft Learn, Contrast themes. Sur le fait que les thèmes de contraste utilisent une palette contrainte d’environ 7:1 ou plus et ne doivent pas être confondus avec les thèmes clairs et sombres ; les quatre thèmes Aquatic, Desert, Dusk et Night sky et l’édition de leurs couleurs ; le basculement avec Alt gauche + Maj gauche + Impr. écran ; les paires premier plan/arrière-plan et les usages des ressources SystemColor ; l’utilisation de GrayText pour l’état désactivé uniquement et de Hotlight pour les liens uniquement ; les cassures dues aux couleurs codées en dur ; les bordures de frontière ; HighContrast dans ThemeDictionaries ; le réglage de HighContrastAdjustment à None ; et la détection avec Microsoft.UI.System.ThemeSettings.  2 3 4 5 6 7 8

  2. Microsoft Learn, Application.SetColorMode(SystemColorMode) Method. Sur l’appel avant que les éléments d’interface soient créés, sur le fait que l’application ne s’adapte pas automatiquement lorsque le paramètre système change même avec System, et sur le fait que le mode de couleur sombre n’est disponible que sous Windows 11 ou une version ultérieure et n’est pas disponible en mode contraste élevé.  2 3 4

  3. Microsoft Learn, Support Dark and Light themes in Win32 apps. Sur la définition du premier plan et de l’arrière-plan dans les modes de couleur ; sur le fait que Windows donne une barre de titre claire par défaut pour la compatibilité parce qu’il ne peut pas savoir si une application prend en charge le mode sombre ; sur la procédure d’obtenir la couleur de premier plan avec UISettings.GetColorValue et de classer clair ou sombre par luminance perçue pour détecter le mode sombre ; sur le suivi avec ColorValuesChanged ; sur l’activation de la barre de titre sombre avec DwmSetWindowAttribute et DWMWA_USE_IMMERSIVE_DARK_MODE (valeur 20) ; et sur le fait que toute la surface doit suivre le mode sombre.  2 3 4 5 6 7 8 9 10

  4. Microsoft Learn, DWMWINDOWATTRIBUTE enumeration (dwmapi.h). Sur le fait que DWMWA_USE_IMMERSIVE_DARK_MODE permet de dessiner le cadre en sombre lorsque le paramètre sombre du système est activé et que toutes les fenêtres sont claires par défaut ; sur les valeurs COLORREF de DWMWA_BORDER_COLOR, DWMWA_CAPTION_COLOR et DWMWA_TEXT_COLOR et le rétablissement du défaut avec DWMWA_COLOR_DEFAULT ; sur la prise en charge à partir de Windows 11 build 22000 ; et sur la prise en charge de DWMWA_SYSTEMBACKDROP_TYPE à partir de la build 22621.  2 3 4

  5. Microsoft Learn, UISettings.ColorValuesChanged Event. Sur l’événement déclenché lorsqu’une valeur de couleur change.  2

  6. Microsoft Learn, What’s new in Windows Forms for .NET 9. Sur la prise en charge préliminaire expérimentale du mode sombre, le changement correspondant de SystemColors lorsque le mode de couleur change, les trois valeurs SystemColorMode Classic, System et Dark, l’appel à Application.SetColorMode dans le code de démarrage, et le masquage de WFO5001.  2 3

  7. Microsoft Learn, What’s new in Windows Forms for .NET 10. Sur l’intégration complète du mode sombre et le fait que SetColorMode n’est plus expérimental, sur le fait que les contrôles communs Win32 à l’intérieur des contrôles dessinés par le propriétaire restent clairs s’ils n’optent pas, et sur la nécessité d’appeler SetStyle(ControlStyles.ApplyThemingImplicitly) avant base.CreateParams à l’intérieur du remplacement de CreateParams parce que le constructeur arrive trop tard.  2 3 4

  8. Microsoft Learn, What’s new in WPF for .NET 9. Sur le thème Fluent prenant en charge le clair/sombre et la couleur d’accent, les quatre valeurs ThemeMode Light, Dark, System et None et le définir sur Application ou Window, l’appliquer via des dictionnaires de ressources, et le fait que définir ThemeMode depuis le code est expérimental et exige le masquage de WPF0001.  2 3 4

  9. Microsoft Learn, What’s new in WPF for .NET 10. Sur le fait que la prise en charge des styles d’interface Fluent est encore en cours, les styles Fluent ajoutés pour DatePicker, GridSplitter, GridView, GroupBox, Hyperlink, Label, NavigationWindow, RichTextBox et TextBox, et les corrections de plantage liées à HighContrast.  2 3

  10. Microsoft Learn, Application.ThemeMode Property. Sur le fait qu’il contrôle si le thème Fluent est chargé en mode clair, sombre ou système et contrôle aussi l’application du matériau d’arrière-plan et du mode sombre à la fenêtre, que ThemeMode et Resources sont conçus pour rester en synchronisation afin d’éviter l’incohérence, et qu’il porte l’attribut Experimental(“WPF0001”) et peut être retiré à l’avenir.  2 3 4

  11. Microsoft Learn, High contrast parameter. Sur l’obtention de la structure HIGHCONTRAST avec SPI_GETHIGHCONTRAST à l’initialisation et lors du traitement de WM_SYSCOLORCHANGE et la vérification de HCF_HIGHCONTRASTON, et, lorsqu’il est activé, le mapping de chaque couleur vers une paire de COLOR_WINDOWTEXT et COLOR_WINDOW ou COLOR_BTNTEXT et COLOR_BTNFACE, l’omission des images bitmap derrière le texte, et le dessin des images multicolores dans les couleurs de premier plan et d’arrière-plan.  2 3

  12. Microsoft Learn, High-contrast mode. Sur le fait qu’Aero a un texte noir et une couleur de sélection bleu clair mais que High Contrast Black a une couleur de sélection noire, ce qui peut produire du texte noir sur noir ; que COLOR_HIGHLIGHTTEXT est conçu pour être apparié avec COLOR_HIGHLIGHT et COLOR_WINDOWTEXT avec COLOR_WINDOW ; de ne pas coder en dur les couleurs de texte ; de construire une interface qui ne dépend pas du thème parce que les utilisateurs personnalisent les couleurs ; de recalculer les couleurs sur WM_THEMECHANGED ; et que SPI_GETHIGHCONTRAST est le seul moyen pris en charge de vérifier.  2 3

  13. Microsoft Learn, Walkthrough: Creating an Accessible Windows-based Application. Sur la détection avec SystemInformation.HighContrast ; l’utilisation du schéma de couleurs système lorsqu’il est activé, l’ajout d’indices visuels à l’information transmise par la couleur, et l’omission des images derrière le texte ; la vérification au démarrage et le suivi de l’événement UserPreferenceChanged ; et l’exemple de basculement des couleurs d’une étiquette avec SystemColors.  2 3

  14. W3C / traduction japonaise par le Web Accessibility Infrastructure Committee (WAIC), Web Content Accessibility Guidelines (WCAG) 2.1, Japanese translation. Sur le critère de succès 1.4.3 (Contrast (Minimum)) avec 4,5:1 pour le texte et 3:1 pour le grand texte, et le critère de succès 1.4.1 (Use of Color).  2 3

  15. Microsoft Learn, Color in Windows. Sur le fait que Windows a deux modes de couleur, clair et sombre, que le choix de la couleur d’accent et du thème se reflète dans toute l’expérience de l’utilisateur, et sur l’assurance du contraste et l’accommodement de la diversité de la vision des couleurs.  2

  16. Microsoft Learn, Reference for Windows 11 and Windows 10 settings. Sur le fait que AppsUseLightTheme et SystemUsesLightTheme sous HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize sont des valeurs DWORD représentant le mode clair/sombre des applications et de Windows. 

  17. Microsoft Learn, Theming in Windows apps. Sur le fait que retirer RequestedTheme fait suivre à l’application le paramètre système, que le système remplace RequestedTheme lorsque l’utilisateur choisit un thème de contraste élevé, et que les modèles personnalisés devraient utiliser des pinceaux de thème plutôt que des couleurs codées en dur. 

  18. Microsoft Learn, DwmSetWindowAttribute function (dwmapi.h). Sur la fonction qui définit les attributs de rendu DWM de la zone non cliente d’une fenêtre, et sa disponibilité à partir de Windows Vista. 

  19. Microsoft Learn, Retrieve a window handle (HWND). Sur l’obtention du Handle depuis WindowInteropHelper dans WPF. 

  20. Microsoft Learn, DWM_SYSTEMBACKDROP_TYPE enumeration (dwmapi.h). Sur le fait que DWMSBT_MAINWINDOW correspond à Mica et DWMSBT_TRANSIENTWINDOW à Acrylic sous Windows 11, que l’effet du matériau peut changer dans les versions futures de Windows, et sur la prise en charge à partir de Windows 11 build 22621. 

  21. Microsoft Learn, UISettings.GetColorValue(UIColorType) Method. Sur la méthode qui renvoie la valeur de couleur du UIColorType spécifié. 

  22. Microsoft Learn, WM_SETTINGCHANGE message. Sur le message envoyé à toutes les fenêtres de premier niveau lorsque SystemParametersInfo change un paramètre à l’échelle du système ou lorsqu’un paramètre de stratégie change. 

  23. Microsoft Learn, SystemEvents.UserPreferenceChanged Event. Sur l’événement statique déclenché lorsqu’une préférence utilisateur change, et la fuite de mémoire qui résulte de ne pas détacher le gestionnaire. 

  24. Microsoft Learn, WM_THEMECHANGED message. Sur le fait qu’il est diffusé à toutes les fenêtres après qu’un thème est activé, désactivé ou basculé, et que les handles de thème existants deviennent invalides et doivent être rouverts. 

  25. Microsoft Learn, WM_SYSCOLORCHANGE message. Sur le fait qu’il est envoyé à toutes les fenêtres de premier niveau lorsqu’un paramètre de couleur système change, que les pinceaux qui utilisent des couleurs système doivent être recréés, et qu’il doit être transmis aux contrôles communs. 

  26. Microsoft Learn, Compiler Error WFO5001. Sur le fait que SetColorMode et SystemColorMode étaient protégés comme fonctionnalités expérimentales à évaluer dans .NET 9, et que l’erreur ne s’applique pas à partir de .NET 10. 

  27. Microsoft Learn, SystemColors.UseAlternativeColorSet Property. Sur le fait que la mettre à true fait que les valeurs KnownColor système renvoient un jeu de couleurs alternatif (actuellement la version du mode sombre), que c’est une fonctionnalité expérimentale sous SYSLIB5002, et que les valeurs KnownColor système renvoient toujours les couleurs Windows actuelles lorsqu’un thème de contraste élevé est actif dans Windows. 

  28. Microsoft Learn, HIGHCONTRASTW structure (winuser.h). Sur HCF_HIGHCONTRASTON (0x00000001) dans dwFlags, et la nécessité de spécifier cbSize lors de l’utilisation avec SPI_GETHIGHCONTRAST. 

  29. Microsoft Learn, SystemParameters.HighContrast Property. Sur la propriété statique WPF qui correspond à SPI_GETHIGHCONTRAST et HCF_HIGHCONTRASTON. 

  30. Microsoft Learn, SystemParameters.StaticPropertyChanged Event. Sur l’événement statique déclenché lorsqu’une propriété quelconque de SystemParameters change.  2

  31. Microsoft Learn, ThemeSettings Class (Microsoft.UI.System). Sur le créer lié à une fenêtre avec CreateForWindowId et recevoir les changements de contraste élevé via l’événement Changed, et sur le fait que relâcher la référence détruit l’objet et que l’événement ne se déclenche plus. 

  32. Microsoft Learn, SystemColors.WindowBrushKey Property. Sur le fait qu’une référence dynamique faite avec la clé de ressource se met à jour automatiquement lorsque le pinceau change, et qu’une référence statique via WindowBrush ne le fait pas. 

  33. Microsoft Learn, ResourceDictionary.ThemeDictionaries Property (Microsoft.UI.Xaml). Sur le fait qu’un contrôle personnalisé avec des dictionnaires de thème Light et Dark devrait aussi fournir un dictionnaire HighContrast, que HighContrast est la clé de repli lorsqu’il n’existe pas d’autre thème de contraste élevé, que Default est utilisé lorsqu’aucun ResourceDictionary pour le thème n’est trouvé, et que des ressources de couleur système telles que SystemColorButtonFaceColor peuvent être utilisées dans HighContrast. 

  34. Microsoft Learn, Windows app development best practices. Sur le fait que Windows 11 s’est éloigné du blanc pur et du noir pur vers des tons plus faciles pour les yeux, et que les thèmes sombres et clairs sont un moyen de s’adapter à la préférence visuelle de l’utilisateur. 

  35. Microsoft Learn, Color (Windows UX guidelines). Sur l’utilisation de la couleur comme renforcement visuel plutôt que comme moyen premier de communication, le choix des couleurs de thème et des couleurs système par usage et l’utilisation du premier plan et de l’arrière-plan en paires assorties, le traitement des changements de thème avec WM_THEMECHANGED, et le fait que High Contrast Black correspond à Aquatic et High Contrast White à Desert sous Windows 11. 

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

J'ai basculé Windows en mode sombre, mais la barre de titre de notre application WinForms interne reste blanche. Pourquoi ?
Parce que Windows n'a aucun moyen de savoir si une application prend en charge le mode sombre, et que, pour la compatibilité, il traite donc chaque fenêtre comme étant en mode clair par défaut. La zone non cliente, barre de titre comprise, est dessinée par le Gestionnaire de fenêtres du Bureau (DWM), et le cadre n'est dessiné en sombre lorsque le système est sombre qu'après que l'application a passé TRUE pour DWMWA_USE_IMMERSIVE_DARK_MODE (valeur 20) via DwmSetWindowAttribute. La prise en charge de cet attribut est documentée pour Windows 11 build 22000 et versions ultérieures. Si vous utilisez Application.SetColorMode dans WinForms sous .NET 9 ou une version ultérieure, ou ThemeMode dans WPF sous .NET 9 ou une version ultérieure, le framework effectue cet appel pour vous, donc l'appeler vous-même n'est nécessaire que dans les applications sous .NET 8 ou antérieur, .NET Framework, ou Win32/MFC. Notez qu'une barre de titre sombre au-dessus d'une zone cliente qui reste blanche est pire, pas mieux. N'activez cet attribut que lorsque vous êtes prêt à repeindre toute l'application en sombre.
Le mode sombre et les thèmes de contraste (contraste élevé) sont-ils la même chose ?
Non. Clair/sombre est le mode de couleur sous Paramètres > Personnalisation > Couleurs, et il utilise une palette large qui inverse la luminosité du premier plan et de l'arrière-plan. Un thème de contraste se choisit sous Paramètres > Accessibilité > Thèmes de contraste et utilise une palette contrainte avec un ratio de contraste d'environ 7:1 ou plus (les quatre thèmes fournis Aquatic, Desert, Dusk et Night sky, plus toutes les couleurs que l'utilisateur a éditées). La documentation Microsoft dit explicitement de ne pas confondre les deux, et le mode sombre n'est pas disponible tant qu'un thème de contraste est actif (SetColorMode dans WinForms ne fournit pas le mode sombre pendant un thème de contraste, et RequestedTheme en XAML est remplacé par le système). Dans l'implémentation, vérifiez d'abord si un thème de contraste est actif et, le cas échéant, remettez-vous entièrement aux couleurs système ; seulement sinon, choisissez la palette claire ou sombre. C'est l'ordre de priorité.
Quel est le chemin le plus court pour faire prendre en charge le mode sombre à une application WinForms ?
Sous .NET 9 ou une version ultérieure, le chemin le plus court est d'appeler Application.SetColorMode(SystemColorMode.System) avant Application.Run dans Program.cs. Sous .NET 9 c'était une fonctionnalité expérimentale, donc WFO5001 devait être masqué dans le fichier projet ; à partir de .NET 10, cela fonctionne sans masquage. L'appel à SetColorMode bascule SystemColors vers un jeu alternatif pour le mode sombre, et les contrôles standard sont dessinés en conséquence. Il y a trois précautions. Premièrement, le mode sombre n'est disponible que sous Windows 11 ou une version ultérieure et est désactivé tant qu'un thème de contraste est actif. Deuxièmement, même avec SystemColorMode.System, l'application ne suit pas un changement du paramètre Windows pendant qu'elle s'exécute (le changement prend effet au lancement suivant). Troisièmement, si un contrôle dessiné par le propriétaire utilise des contrôles communs Win32 tels que les barres de défilement, vous devez remplacer CreateParams et appeler SetStyle(ControlStyles.ApplyThemingImplicitly, true) avant base.CreateParams (le constructeur arrive trop tard).
De quoi une application WPF a-t-elle besoin pour suivre le mode sombre ?
WPF sous .NET 9 ou une version ultérieure livre un nouveau thème qui suit le design Fluent de Windows 11, et écrire ThemeMode="System" sur l'élément Application dans App.xaml suffit à charger le thème Fluent qui correspond au paramètre clair/sombre de Windows. ThemeMode contrôle aussi l'assombrissement de la fenêtre (la barre de titre) et l'application du matériau d'arrière-plan. Cependant, la lecture et l'écriture de la propriété ThemeMode depuis le code restent expérimentales dans .NET 10 (WPF0001), et les styles Fluent eux-mêmes sont décrits comme "still in progress" dans la documentation de .NET 10. Avant de l'adopter dans une application métier, évaluez si les contrôles que vous utilisez s'affichent correctement sous Fluent. Si vous restez sur le thème classique (il en va de même pour .NET 8 et antérieur et pour .NET Framework), préparez un ResourceDictionary clair et un sombre, échangez-les dans MergedDictionaries, référencez-les depuis XAML avec DynamicResource, et utilisez UISettings.ColorValuesChanged pour détecter le basculement. Pour la barre de titre, obtenez le HWND depuis WindowInteropHelper dans SourceInitialized et appelez DwmSetWindowAttribute.
Pourquoi le texte disparaît-il ou devient-il illisible sous un thème de contraste (contraste élevé) ?
Les causes typiques sont des couleurs codées en dur, ou la rupture de l'appariement des couleurs système de premier plan et d'arrière-plan. Sous un thème de contraste, l'utilisateur peut éditer librement les couleurs d'arrière-plan, de texte, de lien et d'autres, donc toute hypothèse du type "le texte sera noir" ou "la ligne sélectionnée sera bleu clair" s'écroule. Par exemple, si seul l'arrière-plan est fixé à #E6E6E6, certains thèmes donnent un premier plan blanc, et du texte blanc sur gris clair devient illisible. Il y a trois principes. Détectez l'état avec SPI_GETHIGHCONTRAST (SystemInformation.HighContrast dans WinForms, SystemParameters.HighContrast dans WPF) ; remplacez chaque couleur par la paire de couleurs système correcte (WindowText avec Window, ButtonText avec ButtonFace, HighlightText avec Highlight) ; et abandonnez les images derrière le texte et les graphismes multicolores, en dessinant uniquement avec les couleurs de premier plan et d'arrière-plan. GrayText ne doit servir à rien d'autre que l'état désactivé, et Hotlight à rien d'autre que les hyperliens. Les changements sont annoncés par WM_SYSCOLORCHANGE et WM_THEMECHANGED (SystemEvents.UserPreferenceChanged dans .NET), donc recalculez les couleurs à cet endroit et repeignez.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog