😺

Win32 アプリでダークモード対応

2022/07/24に公開

Windows の

「個人用設定」→「色」→「色を選択する」(Win10) あるいは「モードを選ぶ」(Win11)

で、ダーク(モード)が選べますが、Windows は Win32 デスクトップ・アプリに対しては互換性のため DPI スケーリングのような自動対応をしてくれないようです。

ということで Windows/vc++ で作成するゲーム系アプリにて、ダークモードに対応するための覚書。

ダークモードかどうかの取得

レジストリを使う場合

現在のテーマがダークかどうかですが、とりあえずレジストリに現在 'ライト' モードか否かを表す DWORD 値があるのでそれを使います。

"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"

の キー 'AppsUseLightTheme' 'SystemUsesLightTheme' がそれで、'ライト' のとき DWORD値が 1、'ダーク'のとき 0 になります。

「色を選択する」「モードを選ぶ」で、'ライト'、'ダーク'、'カスタム' が選べ、'カスタム' ではさらに

  • 「既定のアプリモード」
  • 「既定のWindowsモード」

で 'ライト'、'ダーク' が選べるのですが、「色を選択する」「モードを選ぶ」での 'ライト'、'ダーク' の挙動は、このカスタム2項目を同じ値に設定しているようでした。

これらは

  • 「既定のアプリモード」を変えると 各アプリ/Window の色が変化
  • 「既定のWindowsモード」を変えると タスクバー の色が変化

するようです。

キーのほうですが、

キー 'カスタム'での項目 ハイコントラスト設定時
AppsUseLightTheme 「既定のアプリモード」に連動 1(ライト)
SystemUsesLightTheme 「既定のWindowsモード」に連動 0(ダーク)

のような動作になっていました。

なので、主にアプリで使うのは、「既定のアプリモード」に連動する AppsUseLightTheme のほうでしょう。
SystemUsesLightTheme はタスクトレイに入る場合などに参照すればいいのかもしれません。

※ ハイコントラストの時の挙動は?ですが、試すとそういう挙動だったということで。

// レジストリからDWORD値取得.
bool registryGetDWord(HKEY hKeyParent, char const* key, char const* name, DWORD *pData) {
    DWORD len  = sizeof(DWORD);
    HKEY  hKey = nullptr;
    DWORD rc   = RegOpenKeyExA(hKeyParent, key, 0, KEY_READ, &hKey);
    if (rc == ERROR_SUCCESS)
        rc = RegQueryValueExA(hKey, name, nullptr, nullptr, (LPBYTE)(pData), &len);
    RegCloseKey(hKey);
    return (rc == ERROR_SUCCESS);
}

// 「既定のアプリモード」が ダーク か否か.
bool isAppDarkTheme() {
    DWORD lightMode = 1;
    registryGetDWord(HKEY_CURRENT_USER
        , "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"
        , "AppsUseLightTheme"
        , &lightMode);
    return lightMode == 0;
}

// 「既定のWindowsモード」が ダーク か否か.
bool isSystemDarkTheme() {
    DWORD lightMode = 1;
    registryGetDWord(HKEY_CURRENT_USER
        , "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"
        , "SystemUsesLightTheme"
        , &lightMode);
    return lightMode == 0;
}

※ 昔、自分でキーを設定した記憶があるのでオマカン?と焦るも、何もしていない Windows11 機でもレジストリの値が変動して取れているので大丈夫でしょう。

非公式 API を使う場合

非公式 API ですが uxtheme.dll に

bool WINAPI ShouldAppsUseDarkMode();
bool WINAPI ShouldSystemUseDarkMode();

があるようで、

static HMODULE getUxthemeDll() {
    static HMODULE s_uxthemeDll = nullptr;
    if (!s_uxthemeDll)
        s_uxthemeDll = LoadLibraryA("uxtheme.dll");
    return s_uxthemeDll;
}
bool ShouldAppsUseDarkMode() {
    using ShouldAppsUseDarkMode_t       = bool (WINAPI*)();
    static auto s_shouldAppsUseDarkMode = (ShouldAppsUseDarkMode_t)GetProcAddress(getUxthemeDll(), MAKEINTRESOURCEA(132));
    return s_shouldAppsUseDarkMode();
}
bool ShouldSystemUseDarkMode() {    // Windows10 1903 以降.
    using ShouldSystemUseDarkMode_t       = bool (WINAPI*)();
    static auto s_shouldSystemUseDarkMode = (ShouldSystemUseDarkMode_t)GetProcAddress(getUxthemeDll(), MAKEINTRESOURCEA(138));
    return s_shouldSystemUseDarkMode();
}

のような感じで呼び出せます。

試してみると、

  • ShouldAppsUseDarkMode() は AppsUseLightTheme 同様、「既定のアプリモード」に連動
  • ShouldSystemUseDarkMode() は SystemUseLightTheme 同様、「既定の Windows モード」に連動

しているようでした。

なので、レジストリ版、非公式 API 版、どちらでも同じように使えそうです。

非公式 API とはいえ、この2関数の変動の時期は過ぎてそうで将来の変更はなさそうに思いますが、古い OS も考慮するならば、レジストリ版のほうが無難かもしれません。

タイトルバーのダークモード設定

タイトルバーのダークモード設定は、Windows10 でエクスプローラーがダークモード対応になった 2018年付近から API 対応しているようで、
※参考 Microsoft Win32 アプリでのダーク テーマとライト テーマのサポート

