在 Win32 应用中支持深色和浅色主题

Windows 支持浅色和深色主题作为 Windows 设置中的个性化选项。 Windows 默认使用浅色模式,但用户可以选择深色模式,这会将大部分 UI 更改为深色。 用户可能更喜欢此设置,因为在光线较暗的环境中更容易看到,或者他们可能只是更喜欢颜色较深的界面。 此外,较深的 UI 颜色可以减少某些类型的计算机显示器(例如 OLED 屏幕)的电池使用量。

A split image of an app in light theme on the left, and dark theme on the right.

我们正在努力在不破坏现有应用程序的情况下扩大对深色模式的支持,为此我们提供了更新 Win32 桌面 Windows 应用以支持浅色模式和深色模式的技术指导。

深色模式与浅色模式

设置中的颜色模式(包括浅色和深色模式)是定义操作系统和应用整体前景色和背景色的设置。

“模式” 说明 示例
浅色 具有对比深色前景的浅色背景。

在浅色模式下,通常会在白色或浅色背景上看到黑色或深色文本。
A screenshot of the Alarms & Clock app in light mode
深色 具有对比浅色前景的深色背景。

在深色模式下,通常会在黑色或深色背景上看到白色或浅色文本。
A screenshot of the Alarms & Clocks app in Dark mode

注意

我们之所以使用“黑色或深色”和“白色或浅色”的原因是因为有额外的颜色,例如可以着色各种前景色和背景色的主题色。 因此,在 UI 的某些部分,实际上可能会在深蓝色背景上看到浅蓝色文本,这仍然被认为是可接受的深色模式 UI。

由于不同应用中 UI 的多样性,颜色模式、前景色和背景色更多地是一种指导方针,而不是硬性规定:

  • 前景元素、高亮颜色和文本应比背景色更接近前景色。
  • 大的纯色背景区域和文本背景通常应该比前景色更接近背景色。

实际上,这意味着在深色模式下,大部分 UI 将是深色,而在浅色模式下,大部分 UI 将是浅色。 Windows 中背景的概念是应用中的大面积颜色,或页面颜色。 Windows 中前景的概念是文本颜色。

提示

如果你对前景色在深色模式下为浅色而在浅色模式下为深色感到困惑,将前景色视为“默认文本颜色”可能会有所帮助。

实现对切换颜色模式的支持

在应用程序中实现深色模式支持有多种方法。 一些应用包含两组 UI(一组为浅色,一组为深色)。 某些 Windows UI 框架(例如 WinUI 3)会自动检测系统主题并调整 UI 以遵循系统主题。 若要完全支持深色模式,应用的整个界面必须遵循深色主题。

你可以在 Win32 应用中执行两项主要操作来支持浅色和深色主题。

  • 了解何时启用深色模式

    了解何时在系统设置中启用深色模式可以帮助你了解何时将应用 UI 切换为深色模式主题的 UI。

  • 为 Win32 应用程序启用深色模式标题栏

    并非所有 Win32 应用程序都支持深色模式,因此 Windows 默认为 Win32 应用提供浅色标题栏。 如果准备支持深色模式,可以在启用深色模式时让 Windows 绘制深色标题栏。

注意

本文提供了检测系统主题更改以及为 Win32 应用程序窗口请求浅色或深色标题栏的方法示例。 不包括如何使用深色模式颜色集重新绘制和呈现应用 UI 的细节。

了解何时启用深色模式

第一步是跟踪颜色模式设置本身。 这样,就可以调整应用程序的绘制和呈现代码,以使用深色模式颜色集。 这样做需要应用在启动时读取颜色设置,并知道在应用会话期间颜色设置何时更改。

若要在 Win32 应用程序中执行此操作,请使用 Windows::UI::Color 并检测颜色是否可以分类为浅色或深色。 若要使用 Windows::UI::Color,需要从 winrt 导入(在 pch.h 中)Windows.UI.ViewManagement 标头。

#include <winrt/Windows.UI.ViewManagement.h>

还将该命名空间包含在 main.cpp 中。

using namespace Windows::UI::ViewManagement;

main.cpp 中,使用此函数来检测颜色是否可以归类为浅色。

inline bool IsColorLight(Windows::UI::Color& clr)
{
    return (((5 * clr.G) + (2 * clr.R) + clr.B) > (8 * 128));
}

此函数对颜色的感知亮度进行快速计算,并考虑 RGB 颜色值中的不同通道如何影响对人眼看起来的亮度。 它使用全整数数学来提高典型 CPU 的速度。

注意

这不是真正分析颜色亮度的模型。 它适用于需要你确定颜色是否可以分类为浅色或深色的快速计算。 主题颜色通常可以是浅色但不是纯白色,或者是深色但不是纯黑色。

现在,有一个函数可以检查颜色是否为浅色,你可以使用该函数来检测是否启用了深色模式。

深色模式定义为具有对比浅色前景的深色背景。 由于 IsColorLight 会检查颜色是否被认为是浅色,因此可以使用该函数来查看前景是否是浅色。 如果前景为浅色,则启用深色模式。

为此,需要从系统设置获取前景的 UI 颜色类型。 在 main.cpp 中使用此代码。

auto settings = UISettings();
    
auto foreground = settings.GetColorValue(UIColorType::Foreground);

UISettings 获取 UI 的所有设置,包括颜色。 调用 UISettings.GetColorValue(UIColorType::Foreground),从 UI 设置中获取前景色值。

