Modo oscuro y temas de contraste en aplicaciones Windows — barras de título DWM oscuras, seguimiento del tema del sistema en WinForms/WPF y dibujo en alto contraste

· Actualizado el: · · Modo oscuro, Temas de contraste, Alto contraste, DWM, WinForms, WPF, Windows 11, Accesibilidad, Aplicaciones empresariales, Windows

Historial de revisiones (primera versión, publicada el 2 Sep 2026)
Primera publicación

«Sustituimos los PC de la oficina por Windows 11, y los empleados que usan el modo oscuro se quejan de que solo en nuestra aplicación empresarial la barra de título es de un blanco cegador.» «Un empleado con baja visión activó un tema de contraste y la indicación de estado de la pantalla de pedidos desapareció.» Ambas son quejas que hemos oído más a menudo en el último año o dos.

La primera viene de la configuración de Colores de Windows 11, la segunda de la configuración de Accesibilidad, pero para un desarrollador parecen el mismo problema: la aplicación no sigue el tema. Y de hecho el fundamento de la corrección es común. No codificar los colores de forma fija; leer la configuración del sistema, notar los cambios y volver a pintar. Esos tres puntos.

El artículo anterior, «Introducción a la accesibilidad en aplicaciones Windows», cubría cómo leen una aplicación los lectores de pantalla (UI Automation) y los fundamentos de nomenclatura, teclado y color. Tocaba el seguimiento de los temas de contraste, pero no el modo de color claro/oscuro en sí. Este artículo es su compañero. Conecta los dos ejes del modo de color (claro/oscuro) y de los temas de contraste a nivel de implementación, en este orden: dibujo de la barra de título por DWM (Administrador de ventanas de escritorio), seguimiento del tema del sistema en WinForms/WPF y dibujo bajo un tema de contraste. Los lectores previstos son desarrolladores que construyen y mantienen aplicaciones empresariales en WinForms, WPF o Win32. Los requisitos previos son Windows 11 (las barras de título oscuras requieren la compilación 22000 o posterior) y WinForms/WPF en .NET 9/10 (en .NET Framework 4.8 y .NET 8, algunas partes deben implementarse a mano). La dificultad es intermedia.

El flujo de este artículoLa estructura de este artículo, que conecta en orden los dos ejes de tema, por qué las ventanas son claras de forma predeterminada, las barras de título DWM oscuras, el mecanismo de detección y seguimiento, las implementaciones de WinForms y WPF, el dibujo bajo un tema de contraste, la decisión de una política y la verificaciónOrdenar los dos ejes de temaPor qué el valor predeterminado es claroBarra de título DWM oscuraDetección y seguimientoImplementación WinForms/WPFDibujo bajo un tema de contrasteDecidir una política y verificar

Figura 1: Este artículo recorre en una línea desde ordenar los temas hasta los mecanismos, la implementación, los temas de contraste y la verificación.

1. Primero la conclusión

  • Windows tiene dos ejes de tema. Claro/oscuro (el modo de color) en Configuración > Personalización > Colores, y Configuración > Accesibilidad > Temas de contraste. El segundo es una paleta restringida a una relación de contraste de aproximadamente 7:1 o superior, y es otra cosa que claro/oscuro. El modo oscuro no está disponible mientras un tema de contraste está activo. El orden de precedencia de la detección es «tema de contraste, luego claro/oscuro».12
  • La barra de título de una aplicación existente permanece blanca porque ese es el valor predeterminado de compatibilidad. Windows no tiene forma de saber si una aplicación es compatible con el modo oscuro, así que trata cada ventana como clara de forma predeterminada.3
  • Lo que oscurece la barra de título es DWMWA_USE_IMMERSIVE_DARK_MODE (valor 20) mediante DwmSetWindowAttribute. Pase un BOOL TRUE y el marco se dibuja oscuro cuando el sistema está oscuro. La compatibilidad documentada es Windows 11, compilación 22000 o posterior.43
  • Lea el modo actual con UISettings.GetColorValue y reciba los cambios mediante ColorValuesChanged. El procedimiento oficial de Microsoft es: si el primer plano (el color de texto predeterminado) es claro, el modo es oscuro. El evento no está garantizado en el hilo de UI, así que vuelva a la UI antes de volver a pintar.35
  • WinForms obtuvo Application.SetColorMode en .NET 9, y dejó de ser experimental en .NET 10. Llámalo con SystemColorMode.System antes de Application.Run. Tiene tres restricciones: solo Windows 11, deshabilitado mientras un tema de contraste está activo, y sin seguimiento de cambios de configuración mientras la aplicación se ejecuta.672
  • WPF obtuvo el tema Fluent y ThemeMode en .NET 9. ThemeMode="System" sigue al sistema y también controla el oscurecimiento de la ventana. Sin embargo, manipularlo desde código sigue siendo experimental en .NET 10 (WPF0001), y los estilos Fluent están «in progress». Si se queda en el tema clásico, intercambie ResourceDictionary claros y oscuros referenciados mediante DynamicResource.8910
  • Bajo un tema de contraste, asigne los colores a los pares de colores del sistema correctos, omita las imágenes detrás del texto y dibuje los gráficos de varios colores en los dos colores de primer plano y de fondo. Detecte con SPI_GETHIGHCONTRAST (SystemInformation.HighContrast en WinForms, SystemParameters.HighContrast en WPF); las notificaciones son WM_SYSCOLORCHANGE / WM_THEMECHANGED (SystemEvents.UserPreferenceChanged en .NET).111213
  • La compatibilidad con el modo oscuro no sustituye a la compatibilidad con la accesibilidad. Una paleta oscura sigue necesitando una relación de contraste de 4.5:1, y transmitir información por más que el color solo se exige bajo cualquier tema.1415

En una frase: la compatibilidad con temas significa reunir los colores en un solo sitio, leer la configuración del sistema, notar los cambios y volver a pintar; pero bajo un tema de contraste, remitirse por completo a los pares de colores del sistema.

En el diagrama, una línea continua marca una relación que siempre se cumple y una línea discontinua una relación condicional (las condiciones están en la explicación de cada relación en la página de detalle). La lista completa de relaciones (28 en total, con evidencia y grado de certeza) y las definiciones de los conceptos principales están reunidas en la página de detalle del mapa de conocimiento (en japonés). Datos: JSON-LD / Turtle

2. El «tema» tiene dos ejes — claro/oscuro y temas de contraste

2.1. Claro/oscuro (modo de color)

El modo de color en Configuración > Personalización > Colores de Windows es la configuración que decide el brillo del primer plano y del fondo en todo el sistema operativo y en todas las aplicaciones. La documentación de Microsoft define el claro como «un primer plano oscuro sobre un fondo claro» y el oscuro como «un primer plano claro sobre un fondo oscuro», y añade que primer plano aquí significa «el color de texto predeterminado». En el modo oscuro el primer plano (el texto) es claro y el fondo es oscuro.3

La configuración se almacena en el registro en HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize como los valores DWORD AppsUseLightTheme (el modo de la aplicación) y SystemUsesLightTheme (el modo de Windows en sí), y figura en la referencia de configuración de Microsoft.16 Como se describe más abajo, sin embargo, la forma canónica de leerla desde una aplicación es la clase WinRT UISettings.

2.2. Temas de contraste (alto contraste)

Un tema de contraste, elegido en Configuración > Accesibilidad > Temas de contraste, usa una paleta restringida a una relación de contraste de aproximadamente 7:1 o superior, y existe para usuarios que necesitan una separación visual fuerte del primer plano y del fondo. Windows 11 tiene cuatro integrados, Aquatic, Desert, Dusk y Night sky, y el usuario no solo puede elegir uno de ellos, sino también editar de forma individual los colores de fondo, texto, hipervínculo, texto deshabilitado, texto seleccionado y botón. Alt izquierdo + Mayús izquierdo + Impr Pant cambia un tema de contraste con rapidez, y se aplica Aquatic si no se ha seleccionado ninguno.1

La documentación de Microsoft dice con claridad: «no confunda los temas de contraste con los temas claros y oscuros». Claro/oscuro usa una paleta amplia y no está optimizado para el contraste máximo.1 Y lo importante es que el modo oscuro no está disponible mientras un tema de contraste está activo. Application.SetColorMode de WinForms no proporciona modo oscuro durante un tema de contraste, y RequestedTheme de XAML lo sobrescribe el sistema.217

2.3. El orden de precedencia

La implementación en una aplicación sigue por tanto este orden. Decida primero si hay un tema de contraste activo; si lo hay, remítase por completo a los colores del sistema. Si no, elija la paleta clara o la oscura.

Los dos ejes de tema y el orden de precedenciaEl orden de decisión en el que la aplicación se remite por completo a los pares de colores del sistema si un tema de contraste está activo, y en caso contrario lee el modo de color claro/oscuro y elige la paleta de la aplicaciónNoClaroOscuro¿Tema de contraste activo?Remitirse a los pares de colores del sistema¿Modo de color?Paleta claraPaleta oscuraEl modo oscuro no está disponible

Figura 2: Ponga primero la decisión del tema de contraste, y elija la paleta clara u oscura solo cuando no hay un tema de contraste activo.

3. Por qué las aplicaciones existentes se quedan blancas en el modo oscuro

Una ventana consta de dos áreas: el área no cliente, formada por la barra de título, el marco y los botones de leyenda, y el área de cliente, que dibuja la aplicación. Desde Windows Vista, el área no cliente la compone y dibuja DWM (Administrador de ventanas de escritorio), y la aplicación especifica atributos de cómo se dibuja mediante DwmSetWindowAttribute.18

La documentación de Microsoft explica con franqueza por qué las aplicaciones existentes se quedan blancas. «Windows no sabe si una aplicación puede ser compatible con el modo oscuro, así que asume que no puede por razones de compatibilidad con versiones anteriores.» Marcos como WinUI y Windows App SDK gestionan el modo oscuro de forma nativa, pero las aplicaciones Win32 normalmente no son compatibles con el modo oscuro, así que Windows les da una barra de título clara de forma predeterminada.3

Las dos áreas de una ventana y quién las dibujaEl área no cliente formada por la barra de título y el marco la dibuja DWM, y el área de cliente la dibuja la aplicación o el marco de UI, así que la compatibilidad con el modo oscuro hace falta en ambasVentana de nivel superiorÁrea no cliente (barra de título, marco)Área de cliente (el contenido de la ventana)Compuesta y dibujada por DWMDibujada por la aplicación o el marcoIndicada mediante DwmSetWindowAttributeLa paleta propia de la aplicación