#include <dwmapi.h>
// ダークモードする/しないを設定.
bool setUseImmersiveDarkMode(HWND hwnd, bool dark_mode) {
    // enum { DWMWA_USE_IMMERSIVE_DARK_MODE = 20 };    // dwmapi.h で未定義でもコンパイルするならコメントを外す.
    BOOL value = dark_mode;
    DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &value, sizeof(value));
}

のように簡単にできます。

ただ、dwmapi.h の enum DWMWINDOWATTRIBUTE に DWMWA_USE_IMMERSIVE_DARK_MODE が定義されていないことがあるので(というか自分の環境では未定義)、その場合は自前で定義することになります。
といっても #define でなく enum 定義なので、関数や class 内で同名同値を定義してしまえばヘッダ側定義は隠れて問題ないはず。

MSのDWMWINDOWATTRIBUTEの頁には、DWMWA_USE_IMMERSIVE_DARK_MODE は Windows11 Build22000 以降の対応となっていたりしますが、参考のMSの頁のように Windows10 での使い方が書かれていたりするので Windows10 で使っても大丈夫でしょう。

ダークモードの更新

テーマのライト/ダークの設定に変更あると、起動中のアプリには WM_SETTINGCHANGE が lParam に "ImmersiveColorSet" を伴って WindowProc に来るので、

    case WM_SETTINGCHANGE:
        //if (lParam && wcscmp((wchar_t const*)lParam, L"ImmersiveColorSet") == 0)    // W系環境の場合.
        if (lParam && strscmp((char const*)lParam, "ImmersiveColorSet") == 0)         // A系環境の場合.
        {
            static int s_mode = -1;
            bool dark_mode = isDarkModeTheme();
            if (s_mode != dark_mode) {    // 前回とモードが変わっていれば更新.
                s_mode  = dark_mode;
                setUseImmersiveDarkMode(hWnd, dark_mode);
                (…アプリ内に dark_mode を反映させる処理…)
            }
        }
        break;     // その他はDefWindowProcに任せる...

のように反映することができます。

1回ダーク/ライトを切り替えを行うと、case WM_THEMECHANGED: を十数回通るようなので、ここでは簡易に前回と同値なら処理をスキップしています。

その他のダークモード対応

ゲーム系でほぼ自前(というかImGui)で描画している場合、タイトルバーの変更だけでいいのですが。

そうでないアプリではタイトルバー以外の対応もなるべく OS に頼りたいところです。

とりあえず調べた範囲では、ダークモードの取得で使ったように、Win32 アプリでは uxtheme.dll の非公式 API を用いることになりそうです。

モノトーンの伝説日記:System Menu のテーマ切り替えに不具合? に C# の例があるので、それを参考に Windows10 1903 以降専用でお試し。

enum PreferredAppMode {
    APPMODE_DEFAULT    = 0,
    APPMODE_ALLOWDARK  = 1,
    APPMODE_FORCEDARK  = 2,
    APPMODE_FORCELIGHT = 3,
    APPMODE_MAX        = 4,
};
void RefreshImmersiveColorPolicyState() {
    using RefreshImmersiveColorPolicyState_t       = void (WINAPI*)();
    static auto s_refreshImmersiveColorPolicyState = (RefreshImmersiveColorPolicyState_t)GetProcAddress(getUxthemeDll(), MAKEINTRESOURCEA(104));
    return s_refreshImmersiveColorPolicyState();
}
PreferredAppMode SetPreferredAppMode(PreferredAppMode appMode) {
    using SetPreferredAppMode_t       = PreferredAppMode (WINAPI*)(PreferredAppMode appMode);
    static auto s_setPreferredAppMode = (SetPreferredAppMode_t)GetProcAddress(getUxthemeDll(), MAKEINTRESOURCEA(135));
    return s_setPreferredAppMode(appMode);
}

void resetDarkOrLight(HWND hWnd) {
    bool dark_mode = ShouldAppsUseDarkMode();
    setUseImmersiveDarkMode(hWnd, dark_mode);

    // 参考サイトで言及されている不具合対策を用いてライトにするときは1回別モード(ダーク)にしている.
    SetPreferredAppMode(APPMODE_FORCEDARK);
    RefreshImmersiveColorPolicyState();
    if (!dark_mode) {
        SetPreferredAppMode(APPMODE_FORCELIGHT);
        RefreshImmersiveColorPolicyState();
    }
}

実行環境は Windows10 21H2。
メニューバーやダイアログは変わらないけれど、開いたメニューはダークモード対応していました。
全部をダークモードにしようと思うと個別にいろいろ対処がいりそうです。

※ SetPreferredAppMode の 135 は、Windows10 1903 以前では別名別仕様の関数が割り当てられていたようで、ちゃんと対応するなら何某かのOSバージョンチェックが必要になります。

このへんのことは

ysc3839/win32-darkmode

が一番詳しく情報がありそうで、と言うか、これを用いるのが無難なのかもしれません、といいつつ、デモを実行すると、いきなりダークモード未対応といわれてしまい?
バージョンチェックが細かいのですが Windows10 2004 までの対応のようで、21H2 なので弾かれた模様。

チェックを緩めればダークモードになりましたが、しかし、メニューバーがライトのままなのでした。でした。
そういうものなのか。

OS バージョン間の仕様変更や不具合対策等面倒なことを対処してくれているので、必要ならこれを使うのがベターだと思います。
ただ、ソースは、.h ライブラリと思いきや、実体直書きで、複数 #include するとリンクエラーになりそうなので、本格的に使うならばライブラリ化の作業が必要そうです。

MITライセンスなので弄るのに弊害は少ないですが...
巨大な沼が広がっているようにしかみえないので、このへんで引き上げたいと思います。

Discussion