更新履歴(初版のみ・2026年09月02日公開)
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.22250802)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「Windowsアプリのダークモードとコントラストテーマ対応 ── DWMのダークタイトルバー、WinForms/WPFのシステムテーマ追従、ハイコントラスト時の描画」合同会社小村ソフト. https://doi.org/10.5281/zenodo.22250802 https://comcomponent.com/blog/windows-app-dark-mode-contrast-theme-guide/
- DOI(最新版)
- 10.5281/zenodo.22250802
- DOI(この版)
- 10.5281/zenodo.22250803
「社内のPCをWindows 11に入れ替えたら、ダークモードにしている社員から『うちの業務アプリだけタイトルバーが真っ白で目が痛い』と言われた」「弱視の社員がコントラストテーマを有効にしたら、受注画面のステータス表示が消えた」── どちらも、ここ1〜2年で相談が増えている症状です。
前者はWindows 11の「色」の設定、後者は「アクセシビリティ」の設定に起因しますが、開発者の目には「テーマに追従できていない」という同じ問題に見えます。そして実際、対策の土台は共通しています。色をハードコードせず、システムの設定を読み、変更に気づき、塗り直す。この3点です。
前回の「Windowsアプリのアクセシビリティ入門」では、スクリーンリーダーが読む仕組み(UI Automation)と名前付け・キーボード・色の基本を扱いました。その中でコントラストテーマへの追従には触れましたが、ライト/ダークのカラーモードそのものは扱っていません。本記事はその姉妹編として、カラーモード(ライト/ダーク)とコントラストテーマという2つの軸を、DWM(デスクトップウィンドウマネージャー)によるタイトルバーの描画、WinForms/WPFでのシステムテーマ追従、コントラストテーマ時の描画という順に、実装レベルでつなぎます。対象読者はWinForms/WPF/Win32で業務アプリを開発・保守している開発者、前提環境はWindows 11(ダークタイトルバーはビルド22000以降)と.NET 9/10のWinForms・WPF(.NET Framework 4.8や.NET 8では一部を手動で実装)、難易度は中級です。
flowchart TB
accTitle: 本記事の流れ
accDescr: テーマの2軸の整理から、既定でライトになる理由、DWMのダークタイトルバー、検知と追従の仕組み、WinFormsとWPFの実装、コントラストテーマ時の描画、方針の決め方、検証までを順につなぐ本記事の構成
axes["テーマの2軸を整理"] --> why["既定でライトになる理由"]
why --> dwm["DWMのダークタイトルバー"]
dwm --> detect["検知と追従の仕組み"]
detect --> impl["WinForms/WPFの実装"]
impl --> hc["コントラストテーマ時の描画"]
hc --> policy["方針の決め方と検証"]
図1: 本記事はテーマの整理から仕組み・実装・コントラストテーマ・検証までを一本の流れでつなぐ。
1. まず結論
- Windowsの「テーマ」は2軸あります。「設定 > 個人用設定 > 色」のライト/ダーク(カラーモード)と、「設定 > アクセシビリティ > コントラストテーマ」です。後者はおおむね7:1以上のコントラスト比に制約されたパレットで、ライト/ダークとは別物です。コントラストテーマが有効な間はダークモードは使えません。判定の優先順位は「コントラストテーマ → ライト/ダーク」です。12
- 既存アプリのタイトルバーが白いままなのは、互換性のための既定です。Windowsはアプリがダークに対応しているか知る手段がないため、すべてのウィンドウを既定でライト扱いにします。3
- タイトルバーをダークにするのは
DwmSetWindowAttributeのDWMWA_USE_IMMERSIVE_DARK_MODE(値20)です。BOOLのTRUEを渡すと、システムがダークのときにフレームがダークで描かれます。文書化された対応はWindows 11 ビルド22000以降です。43 - 現在のモードは
UISettings.GetColorValueで読み、変更はColorValuesChangedで受け取ります。前景色(既定の文字色)が明るければダーク、という判定がMicrosoftの公式手順です。イベントはUIスレッドで届く保証がないので、UIへ戻してから塗り直します。35 - WinFormsは.NET 9で
Application.SetColorModeが入り、.NET 10で実験的でなくなりました。SystemColorMode.SystemをApplication.Runより前に呼びます。Windows 11専用、コントラストテーマ中は無効、実行中の設定変更には追従しない、という3つの制約があります。672 - WPFは.NET 9でFluentテーマと
ThemeModeが入りました。ThemeMode="System"で追従し、ウィンドウのダーク化も制御されます。ただしコードからの操作は.NET 10でも実験的(WPF0001)で、Fluentスタイルは「作業中」です。従来テーマのままなら、ライト/ダークのResourceDictionaryをDynamicResourceで差し替えます。8910 - コントラストテーマ時は、色をシステムカラーの正しいペアに写像し、文字の背後の画像を省き、多色の図を前景・背景の2色で描きます。判定は
SPI_GETHIGHCONTRAST(WinFormsはSystemInformation.HighContrast、WPFはSystemParameters.HighContrast)、通知はWM_SYSCOLORCHANGE/WM_THEMECHANGED(.NETではSystemEvents.UserPreferenceChanged)です。111213 - ダークモード対応はアクセシビリティ対応の代わりにはなりません。ダークのパレットでも4.5:1のコントラスト比は必要で、色だけに頼らない伝え方はどのテーマでも必要です。1415
一文でまとめるなら、テーマ対応とは「色を一箇所に集め、システムの設定を読み、変更に気づいて塗り直す。ただしコントラストテーマではシステムカラーのペアに全面的に委ねる」ことです。
この記事の知識マップ
Windowsのテーマは「設定 > 個人用設定 > 色」のライト/ダーク(カラーモード)と「設定 > アクセシビリティ > コントラストテーマ」の2軸で、コントラストテーマが有効な間はダークモードが使えないため判定はコントラストテーマが先です。既存アプリのタイトルバーが白いままなのは互換性のための既定で、DwmSetWindowAttributeのDWMWA_USE_IMMERSIVE_DARK_MODE(値20)にTRUEを渡すとシステムがダークのときにダークで描かれます。現在のモードはUISettingsの前景色の明るさで判定し、変更通知で塗り直します。WinFormsは.NET 9/10のSetColorMode、WPFは.NET 9/10のThemeModeが実装を担い、コントラストテーマ時はSPI_GETHIGHCONTRAST系で判定して色をシステムカラーのペアに写像します。標準サポートの無いランタイムではライト固定を明示しますが、コントラストテーマ対応だけは固定できません。
flowchart LR
accTitle: Windowsアプリのダークモードとコントラストテーマ対応の知識マップ
accDescr: ライト/ダークのカラーモードとコントラストテーマの関係、DwmSetWindowAttributeによるダークタイトルバー、UISettingsによる検知とテーマ変更の通知、WinFormsのSetColorModeとWPFのThemeModeによる実装、コントラストテーマの検知APIとシステムカラーの前景/背景ペア、ライト固定の方針を示す図
windows_color_mode["カラーモード(ライト/ダーク)"]
contrast_theme["コントラストテーマ(ハイコントラスト)"]
dwmwa_use_immersive_dark_mode["DWMWA_USE_IMMERSIVE_DARK_MODE"]
dark_title_bar["ダークタイトルバー"]
dwm_set_window_attribute["DwmSetWindowAttribute"]
windows_11["Windows 11"]
dwm_caption_color_attributes["DWMのタイトルバー色属性"]
dwm_system_backdrop["DWMのシステム背景素材(Mica/Acrylic)"]
uisettings_color_values["UISettingsによるカラーモードの取得と変更通知"]
apps_use_light_theme_registry["AppsUseLightThemeレジストリ値"]
theme_change_notification["テーマ変更の通知"]
app_color_palette["アプリのカラーパレットの集約"]
hardcoded_color["色のハードコード"]
system_color_pairing["システムカラーの前景/背景ペア"]
high_contrast_detection["コントラストテーマの検知API"]
winforms_set_color_mode["WinFormsのApplication.SetColorMode"]
winforms_apply_theming_implicitly["ControlStyles.ApplyThemingImplicitly"]
owner_drawn_control["独自描画コントロール"]
wpf_theme_mode["WPFのThemeMode(Fluentテーマ)"]
wpf_resource_dictionary_swap["WPFのリソース辞書の差し替え"]
wpf["WPF"]
winui_theme_dictionaries["WinUIのThemeDictionaries"]
contrast_theme_support["コントラストテーマ対応"]
contrast_ratio["コントラスト比(達成基準1.4.3)"]
light_fixed_policy["ライト固定の方針"]
dotnet_framework[".NET Framework"]
contrast_theme -->|"両立しない"| windows_color_mode
dwmwa_use_immersive_dark_mode -->|"実装を担う"| dark_title_bar
dark_title_bar -->|"で構成できる"| dwm_set_window_attribute
dark_title_bar -->|"前提とする"| windows_color_mode
dwmwa_use_immersive_dark_mode -->|"前提とする"| windows_11
dwm_caption_color_attributes -->|"で構成できる"| dwm_set_window_attribute
dwm_system_backdrop -->|"で構成できる"| dwm_set_window_attribute
dwm_system_backdrop -->|"前提とする"| windows_11
windows_color_mode -->|"で確認できる"| uisettings_color_values
windows_color_mode -->|"に保存される"| apps_use_light_theme_registry
theme_change_notification -->|"推奨される対応"| windows_color_mode
theme_change_notification -->|"推奨される対応"| contrast_theme
app_color_palette -->|"推奨される対応"| windows_color_mode
hardcoded_color -->|"用いるのは非推奨"| contrast_theme
hardcoded_color -->|"両立しない"| system_color_pairing
system_color_pairing -->|"推奨される対応"| contrast_theme
contrast_theme -->|"で確認できる"| high_contrast_detection
winforms_set_color_mode -->|"実装を担う"| windows_color_mode
winforms_set_color_mode -->|"前提とする"| windows_11
winforms_set_color_mode -->|"両立しない"| contrast_theme
winforms_apply_theming_implicitly -.->|"推奨される対応"| owner_drawn_control
wpf_theme_mode -->|"実装を担う"| windows_color_mode
wpf_theme_mode -->|"実装を担う"| dark_title_bar
wpf_resource_dictionary_swap -.->|"推奨される対応"| wpf
winui_theme_dictionaries -->|"実装を担う"| contrast_theme_support
windows_color_mode -->|"前提とする"| contrast_ratio
light_fixed_policy -.->|"推奨される対応"| dotnet_framework
contrast_theme_support -->|"推奨される対応"| light_fixed_policy
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全28件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
2. 「テーマ」は2軸ある ── ライト/ダークとコントラストテーマ
2.1. ライト/ダーク(カラーモード)
Windowsの「設定 > 個人用設定 > 色」にあるカラーモードは、OSとアプリ全体の前景色と背景色の明暗を定める設定です。Microsoftのドキュメントは、ライトを「明るい背景に暗い前景」、ダークを「暗い背景に明るい前景」と定義し、ここでいう前景とは「既定の文字色」だと補足しています。ダークモードでは前景(文字)が明るく、背景が暗くなります。3
この設定はレジストリの HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize にある AppsUseLightTheme(アプリのモード)と SystemUsesLightTheme(Windows自体のモード)というDWORD値に保存されており、Microsoftの設定リファレンスに載っています。16 ただし後述するとおり、アプリから読む正規の経路はWinRTの UISettings です。
2.2. コントラストテーマ(ハイコントラスト)
「設定 > アクセシビリティ > コントラストテーマ」で選ぶコントラストテーマは、おおむね7:1以上のコントラスト比になるよう制約されたパレットを使い、前景と背景の視覚的な分離を強く必要とするユーザーのためのものです。Windows 11にはAquatic・Desert・Dusk・Night skyの4種が組み込まれており、ユーザーはそこから選ぶだけでなく、背景・文字・ハイパーリンク・無効文字・選択文字・ボタンの各色を個別に編集できます。左Alt+左Shift+PrintScreenで素早く切り替えられ、未選択ならAquaticが適用されます。1
Microsoftのドキュメントは「コントラストテーマをライト/ダークテーマと混同しないでください」と明記しています。ライト/ダークは広いパレットを使い、最大コントラストに最適化されたものではありません。1 そして重要なのは、コントラストテーマが有効な間はダークモードが使えないことです。WinFormsの Application.SetColorMode はコントラストテーマ中はダークを提供せず、XAMLの RequestedTheme もシステムに上書きされます。217
2.3. 判定の優先順位
したがって、アプリの実装は次の順序になります。まずコントラストテーマかどうかを判定し、有効ならシステムカラーに全面的に委ねる。無効なら、ライト/ダークのどちらかのパレットを選ぶ。
flowchart TB
accTitle: テーマの2軸と判定の優先順位
accDescr: コントラストテーマが有効ならシステムカラーのペアに全面的に委ね、無効ならライト/ダークのカラーモードを読んでアプリのパレットを選ぶという判定順序
q1{"コントラストテーマは有効?"}
q1 -->|有効| sys["システムカラーのペアに委ねる"]
q1 -->|無効| q2{"カラーモードは?"}
q2 -->|ライト| light["ライトのパレット"]
q2 -->|ダーク| dark["ダークのパレット"]
sys -.-> note["ダークモードは使えない"]
図2: コントラストテーマの判定を先に置き、無効なときだけライト/ダークのパレットを選ぶ。
3. なぜ既存アプリはダークモードで白いままなのか
ウィンドウは2つの領域からできています。タイトルバー・枠・キャプションボタンからなる非クライアント領域と、アプリが描くクライアント領域です。Windows Vista以降、非クライアント領域はDWM(デスクトップウィンドウマネージャー)が合成して描いており、アプリは DwmSetWindowAttribute で描き方の属性を指定します。18
Microsoftのドキュメントは、既存アプリが白いままの理由を率直に説明しています。「Windowsはアプリケーションがダークモードをサポートできるかどうかを知らないため、後方互換性の理由からサポートできないものと想定する」。WinUIやWindows App SDKのようにダークモードをネイティブに扱うフレームワークもありますが、Win32アプリは多くの場合ダークモードをサポートしていないので、Windowsは既定でライトのタイトルバーを与えます。3
flowchart TB
accTitle: ウィンドウの2つの領域と描画の主体
accDescr: タイトルバーと枠からなる非クライアント領域はDWMが描き、クライアント領域はアプリやUIフレームワークが描くため、ダーク対応は両方に必要になる
win["トップレベルウィンドウ"] --> nc["非クライアント領域(タイトルバー・枠)"]
win --> client["クライアント領域(画面の中身)"]
nc --> dwm["DWMが合成して描く"]
client --> app["アプリやフレームワークが描く"]
dwm -.-> attr["DwmSetWindowAttributeで指示"]
app -.-> palette["アプリ自身のパレット"]
図3: タイトルバーはDWM、中身はアプリが描くので、ダーク対応はDWMへの指示とアプリのパレットの両方が要る。
ここから2つの帰結が出ます。第一に、タイトルバーをダークにするにはアプリがDWMに明示的に頼む必要があります。第二に、頼んだ結果ダークになるのはタイトルバーだけで、クライアント領域は自分で塗り直さなければならないということです。ドキュメントも「ダークモードを完全にサポートするには、アプリのサーフェス全体がダークテーマに従う必要がある」と述べ、公式ガイドが扱うのは検知とタイトルバーまでで、クライアント領域の塗り直し方は扱っていないと断っています。3 タイトルバーだけ黒くして中身が白いままのアプリは、白いままのアプリより不自然です。
flowchart TB
accTitle: 既定でライトになる仕組み
accDescr: Windowsはアプリのダーク対応可否を知らないため互換性のため既定でライトとし、アプリがDWM属性でTRUEを渡したときだけシステムのダーク設定に従ってフレームを描く
unknown["Windowsは対応可否を知らない"] --> def["互換性のため既定はライト"]
def --> q{"アプリがTRUEを渡した?"}
q -->|いいえ| light["常にライトのフレーム"]
q -->|はい| follow["システム設定に従って描く"]
図4: 対応可否を知らないWindowsは既定でライトにし、アプリの明示的な指示があったときだけ追従する。
4. DWMのダークタイトルバー ── DwmSetWindowAttribute
4.1. DWMWA_USE_IMMERSIVE_DARK_MODE
タイトルバーをダークにする属性は DWMWA_USE_IMMERSIVE_DARK_MODE です。DWMWINDOWATTRIBUTE 列挙体の説明はこうです。「ダークモードのシステム設定が有効なとき、このウィンドウのフレームをダークモードの色で描くことを許可する。互換性の理由から、すべてのウィンドウはシステム設定にかかわらず既定でライトモードになる。pvAttribute はBOOLを指し、TRUEでダークモードを尊重し、FALSEで常にライトモードになる。Windows 11 ビルド22000以降で対応」。4
つまりTRUEは「ダークにしろ」ではなく「システムがダークならダークにしてよい」という許可です。アプリ側がクライアント領域をダークに塗れる準備があるなら、TRUEを渡すだけでタイトルバーはシステム設定に追従します。逆に、アプリを常にライトで表示する設計(後述の「ライト固定」)なら、既定のままFALSEで問題ありません。
公式ガイドのC++コードは次の形です。ヘッダーに定数がない古いSDKのために値20を自前で定義する手順まで含まれています。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(ビルド22000)以降か。Windows 10以降のsupportedOSを宣言した
// マニフェストが前提(無いとバージョンが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: システムがダークならタイトルバーもダークで描いてよい
void ApplyTitleBarTheme(HWND hwnd, bool honorDarkMode)
{
if (!IsWindows11OrGreater())
{
// 文書化された対応はビルド22000以降。それ未満では呼ばず、既定(ライト)に従う
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))
{
// 対応OSでの失敗は異常。黙って握りつぶさず、HRESULTを記録して表面化させる
LogWarning(L"DwmSetWindowAttribute(DWMWA_USE_IMMERSIVE_DARK_MODE) failed: 0x%08X", hr);
}
}
OSバージョンで呼び分けている理由を一言添えておきます。文書化された対応はWindows 11 ビルド22000以降です。4 Windows 10でも同じ値が効くという報告は珍しくありませんが、文書化されていない挙動に業務アプリの表示を依存させるべきではありません。「呼んでみて失敗したら諦める」という設計にすると、Windows 10で呼び出しが成功してしまったときに、文書化されていない挙動の上でタイトルバーがダークになります。ビルド22000未満では呼ばずに「ライトのタイトルバーになる」という文書化された既定に従い、対応OSでの失敗はHRESULTをログに残して表面化させる、それだけです。値19を使う古い手順や、uxtheme.dllの序数エクスポートを呼んでコモンコントロールをダークにする手法もインターネットには出回っていますが、いずれも非公開APIで、更新で挙動が変わっても誰も保証してくれません。
4.2. 呼ぶタイミング ── HWNDが生きているとき、そして作り直されるたび
DwmSetWindowAttribute はHWNDに対して呼ぶので、ウィンドウハンドルが生成された後でなければなりません。そしてWinFormsのフォームは、ShowInTaskbar の変更などでハンドルが再生成されることがあります。再生成後の新しいHWNDには属性が乗っていないので、呼ぶ場所は「コンストラクター」ではなく「ハンドルが生成されるたびに呼ばれる場所」です。WinFormsなら OnHandleCreated、WPFなら SourceInitialized です。
flowchart TB
accTitle: DwmSetWindowAttributeを呼ぶタイミング
accDescr: ウィンドウハンドルの生成後にDWM属性を設定し、ハンドルが再生成されたときは新しいハンドルに再度設定し、テーマ変更の通知を受けたときにも設定し直す流れ
create["HWND生成"] --> apply["DWM属性を設定"]
apply --> run["表示中"]
run -->|ハンドル再生成| create
run -->|テーマ変更の通知| apply
図5: DWM属性はHWNDに紐づくので、生成のたびと再生成のたびに設定し直す。
WinFormsでのP/Invokeは次のとおりです(DllImport の安全な書き方は「C#からWin32 APIを安全に呼ぶ」を参照してください)。
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()
{
// 文書化された対応はビルド22000以降。それ未満では呼ばず、既定(ライト)に従う。
// .NET Frameworkでも使える判定。ただし.NET Frameworkでは、Windows 10以降の
// supportedOSを宣言したマニフェストが無いとバージョンがWindows 8相当に丸められる
// (.NET 5以降なら OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000) でもよい)
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) = システムがダークならダークで描いてよい。0(FALSE) = 常にライト
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);
}
}
}
このコードが必要なのは、.NET 8以前・.NET Framework・Win32/MFCのアプリです。.NET 9以降のWinFormsで Application.SetColorMode を使う場合、および.NET 9以降のWPFで ThemeMode を使う場合は、ウィンドウのダーク化をフレームワーク側が引き受けます(ThemeMode のドキュメントは「ウィンドウへの背景素材とダークモードの適用も制御する」と明記しています)。10 二重に呼んで害はありませんが、責任の所在が曖昧になるので、どちらか一方に決めてください。
WPFでは SourceInitialized の時点でHWNDが確定します。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); // 中身は前述のP/Invoke
}
}
4.3. タイトルバーの色・文字色・枠線色と背景素材
Windows 11では、ダーク/ライトの二択だけでなく、タイトルバーの色そのものを指定する属性も追加されました。
| 属性 | 値 | 内容 | 対応ビルド |
|---|---|---|---|
DWMWA_USE_IMMERSIVE_DARK_MODE |
20 | システムがダークならフレームをダークで描く(BOOL) | 22000 |
DWMWA_BORDER_COLOR |
34 | ウィンドウ枠の色(COLORREF)。DWMWA_COLOR_NONE で枠を消せる |
22000 |
DWMWA_CAPTION_COLOR |
35 | タイトルバーの色(COLORREF) | 22000 |
DWMWA_TEXT_COLOR |
36 | タイトル文字の色(COLORREF) | 22000 |
DWMWA_SYSTEMBACKDROP_TYPE |
38 | システム描画の背景素材(MicaやAcrylic) | 22621 |
色を指定する3つの属性は、DWMWA_COLOR_DEFAULT(0xFFFFFFFF)を渡すとシステム既定に戻ります。枠線色については「ウィンドウのアクティブ化のような状態変化に応じて色を変えるのはアプリの責任」とされている点に注意してください。4 背景素材は DWM_SYSTEMBACKDROP_TYPE 列挙体で指定し、DWMSBT_MAINWINDOW がWindows 11ではMica、DWMSBT_TRANSIENTWINDOW がAcrylicに相当しますが、「素材の効果は将来のWindowsで変わりうる」と明記されています。20
業務アプリでの使いどころは慎重に考えてください。タイトルバーをブランドカラーで塗ると、その色の上でタイトル文字とキャプションボタンのコントラストを自分で保証する責任が生じます。ダーク/ライトの2つの状態に加えてアクティブ/非アクティブの組み合わせも増えます。多くの業務アプリにとって正解は「システム既定に従う(値20をTRUEにするだけ)」であり、ブランド色は本当に必要なときの選択肢です。
flowchart TB
accTitle: タイトルバーの色をどう決めるか
accDescr: システム既定に従うなら値20をTRUEにするだけで済むが、ブランド色で塗ると文字とキャプションボタンのコントラストの保証とアクティブ/非アクティブの色管理がアプリの責任になり、戻すときはDWMWA_COLOR_DEFAULTを渡す
q{"タイトルバーの色は?"}
q -->|システム既定に従う| dark["値20をTRUEにするだけ"]
q -->|ブランド色で塗る| brand["色属性(34〜36)を指定"]
brand --> resp["文字とボタンのコントラストを自分で保証"]
brand --> states["アクティブ/非アクティブも自分で管理"]
brand -.-> reset["戻すときはCOLOR_DEFAULT"]
図6: ブランド色を選ぶとコントラストと状態管理の責任がアプリ側に移り、多くの業務アプリでは既定に従うのが正解になる。
5. システムテーマの検知と追従 ── 読む・気づく・塗り直す
クライアント領域を追従させる仕事は、3つに分解できます。現在の設定を読む、変更に気づく、そして塗り直すです。
flowchart TB
accTitle: 検知と追従の3ステップ
accDescr: 起動時にUISettingsで現在のカラーモードを読み、ColorValuesChangedなどの通知で変更に気づき、UIスレッドへ戻してアプリのパレットを塗り直すというループ
read["読む: UISettings.GetColorValue"] --> paint["塗り直す: パレットを再適用"]
notice["気づく: ColorValuesChanged"] --> ui["UIスレッドへ戻す"]
ui --> read
paint -.-> dwm["DWM属性も設定し直す"]
図7: 起動時に読み、通知で気づき、UIスレッドへ戻して塗り直すループがテーマ追従の骨格になる。
5.1. 読む ── UISettingsと「前景が明るければダーク」
Microsoftの公式手順は、WinRTの Windows.UI.ViewManagement.UISettings を使います。GetColorValue(UIColorType::Foreground) で前景色(既定の文字色)を取り、その知覚輝度を整数演算で見積もって「明るいか」を判定し、前景が明るければダークモードと判断します。ドキュメントは、この式が厳密な輝度解析のモデルではなく、明暗の分類に十分な近似だと断っています。321
UISettings はWinRTのクラスですが、C#のWPF/WinFormsからも TargetFramework を net8.0-windows10.0.19041.0 のようなWindows SDKバージョン付きにすれば直接呼べます(仕組みは「WinRTはCOMである」を参照してください)。レジストリの AppsUseLightTheme を直接読む方法もありますが、レジストリは設定の保存場所であってAPI契約ではないので、読むなら UISettings を正とし、レジストリは診断用に留めるのが筋です。
flowchart TB
accTitle: カラーモードを読む経路
accDescr: 正規の経路はWinRTのUISettingsで前景色を取って明暗を判定する方法で、レジストリのAppsUseLightThemeは保存場所であり診断用に留める
q["現在はライトかダークか"] --> uis["UISettings.GetColorValue"]
q -.-> reg["レジストリAppsUseLightTheme"]
uis --> fg["前景色の知覚輝度を判定"]
fg --> ans["明るければダーク"]
reg -.-> diag["保存場所。診断用に留める"]
図8: 正規の読み取り経路はUISettingsで、レジストリは保存場所にすぎない。
5.2. 気づく ── ColorValuesChangedはUIスレッドで届かない
変更の検知にも UISettings を使います。ColorValuesChanged イベントは色の値が変わったときに発生し、公式ガイドもこのイベントで設定変更を追跡しています。53 ここで実務上の注意が1つあります。このイベントはUIスレッドで届く保証がありません。WPFの Dispatcher やWinFormsの Control.Invoke、あるいは両方で使える SynchronizationContext でUIスレッドへ戻してから、コントロールに触ってください。UIスレッドとの付き合い方は「WPF/WinFormsのUIスレッドとasync/await」にまとめています。
また、アクセントカラーの変更でもこのイベントは発生します。ライト/ダークが変わったときだけ塗り直したいなら、イベントのたびに判定し直して前回と違うときだけ通知します。そして第2章の優先順位のとおり、コントラストテーマが有効なら前景色の明るさを見てはいけません。Aquaticのような暗い背景のコントラストテーマでは前景が明るいので、明るさだけを見ると「ダーク」と誤判定します。コントラストテーマは独立した状態として先に判定します。
次のクラスは、その判定順序をひとつにまとめたものです。UIスレッドへ戻すための SynchronizationContext と、コントラストテーマの判定(WinFormsなら SystemInformation.HighContrast、WPFなら SystemParameters.HighContrast、第8章)を呼び出し側から渡します。生成のタイミングに注意してください。WinFormsでは Program.Main の時点ではまだメッセージループも Control も無く、SynchronizationContext.Current は null です。フォームのコンストラクターや OnLoad のように、コントロールが生成された後に SynchronizationContext.Current を渡します。WPFなら new DispatcherSynchronizationContext(Application.Current.Dispatcher) を渡せます。
using Windows.UI.ViewManagement; // TargetFramework: net8.0-windows10.0.19041.0 以降
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: UIスレッドのSynchronizationContext。コントロール生成後の
// SynchronizationContext.Current や、WPFなら DispatcherSynchronizationContext を渡す
// 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()
{
// 判定順は第2章のとおり: コントラストテーマが先。暗い背景のコントラストテーマは
// 前景が明るいので、明るさだけを見ると「ダーク」と誤判定する
if (_isHighContrast()) return ThemeState.HighContrast;
// 公式ガイドと同じ判定: 前景(既定の文字色)が明るければダーク
var fg = _settings.GetColorValue(UIColorType.Foreground);
bool isDark = (5 * fg.G + 2 * fg.R + fg.B) > 8 * 128;
return isDark ? ThemeState.Dark : ThemeState.Light;
}
// UIスレッドから呼ぶ。UserPreferenceChanged など別経路の通知からも呼べる
public void Refresh()
{
if (_disposed) return;
var next = Read();
// ライト/ダーク間では、アクセントカラーだけの変更などを無視するため同じ状態なら通知しない。
// コントラストテーマ中は例外: ユーザーがテーマの色を編集しても状態は HighContrast のまま
// なので、同じ状態でも通知してシステムカラーを取り直させる
if (next == Current && next != ThemeState.HighContrast) return;
Current = next;
Changed?.Invoke(this, EventArgs.Empty);
}
private void OnColorValuesChanged(UISettings sender, object args)
{
// UIスレッドで届く保証がないので、UIへ戻してから判定と通知を行う。
// Dispose後に届いた(既にキューに入っていた)呼び出しは Refresh 側のフラグで無視する
_ui.Post(_ => Refresh(), null);
}
public void Dispose()
{
// 購読解除は今後の配信を止めるだけで、UIスレッドに投げ込み済みの呼び出しは残る。
// フラグを立てて、残った呼び出しの側で無視させる(UIスレッドで呼ぶこと)
_disposed = true;
_settings.ColorValuesChanged -= OnColorValuesChanged;
}
}
Win32レベルでは、設定変更時に WM_SETTINGCHANGE がすべてのトップレベルウィンドウに送られ、22 .NETではそれが SystemEvents.UserPreferenceChanged として届きます。23 ビジュアルスタイルの切り替え(コントラストテーマの有効化を含む)では WM_THEMECHANGED が、24 システムカラーの変更では WM_SYSCOLORCHANGE が来ます。25 通知の種類ごとに別の処理を書き分けるより、どの通知が来ても同じ「読んで塗り直す」処理を呼ぶほうが壊れにくく、これは前回の記事で紹介したコントラストテーマの追従とも同じ形になります。上の SystemThemeWatcher でいえば、UserPreferenceChanged や StaticPropertyChanged のハンドラーからも Refresh() を呼ぶ、ということです。
flowchart TB
accTitle: テーマ変更の通知経路とスレッド
accDescr: UISettingsのColorValuesChangedはUIスレッド以外で届くことがあるためUIへ戻し、WM_SETTINGCHANGEはSystemEvents.UserPreferenceChangedとして届き、WM_THEMECHANGEDとWM_SYSCOLORCHANGEはウィンドウプロシージャに届く。いずれも同じ再適用処理へ集約する
cvc["ColorValuesChanged"] --> marshal["UIスレッドへ戻す"]
upc["UserPreferenceChanged"] --> reapply["読んで塗り直す"]
wm["WM_THEMECHANGED等"] --> reapply
marshal --> reapply
図9: 通知の経路は複数あるが、すべて同じ「読んで塗り直す」処理に集約する。
5.3. 塗り直す ── 色を一箇所に集める
塗り直しを可能にする前提は、色が一箇所に集まっていることです。Color.White や #FFFFFF がフォームやXAMLのあちこちに散らばっていると、塗り直す場所を列挙できません。WinFormsなら「パレット」クラス(ライト用とダーク用の2つのインスタンス)を作り、コントロールは起動時と通知時にそこから色を取る。WPFなら色をResourceDictionaryに集め、XAMLは DynamicResource で参照する。WinUIならこの構造は ThemeDictionaries として最初から用意されています。
flowchart TB
accTitle: 色を一箇所に集める構造
accDescr: ライト用とダーク用のパレットをアプリに1つずつ持ち、現在のモードに応じて選んだパレットから各画面が色を取ることで、塗り直しの対象を列挙できるようにする
mode["現在のモード"] --> sel{"どちらを使う?"}
sel -->|ライト| pl["ライトのパレット"]
sel -->|ダーク| pd["ダークのパレット"]
pl --> screens["各画面・各コントロール"]
pd --> screens
hard["散らばったColor.White"] -.-> cannot["塗り直す場所を列挙できない"]
図10: パレットを一箇所に持てば塗り直しが列挙できるが、散らばったハードコードはできない。
この「色を集める」作業は、コントラストテーマ対応でも、後述するコントラスト比の確認でも、そのまま効いてきます。ダークモード対応の最大のコストはAPI呼び出しではなく、この整理です。
6. WinFormsでの実装
6.1. .NET 9/10 ── Application.SetColorMode
WinFormsには.NET 9でダークモードの予備的サポートが入り、.NET 10で「完全に統合」されました。Application.SetColorMode に渡す値は3つです。67
SystemColorMode.Classic── 既定。従来どおりのライト。SystemColorMode.System── Windowsのライト/ダーク設定に従う。SystemColorMode.Dark── ダーク。
呼ぶ場所は Application.Run の前、UI要素を作る前です。.NET 9では実験的機能のため、プロジェクトファイルで WFO5001 を抑止しないとコンパイルエラーになりましたが、.NET 10からこのエラーは出ません。26
static class Program
{
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
Application.SetColorMode(SystemColorMode.System); // UIを作る前に呼ぶ
Application.Run(new MainForm());
}
}
カラーモードが変わると System.Drawing.SystemColors が対応する色に切り替わり、標準コントロールはそれに従って描かれます。6 仕組みとしては、SystemColors.UseAlternativeColorSet という実験的プロパティ(SYSLIB5002)が「システムの KnownColor に代替の色セット(現在はダークモード版)を返させる」もので、Win32のシステムカラーそのものはライト/ダーク設定で変わらないため、.NET側で代替セットを持っているわけです。同じドキュメントは、コントラストテーマが有効なときは常にWindowsの現在の色を返すとも書いています。27
SetColorMode のドキュメントに書かれた制約を3つ押さえてください。2
- ダークのカラーモードはWindows 11以降でのみ利用できます。
- コントラストテーマが有効なときはダークモードは利用できません。
SystemColorMode.Systemを指定しても、実行中にWindowsの設定が変わったときにアプリは自動では追従しません。
3番目は業務アプリで問い合わせになりやすい点です。起動時のWindows設定で決まり、次回起動時に反映される、という挙動を利用者向けの説明に書いておいてください。実行中の切り替えにどうしても追従したいなら、フォームを作り直す設計が必要になり、割に合わないことがほとんどです。
flowchart TB
accTitle: SetColorModeの流れと制約
accDescr: SetColorModeはApplication.Runの前に呼び、SystemColorsが代替セットに切り替わって標準コントロールが従う。Windows 11専用、コントラストテーマ中は無効、実行中の設定変更には追従しないという3つの制約がある
sc["SetColorMode(System)"] --> before["Application.Runの前"]
before --> colors["SystemColorsが代替セットへ"]
colors --> ctrls["標準コントロールが従う"]
sc -.-> c1["Windows 11専用"]
sc -.-> c2["コントラストテーマ中は無効"]
sc -.-> c3["実行中の変更に追従しない"]
図11: SetColorModeは起動前に一度だけ効き、3つの文書化された制約を持つ。
6.2. 独自描画のコントロールとApplyThemingImplicitly
標準コントロールはアプリのカラーモードに従いますが、.NET 10のドキュメントは2つの例外的なケースを挙げています。自分で構成・描画しているコントロールの中でスクロールバーなどのWin32コモンコントロールを使っている場合、それらは明示的にオプトインしないとライトのまま残ります。逆に、テーマに従う既存のコントロールを継承して描画を完全に自前で制御したい場合は、オプトアウトします。7
どちらも Control.CreateParams をオーバーライドし、base.CreateParams を読む前に SetStyle(ControlStyles.ApplyThemingImplicitly, true/false) を呼びます。基底クラスのコンストラクターが CreateParams を読むため、自分のコンストラクターでは間に合わない、という点がこのAPIの落とし穴です。7
public partial class GanttChartControl : Control
{
protected override CreateParams CreateParams
{
get
{
// base.CreateParams を読む前に設定する。コンストラクターでは遅い
SetStyle(ControlStyles.ApplyThemingImplicitly, true);
return base.CreateParams;
}
}
}
flowchart TB
accTitle: ApplyThemingImplicitlyを設定できるタイミング
accDescr: 基底クラスのコンストラクターがCreateParamsを読む時点でApplyThemingImplicitlyが決まるため、CreateParamsのオーバーライド内でbase.CreateParamsより前にSetStyleを呼ぶ必要があり、派生クラスのコンストラクターでは遅い
basector["基底コンストラクター"] --> cp["CreateParamsを読む"]
cp --> st["ここまでにSetStyleが必要"]
st --> derived["派生クラスのコンストラクター"]
derived -.-> late["ここで呼んでも遅い"]
図12: ApplyThemingImplicitlyは基底コンストラクターがCreateParamsを読む前に決まっていなければならない。
独自描画(OnPaint でのGDI+描画)そのものは、SystemColors / SystemBrushes / SystemPens を使っていれば代替セットに追従します。Color.White で塗っている箇所は、ここでも前述のパレットに置き換えます。
6.3. .NET Framework 4.8と.NET 8以前 ── 「ライト固定」を明示する
SetColorMode がない環境では、ダークモードの標準サポートはありません。選択肢は2つです。
- ライト固定を明示する。DWM属性は既定のFALSEのまま(常にライトのタイトルバー)、クライアント領域も従来どおり。コントラストテーマ対応(第8章)だけは必ず行う。
- 自前で全面対応する。パレットを集約し、
UISettingsで読んで追従し、DWM属性をTRUEにし、コモンコントロールの見た目を含めてすべて塗り直す。
2は、Win32コモンコントロール(スクロールバー、ヘッダー、ツリーの展開ボタンなど)の描画をアプリが完全には制御できないため、「ほぼダークだが所々ライト」という状態になりがちです。公式ガイドが「サーフェス全体が従う必要がある」と述べるとおり、3 中途半端なダークは、ライト固定より悪い体験になります。既存資産に対しては、1を選んで「このアプリはライト表示」と明示し、.NET 10へ移行するタイミングで SetColorMode に切り替えるのが、現実的で説明もしやすい方針です。
flowchart TB
accTitle: WinFormsのランタイム別の選択肢
accDescr: .NET 10以降ならSetColorModeを使い、.NET 9なら同じAPIをWFO5001の抑止付きで使い、.NET 8以前や.NET Frameworkではライト固定を明示するかコモンコントロールまで含めて自前で全面対応するかを選ぶ
q{"ランタイムは?"}
q -->|.NET 10以降| n10["SetColorMode(System)"]
q -->|.NET 9| n9["SetColorMode + WFO5001抑止"]
q -->|.NET 8以前 / .NET Framework| legacy{"どうする?"}
legacy -->|推奨| fixed["ライト固定を明示"]
legacy -->|覚悟があれば| full["自前で全面対応"]
full -.-> partial["コモンコントロールが残る"]
図13: SetColorModeが使えないランタイムでは、ライト固定の明示が現実的な既定になる。
7. WPFでの実装
7.1. .NET 9/10 ── FluentテーマとThemeMode
.NET 9のWPFには、Windows 11のFluentデザインに沿った新しいテーマが同梱され、ライト/ダークとアクセントカラーに対応しました。適用方法は2つあります。ThemeMode プロパティを設定するか、PresentationFramework.Fluent のリソースディクショナリを MergedDictionaries に追加するかです。8
ThemeMode の値は Light / Dark / System / None(既定。従来のAero2テーマ)の4つで、Application に設定すればアプリ全体、Window に設定すればそのウィンドウだけに効きます。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 はFluentテーマの辞書をリソースに読み込むだけでなく、「ウィンドウへの背景素材とダークモードの適用も制御する」と明記されています。つまり、第4章のDWM属性はWPF側が面倒を見ます。また ThemeMode と Resources は同期して動くよう設計されており、ウィンドウはダークなのに中のコントロールはライト、という不整合を避けるためだと説明されています。10
flowchart TB
accTitle: WPFのThemeModeが行うこと
accDescr: ThemeModeをSystemにするとWindowsの設定に応じたFluentテーマの辞書がリソースに読み込まれ、あわせてウィンドウのダークモードと背景素材の適用も制御される
tm["ThemeMode=System"] --> read["Windowsの設定を読む"]
read --> dict["Fluent辞書をリソースへ読み込む"]
read --> win["ウィンドウのダーク化と背景素材"]
dict --> sync["Resourcesと同期して不整合を防ぐ"]
図14: ThemeModeはFluent辞書の読み込みとウィンドウのダーク化を一緒に制御する。
採用前に知っておくべき注意点が2つあります。第一に、ThemeMode をコードから読み書きする操作は実験的機能で、アクセスすると WPF0001 エラーになります。抑止すれば Application.Current.ThemeMode = ThemeMode.Dark のように書けますが、APIリファレンスは.NET 10でも [Experimental("WPF0001")] のままで、「将来削除される可能性がある」と注記しています。810 第二に、Fluentスタイルの対応は.NET 10でも「作業中」です。.NET 10でDatePicker・GridSplitter・GroupBox・TextBoxなどのスタイルが追加され、HighContrast関連のクラッシュが修正されましたが、9 裏を返せば.NET 9のFluentにはそれらが欠けていたということです。業務アプリで使っているコントロール(特にDataGridやサードパーティ製)がFluentで崩れないかを、採用判断の前に実機で確認してください。
7.2. 従来テーマのまま追従する ── ResourceDictionaryの差し替え
Fluentを採用しない(あるいは.NET 8以前・.NET Frameworkの)WPFでは、ダークモードの標準サポートはありません。Win32のシステムカラーはライト/ダーク設定で変わらないので、WPFの SystemColors を参照していても暗くはなりません。自前で追従させる構造は次のとおりです。
- ライト用
Themes/Light.xamlとダーク用Themes/Dark.xamlに、同じキーで色とブラシを定義する。 - XAMLからは
{DynamicResource App.WindowBackgroundBrush}のようにDynamicResourceで参照する(StaticResourceは読み込み時に固定されるので差し替えに追従しません)。 - 第5章の
SystemThemeWatcherの通知で、MergedDictionariesの該当辞書を入れ替える。コントラストテーマ中はどちらの辞書を入れても、8.4のトリガーがシステムカラーへ置き換えるので、ライト用を入れておく。
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);
// ダークのときだけダーク辞書。コントラストテーマ中はシステムカラー(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; // 同じ位置で入れ替える
}
}
}
flowchart TB
accTitle: WPFでのリソース辞書の差し替え
accDescr: ライト用とダーク用の辞書に同じキーで色を定義し、XAMLはDynamicResourceで参照し、テーマ変更の通知でMergedDictionaries内の辞書を入れ替えると参照先が更新される
notify["テーマ変更の通知"] --> swap["MergedDictionariesの辞書を入れ替え"]
light["Light.xaml(同じキー)"] --> swap
dark["Dark.xaml(同じキー)"] --> swap
swap --> dyn["DynamicResource参照が更新"]
static["StaticResource参照"] -.-> stale["読み込み時の値のまま"]
図15: 同じキーの辞書を入れ替えれば、DynamicResource参照だけが追従する。
標準コントロールのテンプレート(ボタンの背景やスクロールバーの色)は従来テーマの色を持っているので、ここでも「アプリ独自の面はダーク、標準コントロールはライト」になる箇所が残ります。必要なコントロールごとにスタイルを上書きしていく作業量を見積もったうえで、Fluent採用か、ライト固定かと比較してください。
7.3. タイトルバー
ThemeMode を使うならWPFが面倒を見ます。従来テーマのまま自前で追従する場合は、4.2で示した OnSourceInitialized からのDWM属性設定を使い、SystemThemeWatcher の通知でも設定し直します。
8. コントラストテーマ時の描画 ── システムカラーのペアを守る
8.1. 検知と通知
Win32では、SystemParametersInfo に SPI_GETHIGHCONTRAST を渡して HIGHCONTRAST 構造体を受け取り、dwFlags の HCF_HIGHCONTRASTON ビットで判定します。呼び出し時に cbSize を設定しておく必要があります。1128 Microsoftはこれを「ハイコントラストの有無を確認する唯一のサポートされた方法」と位置づけています。12
bool IsContrastThemeActive()
{
HIGHCONTRASTW hc{};
hc.cbSize = sizeof(hc);
if (!::SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(hc), &hc, 0))
{
// 失敗を既定値で覆い隠さない。原因が分かるようエラーコード付きで表面化させる
throw std::system_error(::GetLastError(), std::system_category(),
"SystemParametersInfo(SPI_GETHIGHCONTRAST)");
}
return (hc.dwFlags & HCF_HIGHCONTRASTON) != 0;
}
各フレームワークには、この呼び出しを包んだプロパティがあります。
| 環境 | 判定 | 変更の通知 |
|---|---|---|
| Win32 / MFC | SPI_GETHIGHCONTRAST + HCF_HIGHCONTRASTON |
WM_SYSCOLORCHANGE、WM_THEMECHANGED |
| WinForms | SystemInformation.HighContrast |
SystemEvents.UserPreferenceChanged |
| WPF | SystemParameters.HighContrast(SPI_GETHIGHCONTRAST に対応) |
SystemParameters.StaticPropertyChanged |
| WinUI 3 | ThemeSettings.HighContrast(Microsoft.UI.System) |
ThemeSettings.Changed |
WinFormsのアクセシビリティ手引きは、起動時に HighContrast を確認し、UserPreferenceChanged で変化に応じるよう求めています。13 WPFの SystemParameters.HighContrast は SPI_GETHIGHCONTRAST と HCF_HIGHCONTRASTON に対応付けられており、29 静的プロパティの変化は StaticPropertyChanged で通知されます。30 WinUI 3の ThemeSettings は CreateForWindowId でウィンドウに紐づけて作り、Changed イベントを購読しますが、オブジェクトへの参照を保持し続けないとイベントが止まる点に注意が必要です。31
flowchart TB
accTitle: コントラストテーマの検知経路
accDescr: Win32のSPI_GETHIGHCONTRASTが唯一のサポートされた判定方法で、WinFormsのSystemInformation.HighContrast、WPFのSystemParameters.HighContrast、WinUI 3のThemeSettings.HighContrastはそれぞれのフレームワークからこれを包んだ形で提供される
spi["SPI_GETHIGHCONTRAST(唯一の判定方法)"] --> wf["WinForms SystemInformation"]
spi --> wpf["WPF SystemParameters"]
spi --> winui["WinUI 3 ThemeSettings"]
spi --> win32["Win32 直接呼ぶ"]
図16: 判定の根は1つのWin32 APIで、各フレームワークはそれを包んだプロパティを持つ。
8.2. 描画の原則 ── 前景と背景のペア
Microsoftの「High contrast parameter」は、ハイコントラストが有効なときにアプリが行うべきことを3つ挙げています。11
- すべての色を、前景色と背景色の1組に写像する。
GetSysColorでCOLOR_WINDOWTEXTとCOLOR_WINDOWの組、またはCOLOR_BTNTEXTとCOLOR_BTNFACEの組を使う。 - 文字の背後に表示するビットマップ画像を省く。ハイコントラストを必要とするユーザーにとって視覚的な妨げになる。
- 多色で描く画像は、文字用の前景色と背景色で描く。
「ペア」が肝心です。Windows 8以降のガイドは、COLOR_HIGHLIGHTTEXT は COLOR_HIGHLIGHT の背景と、COLOR_WINDOWTEXT は COLOR_WINDOW の背景と組み合わせる前提で作られていると説明し、文字色をハードコードしないこと、ユーザーが色をカスタマイズするので、適用中のテーマに依存しないUIにすることを求めています。12 「Aero では文字は常に黒で選択色は薄い青だが、High Contrast Black では選択色が黒になる。黒い文字を前提にしてシステムの選択色を使うと、黒地に黒文字になる」という同ガイドの例は、冒頭の「ステータス表示が消えた」そのものです。
Windows 11のコントラストテーマの手引きは、この対応関係を表にしています。1
| 用途 | 前景 | 背景 |
|---|---|---|
| 見出し・本文・リスト・枠線・操作できないUI | SystemColorWindowText |
SystemColorWindow |
| ハイパーリンク | SystemColorHotlight |
SystemColorWindow |
| 無効・非アクティブなUI | SystemColorGrayText |
SystemColorWindow |
| 選択・ホバー・押下・進行中 | SystemColorHighlightText |
SystemColorHighlight |
| ボタンなど操作できるUI | SystemColorButtonText |
SystemColorButtonFace |
そして「やってはいけないこと」も明記されています。GrayTextを補足文やヒント文に使わない(無効状態専用)、Hotlightをハイパーリンク以外に使わない、互換性のない前景/背景を混ぜない、見た目だけで色を選ばない(ユーザーは実際に色を変える)。ページ・ペイン・ポップアップの背景は SystemColorWindow を基準にし、隣り合う面の背景が同じ色になるので、必要な境界だけコントラストテーマ専用の枠線で区切る(フライアウトやダイアログは2pxを推奨)、という設計指針もあります。1
flowchart TB
accTitle: ペアを崩したときに読めなくなる流れ
accDescr: 文字は黒だと決め打ちして選択背景だけをシステムの選択色にすると、High Contrast Blackでは選択色が黒になり黒地に黒文字になる。前景と背景をペアで取ればユーザーが色を編集しても読める
assume["文字は黒と決め打ち"] --> hl["選択背景だけシステムの選択色"]
hl --> black["High Contrast Blackでは選択色が黒"]
black --> broken["黒地に黒文字"]
pair["前景と背景をペアで取る"] --> ok["ユーザーが色を編集しても読める"]
edit["ユーザーが色を編集"] -.-> assume
図17: 片方だけをシステム色にすると黒地に黒文字になりうるが、ペアで取れば色が編集されても読める。
flowchart TB
accTitle: コントラストテーマ時の描画判断
accDescr: コントラストテーマが有効なら、色をシステムカラーのペアに写像し、文字の背後の画像を省き、多色の画像を前景と背景の2色で描き、ハードコードした色を使わない
on["コントラストテーマ有効"] --> map["色をペアに写像"]
on --> img["文字の背後の画像を省く"]
on --> multi["多色の図を2色で描く"]
on --> nohard["ハードコード色を使わない"]
図18: コントラストテーマが有効なときの描画は、写像・省略・2色化・脱ハードコードの4点に集約される。
8.3. WinFormsでの実装
WinFormsの標準コントロールは、ForeColor / BackColor を既定のままにしていればシステムカラーに従います。独自の色を付けている箇所と独自描画だけを、判定に応じて切り替えます。手引きの例は、通常時は青地に黄文字のラベルを、ハイコントラスト時は SystemColors.Window / SystemColors.WindowText に戻すものです。13 先ほどのパレット構造に、コントラストテーマ用の分岐を足した形になります。
using Microsoft.Win32;
public partial class OrderForm : Form
{
private readonly SynchronizationContext _ui;
public OrderForm()
{
InitializeComponent();
// コントロール生成後なので WindowsFormsSynchronizationContext が入っている
_ui = SynchronizationContext.Current
?? throw new InvalidOperationException("UIスレッドで生成してください。");
ApplyColorScheme();
SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged;
}
private void ApplyColorScheme()
{
if (SystemInformation.HighContrast)
{
// ペアを守ってシステムカラーに全面的に委ね、文字の背後の画像を外す
statusLabel.BackColor = SystemColors.Window;
statusLabel.ForeColor = SystemColors.WindowText;
headerPanel.BackgroundImage = null;
}
else
{
var p = AppPalette.Current; // ライト/ダークのパレット(第5章)
statusLabel.BackColor = p.PanelBackground;
statusLabel.ForeColor = p.PanelForeground;
headerPanel.BackgroundImage = Properties.Resources.HeaderPattern;
}
}
private void OnUserPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e)
{
// このイベントもUIスレッドで届く保証がない。UIへ戻してから、カテゴリで絞らずに判定し直す
_ui.Post(_ =>
{
if (IsDisposed) return;
ApplyColorScheme();
}, null);
}
// 静的イベントなので外さないとフォームがリークする。閉じられずに破棄される経路も
// 通るDispose(bool)で外す(デザイナー生成のDispose(bool)がある場合はそちらに書く)
protected override void Dispose(bool disposing)
{
if (disposing)
{
SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged;
}
base.Dispose(disposing);
}
}
OnPaint での独自描画は、SystemBrushes.Window / SystemPens.WindowText などペアを守ったシステムブラシで描き、ステータスを表す色付きの丸のような多色の図は、前景色の枠線と文字(「稼働」「停止」)に置き換えます。色だけに頼らない表示は、前回の記事で扱った達成基準1.4.1の話と同じです。
flowchart TB
accTitle: WinFormsの配色適用の分岐
accDescr: 起動時とUserPreferenceChangedのたびにApplyColorSchemeを呼び、SystemInformation.HighContrastが真ならシステムカラーのペアに委ねて背景画像を外し、偽ならライト/ダークのパレットから色を取る
start["起動時 / UserPreferenceChanged"] --> apply["ApplyColorScheme"]
apply --> q{"SystemInformation.HighContrast?"}
q -->|真| sys["SystemColorsのペアに委ねる"]
sys --> noimg["背景画像を外す"]
q -->|偽| pal["ライト/ダークのパレットから取る"]
図19: WinFormsでは起動時と通知のたびに同じ処理を呼び、コントラストテーマならシステムカラーに委ねる。
8.4. WPFでの実装
WPFの SystemColors は、WindowBrushKey のようなリソースキーを DynamicResource で参照すると、ブラシが変わったときに自動的に更新されます(WindowBrush を直接使う静的参照は更新されません)。32 コントラストテーマ中だけ見た目を切り替えるには、SystemParameters.HighContrast の値を DataTrigger から参照します。ただし、SystemParameters.HighContrast は静的プロパティなので、そのままではバインディングの生きた参照元になりません。StaticPropertyChanged を購読して値を保持し、INotifyPropertyChanged で通知する小さなプロキシを1つ用意し、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()
{
// SystemParameters の静的プロパティが変わる(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)));
};
}
}
<!-- xmlns:local="clr-namespace:OrderEntry" を宣言しておく -->
<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">
<!-- ペアを守る: 背景はWindow、枠線と文字はWindowText。境界は太めに -->
<Setter Property="Background"
Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
<Setter Property="BorderBrush"
Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"/>
<!-- 中の文字にも継承させる。Foregroundを明示した子要素は継承を断ち切るので注意 -->
<Setter Property="TextElement.Foreground"
Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"/>
<Setter Property="BorderThickness" Value="2"/>
</DataTrigger>
</Style.Triggers>
</Style>
flowchart TB
accTitle: WPFのシステムカラー参照とコントラストテーマのトリガー
accDescr: SystemColorsのリソースキーをDynamicResourceで参照するとブラシの変更に自動で追従し、StaticPropertyChangedを購読するプロキシのIsHighContrastにバインドしたトリガーが実行中の切り替えに反応してペアを守った色に切り替える。WindowBrushの直接参照は更新されない
key["WindowBrushKeyをDynamicResource参照"] --> auto["ブラシ変更に自動追従"]
spc["StaticPropertyChanged"] --> proxy["プロキシのIsHighContrast"]
proxy --> trig["DataTriggerが反応"]
trig --> pair["ペアを守った色に切り替え"]
direct["WindowBrushを直接参照"] -.-> stale["更新されない"]
図20: WPFはリソースキーの動的参照と、静的プロパティの変化を中継するプロキシへのバインディングで、実行中の切り替えに追従する。
Fluentテーマを使っている場合も、.NET 10でHighContrast関連のクラッシュ修正が入ったことを踏まえ、9 コントラストテーマでの動作確認を採用条件に含めてください。
8.5. WinUI 3での実装
WinUI 3は、標準コントロールがライト/ダーク/コントラストテーマに最初から追従し、アプリ独自の色は ResourceDictionary.ThemeDictionaries に Default(ダーク)・Light・HighContrast のキーで定義します。HighContrast では色をハードコードせず、SystemColorWindowColor のような動的なシステムカラーを ThemeResource で参照します。Light/Dark を持つカスタムコントロールには必ず HighContrast も用意し、HighContrast は他の名前付きハイコントラストテーマが見つからないときのフォールバックキーになります。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>
もう1つ、WinUIには HighContrastAdjustment という仕組みがあり、既定で有効です。これはコントラストを保つために白文字と黒のハイライト背景を強制するもので、システムカラーを正しく使ったテーマ辞書を用意したなら None に設定して、自分のスタイルを通すのが手引きの推奨です。1
flowchart TB
accTitle: WinUIのThemeDictionariesの解決
accDescr: 現在のテーマに応じてDefault(ダーク)、Light、HighContrastのいずれかの辞書が選ばれ、HighContrastでは動的なシステムカラーをThemeResourceで参照する。HighContrastは名前付きハイコントラストテーマが無いときのフォールバックキーになる
theme{"現在のテーマは?"}
theme -->|ダーク| def["Default辞書"]
theme -->|ライト| light["Light辞書"]
theme -->|コントラストテーマ| hc["HighContrast辞書"]
hc --> sysc["SystemColor系をThemeResourceで参照"]
hc -.-> fb["名前付きテーマが無いときのフォールバック"]
図21: WinUIではテーマごとの辞書が自動で選ばれ、HighContrast辞書はシステムカラーを参照する。
9. ダークモード対応はアクセシビリティ対応の代わりにならない
ダークモード対応を「アクセシビリティ対応をした」と報告するのは誤りです。両者の関係は次のように整理できます。
- ダークのパレットにもコントラスト比の基準は同じように適用されます。WCAGの達成基準1.4.3はテキストに4.5:1、大きな文字に3:1を求めており、これは背景が暗くても変わりません。14 暗い灰色の背景に中間の灰色の文字を置くダークデザインは、ライトの「白地に薄い灰色」と同じ問題を抱えます。
- 純粋な黒と白を避けるのがWindows 11の設計です。Microsoftのベストプラクティスは、Windows 11が純白と純黒を避けて目に優しい色調にしたと説明しています。34 逆に、ダークモードで背景を
#000000にすると、明るい文字との間で強すぎるコントラストによる滲み(ハレーション)を訴える人がいます。 - 色覚の多様性への配慮はテーマと無関係に必要です。Microsoftのカラーガイドは、色を主要な伝達手段にせず視覚的な補強として使うこと、赤と緑の組み合わせを唯一の区別にしないことを求めています。3515
- コントラストテーマはダークモードとは独立した要求です。第2章のとおり、コントラストテーマが有効な間はダークモードが使えないので、ダーク対応がどれだけ完璧でも、コントラストテーマの利用者には届きません。
flowchart TB
accTitle: ダークモードとアクセシビリティの関係
accDescr: ダークモード対応は見た目の好みや環境への配慮であり、コントラスト比、色だけに頼らない伝え方、コントラストテーマ対応というアクセシビリティの要件はテーマに関係なく別に満たす必要がある
dark["ダークモード対応"] --> pref["好みと環境への配慮"]
a11y["アクセシビリティ"] --> ratio["コントラスト比4.5:1"]
a11y --> sole["色だけに頼らない"]
a11y --> ct["コントラストテーマ対応"]
dark -.->|代わりにならない| a11y
図22: ダークモード対応は好みと環境への配慮で、アクセシビリティの要件は別に満たす。
一方で、第5章の「色を一箇所に集める」作業は両方の土台になります。パレットが1つの場所にあれば、ライトとダークのそれぞれでコントラスト比を実測する対象を列挙でき、コントラストテーマ用の分岐も同じ場所に書けます。ダーク対応をきっかけにパレットを集約し、そのついでにコントラスト比とコントラストテーマを点検するのが、いちばん投資効率の良い順序です。
10. 方針の決め方 ── アプリ種別ごとの推奨
| アプリの種類 | 推奨する方針 |
|---|---|
| 新規のWinForms(.NET 10) | SetColorMode(System) を使う。独自描画は SystemColors ベースにし、コモンコントロールを含む独自コントロールは ApplyThemingImplicitly でオプトイン |
| 新規のWPF(.NET 9/10) | ThemeMode="System" を、使用コントロールの表示確認とコントラストテーマの動作確認を通したうえで採用。難しければ従来テーマ+辞書差し替え |
| 既存のWinForms/WPF(.NET Framework 4.8、.NET 8以前) | 「ライト固定」を明示し、DWM属性は既定(FALSE)のまま。コントラストテーマ対応は必ず行い、.NET 10移行時にダーク対応へ |
| WinUI 3 | 既定でシステムに追従。独自の色は ThemeDictionaries に HighContrast を含めて定義し、HighContrastAdjustment を None に |
| Win32 / MFC | DWM属性+自前パレット+WM_THEMECHANGED / WM_SYSCOLORCHANGE での再計算。公式ガイドが扱うのは検知とタイトルバーまでで、コモンコントロールの塗り直しは範囲外 |
flowchart TB
accTitle: 既存資産がダーク対応へ至る道筋
accDescr: 既存資産はまずライト固定を明示し、コントラストテーマ対応を必ず済ませ、パレットを集約して準備したうえで.NET 10へ移行し、SetColorModeやThemeModeに切り替える。中途半端なダーク対応はライト固定より悪い体験になるので通らない
now["ライト固定を明示(現在)"] --> ct["コントラストテーマ対応(必須)"]
ct --> pal["パレットの集約(準備)"]
pal --> mig[".NET 10へ移行"]
mig --> dark["SetColorMode / ThemeModeへ切り替え"]
now -.->|通らない| half["中途半端なダーク対応"]
half -.-> worse["ライト固定より悪い体験"]
図23: 既存資産はライト固定から始めて、コントラストテーマ対応とパレット集約を経て.NET 10移行時にダーク対応へ進む。
「ライト固定」は敗北ではありません。Windowsの既定の挙動そのものであり、ドキュメント化された動作です。中途半端なダーク対応で「タイトルバーだけ黒い」「スクロールバーだけ白い」アプリを出すより、ライトで一貫しているほうが利用者にとってはるかにましです。ただし、コントラストテーマ対応だけは「固定」できません。これは前回の記事で扱った合理的配慮の領域であり、テーマの好みではなく、使えるかどうかの問題だからです。
11. 検証チェックリスト
対応が終わったら、次の順で実機確認します。すべて設定画面から数十秒で切り替えられます。
- ライト/ダークを実行中に切り替える。「設定 > 個人用設定 > 色」でモードを変え、タイトルバーとクライアント領域の両方が追従するか、あるいは「次回起動時に反映」という仕様どおりか(WinFormsの
SetColorModeは追従しません)を確認する。 - ハンドル再生成を起こす。WinFormsなら
ShowInTaskbarを実行中に切り替えて、タイトルバーの属性が保たれるか確認する。 - コントラストテーマを4種すべてで試す。左Alt+左Shift+PrintScreenで切り替え、Aquatic・Desert・Dusk・Night skyのそれぞれで、文字・枠線・選択行・無効項目・リンクが読めるか確認する。1
- コントラストテーマの色を編集する。ユーザーは実際に色を変えます。背景を極端な色に編集して、ハードコードが残っていないか炙り出す。
- ログを見る。対応OSで
DwmSetWindowAttributeやSystemParametersInfoが失敗したときに記録されているか、ビルド22000未満のWindows 10ではDWM属性を呼ばずにライトのまま起動しているかを確認する。 - コントラスト比を実測する。ライトとダークのそれぞれで、パレットの文字色と背景色の組み合わせを4.5:1で点検する。14
flowchart TB
accTitle: テーマ対応の検証手順
accDescr: 実行中のライト/ダーク切り替え、ハンドル再生成、4種のコントラストテーマ、テーマの色の編集、失敗ログの確認、コントラスト比の実測という順で実機確認する
s1["実行中にライト/ダーク切替"] --> s2["ハンドル再生成"]
s2 --> s3["コントラストテーマ4種"]
s3 --> s4["テーマの色を編集"]
s4 --> s5["失敗ログの確認"]
s5 --> s6["コントラスト比の実測"]
図24: 検証は設定の切り替えから始めて、ログとコントラスト比の確認で締める。
12. まとめ
- Windowsのテーマは「ライト/ダーク」と「コントラストテーマ」の2軸で、後者が有効な間はダークモードが使えません。判定はコントラストテーマが先です。
- 既存アプリのタイトルバーが白いのは互換性のための既定で、
DwmSetWindowAttributeのDWMWA_USE_IMMERSIVE_DARK_MODE(値20、Windows 11 ビルド22000以降)にTRUEを渡すと、システムがダークのときにダークで描かれます。HWND生成のたびに設定し、失敗はログに残します。 - 現在のモードは
UISettings.GetColorValueの前景色の明るさで判定し、ColorValuesChangedで変更に気づき、UIスレッドへ戻して塗り直します。色は一箇所に集めておきます。 - WinFormsは.NET 9/10の
Application.SetColorMode(SystemColorMode.System)。Windows 11専用、コントラストテーマ中は無効、実行中の変更に追従しない、の3制約と、独自コントロールのApplyThemingImplicitlyを押さえます。 - WPFは.NET 9/10の
ThemeMode="System"。コードからの操作は実験的でFluentは作業中なので、評価のうえ採用します。従来テーマなら辞書差し替え+DynamicResourceです。 - コントラストテーマ時は
SPI_GETHIGHCONTRAST系で判定し、色をシステムカラーのペアに写像し、文字の背後の画像を省き、多色の図を2色で描きます。GrayTextは無効状態、Hotlightはリンク専用です。 - ダーク対応はアクセシビリティ対応の代わりになりません。ダークでも4.5:1、色だけに頼らない、コントラストテーマ対応は別途必要です。
- 既存資産は「ライト固定」の明示が現実解で、コントラストテーマ対応だけは固定できません。
最初の一歩としておすすめなのは、主力画面を1つ選び、まず左Alt+左Shift+PrintScreenでコントラストテーマを有効にして眺め、同じキーで戻してから、Windowsの色設定をダークに切り替えてみることです(コントラストテーマが有効な間はダークモードが使えないので、2つは別々に試します)。数分で、自社のアプリが「色をどこに持っているか」が見えてきます。
関連記事
- Windowsアプリのアクセシビリティ入門 ── UI Automationと合理的配慮の義務化に備える
- WinRTはCOMである ── IInspectable・.winmd・言語プロジェクション、そしてWinUIが今もバイナリ契約の上に乗っている理由
- C#からWin32 APIを安全に呼ぶ ── P/Invoke実務ガイド(DllImport / LibraryImport / CsWin32)
- WPF/WinFormsのUIスレッドとasync/await 一枚まとめ
- WinFormsの高DPI対応 ── 4Kモニターでぼやける・崩れる原因と現実的な対処
- WPFの高DPI対応 ── 「DPIに強いはず」なのにぼやける・にじむ原因と対処
- WinForms/WPF/WinUIの選び方 - 実務判断表
関連する相談領域
合同会社小村ソフトでは、WinForms/WPF業務アプリのダークモード対応(パレットの集約、.NET 9/10の SetColorMode / ThemeMode への移行評価、DWM属性の組み込み)、コントラストテーマでの表示崩れの診断と修正、Win32/MFC資産のテーマ追従の相談を扱っています。「ダークモードにしたら社員から苦情が来た」という段階からで構いません。
参考リンク
-
Microsoft Learn, Contrast themes. コントラストテーマがおおむね7:1以上の制約されたパレットを使いライト/ダークテーマと混同してはならないこと、Aquatic・Desert・Dusk・Night skyの4種と色の編集、左Alt+左Shift+PrintScreenでの切り替え、SystemColor系リソースの前景/背景のペアと用途、GrayTextを無効状態専用にしHotlightをリンク専用にすること、ハードコード色による崩れ、境界の枠線、ThemeDictionariesのHighContrast、HighContrastAdjustmentをNoneにすること、Microsoft.UI.System.ThemeSettingsによる検知について。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, Application.SetColorMode(SystemColorMode) Method. UI要素を作る前に呼ぶこと、Systemを指定してもシステム設定が変わったときにアプリが自動では適応しないこと、ダークのカラーモードがWindows 11以降でのみ利用でき、ハイコントラストモードのときは利用できないことについて。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Support Dark and Light themes in Win32 apps. カラーモードにおける前景と背景の定義、Windowsがアプリのダーク対応可否を知らないため互換性のため既定でライトのタイトルバーを与えること、UISettings.GetColorValueで前景色を取り知覚輝度で明暗を判定してダークモードを検知する手順、ColorValuesChangedによる追跡、DwmSetWindowAttributeとDWMWA_USE_IMMERSIVE_DARK_MODE(値20)によるダークタイトルバーの有効化、サーフェス全体がダークに従う必要があることについて。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10
-
Microsoft Learn, DWMWINDOWATTRIBUTE enumeration (dwmapi.h). DWMWA_USE_IMMERSIVE_DARK_MODEがシステムのダーク設定が有効なときにフレームをダークで描くことを許可しすべてのウィンドウが既定でライトになること、DWMWA_BORDER_COLOR・DWMWA_CAPTION_COLOR・DWMWA_TEXT_COLORのCOLORREF指定とDWMWA_COLOR_DEFAULTによる既定への復帰、Windows 11 ビルド22000以降の対応、DWMWA_SYSTEMBACKDROP_TYPEのビルド22621以降の対応について。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, UISettings.ColorValuesChanged Event. 色の値が変わったときに発生するイベントについて。 ↩ ↩2
-
Microsoft Learn, What’s new in Windows Forms for .NET 9. 実験的なダークモードの予備的サポート、カラーモード変更時にSystemColorsが対応して変わること、SystemColorModeのClassic・System・Darkの3値、Application.SetColorModeを起動コードで呼ぶこと、WFO5001の抑止について。 ↩ ↩2 ↩3
-
Microsoft Learn, What’s new in Windows Forms for .NET 10. ダークモードの完全統合とSetColorModeが実験的でなくなったこと、独自描画コントロール内のWin32コモンコントロールがオプトインしないとライトのまま残ること、CreateParamsのオーバーライド内でbase.CreateParamsより前にSetStyle(ControlStyles.ApplyThemingImplicitly)を呼ぶ必要がありコンストラクターでは間に合わないことについて。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, What’s new in WPF for .NET 9. ライト/ダークとアクセントカラーに対応したFluentテーマ、ThemeModeのLight・Dark・System・Noneの4値とApplication/Windowへの設定、リソースディクショナリによる適用、コードからのThemeMode設定が実験的でWPF0001の抑止が必要なことについて。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, What’s new in WPF for .NET 10. Fluent UIスタイル対応がまだ作業中であること、DatePicker・GridSplitter・GridView・GroupBox・Hyperlink・Label・NavigationWindow・RichTextBox・TextBoxのFluentスタイル追加、HighContrast関連のクラッシュ修正について。 ↩ ↩2 ↩3
-
Microsoft Learn, Application.ThemeMode Property. Fluentテーマをライト・ダーク・システムのどのモードで読み込むかを制御し、ウィンドウへの背景素材とダークモードの適用も制御すること、ThemeModeとResourcesが同期して不整合を避けるよう設計されていること、Experimental(“WPF0001”)属性が付き将来削除される可能性があることについて。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, High contrast parameter. 初期化時とWM_SYSCOLORCHANGE処理時にSPI_GETHIGHCONTRASTでHIGHCONTRAST構造体を取得しHCF_HIGHCONTRASTONを確認すること、有効なときはすべての色をCOLOR_WINDOWTEXTとCOLOR_WINDOW、またはCOLOR_BTNTEXTとCOLOR_BTNFACEの1組に写像し、文字の背後のビットマップ画像を省き、多色の画像を前景色と背景色で描くことについて。 ↩ ↩2 ↩3
-
Microsoft Learn, High-contrast mode. Aeroでは黒い文字と薄い青の選択色だがHigh Contrast Blackでは選択色が黒になり黒地に黒文字になりうること、COLOR_HIGHLIGHTTEXTはCOLOR_HIGHLIGHTと、COLOR_WINDOWTEXTはCOLOR_WINDOWと組み合わせる前提であること、文字色をハードコードしないこと、ユーザーが色をカスタマイズするためテーマに依存しないUIにすること、WM_THEMECHANGEDで色を再計算すること、SPI_GETHIGHCONTRASTが唯一のサポートされた確認方法であることについて。 ↩ ↩2 ↩3
-
Microsoft Learn, Walkthrough: Creating an Accessible Windows-based Application. SystemInformation.HighContrastによる判定、有効時にシステムの配色を使い色で伝える情報に視覚的手がかりを添え文字の背後の画像を省くこと、起動時の確認とUserPreferenceChangedイベントへの追従、SystemColorsによるラベルの配色切り替えの例について。 ↩ ↩2 ↩3
-
W3C / ウェブアクセシビリティ基盤委員会(WAIC)訳, Web Content Accessibility Guidelines (WCAG) 2.1 日本語訳. 達成基準1.4.3(コントラスト(最低限))のテキスト4.5:1・大きな文字3:1と、達成基準1.4.1(色の使用)について。 ↩ ↩2 ↩3
-
Microsoft Learn, Color in Windows. Windowsがライトとダークの2つのカラーモードを持つこと、アクセントカラーとテーマの選択がユーザーの体験全体に反映されること、コントラストの確保と色覚多様性への配慮について。 ↩ ↩2
-
Microsoft Learn, Reference for Windows 11 and Windows 10 settings. HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize配下のAppsUseLightThemeとSystemUsesLightThemeがアプリとWindowsのライト/ダークを表すDWORD値であることについて。 ↩
-
Microsoft Learn, Theming in Windows apps. RequestedThemeを外すとシステム設定に従うこと、ユーザーがハイコントラストテーマを選ぶとシステムがRequestedThemeを上書きすること、カスタムテンプレートでは色をハードコードせずテーマブラシを使うことについて。 ↩
-
Microsoft Learn, DwmSetWindowAttribute function (dwmapi.h). ウィンドウの非クライアント領域のDWM描画属性を設定する関数と、Windows Vista以降で利用できることについて。 ↩
-
Microsoft Learn, Retrieve a window handle (HWND). WPFのWindowInteropHelperからHandleを取得する方法について。 ↩
-
Microsoft Learn, DWM_SYSTEMBACKDROP_TYPE enumeration (dwmapi.h). DWMSBT_MAINWINDOWがWindows 11ではMica、DWMSBT_TRANSIENTWINDOWがAcrylicに相当し、素材の効果が将来のWindowsで変わりうること、Windows 11 ビルド22621以降の対応について。 ↩
-
Microsoft Learn, UISettings.GetColorValue(UIColorType) Method. 指定したUIColorTypeの色の値を返すメソッドについて。 ↩
-
Microsoft Learn, WM_SETTINGCHANGE message. SystemParametersInfoがシステム全体の設定を変更したときやポリシー設定が変わったときにすべてのトップレベルウィンドウへ送られるメッセージについて。 ↩
-
Microsoft Learn, SystemEvents.UserPreferenceChanged Event. ユーザー設定が変更されたときに発生する静的イベントと、ハンドラーを外さないとメモリリークになることについて。 ↩
-
Microsoft Learn, WM_THEMECHANGED message. テーマの有効化・無効化・切り替えの後にすべてのウィンドウへブロードキャストされ、既存のテーマハンドルが無効になるため開き直す必要があることについて。 ↩
-
Microsoft Learn, WM_SYSCOLORCHANGE message. システムカラー設定の変更時にすべてのトップレベルウィンドウへ送られ、システムカラーを使うブラシは作り直し、コモンコントロールへ転送する必要があることについて。 ↩
-
Microsoft Learn, Compiler Error WFO5001. .NET 9でSetColorModeとSystemColorModeが評価目的の実験的機能として保護され、.NET 10以降ではこのエラーが適用されないことについて。 ↩
-
Microsoft Learn, SystemColors.UseAlternativeColorSet Property. trueにするとシステムのKnownColorが代替の色セット(現在はダークモード版)を返すこと、SYSLIB5002の実験的機能であること、Windowsでハイコントラストテーマが有効なときはシステムのKnownColorが常に現在のWindowsの色を返すことについて。 ↩
-
Microsoft Learn, HIGHCONTRASTW structure (winuser.h). dwFlagsのHCF_HIGHCONTRASTON(0x00000001)と、SPI_GETHIGHCONTRASTで使うときにcbSizeを指定する必要があることについて。 ↩
-
Microsoft Learn, SystemParameters.HighContrast Property. SPI_GETHIGHCONTRASTとHCF_HIGHCONTRASTONに対応付けられたWPFの静的プロパティについて。 ↩
-
Microsoft Learn, SystemParameters.StaticPropertyChanged Event. SystemParametersのいずれかのプロパティが変わったときに発生する静的イベントについて。 ↩ ↩2
-
Microsoft Learn, ThemeSettings Class (Microsoft.UI.System). CreateForWindowIdでウィンドウに紐づけて作りChangedイベントでハイコントラストの変化を受け取ること、参照を解放するとオブジェクトが破棄されイベントが発生しなくなることについて。 ↩
-
Microsoft Learn, SystemColors.WindowBrushKey Property. リソースキーで動的参照を作るとブラシの変更時に自動更新され、WindowBrushによる静的参照は自動更新されないことについて。 ↩
-
Microsoft Learn, ResourceDictionary.ThemeDictionaries Property (Microsoft.UI.Xaml). LightとDarkのテーマ辞書を持つカスタムコントロールにはHighContrastの辞書も用意すること、HighContrastが他のハイコントラストテーマが無いときのフォールバックキーであること、DefaultがテーマのResourceDictionaryが見つからないときに使われること、SystemColorButtonFaceColorのようなシステムカラーリソースをHighContrastで使えることについて。 ↩
-
Microsoft Learn, Windows app development best practices. Windows 11が純白と純黒を避けて目に優しい色調に更新したことと、ダーク/ライトテーマがユーザーの視覚的な好みへの適応手段であることについて。 ↩
-
Microsoft Learn, Color (Windows UX guidelines). 色を主要な伝達手段ではなく視覚的な補強として使うこと、目的に基づいてテーマカラーとシステムカラーを選び前景と背景を対応する組み合わせで使うこと、テーマ変更をWM_THEMECHANGEDで処理すること、High Contrast BlackがWindows 11ではAquatic、High Contrast WhiteがDesertに当たることについて。 ↩
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
Windowsアプリのアクセシビリティ入門 ── UI Automationと合理的配慮の義務化に備える
2024年4月施行の改正障害者差別解消法を背景に、スクリーンリーダーがWindowsアプリを読む仕組みUI Automationを軸として、WinForms/WPFでの名前付け、キーボード操作、コントラスト、検証ツールまで実務目線で整理します。
Windowsアプリ 外注・受託開発を依頼する前に整理したいこと
Windowsアプリの外注・受託開発を依頼する前に、既存ソフト改修、装置連携、COM/ActiveX、配布・更新、保守の整理ポイントを解説します。
「応答なし」の正体 ── Windowsがアプリを「固まった」と判定する仕組みと、固まらない設計
Windowsの「応答なし」は、ウィンドウが5秒間メッセージを取り出さないとOSが判定し、幽霊ウィンドウに差し替える仕組みです。判定の内部動作から、固まる定番原因、UIスレッドから重い処理を追い出す設計、ハングの調査手順まで解説します。
クリップボードとドラッグ&ドロップの仕組み ── OLEデータ転送を業務アプリで正しく扱う
Excelの表を貼ると崩れる・コピー元を閉じると貼れない――正体は同じ内容を複数形式で置くクリップボードの仕組みです。標準形式・遅延レンダリング・OLEドラッグ&ドロップから履歴とクラウド同期のポリシーまで解説します。
WinForms/WPFアプリにEntra ID認証を組み込む ── MSAL.NETとWAMブローカーの実務構成
WinForms/WPFデスクトップアプリにEntra ID認証を組み込む手順を整理します。パブリッククライアントの考え方、アプリ登録、MSAL.NETのAcquireTokenSilent、WAMブローカー、トークンキャッシュの永続化まで解説します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
UI スレッド / タイマーテーマ
WPF / WinForms、UI スレッド、async/await、タイマー設計を整理するトピックです。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- Windowsをダークモードにしても、自社のWinFormsアプリのタイトルバーだけ白いままです。なぜですか?
- Windowsは、アプリがダークモードに対応しているかどうかを知る手段を持たないため、互換性のためにすべてのウィンドウを既定でライトモード扱いにしているからです。タイトルバーを含む非クライアント領域はデスクトップウィンドウマネージャー(DWM)が描いており、アプリ側がDwmSetWindowAttributeでDWMWA_USE_IMMERSIVE_DARK_MODE(値20)にTRUEを渡して初めて、システムがダークのときにダークで描かれます。この属性の対応はWindows 11 ビルド22000以降と文書化されています。.NET 9以降のWinFormsでApplication.SetColorModeを使う場合や、.NET 9以降のWPFでThemeModeを使う場合は、この呼び出しをフレームワーク側が引き受けるため、自前で呼ぶのは.NET 8以前や.NET Framework、Win32/MFCのアプリに限られます。なお、タイトルバーだけ黒くしてクライアント領域が白いままだと、かえって不自然になります。アプリ全体をダークに塗り替える準備ができたときにだけ、この属性を有効にしてください。
- ダークモードとコントラストテーマ(ハイコントラスト)は同じものですか?
- 別物です。ライト/ダークは「設定 > 個人用設定 > 色」のカラーモードで、前景と背景の明暗を入れ替える広いパレットを使います。コントラストテーマは「設定 > アクセシビリティ > コントラストテーマ」で選ぶもので、おおむね7:1以上のコントラスト比になる制約されたパレット(Aquatic・Desert・Dusk・Night skyの4種と、ユーザーが色を編集したもの)を使います。Microsoftのドキュメントは両者を混同しないよう明記しており、コントラストテーマが有効な間はダークモードは使えません(WinFormsのSetColorModeはコントラストテーマ中はダークを提供せず、XAMLのRequestedThemeもシステムに上書きされます)。アプリの実装では、まずコントラストテーマかどうかを判定してシステムカラーに全面的に委ね、そうでなければライト/ダークのパレットを選ぶ、という優先順位にしてください。
- WinFormsアプリをダークモードに対応させる最短の方法は何ですか?
- .NET 9以降なら、Program.csのApplication.Runより前でApplication.SetColorMode(SystemColorMode.System)を呼ぶのが最短です。.NET 9では実験的機能のため、プロジェクトファイルでWFO5001を抑止する必要がありましたが、.NET 10からは抑止なしで使えます。SetColorModeを呼ぶとSystemColorsがダーク用の代替セットに切り替わり、標準コントロールはそれに従って描かれます。注意点は3つあります。第一に、ダークモードはWindows 11以降でのみ利用でき、コントラストテーマ中は無効です。第二に、SystemColorMode.Systemを指定しても、アプリ実行中にWindowsの設定が変わったときに自動では追従しません(次回起動時に反映されます)。第三に、独自描画のコントロールでスクロールバーなどのWin32コモンコントロールを使っている場合は、CreateParamsをオーバーライドしてSetStyle(ControlStyles.ApplyThemingImplicitly, true)をbase.CreateParamsより前に呼ぶ必要があります(コンストラクターでは間に合いません)。
- WPFアプリでは何をすればダークモードに追従しますか?
- .NET 9以降のWPFには、Windows 11のFluentデザインに沿った新しいテーマが同梱されており、App.xamlのApplication要素にThemeMode="System"と書くだけで、Windowsのライト/ダーク設定に合わせたFluentテーマが読み込まれます。ThemeModeはウィンドウのダーク化(タイトルバー)と背景素材の適用も制御します。ただし、ThemeModeプロパティをコードから読み書きする操作は.NET 10でも実験的(WPF0001)で、Fluentスタイル自体も.NET 10のドキュメントで「まだ作業中」とされています。業務アプリで採用する場合は、使っているコントロールがFluentで崩れないかを評価してから決めてください。従来のテーマのまま(.NET 8以前や.NET Frameworkも同様)対応する場合は、ライト用とダーク用のResourceDictionaryを用意してMergedDictionariesで差し替え、XAML側はDynamicResourceで参照し、切り替えの検知にはUISettings.ColorValuesChangedを使います。タイトルバーはSourceInitializedでWindowInteropHelperからHWNDを取り、DwmSetWindowAttributeを呼びます。
- コントラストテーマ(ハイコントラスト)で文字が消えたり読めなくなるのはなぜですか?
- 色をハードコードしているか、システムカラーの前景と背景の組み合わせ(ペア)を崩しているのが典型的な原因です。コントラストテーマではユーザーが背景・文字・リンクなどの色を自由に編集できるため、「文字は黒だろう」「選択行は薄い青だろう」という前提はすべて崩れます。たとえば背景だけ#E6E6E6に固定していると、テーマによっては前景が白になり、白文字が薄い灰色の上に乗って読めなくなります。原則は3つです。SPI_GETHIGHCONTRAST(WinFormsならSystemInformation.HighContrast、WPFならSystemParameters.HighContrast)で状態を判定すること、色をすべてシステムカラーの正しいペア(WindowTextとWindow、ButtonTextとButtonFace、HighlightTextとHighlight)に置き換えること、文字の背後の画像や多色の図をやめて前景色・背景色だけで描くことです。GrayTextは無効状態、Hotlightはハイパーリンク以外に使ってはいけません。変更はWM_SYSCOLORCHANGEやWM_THEMECHANGED(.NETではSystemEvents.UserPreferenceChanged)で通知されるので、そこで色を計算し直して再描画します。