Figura 3: DWM dibuja la barra de título y la aplicación dibuja el contenido, así que la compatibilidad con el modo oscuro necesita tanto una instrucción a DWM como la paleta de la aplicación.

Siguen dos consecuencias. Primera, para oscurecer la barra de título, la aplicación debe pedírselo explícitamente a DWM. Segunda, lo que se oscurece como resultado de pedirlo es solo la barra de título; el área de cliente debe volver a pintarla la aplicación misma. La documentación también dice que «para ser plenamente compatible con el modo oscuro, toda la superficie de la aplicación debe seguir el tema oscuro», y señala que la guía oficial cubre solo la detección y la barra de título, no cómo volver a pintar el área de cliente.3 Una aplicación con barra de título negra y contenido blanco parece menos natural que una que se queda blanca de cabo a rabo.

Cómo el valor predeterminado se vuelve claroWindows no sabe si una aplicación es compatible con el modo oscuro, así que por compatibilidad el valor predeterminado es claro, y solo cuando la aplicación pasa TRUE mediante el atributo DWM dibuja el marco según la configuración oscura del sistemaNoWindows no puede saber si la aplicación es compatibleEl valor predeterminado es claro, por compatibilidad¿La aplicación pasó TRUE?Siempre un marco claroDibujado según la configuración del sistema

Figura 4: Al no saber si la aplicación es compatible, Windows pone el valor predeterminado en claro y solo sigue al sistema cuando la aplicación lo dice de forma explícita.

4. La barra de título DWM oscura — DwmSetWindowAttribute

4.1. DWMWA_USE_IMMERSIVE_DARK_MODE

El atributo que oscurece la barra de título es DWMWA_USE_IMMERSIVE_DARK_MODE. La enumeración DWMWINDOWATTRIBUTE lo describe así: «Permite que el marco de esta ventana se dibuje en colores de modo oscuro cuando está activada la configuración de sistema de modo oscuro. Por razones de compatibilidad, todas las ventanas están en modo claro de forma predeterminada con independencia de la configuración del sistema. El parámetro pvAttribute apunta a un valor de tipo BOOL. TRUE para respetar el modo oscuro de la ventana, FALSE para usar siempre el modo claro. Este valor es compatible a partir de Windows 11 Build 22000.»4

En otras palabras, TRUE no significa «házlo oscuro»; es un permiso que dice «puedes dibujarlo oscuro si el sistema está oscuro». Si la aplicación está preparada para pintar su área de cliente en oscuro, pasar TRUE basta para que la barra de título siga la configuración del sistema. A la inversa, si la aplicación está diseñada para mostrarse siempre en modo claro (la política de «claro fijo» que se describe más adelante), dejar el valor predeterminado FALSE está bien.

El código C++ de la guía oficial tiene la forma siguiente. Incluye incluso el paso de definir usted mismo el valor 20 para SDK más antiguos cuyos encabezados no tienen 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 (compilación 22000) o posterior? Supone un manifiesto que declara supportedOS
// para Windows 10 o posterior (sin uno, la versión se redondea a 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 barra de título puede dibujarse oscura cuando el sistema está oscuro
void ApplyTitleBarTheme(HWND hwnd, bool honorDarkMode)
{
    if (!IsWindows11OrGreater())
    {
        // La compatibilidad documentada es la compilación 22000 o posterior. Por debajo, no llamarlo y seguir el valor predeterminado (claro)
        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 error en un SO compatible es anómalo. No tragarlo en silencio; registrar el HRESULT y hacerlo visible
        LogWarning(L"DwmSetWindowAttribute(DWMWA_USE_IMMERSIVE_DARK_MODE) failed: 0x%08X", hr);
    }
}

Una palabra sobre por qué la llamada está condicionada a la versión del SO. La compatibilidad documentada es Windows 11, compilación 22000 o posterior.4 Los informes de que el mismo valor funciona en Windows 10 no son raros, pero la visualización de una aplicación empresarial no debería depender de un comportamiento no documentado. Si el diseño es «probar la llamada y renunciar si falla», entonces cuando la llamada acierta por casualidad en Windows 10, la barra de título se oscurece encima de un comportamiento no documentado. Por debajo de la compilación 22000, no llamarlo y seguir el valor predeterminado documentado de una barra de título clara; en un SO compatible, registrar el HRESULT de un error y hacerlo visible. Eso es todo. En Internet también circula un procedimiento más antiguo que usa el valor 19, y una técnica que llama a exportaciones ordinales de uxtheme.dll para oscurecer los controles comunes, pero ambos son API no documentadas, y nadie las garantiza cuando una actualización cambia su comportamiento.

4.2. Cuándo llamarlo — mientras el HWND está vivo, y cada vez que se recrea

DwmSetWindowAttribute se llama sobre un HWND, así que debe ejecutarse después de que se haya creado el identificador de ventana. Y un formulario de WinForms puede tener su identificador recreado, por ejemplo cuando cambia ShowInTaskbar. El nuevo HWND tras la recreación no lleva ningún atributo, así que el lugar para llamarlo no es el constructor sino el lugar que se ejecuta cada vez que se crea un identificador: OnHandleCreated en WinForms, SourceInitialized en WPF.

Cuándo llamar a DwmSetWindowAttributeEstablecer el atributo DWM después de crear el identificador de ventana, establecerlo de nuevo en el nuevo identificador cuando el identificador se recrea, y establecerlo de nuevo cuando llega una notificación de cambio de temaIdentificador recreadoNotificación de cambio de temaHWND creadoEstablecer el atributo DWMMostrado

Figura 5: El atributo DWM está ligado al HWND, así que establézcalo de nuevo en cada creación y en cada recreación.

El P/Invoke en WinForms tiene este aspecto (para la forma segura de escribir DllImport, véase «Cómo llamar a la API de Win32 desde C# de forma segura»).

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 compatibilidad documentada es la compilación 22000 o posterior. Por debajo, no llamarlo y seguir el valor predeterminado (claro).
        // Esta comprobación también funciona en .NET Framework. En .NET Framework, sin embargo, sin un manifiesto que
        // declare supportedOS para Windows 10 o posterior, la versión se redondea a Windows 8
        // (en .NET 5 o posterior, OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000) también funciona)
        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) = puede dibujarse oscuro cuando el sistema está oscuro. 0 (FALSE) = siempre claro
        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);
        }
    }
}

Este código lo necesitan las aplicaciones en .NET 8 o anterior, .NET Framework y Win32/MFC. Cuando usa Application.SetColorMode en WinForms en .NET 9 o posterior, o ThemeMode en WPF en .NET 9 o posterior, el marco se encarga de oscurecer la ventana (la documentación de ThemeMode indica que «también controla la aplicación del material de fondo y del modo oscuro a la ventana»).10 Llamarlo dos veces no hace daño, pero difumina quién es responsable, así que elija uno u otro.

En WPF, el HWND queda fijado en SourceInitialized. Obtenga el identificador de 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); // el cuerpo es el P/Invoke mostrado más arriba
    }
}

4.3. Color de la barra de título, color del texto, color del borde y material de fondo

Windows 11 añadió atributos que especifican el color de la barra de título en sí, más allá de la elección binaria de oscuro o claro.

Atributo Valor Significado Compilación compatible
DWMWA_USE_IMMERSIVE_DARK_MODE 20 Dibujar el marco oscuro cuando el sistema está oscuro (BOOL) 22000
DWMWA_BORDER_COLOR 34 Color del borde de la ventana (COLORREF). DWMWA_COLOR_NONE quita el borde 22000
DWMWA_CAPTION_COLOR 35 Color de la barra de título (COLORREF) 22000
DWMWA_TEXT_COLOR 36 Color del texto del título (COLORREF) 22000
DWMWA_SYSTEMBACKDROP_TYPE 38 Material de fondo dibujado por el sistema (Mica o Acrylic) 22621

Para los tres atributos de color, pasar DWMWA_COLOR_DEFAULT (0xFFFFFFFF) restaura el valor predeterminado del sistema. Tenga en cuenta que para el color del borde, «es responsabilidad de la aplicación cambiar el color en respuesta a cambios de estado como la activación de la ventana».4 El material de fondo se especifica con la enumeración DWM_SYSTEMBACKDROP_TYPE; en Windows 11, DWMSBT_MAINWINDOW corresponde a Mica y DWMSBT_TRANSIENTWINDOW a Acrylic, pero la documentación indica que «el efecto del material puede cambiar en versiones futuras de Windows».20

Piense con cuidado dónde encaja esto en una aplicación empresarial. Una vez pinta la barra de título en un color de marca, usted pasa a ser responsable de garantizar el contraste del texto del título y de los botones de leyenda sobre ese color. A los dos estados oscuro y claro se multiplican las combinaciones con activo e inactivo. Para la mayoría de las aplicaciones empresariales la respuesta correcta es «seguir el valor predeterminado del sistema (limitarse a poner el valor 20 en TRUE)», y un color de marca es una opción para cuando de verdad hace falta.

Cómo decidir el color de la barra de títuloSeguir el valor predeterminado del sistema solo exige poner el valor 20 en TRUE, pero pintar un color de marca hace a la aplicación responsable de garantizar el contraste del texto y de los botones de leyenda y de gestionar los colores activo e inactivo, y restaurar pasa DWMWA_COLOR_DEFAULTSeguir el valor predeterminado del sistemaPintar un color de marca¿Color de la barra de título?Limitarse a poner el valor 20 en TRUEEspecificar los atributos de color (34 a 36)Garantizar usted mismo el contraste del texto y de los botonesGestionar usted mismo activo/inactivoRestaurar con COLOR_DEFAULT

Figura 6: Elegir un color de marca desplaza la responsabilidad del contraste y de la gestión de estado a la aplicación, así que para la mayoría de las aplicaciones empresariales seguir el valor predeterminado es la respuesta correcta.

5. Detectar y seguir el tema del sistema — leer, notar, volver a pintar

El trabajo de hacer que el área de cliente siga el tema se descompone en tres partes: leer la configuración actual, notar los cambios y volver a pintar.

Los tres pasos de la detección y el seguimientoEl bucle de leer el modo de color actual con UISettings al inicio, notar los cambios mediante notificaciones como ColorValuesChanged, volver al hilo de UI y volver a pintar la paleta de la aplicaciónLeer: UISettings.GetColorValueVolver a pintar: reaplicar la paletaNotar: ColorValuesChangedVolver al hilo de UIEstablecer de nuevo también el atributo DWM