现在,可以运行检查以查看前景是否被认为是浅色(在 main.cpp 中)。

bool isDarkMode = static_cast<bool>(IsColorLight(foreground));

wprintf(L"\nisDarkMode: %u\n", isDarkMode);
  • 如果前景为浅色,则 isDarkMode 计算结果为 1 (true),表示启用深色模式。
  • 如果前景为深色,则 isDarkMode 计算结果为 0 (false),表示不启用深色模式。

若要在应用会话期间自动跟踪深色模式设置何时更改,可以像这样包装检查。

auto revoker = settings.ColorValuesChanged([settings](auto&&...)
{
    auto foregroundRevoker = settings.GetColorValue(UIColorType::Foreground);
    bool isDarkModeRevoker = static_cast<bool>(IsColorLight(foregroundRevoker));
    wprintf(L"isDarkModeRevoker: %d\n", isDarkModeRevoker);
});

完整代码应如下所示。

inline bool IsColorLight(Windows::UI::Color& clr)
{
    return (((5 * clr.G) + (2 * clr.R) + clr.B) > (8 * 128));
}

int main()
{
    init_apartment();

    auto settings = UISettings();
    auto foreground = settings.GetColorValue(UIColorType::Foreground);

    bool isDarkMode = static_cast<bool>(IsColorLight(foreground));
    wprintf(L"\nisDarkMode: %u\n", isDarkMode);

    auto revoker = settings.ColorValuesChanged([settings](auto&&...)
        {
            auto foregroundRevoker = settings.GetColorValue(UIColorType::Foreground);
            bool isDarkModeRevoker = static_cast<bool>(IsColorLight(foregroundRevoker));
            wprintf(L"isDarkModeRevoker: %d\n", isDarkModeRevoker);
        });
    
    static bool s_go = true;
    while (s_go)
    {
        Sleep(50);
    }
}

运行此代码时:

如果启用深色模式,则 isDarkMode 计算结果为 1。

A screenshot of an app in dark mode.

将设置从深色模式更改为浅色模式将使 isDarkModeRevoker 计算结果为 0。

A screenshot of an app in light mode.

为 Win32 应用程序启用深色模式标题栏

Windows 不知道应用程序是否支持深色模式,因此出于向后兼容性的原因,它假定不支持。 某些 Windows 开发框架(例如 Windows 应用 SDK)原生支持深色模式,并且无需任何额外代码即可更改某些 UI 元素。 Win32 应用通常不支持深色模式,因此 Windows 默认为 Win32 应用提供浅色标题栏。

但是,对于任何使用标准 Windows 标题栏的应用,你可以在系统为深色模式时启用深色版本的标题栏。 若要启用深色标题栏,请使用窗口属性 DWMWA_USE_IMMERSIVE_DARK_MODE 在最上层窗口上调用名为 DwmSetWindowAttribute桌面窗口管理器 (DWM) 函数。 (DWM 呈现窗口的属性。)

以下示例假设你有一个具有标准标题栏的窗口,就像这段代码创建的那样。

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   hInst = hInstance; // Store instance handle in our global variable

   HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, 0, 
     CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

   if (!hWnd)
   {
      return FALSE;
   }

   ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);

   return TRUE;
}

首先,你需要像这样导入 DWM API。

#include <dwmapi.h>

然后,在 InitInstance 函数上方定义 DWMWA_USE_IMMERSIVE_DARK_MODE 宏。

#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
…

最后,你可以使用 DWM API 将标题栏设置为使用深色。 在这里,你创建一个名为 valueBOOL 并将其设置为 TRUE。 此 BOOL 用于触发此 Windows 属性设置。 然后,你可以使用 DwmSetWindowAttribute 将窗口属性更改为使用深色模式颜色。

BOOL value = TRUE;
::DwmSetWindowAttribute(hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &value, sizeof(value));

以下部分更详细地解释了此调用的作用。

DwmSetWindowAttribute 的语法块如下所示。

HRESULT DwmSetWindowAttribute(
       HWND    hwnd,
       DWORD   dwAttribute,
  [in] LPCVOID pvAttribute,
       DWORD   cbAttribute
);

hWnd(要更改的窗口的句柄)作为第一个参数传递后,你需要将 DWMWA_USE_IMMERSIVE_DARK_MODE 作为 dwAttribute 参数传递。 这是 DWM API 中的一个常量,当启用深色模式系统设置时,可以使用它来以深色模式颜色绘制 Windows 框架。 如果切换到浅色模式,则必须将 DWMWA_USE_IMMERSIVE_DARK_MODE 从 20 更改为 0,标题栏才能以浅色模式颜色绘制。

pvAttribute 参数指向 BOOL 类型的值(这就是你之前创建 BOOL 值的原因)。 需要将 pvAttribute 设置为 TRUE 才能支持窗口的深色模式。 如果 pvAttributeFALSE,则窗口将使用浅色模式。

最后,cbAttribute 需要在 pvAttribute 中设置属性的大小。 为了轻松做到这一点,我们传入 sizeof(value)

绘制深色窗口标题栏的代码应如下所示。

#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif


BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   hInst = hInstance; // Store instance handle in our global variable

   HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

   BOOL value = TRUE;
   ::DwmSetWindowAttribute(hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &value, sizeof(value));

   if (!hWnd)
   {
      return FALSE;
   }

   ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);

   return TRUE;
}

运行此代码时,应用标题栏应为深色:

A screenshot of an app with a dark title bar.

另请参阅