Figura 7: Leer al inicio, notar mediante notificaciones, volver al hilo de UI y volver a pintar: ese bucle es el esqueleto del seguimiento de tema.

5.1. Leer — UISettings y «si el primer plano es claro, es oscuro»

El procedimiento oficial de Microsoft usa la clase WinRT Windows.UI.ViewManagement.UISettings. Obtenga el color de primer plano (el color de texto predeterminado) con GetColorValue(UIColorType::Foreground), estime su luminancia percibida con aritmética entera para decidir si es «claro», y concluya modo oscuro si el primer plano es claro. La documentación señala que la fórmula no es un modelo de luminancia riguroso, solo una aproximación suficiente para clasificar claro y oscuro.321

UISettings es una clase WinRT, pero las aplicaciones C# WPF y WinForms pueden llamarla directamente si el TargetFramework lleva una versión de SDK de Windows, como net8.0-windows10.0.19041.0 (para cómo funciona, véase «WinRT es COM»). También podría leer AppsUseLightTheme del registro de forma directa, pero el registro es el lugar donde se almacena la configuración, no un contrato de API; si lo lee, trate UISettings como la fuente de verdad y reserve el registro para el diagnóstico.

Formas de leer el modo de colorEl camino canónico es obtener el color de primer plano mediante WinRT UISettings y clasificarlo como claro u oscuro; el valor de registro AppsUseLightTheme es la ubicación de almacenamiento y debería reservarse para el diagnóstico¿Claro u oscuro ahora mismo?UISettings.GetColorValueRegistro AppsUseLightThemeJuzgar la luminancia percibida del primer planoClaro significa modo oscuroUbicación de almacenamiento. Reservar para el diagnóstico

Figura 8: El camino de lectura canónico es UISettings; el registro es solo el lugar donde se almacena la configuración.

5.2. Notar — ColorValuesChanged no llega en el hilo de UI

UISettings también sirve para detectar cambios. El evento ColorValuesChanged se dispara cuando cambia un valor de color, y la guía oficial usa este evento para seguir los cambios de configuración.53 Aquí aplica una precaución práctica. Este evento no está garantizado en el hilo de UI. Vuelva al hilo de UI con el Dispatcher de WPF, Control.Invoke de WinForms o el SynchronizationContext que funciona en ambos, antes de tocar ningún control. El trabajo con el hilo de UI se resume en «WPF/WinForms: async y el hilo de UI, resumidos en una hoja».

El evento también se dispara cuando cambia el color de énfasis. Si quiere volver a pintar solo cuando cambia claro/oscuro, reevalúe en cada evento y notifique solo cuando el resultado difiere de la última vez. Y, según el orden de precedencia del capítulo 2, no mire el brillo del primer plano mientras un tema de contraste está activo. Un tema de contraste con fondo oscuro como Aquatic tiene un primer plano claro, así que el brillo solo lo clasificaría mal como «oscuro». Trate el tema de contraste como un estado independiente y decídalo primero.

La clase siguiente reúne ese orden de decisión en un solo sitio. El SynchronizationContext usado para volver al hilo de UI y la comprobación de tema de contraste (SystemInformation.HighContrast en WinForms, SystemParameters.HighContrast en WPF; véase el capítulo 8) los pasa el llamador. Cuidado con el momento de crearla. En WinForms, en el punto de Program.Main aún no hay bucle de mensajes ni Control, así que SynchronizationContext.Current es null. Pase SynchronizationContext.Current después de que existan los controles, por ejemplo desde el constructor del formulario o OnLoad. En WPF puede pasar new DispatcherSynchronizationContext(Application.Current.Dispatcher).

using Windows.UI.ViewManagement; // TargetFramework: net8.0-windows10.0.19041.0 o posterior

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: el SynchronizationContext del hilo de UI. Pase SynchronizationContext.Current
    //     después de que existan los controles, o un DispatcherSynchronizationContext en 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()
    {
        // Orden de decisión como en el capítulo 2: primero el tema de contraste. Un tema de contraste con
        // fondo oscuro tiene un primer plano claro, así que el brillo solo lo clasificaría mal como «oscuro»
        if (_isHighContrast()) return ThemeState.HighContrast;
        // La misma prueba que la guía oficial: oscuro si el primer plano (color de texto predeterminado) es claro
        var fg = _settings.GetColorValue(UIColorType.Foreground);
        bool isDark = (5 * fg.G + 2 * fg.R + fg.B) > 8 * 128;
        return isDark ? ThemeState.Dark : ThemeState.Light;
    }

    // Llamar desde el hilo de UI. También se puede llamar desde otros caminos de notificación como UserPreferenceChanged
    public void Refresh()
    {
        if (_disposed) return;
        var next = Read();
        // Entre claro y oscuro, no notificar cuando el estado no ha cambiado, para ignorar cambios de solo color de énfasis.
        // Durante un tema de contraste esta es la excepción: el estado sigue en HighContrast aunque el usuario edite
        // los colores del tema, así que notificar incluso para el mismo estado para que se vuelvan a leer los colores del sistema
        if (next == Current && next != ThemeState.HighContrast) return;
        Current = next;
        Changed?.Invoke(this, EventArgs.Empty);
    }

    private void OnColorValuesChanged(UISettings sender, object args)
    {
        // No está garantizado en el hilo de UI, así que volver a la UI antes de decidir y notificar.
        // Una llamada que llega después de Dispose (ya en cola) la ignora el indicador de Refresh
        _ui.Post(_ => Refresh(), null);
    }

    public void Dispose()
    {
        // Cancelar la suscripción solo detiene entregas futuras; las llamadas ya enviadas al hilo de UI permanecen.
        // Poner el indicador para que las llamadas restantes se ignoren a sí mismas (llamar esto en el hilo de UI)
        _disposed = true;
        _settings.ColorValuesChanged -= OnColorValuesChanged;
    }
}

A nivel Win32, WM_SETTINGCHANGE se envía a cada ventana de nivel superior cuando cambia una configuración,22 y en .NET llega como SystemEvents.UserPreferenceChanged.23 Un cambio de estilo visual (incluida la activación de un tema de contraste) trae WM_THEMECHANGED,24 y un cambio de color del sistema trae WM_SYSCOLORCHANGE.25 En lugar de escribir un tratamiento distinto para cada tipo de notificación, llame a la misma rutina «leer y volver a pintar» llegue la notificación que llegue; es más difícil de romper, y tiene la misma forma que el seguimiento de temas de contraste presentado en el artículo anterior. En términos del SystemThemeWatcher anterior, significa llamar también a Refresh() desde los controladores de UserPreferenceChanged y StaticPropertyChanged.

Caminos y hilos de notificación de cambio de temaColorValuesChanged de UISettings puede llegar fuera del hilo de UI y se vuelve a él, WM_SETTINGCHANGE llega como SystemEvents.UserPreferenceChanged, y WM_THEMECHANGED y WM_SYSCOLORCHANGE llegan al procedimiento de ventana. Todos convergen en la misma rutina de reaplicaciónColorValuesChangedVolver al hilo de UIUserPreferenceChangedLeer y volver a pintarWM_THEMECHANGED etc.

Figura 9: Hay varios caminos de notificación, pero todos convergen en la misma rutina «leer y volver a pintar».

5.3. Volver a pintar — reunir los colores en un solo sitio

La condición previa que hace posible volver a pintar es que los colores estén reunidos en un solo sitio. Si Color.White y #FFFFFF están dispersos por formularios y XAML, no puede enumerar los sitios que hay que volver a pintar. En WinForms, cree una clase «paleta» (dos instancias, una clara y una oscura) y haga que los controles tomen sus colores de ella al inicio y en cada notificación. En WPF, reúna los colores en un ResourceDictionary y refiéralos desde XAML con DynamicResource. En WinUI, esta estructura se proporciona desde el principio como ThemeDictionaries.

La estructura que reúne los colores en un solo sitioLa aplicación tiene una paleta clara y una paleta oscura, y cada pantalla toma sus colores de la paleta elegida para el modo actual, así que se pueden enumerar los destinos de un repintadoClaroOscuroModo actual¿Cuál?Paleta claraPaleta oscuraCada pantalla y cada controlColor.White dispersoNo se puede enumerar qué volver a pintar

Figura 10: Con la paleta en un solo sitio se puede enumerar un repintado; con colores fijos dispersos, no.

Este trabajo de «reunir los colores» se paga de forma directa en la compatibilidad con temas de contraste y en las comprobaciones de relación de contraste que se describen más adelante. El mayor coste de la compatibilidad con el modo oscuro no son las llamadas a API, sino esta limpieza.

6. Implementación en WinForms

6.1. .NET 9/10 — Application.SetColorMode

WinForms obtuvo una compatibilidad preliminar con el modo oscuro en .NET 9, y se «integró por completo» en .NET 10. Application.SetColorMode acepta tres valores.67

  • SystemColorMode.Classic — el valor predeterminado. Claro, como antes.
  • SystemColorMode.System — seguir la configuración claro/oscuro de Windows.
  • SystemColorMode.Dark — oscuro.

Llámalo antes de Application.Run, antes de crear ningún elemento de UI. En .NET 9 era una característica experimental, así que era un error de compilación a menos que se suprimiera WFO5001 en el archivo de proyecto; a partir de .NET 10 el error ya no aparece.26

static class Program
{
    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();
        Application.SetColorMode(SystemColorMode.System); // llamar antes de crear cualquier UI
        Application.Run(new MainForm());
    }
}

Cuando cambia el modo de color, System.Drawing.SystemColors cambia a los colores coincidentes, y los controles estándar se dibujan en consecuencia.6 Por debajo, la propiedad experimental SystemColors.UseAlternativeColorSet (SYSLIB5002) «hace que los valores KnownColor del sistema devuelvan un conjunto de colores alternativo (actualmente la versión del modo oscuro)»; como los colores del sistema de Win32 en sí no cambian con la configuración claro/oscuro, .NET lleva el conjunto alternativo de su lado. La misma documentación también dice que cuando un tema de contraste está activo, siempre se devuelven los colores actuales de Windows.27

Tenga presentes las tres restricciones escritas en la documentación de SetColorMode.2

  1. El modo de color oscuro solo está disponible en Windows 11 o posterior.
  2. El modo oscuro no está disponible cuando un tema de contraste está activo.
  3. Incluso con SystemColorMode.System, la aplicación no sigue automáticamente un cambio de la configuración de Windows mientras se ejecuta.

La tercera tiende a generar preguntas de soporte en las aplicaciones empresariales. Escriba en la documentación dirigida al usuario que el modo lo decide la configuración de Windows al inicio y surte efecto en el siguiente arranque. Si absolutamente debe seguir un cambio en tiempo de ejecución, necesita un diseño que recree los formularios, y casi nunca merece la pena.

El flujo y las restricciones de SetColorModeSetColorMode se llama antes de Application.Run, SystemColors cambia al conjunto alternativo y los controles estándar siguen. Tiene tres restricciones: solo Windows 11, deshabilitado durante un tema de contraste, y sin seguimiento de cambios de configuración en tiempo de ejecuciónSetColorMode (System)Antes de Application.RunSystemColors cambia al conjunto alternativoLos controles estándar siguenSolo Windows 11Deshabilitado durante un tema de contrasteNo sigue los cambios en tiempo de ejecución

Figura 11: SetColorMode surte efecto una vez antes del inicio y tiene tres restricciones documentadas.

6.2. Controles dibujados por el propietario y ApplyThemingImplicitly

Los controles estándar siguen el modo de color de la aplicación, pero la documentación de .NET 10 enumera dos casos excepcionales. Si un control que usted compone y dibuja usa controles comunes de Win32 como barras de desplazamiento, estos se quedan claros a menos que opten de forma explícita. A la inversa, si hereda un control existente que sigue el tema y quiere el control total de su dibujo usted mismo, se sale.7

En ambos casos, invalide Control.CreateParams y llame a SetStyle(ControlStyles.ApplyThemingImplicitly, true/false) antes de leer base.CreateParams. La trampa de esta API es que el constructor de la clase base lee CreateParams, así que su propio constructor llega demasiado tarde.7

public partial class GanttChartControl : Control
{
    protected override CreateParams CreateParams
    {
        get
        {
            // Establecerlo antes de leer base.CreateParams. El constructor llega demasiado tarde
            SetStyle(ControlStyles.ApplyThemingImplicitly, true);
            return base.CreateParams;
        }
    }
}
Cuándo se puede establecer ApplyThemingImplicitlyApplyThemingImplicitly se decide en el punto en que el constructor de la clase base lee CreateParams, así que SetStyle debe llamarse antes de base.CreateParams dentro de la invalidación de CreateParams, y el constructor de la clase derivada llega demasiado tardeConstructor de la clase baseLee CreateParamsSetStyle debe ocurrir para entoncesConstructor de la clase derivadaLlamarlo aquí llega demasiado tarde

Figura 12: ApplyThemingImplicitly debe estar decidido antes de que el constructor de la clase base lea CreateParams.

El dibujo por el propietario en sí (dibujo GDI+ en OnPaint) sigue el conjunto alternativo mientras use SystemColors / SystemBrushes / SystemPens. Cualquier sitio que pinte con Color.White se sustituye por la paleta descrita más arriba, también aquí.

6.3. .NET Framework 4.8 y .NET 8 o anterior — declarar «claro fijo»

En entornos sin SetColorMode no hay compatibilidad estándar con el modo oscuro. Hay dos opciones.

  1. Declarar claro fijo. Deje el atributo DWM en su valor predeterminado FALSE (siempre una barra de título clara) y deje el área de cliente como está. La compatibilidad con temas de contraste (capítulo 8) sigue siendo obligatoria.
  2. Implementar la compatibilidad completa usted mismo. Consolide la paleta, lea y siga con UISettings, ponga el atributo DWM en TRUE y vuelva a pintar todo, incluido el aspecto de los controles comunes.

La opción 2 tiende a acabar «en su mayoría oscuro, pero claro en sitios», porque la aplicación no puede controlar por completo el dibujo de los controles comunes de Win32 (barras de desplazamiento, encabezados, botones de expansión de árbol, y así). Como dice la guía oficial, «toda la superficie debe seguir»;3 un modo oscuro a medias es una peor experiencia que el claro fijo. Para activos existentes, elija la opción 1, declare «esta aplicación se muestra en modo claro» y pase a SetColorMode cuando migre a .NET 10. Esa es la política realista, y la más fácil de explicar.

Opciones de WinForms según el entorno de ejecuciónEn .NET 10 o posterior usar SetColorMode, en .NET 9 la misma API con WFO5001 suprimido, y en .NET 8 o anterior o .NET Framework elegir entre declarar claro fijo e implementar la compatibilidad completa usted mismo hasta los controles comunes.NET 10 o posterior.NET 9.NET 8 o anterior / .NET FrameworkRecomendadoSi está preparado para ello¿Entorno de ejecución?SetColorMode (System)SetColorMode + suprimir WFO5001¿Qué hacer?Declarar claro fijoCompatibilidad completa usted mismoLos controles comunes quedan

Figura 13: En entornos de ejecución sin SetColorMode, declarar claro fijo es el valor predeterminado realista.

7. Implementación en WPF

7.1. .NET 9/10 — El tema Fluent y ThemeMode

WPF en .NET 9 incluye un tema nuevo que sigue el diseño Fluent de Windows 11, con compatibilidad con claro/oscuro y el color de énfasis. Hay dos formas de aplicarlo: establecer la propiedad ThemeMode, o añadir el diccionario de recursos PresentationFramework.Fluent a MergedDictionaries.8

ThemeMode toma cuatro valores, Light / Dark / System / None (el valor predeterminado; el tema clásico Aero2). Establecido en Application afecta a toda la aplicación; establecido en una Window afecta solo a esa ventana.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 no solo carga los diccionarios de tema Fluent en los recursos; la documentación indica que «también controla la aplicación del material de fondo y del modo oscuro a la ventana». En otras palabras, WPF se encarga del atributo DWM del capítulo 4. ThemeMode y Resources también están diseñados para permanecer sincronizados, lo que la documentación explica para evitar una inconsistencia en la que la ventana está oscura pero los controles de dentro están claros.10

Qué hace ThemeMode en WPFEstablecer ThemeMode en System carga los diccionarios de tema Fluent que coinciden con la configuración de Windows en los recursos y también controla la aplicación del modo oscuro y del material de fondo a la ventanaThemeMode=SystemLeer la configuración de WindowsCargar los diccionarios Fluent en los recursosOscurecer la ventana y aplicar el fondoPermanecer sincronizado con Resources para evitar inconsistencia

Figura 14: ThemeMode controla juntos la carga de los diccionarios Fluent y el oscurecimiento de la ventana.

Hay dos precauciones que conviene conocer antes de adoptarlo. Primera, leer y escribir ThemeMode desde código es una característica experimental, y acceder a ella produce el error WPF0001. Si lo suprime puede escribir Application.Current.ThemeMode = ThemeMode.Dark, pero la referencia de API sigue llevando [Experimental("WPF0001")] en .NET 10, con la nota de que «puede eliminarse en el futuro».810 Segunda, la cobertura de estilos Fluent sigue «in progress» en .NET 10. .NET 10 añadió estilos para DatePicker, GridSplitter, GroupBox, TextBox y otros, y corrigió bloqueos relacionados con HighContrast,9 lo que, leído al revés, significa que el tema Fluent de .NET 9 no los tenía. Antes de decidir adoptarlo, confirme en una máquina real que los controles que usa su aplicación empresarial (DataGrid y controles de terceros en particular) no se rompen bajo Fluent.

7.2. Seguir el tema en el tema clásico — intercambiar ResourceDictionary

En WPF sin Fluent (o en .NET 8 o anterior, o .NET Framework) no hay compatibilidad estándar con el modo oscuro. Los colores del sistema de Win32 no cambian con la configuración claro/oscuro, así que referenciar los SystemColors de WPF no oscurecerá nada. La estructura para seguir el tema usted mismo es la siguiente.

  1. Defina colores y pinceles con las mismas claves en Themes/Light.xaml para claro y Themes/Dark.xaml para oscuro.
  2. Refiéralos desde XAML con DynamicResource, como en {DynamicResource App.WindowBackgroundBrush} (StaticResource queda fijo en la carga y no sigue un intercambio).
  3. Ante una notificación del SystemThemeWatcher del capítulo 5, intercambie el diccionario correspondiente en MergedDictionaries. Durante un tema de contraste, el desencadenador de la sección 8.4 sustituye los colores por colores del sistema ponga el diccionario que ponga, así que ponga el claro.
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);
        // El diccionario oscuro solo cuando está oscuro. Durante un tema de contraste, remitirse a los colores del sistema (sección 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; // intercambiar en el sitio, en la misma posición
        }
    }
}
Intercambiar diccionarios de recursos en WPFDefinir colores con las mismas claves en un diccionario claro y uno oscuro, referenciarlos desde XAML con DynamicResource, e intercambiar el diccionario en MergedDictionaries ante una notificación de cambio de tema para que las referencias se actualicenNotificación de cambio de temaIntercambiar el diccionario en MergedDictionariesLight.xaml (mismas claves)Dark.xaml (mismas claves)Las referencias DynamicResource se actualizanReferencias StaticResourceConservan el valor de la carga

Figura 15: Intercambie diccionarios con las mismas claves, y solo las referencias DynamicResource siguen.

Las plantillas de los controles estándar (fondos de botones, colores de barras de desplazamiento) llevan los colores del tema clásico, así que también aquí habrá sitios donde «las superficies propias de la aplicación están oscuras pero los controles estándar están claros». Estime el trabajo de invalidar el estilo de cada control que necesite, y luego compárelo con adoptar Fluent o el claro fijo.

7.3. La barra de título

Si usa ThemeMode, WPF se encarga. Si sigue el tema usted mismo en el tema clásico, use el establecimiento del atributo DWM desde OnSourceInitialized mostrado en la sección 4.2, y establézcalo de nuevo ante las notificaciones de SystemThemeWatcher.

8. Dibujo bajo un tema de contraste — conservar los pares de colores del sistema

8.1. Detección y notificación

En Win32, pase SPI_GETHIGHCONTRAST a SystemParametersInfo para recibir una estructura HIGHCONTRAST, y compruebe el bit HCF_HIGHCONTRASTON de dwFlags. cbSize debe estar establecido antes de la llamada.1128 Microsoft lo sitúa como «la única forma compatible de comprobar si el alto contraste está activado».12

bool IsContrastThemeActive()
{
    HIGHCONTRASTW hc{};
    hc.cbSize = sizeof(hc);
    if (!::SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(hc), &hc, 0))
    {
        // No ocultar un error detrás de un valor predeterminado. Hacerlo visible con el código de error para que se pueda encontrar la causa
        throw std::system_error(::GetLastError(), std::system_category(),
                                "SystemParametersInfo(SPI_GETHIGHCONTRAST)");
    }
    return (hc.dwFlags & HCF_HIGHCONTRASTON) != 0;
}

Cada marco tiene una propiedad que envuelve esta llamada.

Entorno Detección Notificación de cambio
Win32 / MFC SPI_GETHIGHCONTRAST + HCF_HIGHCONTRASTON WM_SYSCOLORCHANGE, WM_THEMECHANGED
WinForms SystemInformation.HighContrast SystemEvents.UserPreferenceChanged
WPF SystemParameters.HighContrast (se asigna a SPI_GETHIGHCONTRAST) SystemParameters.StaticPropertyChanged
WinUI 3 ThemeSettings.HighContrast (Microsoft.UI.System) ThemeSettings.Changed

El tutorial de accesibilidad de WinForms pide comprobar HighContrast al inicio y responder a los cambios mediante UserPreferenceChanged.13 SystemParameters.HighContrast de WPF se asigna a SPI_GETHIGHCONTRAST y HCF_HIGHCONTRASTON,29 y los cambios de las propiedades estáticas se anuncian mediante StaticPropertyChanged.30 ThemeSettings de WinUI 3 se crea ligado a una ventana con CreateForWindowId y usted se suscribe a su evento Changed, pero tenga en cuenta que los eventos se detienen a menos que siga manteniendo una referencia al objeto.31

Caminos de detección de temas de contrasteWin32 SPI_GETHIGHCONTRAST es el único método de detección compatible, y SystemInformation.HighContrast en WinForms, SystemParameters.HighContrast en WPF y ThemeSettings.HighContrast en WinUI 3 los proporciona cada marco como envoltorios alrededorSPI_GETHIGHCONTRAST (el único método de detección)WinForms SystemInformationWPF SystemParametersWinUI 3 ThemeSettingsWin32: llamarlo de forma directa

Figura 16: La raíz de la detección es una sola API de Win32, y cada marco tiene una propiedad que la envuelve.

8.2. Principios de dibujo — pares de primer plano y fondo

El «High contrast parameter» de Microsoft enumera tres cosas que una aplicación debe hacer cuando el alto contraste está activado.11

  1. Asignar cada color a un par de colores de primer plano y de fondo. Use GetSysColor con el par COLOR_WINDOWTEXT y COLOR_WINDOW, o el par COLOR_BTNTEXT y COLOR_BTNFACE.
  2. Omitir las imágenes de mapa de bits mostradas detrás del texto. Son un obstáculo visual para los usuarios que necesitan alto contraste.
  3. Dibujar las imágenes de varios colores en los colores de primer plano y de fondo usados para el texto.

El «par» es el nudo. La guía de Windows 8 y posteriores explica que COLOR_HIGHLIGHTTEXT está diseñado para combinarse con el fondo COLOR_HIGHLIGHT y COLOR_WINDOWTEXT con el fondo COLOR_WINDOW, y pide no codificar de forma fija los colores de texto y, como los usuarios personalizan los colores, construir una UI que no dependa del tema en vigor.12 El ejemplo de la misma guía, «en Aero el texto siempre es negro y el color de selección es azul claro, pero en High Contrast Black el color de selección es negro. Si supone texto negro y usa el color de selección del sistema, obtiene texto negro sobre negro», es exactamente la queja «la indicación de estado desapareció» de la apertura.

La guía de temas de contraste de Windows 11 tabula los emparejamientos.1

Uso Primer plano Fondo
Encabezados, cuerpo de texto, listas, bordes, UI no interactiva SystemColorWindowText SystemColorWindow
Hipervínculos SystemColorHotlight SystemColorWindow
UI deshabilitada o inactiva SystemColorGrayText SystemColorWindow
Seleccionado, hover, pulsado, en curso SystemColorHighlightText SystemColorHighlight
UI interactiva como botones SystemColorButtonText SystemColorButtonFace

Lo que no hay que hacer también está enunciado. No use GrayText para texto complementario o de sugerencia (es solo para el estado deshabilitado); no use Hotlight para nada que no sean hipervínculos; no mezcle primeros planos y fondos incompatibles; no elija colores por aspecto (los usuarios de verdad los cambian). También hay una pauta de diseño según la cual los fondos de páginas, paneles y elementos emergentes se basan en SystemColorWindow, así que las superficies adyacentes acaban con el mismo color de fondo, y solo los límites que importan se separan con un borde usado solo bajo temas de contraste (se recomiendan 2 px para menús emergentes y cuadros de diálogo).1

Cómo romper el par hace ilegible el textoSuponer que el texto es negro y usar el color de selección del sistema solo para el fondo de selección da negro sobre negro en High Contrast Black, donde el color de selección es negro. Tomar primer plano y fondo como un par mantiene el texto legible incluso cuando el usuario edita los coloresSuponer que el texto es negroSolo el fondo de selección usa el color de selección del sistemaEn High Contrast Black el color de selección es negroTexto negro sobre negroTomar primer plano y fondo como un parLegible incluso cuando el usuario edita los coloresEl usuario edita los colores

Figura 17: Usar un color del sistema para un solo lado puede dar negro sobre negro, pero tomar el par sigue siendo legible incluso cuando se editan los colores.

Decisiones de dibujo bajo un tema de contrasteCuando un tema de contraste está activo, asignar los colores a los pares de colores del sistema, omitir las imágenes detrás del texto, dibujar las imágenes de varios colores en los dos colores de primer plano y de fondo, y no usar colores codificados de forma fijaTema de contraste activoAsignar los colores a paresOmitir las imágenes detrás del textoDibujar los gráficos de varios colores en dos coloresSin colores codificados de forma fija

Figura 18: El dibujo bajo un tema de contraste activo se resume en cuatro puntos: asignar, omitir, dos colores y no codificar de forma fija.

8.3. Implementación en WinForms

Los controles estándar de WinForms siguen los colores del sistema mientras ForeColor / BackColor se dejan en sus valores predeterminados. Solo los sitios con colores personalizados y el dibujo por el propietario se cambian según la comprobación. El ejemplo del tutorial toma una etiqueta que es amarilla sobre azul en condiciones normales y la restaura a SystemColors.Window / SystemColors.WindowText bajo alto contraste.13 Equivale a añadir una rama de tema de contraste a la estructura de paleta de más arriba.

using Microsoft.Win32;

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

    public OrderForm()
    {
        InitializeComponent();
        // Los controles existen ahora, así que hay un WindowsFormsSynchronizationContext
        _ui = SynchronizationContext.Current
              ?? throw new InvalidOperationException("Create this form on the UI thread.");
        ApplyColorScheme();
        SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged;
    }

    private void ApplyColorScheme()
    {
        if (SystemInformation.HighContrast)
        {
            // Conservar el par, remitirse por completo a los colores del sistema y quitar la imagen detrás del texto
            statusLabel.BackColor = SystemColors.Window;
            statusLabel.ForeColor = SystemColors.WindowText;
            headerPanel.BackgroundImage = null;
        }
        else
        {
            var p = AppPalette.Current; // la paleta claro/oscuro (capítulo 5)
            statusLabel.BackColor = p.PanelBackground;
            statusLabel.ForeColor = p.PanelForeground;
            headerPanel.BackgroundImage = Properties.Resources.HeaderPattern;
        }
    }

    private void OnUserPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e)
    {
        // Este evento tampoco está garantizado en el hilo de UI. Volver a la UI y luego reevaluar sin filtrar por categoría
        _ui.Post(_ =>
        {
            if (IsDisposed) return;
            ApplyColorScheme();
        }, null);
    }

    // Un evento estático, así que el formulario tiene fugas si no se desasocia. Desasociar en Dispose(bool), que también se ejecuta
    // en los caminos en que el formulario se libera sin cerrarse (si existe el Dispose(bool) generado por el diseñador, póngalo ahí)
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged;
        }
        base.Dispose(disposing);
    }
}

El dibujo por el propietario en OnPaint usa pinceles del sistema que conservan el par, como SystemBrushes.Window / SystemPens.WindowText, y los gráficos de varios colores como un punto de color que representa un estado se sustituyen por un borde y texto («Running», «Stopped») en el color de primer plano. Transmitir información por más que el color solo es el mismo punto que el criterio de conformidad 1.4.1 del artículo anterior.

Ramificación del esquema de color de WinFormsApplyColorScheme se llama al inicio y en cada UserPreferenceChanged; si SystemInformation.HighContrast es verdadero se remite a los pares de colores del sistema y quita la imagen de fondo, y si es falso toma los colores de la paleta claro/oscuroVerdaderoFalsoInicio / UserPreferenceChangedApplyColorScheme¿SystemInformation.HighContrast?Remitirse a los pares de SystemColorsQuitar la imagen de fondoTomar los colores de la paleta claro/oscuro

Figura 19: En WinForms se llama a la misma rutina al inicio y en cada notificación, y bajo un tema de contraste se remite a los colores del sistema.

8.4. Implementación en WPF

Los SystemColors de WPF se actualizan automáticamente cuando cambia un pincel si referencia una clave de recurso como WindowBrushKey mediante DynamicResource (una referencia estática que usa WindowBrush de forma directa no se actualiza).32 Para cambiar el aspecto solo durante un tema de contraste, referencie el valor de SystemParameters.HighContrast desde un DataTrigger. Sin embargo, SystemParameters.HighContrast es una propiedad estática, así que por sí sola no es un origen de enlace vivo. Prepare un proxy pequeño que se suscriba a StaticPropertyChanged, conserve el valor y notifique mediante INotifyPropertyChanged, y enlace con esa instancia como 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()
    {
        // Se dispara cuando cambia una propiedad estática de SystemParameters (se vuelve a leer SPI_GETHIGHCONTRAST)
        SystemParameters.StaticPropertyChanged += (_, e) =>
        {
            if (!string.IsNullOrEmpty(e.PropertyName)
                && e.PropertyName != nameof(SystemParameters.HighContrast)) return;
            IsHighContrast = SystemParameters.HighContrast;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsHighContrast)));
        };
    }
}
<!-- Declarar xmlns:local="clr-namespace:OrderEntry" de antemano -->
<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">
            <!-- Conservar el par: el fondo es Window, el borde y el texto son WindowText. Engrosar el límite -->
            <Setter Property="Background"
                    Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
            <Setter Property="BorderBrush"
                    Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"/>
            <!-- Dejar que el texto de dentro también lo herede. Tenga en cuenta que un hijo que establece Foreground de forma explícita corta la herencia -->
            <Setter Property="TextElement.Foreground"
                    Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"/>
            <Setter Property="BorderThickness" Value="2"/>
        </DataTrigger>
    </Style.Triggers>
</Style>
Referencias de colores del sistema de WPF y el desencadenador de tema de contrasteReferenciar claves de recurso de SystemColors mediante DynamicResource sigue los cambios de pincel de forma automática, y un desencadenador enlazado a IsHighContrast en un proxy que se suscribe a StaticPropertyChanged reacciona a un cambio en tiempo de ejecución y cambia a colores que conservan el par. Una referencia directa a WindowBrush no se actualizaReferenciar WindowBrushKey mediante DynamicResourceSigue los cambios de pincel de forma automáticaStaticPropertyChangedIsHighContrast en el proxyDataTrigger reaccionaCambiar a colores que conservan el parReferenciar WindowBrush de forma directaNo se actualiza

Figura 20: WPF sigue un cambio en tiempo de ejecución mediante referencias dinámicas a claves de recurso y un enlace a un proxy que retransmite los cambios de la propiedad estática.

Si usa el tema Fluent, tenga presente que .NET 10 incluía correcciones de bloqueos relacionadas con HighContrast,9 y haga de la verificación bajo un tema de contraste una condición de adopción.

8.5. Implementación en WinUI 3

En WinUI 3, los controles estándar siguen los temas claros, oscuros y de contraste desde el principio, y los colores propios de la aplicación se definen en ResourceDictionary.ThemeDictionaries bajo las claves Default (oscuro), Light y HighContrast. Bajo HighContrast, no codifique los colores de forma fija; referencie colores del sistema dinámicos como SystemColorWindowColor mediante ThemeResource. Un control personalizado que tiene Light/Dark debe tener siempre también HighContrast, y HighContrast es la clave de reserva que se usa cuando no se encuentra ningún otro tema de alto contraste con nombre.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>

Una cosa más: WinUI tiene un mecanismo llamado HighContrastAdjustment, activado de forma predeterminada. Fuerza texto blanco y un fondo de resaltado negro para preservar el contraste, y la guía recomienda que una vez haya preparado diccionarios de tema que usen los colores del sistema de forma correcta, lo ponga en None para que se apliquen sus propios estilos.1

Cómo resuelve WinUI ThemeDictionariesSegún el tema actual, se elige uno de los diccionarios Default (oscuro), Light y HighContrast, y bajo HighContrast se referencian colores del sistema dinámicos mediante ThemeResource. HighContrast es la clave de reserva cuando no hay un tema de alto contraste con nombreOscuroClaroTema de contraste¿Tema actual?Diccionario DefaultDiccionario LightDiccionario HighContrastReferenciar recursos SystemColor mediante ThemeResourceReserva cuando no hay un tema con nombre

Figura 21: En WinUI el diccionario de cada tema se elige de forma automática, y el diccionario HighContrast referencia los colores del sistema.

9. La compatibilidad con el modo oscuro no sustituye a la compatibilidad con la accesibilidad

Informar de la compatibilidad con el modo oscuro como «hicimos el trabajo de accesibilidad» es un error. La relación entre ambos se puede plantear así.

  • El criterio de relación de contraste se aplica igual a la paleta oscura. El criterio de conformidad WCAG 1.4.3 exige 4.5:1 para el texto y 3:1 para el texto grande, y eso no cambia cuando el fondo es oscuro.14 Un diseño oscuro que pone texto gris medio sobre un fondo gris oscuro tiene el mismo problema que «gris claro sobre blanco» en el modo claro.
  • Evitar el negro puro y el blanco puro es el diseño de Windows 11. Las prácticas recomendadas de Microsoft explican que Windows 11 se alejó del blanco puro y del negro puro hacia tonos más suaves para los ojos.34 A la inversa, un fondo #000000 en el modo oscuro lleva a algunas personas a quejarse de halo, un efecto de blooming causado por un contraste excesivo contra el texto claro.
  • Acomodar la diversidad de la visión del color hace falta con independencia del tema. La guía de color de Microsoft pide usar el color como refuerzo visual en lugar de como medio principal de comunicación, y no hacer nunca de la combinación de rojo y verde la única distinción.3515
  • Los temas de contraste son un requisito independiente del modo oscuro. Como explicó el capítulo 2, el modo oscuro no está disponible mientras un tema de contraste está activo, así que por perfecta que sea su compatibilidad con el modo oscuro, nunca llega a los usuarios de temas de contraste.
La relación entre el modo oscuro y la accesibilidadLa compatibilidad con el modo oscuro es una cuestión de preferencia visual y entorno, mientras que los requisitos de accesibilidad de relación de contraste, transmitir información por más que el color solo y la compatibilidad con temas de contraste deben cumplirse por separado con independencia del temaNo sustituye aCompatibilidad con el modo oscuroPreferencia y entornoAccesibilidadRelación de contraste 4.5:1No el color soloCompatibilidad con temas de contraste

Figura 22: La compatibilidad con el modo oscuro aborda la preferencia y el entorno; los requisitos de accesibilidad deben cumplirse por separado.

Por otro lado, el trabajo de «reunir los colores en un solo sitio» del capítulo 5 es el fundamento de ambos. Con la paleta en un solo sitio, puede enumerar qué medir para la relación de contraste en claro y en oscuro, y la rama de tema de contraste se puede escribir en el mismo sitio. Use la compatibilidad con el modo oscuro como ocasión para consolidar la paleta, e inspeccione las relaciones de contraste y los temas de contraste de paso. Ese es el orden con el mejor retorno de la inversión.

10. Decidir una política — recomendaciones por tipo de aplicación

Tipo de aplicación Política recomendada
WinForms nuevo (.NET 10) Usar SetColorMode(System). Basar el dibujo por el propietario en SystemColors, y hacer que los controles personalizados que contienen controles comunes opten con ApplyThemingImplicitly
WPF nuevo (.NET 9/10) Adoptar ThemeMode="System" después de comprobar cómo se representan los controles que usa y cómo se comporta la aplicación bajo temas de contraste. Si es difícil, tema clásico más intercambio de diccionarios
WinForms/WPF existente (.NET Framework 4.8, .NET 8 o anterior) Declarar «claro fijo» y dejar el atributo DWM en su valor predeterminado (FALSE). La compatibilidad con temas de contraste es obligatoria; pasar a la compatibilidad con el modo oscuro al migrar a .NET 10
WinUI 3 Sigue al sistema de forma predeterminada. Definir los colores propios de la aplicación en ThemeDictionaries incluido HighContrast, y poner HighContrastAdjustment en None
Win32 / MFC Atributo DWM + paleta propia + recálculo en WM_THEMECHANGED / WM_SYSCOLORCHANGE. La guía oficial cubre solo la detección y la barra de título; volver a pintar los controles comunes queda fuera de su alcance
El camino de los activos existentes a la compatibilidad con el modo oscuroLos activos existentes declaran primero claro fijo, completan sin falta la compatibilidad con temas de contraste, consolidan la paleta como preparación, luego migran a .NET 10 y pasan a SetColorMode o ThemeMode. Un modo oscuro a medias es una peor experiencia que el claro fijo, así que ese camino no se tomaNo se tomaDeclarar claro fijo (ahora)Compatibilidad con temas de contraste (obligatoria)Consolidar la paleta (preparación)Migrar a .NET 10Pasar a SetColorMode / ThemeModeModo oscuro a mediasPeor experiencia que el claro fijo

Figura 23: Los activos existentes parten del claro fijo, pasan por la compatibilidad con temas de contraste y la consolidación de la paleta, y van a la compatibilidad con el modo oscuro al migrar a .NET 10.

El «claro fijo» no es una derrota. Es el comportamiento predeterminado de Windows mismo, y un comportamiento documentado. Una aplicación que es consistentemente clara es mucho mejor para los usuarios que un modo oscuro a medias que se entrega con «solo la barra de título negra» o «solo las barras de desplazamiento blancas». Sin embargo, la compatibilidad con temas de contraste es lo único que no se puede «fijar». Entra en el ajuste razonable cubierto en el artículo anterior: es una cuestión de si la aplicación es usable, no una cuestión de preferencia de tema.

11. Lista de verificación

Una vez hecho el trabajo, verifique en una máquina real en el orden siguiente. Cada paso se puede cambiar desde la aplicación Configuración en cuestión de segundos.

  1. Cambiar claro/oscuro mientras la aplicación se ejecuta. Cambie el modo en Configuración > Personalización > Colores, y confirme que tanto la barra de título como el área de cliente siguen, o que la aplicación se comporta como se especifica con «surte efecto en el siguiente inicio» (SetColorMode en WinForms no sigue).
  2. Provocar una recreación de identificador. En WinForms, alterne ShowInTaskbar en tiempo de ejecución y confirme que se conserva el atributo de la barra de título.
  3. Probar los cuatro temas de contraste. Alterne con Alt izquierdo + Mayús izquierdo + Impr Pant, y confirme en cada uno de Aquatic, Desert, Dusk y Night sky que el texto, los bordes, las filas seleccionadas, los elementos deshabilitados y los vínculos son legibles.1
  4. Editar los colores de un tema de contraste. Los usuarios de verdad cambian los colores. Edite el fondo a un color extremo para sacar a la luz los colores que aún estén codificados de forma fija.
  5. Revisar los registros. Confirme que un error de DwmSetWindowAttribute o SystemParametersInfo en un SO compatible queda registrado, y que en Windows 10 por debajo de la compilación 22000 la aplicación arranca clara sin llamar al atributo DWM.
  6. Medir las relaciones de contraste. En claro y en oscuro, compruebe cada combinación de color de texto y de fondo de la paleta frente a 4.5:1.14
Pasos de verificación de la compatibilidad con temasVerificar en una máquina real en este orden: cambiar claro/oscuro en tiempo de ejecución, recreación de identificador, los cuatro temas de contraste, editar los colores del tema, revisar los registros de error y medir las relaciones de contrasteCambiar claro/oscuro en tiempo de ejecuciónRecreación de identificadorLos cuatro temas de contrasteEditar los colores del temaRevisar los registros de errorMedir las relaciones de contraste

Figura 24: La verificación empieza por cambiar la configuración y cierra con revisar los registros y las relaciones de contraste.

12. Resumen

  • Los temas de Windows tienen dos ejes, claro/oscuro y temas de contraste, y el modo oscuro no está disponible mientras el segundo está activo. Detecte primero el tema de contraste.
  • La barra de título de una aplicación existente es blanca porque ese es el valor predeterminado de compatibilidad; pase TRUE para DWMWA_USE_IMMERSIVE_DARK_MODE (valor 20, Windows 11, compilación 22000 o posterior) mediante DwmSetWindowAttribute, y se dibuja oscura cuando el sistema está oscuro. Establézcalo cada vez que se crea el HWND, y registre los errores.
  • Decida el modo actual a partir del brillo del color de primer plano de UISettings.GetColorValue, note los cambios con ColorValuesChanged, vuelva al hilo de UI y vuelva a pintar. Mantenga los colores reunidos en un solo sitio.
  • WinForms: Application.SetColorMode(SystemColorMode.System) en .NET 9/10. Conozca las tres restricciones (solo Windows 11, deshabilitado durante un tema de contraste, sin seguimiento de cambios en tiempo de ejecución) y ApplyThemingImplicitly para controles personalizados.
  • WPF: ThemeMode="System" en .NET 9/10. La manipulación desde código es experimental y Fluent está en curso, así que evalúe antes de adoptar. En el tema clásico, intercambio de diccionarios más DynamicResource.
  • Bajo un tema de contraste, detecte con la familia SPI_GETHIGHCONTRAST, asigne los colores a los pares de colores del sistema, omita las imágenes detrás del texto y dibuje los gráficos de varios colores en dos colores. GrayText es para el estado deshabilitado, Hotlight solo para vínculos.
  • La compatibilidad con el modo oscuro no sustituye a la compatibilidad con la accesibilidad. 4.5:1 sigue aplicándose en el modo oscuro, la información no debe depender solo del color, y la compatibilidad con temas de contraste se exige por separado.
  • Para activos existentes, declarar «claro fijo» es la respuesta realista, y la compatibilidad con temas de contraste es lo único que no se puede fijar.

Como primer paso recomendado, elija una pantalla principal, active primero un tema de contraste con Alt izquierdo + Mayús izquierdo + Impr Pant y mírela, vuelva con las mismas teclas y luego cambie la configuración de color de Windows a oscuro (el modo oscuro no está disponible mientras un tema de contraste está activo, así que pruebe ambos por separado). En unos minutos verá «dónde guarda los colores su aplicación».

Artículos relacionados

Áreas de consultoría relacionadas

KomuraSoft LLC se encarga de la compatibilidad con el modo oscuro de aplicaciones empresariales WinForms/WPF (consolidar la paleta, evaluar una migración a SetColorMode / ThemeMode en .NET 9/10, integrar el atributo DWM), de diagnosticar y corregir roturas de visualización bajo temas de contraste, y de consultas sobre el seguimiento de tema en activos Win32/MFC. Empezar desde la etapa de «los empleados se quejaron en cuanto pasaron al modo oscuro» está bien.

Referencias

  1. Microsoft Learn, Contrast themes. Sobre que los temas de contraste usan una paleta restringida de aproximadamente 7:1 o superior y no deben confundirse con los temas claros y oscuros; los cuatro temas Aquatic, Desert, Dusk y Night sky y la edición de sus colores; el cambio con Alt izquierdo + Mayús izquierdo + Impr Pant; los pares de primer plano/fondo y los usos de los recursos SystemColor; usar GrayText solo para el estado deshabilitado y Hotlight solo para vínculos; las roturas por colores codificados de forma fija; los bordes de límite; HighContrast en ThemeDictionaries; poner HighContrastAdjustment en None; y la detección con Microsoft.UI.System.ThemeSettings.  2 3 4 5 6 7 8

  2. Microsoft Learn, Application.SetColorMode(SystemColorMode) Method. Sobre llamarlo antes de crear elementos de UI, sobre que la aplicación no se adapta automáticamente cuando cambia la configuración del sistema incluso con System, y sobre que el modo de color oscuro solo está disponible en Windows 11 o posterior y no está disponible en modo de alto contraste.  2 3 4

  3. Microsoft Learn, Support Dark and Light themes in Win32 apps. Sobre la definición de primer plano y fondo en los modos de color; sobre que Windows da una barra de título clara de forma predeterminada por compatibilidad porque no puede saber si una aplicación es compatible con el modo oscuro; sobre el procedimiento de obtener el color de primer plano con UISettings.GetColorValue y clasificar claro u oscuro por luminancia percibida para detectar el modo oscuro; sobre el seguimiento con ColorValuesChanged; sobre activar la barra de título oscura con DwmSetWindowAttribute y DWMWA_USE_IMMERSIVE_DARK_MODE (valor 20); y sobre que toda la superficie debe seguir el modo oscuro.  2 3 4 5 6 7 8 9 10

  4. Microsoft Learn, DWMWINDOWATTRIBUTE enumeration (dwmapi.h). Sobre que DWMWA_USE_IMMERSIVE_DARK_MODE permite dibujar el marco oscuro cuando está activada la configuración oscura del sistema y que todas las ventanas son claras de forma predeterminada; sobre los valores COLORREF de DWMWA_BORDER_COLOR, DWMWA_CAPTION_COLOR y DWMWA_TEXT_COLOR y restaurar el valor predeterminado con DWMWA_COLOR_DEFAULT; sobre la compatibilidad a partir de Windows 11, compilación 22000; y sobre la compatibilidad de DWMWA_SYSTEMBACKDROP_TYPE a partir de la compilación 22621.  2 3 4

  5. Microsoft Learn, UISettings.ColorValuesChanged Event. Sobre el evento que se dispara cuando cambia un valor de color.  2

  6. Microsoft Learn, What’s new in Windows Forms for .NET 9. Sobre la compatibilidad preliminar experimental con el modo oscuro, el cambio correspondiente de SystemColors cuando cambia el modo de color, los tres valores SystemColorMode Classic, System y Dark, la llamada a Application.SetColorMode en el código de inicio, y la supresión de WFO5001.  2 3

  7. Microsoft Learn, What’s new in Windows Forms for .NET 10. Sobre la integración completa del modo oscuro y que SetColorMode ya no es experimental, sobre que los controles comunes de Win32 dentro de controles dibujados por el propietario se quedan claros si no optan, y sobre la necesidad de llamar a SetStyle(ControlStyles.ApplyThemingImplicitly) antes de base.CreateParams dentro de la invalidación de CreateParams porque el constructor llega demasiado tarde.  2 3 4

  8. Microsoft Learn, What’s new in WPF for .NET 9. Sobre el tema Fluent compatible con claro/oscuro y el color de énfasis, los cuatro valores ThemeMode Light, Dark, System y None y establecerlo en Application o Window, aplicarlo mediante diccionarios de recursos, y que establecer ThemeMode desde código es experimental y exige suprimir WPF0001.  2 3 4

  9. Microsoft Learn, What’s new in WPF for .NET 10. Sobre que la compatibilidad con estilos de interfaz Fluent sigue en curso, los estilos Fluent añadidos para DatePicker, GridSplitter, GridView, GroupBox, Hyperlink, Label, NavigationWindow, RichTextBox y TextBox, y las correcciones de bloqueos relacionadas con HighContrast.  2 3

  10. Microsoft Learn, Application.ThemeMode Property. Sobre que controla si el tema Fluent se carga en modo claro, oscuro o del sistema y también controla la aplicación del material de fondo y del modo oscuro a la ventana, que ThemeMode y Resources están diseñados para permanecer sincronizados a fin de evitar inconsistencia, y que lleva el atributo Experimental(“WPF0001”) y puede eliminarse en el futuro.  2 3 4

  11. Microsoft Learn, High contrast parameter. Sobre obtener la estructura HIGHCONTRAST con SPI_GETHIGHCONTRAST en la inicialización y al tratar WM_SYSCOLORCHANGE y comprobar HCF_HIGHCONTRASTON, y, cuando está activado, asignar cada color a un par de COLOR_WINDOWTEXT y COLOR_WINDOW o COLOR_BTNTEXT y COLOR_BTNFACE, omitir las imágenes de mapa de bits detrás del texto, y dibujar las imágenes de varios colores en los colores de primer plano y de fondo.  2 3

  12. Microsoft Learn, High-contrast mode. Sobre que Aero tiene texto negro y un color de selección azul claro pero High Contrast Black tiene un color de selección negro, lo que puede producir texto negro sobre negro; que COLOR_HIGHLIGHTTEXT está diseñado para emparejarse con COLOR_HIGHLIGHT y COLOR_WINDOWTEXT con COLOR_WINDOW; no codificar de forma fija los colores de texto; construir una UI que no dependa del tema porque los usuarios personalizan los colores; recalcular los colores en WM_THEMECHANGED; y que SPI_GETHIGHCONTRAST es la única forma compatible de comprobar.  2 3

  13. Microsoft Learn, Walkthrough: Creating an Accessible Windows-based Application. Sobre la detección con SystemInformation.HighContrast; usar el esquema de color del sistema cuando está activado, añadir indicios visuales a la información transmitida por color, y omitir las imágenes detrás del texto; comprobar al inicio y seguir el evento UserPreferenceChanged; y el ejemplo de cambiar los colores de una etiqueta con SystemColors.  2 3

  14. W3C / traducción japonesa del Web Accessibility Infrastructure Committee (WAIC), Web Content Accessibility Guidelines (WCAG) 2.1, Japanese translation. Sobre el criterio de conformidad 1.4.3 (Contrast (Minimum)) con 4.5:1 para el texto y 3:1 para el texto grande, y el criterio de conformidad 1.4.1 (Use of Color).  2 3

  15. Microsoft Learn, Color in Windows. Sobre que Windows tiene dos modos de color, claro y oscuro, que la elección del color de énfasis y del tema se refleja en toda la experiencia del usuario, y sobre garantizar el contraste y acomodar la diversidad de la visión del color.  2

  16. Microsoft Learn, Reference for Windows 11 and Windows 10 settings. Sobre que AppsUseLightTheme y SystemUsesLightTheme en HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize son valores DWORD que representan el modo claro/oscuro de las aplicaciones y de Windows. 

  17. Microsoft Learn, Theming in Windows apps. Sobre que quitar RequestedTheme hace que la aplicación siga la configuración del sistema, que el sistema sobrescribe RequestedTheme cuando el usuario elige un tema de alto contraste, y que las plantillas personalizadas deberían usar pinceles de tema en lugar de colores codificados de forma fija. 

  18. Microsoft Learn, DwmSetWindowAttribute function (dwmapi.h). Sobre la función que establece los atributos de representación DWM del área no cliente de una ventana, y su disponibilidad a partir de Windows Vista. 

  19. Microsoft Learn, Retrieve a window handle (HWND). Sobre obtener el Handle de WindowInteropHelper en WPF. 

  20. Microsoft Learn, DWM_SYSTEMBACKDROP_TYPE enumeration (dwmapi.h). Sobre que DWMSBT_MAINWINDOW corresponde a Mica y DWMSBT_TRANSIENTWINDOW a Acrylic en Windows 11, que el efecto del material puede cambiar en versiones futuras de Windows, y sobre la compatibilidad a partir de Windows 11, compilación 22621. 

  21. Microsoft Learn, UISettings.GetColorValue(UIColorType) Method. Sobre el método que devuelve el valor de color del UIColorType especificado. 

  22. Microsoft Learn, WM_SETTINGCHANGE message. Sobre el mensaje enviado a todas las ventanas de nivel superior cuando SystemParametersInfo cambia una configuración de todo el sistema o cuando cambia una configuración de directiva. 

  23. Microsoft Learn, SystemEvents.UserPreferenceChanged Event. Sobre el evento estático que se dispara cuando cambia una preferencia de usuario, y la pérdida de memoria que resulta de no desasociar el controlador. 

  24. Microsoft Learn, WM_THEMECHANGED message. Sobre que se difunde a todas las ventanas después de que se activa, se desactiva o se cambia un tema, y que los identificadores de tema existentes se vuelven inválidos y deben reabrirse. 

  25. Microsoft Learn, WM_SYSCOLORCHANGE message. Sobre que se envía a todas las ventanas de nivel superior cuando cambia una configuración de color del sistema, que los pinceles que usan colores del sistema deben recrearse, y que debe reenviarse a los controles comunes. 

  26. Microsoft Learn, Compiler Error WFO5001. Sobre que SetColorMode y SystemColorMode estaban protegidos como características experimentales para evaluación en .NET 9, y que el error no se aplica a partir de .NET 10. 

  27. Microsoft Learn, SystemColors.UseAlternativeColorSet Property. Sobre que establecerlo en true hace que los valores KnownColor del sistema devuelvan un conjunto de colores alternativo (actualmente la versión del modo oscuro), que es una característica experimental bajo SYSLIB5002, y que los valores KnownColor del sistema siempre devuelven los colores actuales de Windows cuando un tema de alto contraste está activo en Windows. 

  28. Microsoft Learn, HIGHCONTRASTW structure (winuser.h). Sobre HCF_HIGHCONTRASTON (0x00000001) en dwFlags, y la necesidad de especificar cbSize al usarlo con SPI_GETHIGHCONTRAST. 

  29. Microsoft Learn, SystemParameters.HighContrast Property. Sobre la propiedad estática de WPF que se asigna a SPI_GETHIGHCONTRAST y HCF_HIGHCONTRASTON. 

  30. Microsoft Learn, SystemParameters.StaticPropertyChanged Event. Sobre el evento estático que se dispara cuando cambia cualquier propiedad de SystemParameters.  2

  31. Microsoft Learn, ThemeSettings Class (Microsoft.UI.System). Sobre crearlo ligado a una ventana con CreateForWindowId y recibir cambios de alto contraste mediante el evento Changed, y sobre que soltar la referencia destruye el objeto y el evento ya no se dispara. 

  32. Microsoft Learn, SystemColors.WindowBrushKey Property. Sobre que una referencia dinámica hecha con la clave de recurso se actualiza de forma automática cuando cambia el pincel, y que una referencia estática mediante WindowBrush no lo hace. 

  33. Microsoft Learn, ResourceDictionary.ThemeDictionaries Property (Microsoft.UI.Xaml). Sobre que un control personalizado con diccionarios de tema Light y Dark también debería proporcionar un diccionario HighContrast, que HighContrast es la clave de reserva cuando no existe otro tema de alto contraste, que Default se usa cuando no se encuentra un ResourceDictionary para el tema, y que se pueden usar recursos de color del sistema como SystemColorButtonFaceColor en HighContrast. 

  34. Microsoft Learn, Windows app development best practices. Sobre que Windows 11 se alejó del blanco puro y del negro puro hacia tonos más suaves para los ojos, y que los temas oscuros y claros son un medio de adaptarse a la preferencia visual del usuario. 

  35. Microsoft Learn, Color (Windows UX guidelines). Sobre usar el color como refuerzo visual en lugar de como medio principal de comunicación, elegir colores de tema y colores del sistema por propósito y usar primer plano y fondo en pares coincidentes, tratar los cambios de tema con WM_THEMECHANGED, y sobre que High Contrast Black corresponde a Aquatic y High Contrast White a Desert en Windows 11. 

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

Cambié Windows al modo oscuro, pero la barra de título de nuestra aplicación WinForms interna sigue blanca. ¿Por qué?
Porque Windows no tiene forma de saber si una aplicación es compatible con el modo oscuro, así que por compatibilidad trata cada ventana como modo claro de forma predeterminada. El área no cliente, incluida la barra de título, la dibuja el Administrador de ventanas de escritorio (DWM), y el marco se dibuja oscuro cuando el sistema está oscuro solo después de que la aplicación pasa TRUE para DWMWA_USE_IMMERSIVE_DARK_MODE (valor 20) mediante DwmSetWindowAttribute. La compatibilidad con este atributo está documentada para Windows 11, compilación 22000 y posteriores. Si usa Application.SetColorMode en WinForms en .NET 9 o posterior, o ThemeMode en WPF en .NET 9 o posterior, el marco hace esta llamada por usted, así que llamarlo usted mismo solo hace falta en aplicaciones de .NET 8 o anterior, .NET Framework o Win32/MFC. Tenga en cuenta que una barra de título oscura sobre un área de cliente que sigue blanca queda peor, no mejor. Active este atributo solo cuando esté listo para volver a pintar toda la aplicación en oscuro.
¿El modo oscuro y los temas de contraste (alto contraste) son lo mismo?
No. Claro/oscuro es el modo de color en Configuración > Personalización > Colores, y usa una paleta amplia que intercambia el brillo del primer plano y del fondo. Un tema de contraste se elige en Configuración > Accesibilidad > Temas de contraste y usa una paleta restringida con una relación de contraste de aproximadamente 7:1 o superior (los cuatro temas incluidos Aquatic, Desert, Dusk y Night sky, más los colores que el usuario haya editado). La documentación de Microsoft dice explícitamente que no se confundan ambos, y el modo oscuro no está disponible mientras un tema de contraste está activo (SetColorMode en WinForms no proporciona modo oscuro durante un tema de contraste, y RequestedTheme en XAML lo sobrescribe el sistema). En la implementación, compruebe primero si hay un tema de contraste activo y, si lo hay, remítase por completo a los colores del sistema; solo entonces elija la paleta clara u oscura. Ese es el orden de precedencia.
¿Cuál es la forma más corta de hacer que una aplicación WinForms sea compatible con el modo oscuro?
En .NET 9 o posterior, el camino más corto es llamar a Application.SetColorMode(SystemColorMode.System) antes de Application.Run en Program.cs. En .NET 9 era una característica experimental, así que había que suprimir WFO5001 en el archivo de proyecto; a partir de .NET 10 funciona sin supresión. Llamar a SetColorMode cambia SystemColors a un conjunto alternativo para el modo oscuro, y los controles estándar se dibujan en consecuencia. Hay tres advertencias. Primera, el modo oscuro solo está disponible en Windows 11 o posterior y está deshabilitado mientras un tema de contraste está activo. Segunda, incluso con SystemColorMode.System, la aplicación no sigue un cambio de la configuración de Windows mientras se ejecuta (el cambio surte efecto en el siguiente inicio). Tercera, si un control dibujado por el propietario usa controles comunes de Win32 como barras de desplazamiento, debe invalidar CreateParams y llamar a SetStyle(ControlStyles.ApplyThemingImplicitly, true) antes de base.CreateParams (el constructor llega demasiado tarde).
¿Qué necesita una aplicación WPF para seguir el modo oscuro?
WPF en .NET 9 o posterior incluye un tema nuevo que sigue el diseño Fluent de Windows 11, y escribir ThemeMode="System" en el elemento Application de App.xaml basta para cargar el tema Fluent que coincide con la configuración claro/oscuro de Windows. ThemeMode también controla el oscurecimiento de la ventana (la barra de título) y la aplicación del material de fondo. Sin embargo, leer y escribir la propiedad ThemeMode desde código sigue siendo experimental en .NET 10 (WPF0001), y los estilos Fluent en sí se describen como "still in progress" en la documentación de .NET 10. Antes de adoptarlo en una aplicación empresarial, evalúe si los controles que usa se representan correctamente bajo Fluent. Si se queda en el tema clásico (lo mismo vale para .NET 8 y anterior y para .NET Framework), prepare un ResourceDictionary claro y uno oscuro, intercámbielos en MergedDictionaries, refiéralos desde XAML con DynamicResource y use UISettings.ColorValuesChanged para detectar el cambio. Para la barra de título, obtenga el HWND de WindowInteropHelper en SourceInitialized y llame a DwmSetWindowAttribute.
¿Por qué el texto desaparece o se vuelve ilegible bajo un tema de contraste (alto contraste)?
Las causas típicas son colores codificados de forma fija, o romper el emparejamiento de los colores del sistema de primer plano y de fondo. Bajo un tema de contraste el usuario puede editar libremente el fondo, el texto, el vínculo y otros colores, así que cualquier supuesto del tipo "el texto será negro" o "la fila seleccionada será azul claro" se viene abajo. Por ejemplo, si solo el fondo está fijo en #E6E6E6, algunos temas dan un primer plano blanco, y el texto blanco sobre gris claro se vuelve ilegible. Hay tres principios. Detecte el estado con SPI_GETHIGHCONTRAST (SystemInformation.HighContrast en WinForms, SystemParameters.HighContrast en WPF); sustituya cada color por el par de colores del sistema correcto (WindowText con Window, ButtonText con ButtonFace, HighlightText con Highlight); y deje de lado las imágenes detrás del texto y los gráficos de varios colores, dibujando solo con los colores de primer plano y de fondo. GrayText no debe usarse para nada que no sea el estado deshabilitado, ni Hotlight para nada que no sean hipervínculos. Los cambios los anuncian WM_SYSCOLORCHANGE y WM_THEMECHANGED (SystemEvents.UserPreferenceChanged en .NET), así que vuelva a calcular los colores ahí y vuelva a pintar.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog