WinUI 3で画面を作っていると、ライトテーマではきれいに見えるのに、Windowsをダークテーマへ切り替えた瞬間に文字が読めなくなることがあります。原因になりやすいのが、背景色や文字色をXAMLへ直接書いてしまう実装です。
WinUIには、現在のテーマに合わせて値を切り替えるThemeResourceと、特定のUI要素へテーマを指定するRequestedThemeがあります。今回はこの2つの役割を整理し、ライト・ダーク・ハイコントラストに追従しやすい画面を作る方法を解説します。
なぜ色を直接指定すると困るのか
たとえばカードUIを作るとき、Background="#FFFFFF"、Foreground="#000000"と書けばライトテーマでは問題なく見えます。しかしWindows側がダークテーマになっても、この値は変わりません。周囲の標準コントロールだけが暗くなり、自作部分だけ白いまま残る、といった不統一が起きます。
さらに重要なのがアクセシビリティです。Windowsにはハイコントラストテーマがあり、ユーザー自身が見やすい配色を選ぶ場合があります。アプリが固定色を多用すると、その選択をUIへ反映しにくくなります。
WinUIの標準コントロールはテーマリソースを利用しており、テーマ変更へ追従する仕組みを持っています。自作UIも同じ考え方へ寄せることで、Windows全体と調和しやすくなります。
ThemeResourceとStaticResourceは何が違うのか
XAMLではリソースを参照するときに{StaticResource}と{ThemeResource}を使えます。見た目は似ていますが、評価されるタイミングが違います。
StaticResourceはXAMLの読み込み時に値を解決し、その後テーマが変わっても基本的に再評価されません。一方、ThemeResourceはアプリ起動時だけでなく、実行中にテーマが変わった場合にも再評価されます。Microsoft Learnでも、この再評価が両者の重要な違いとして説明されています。
つまり、色やブラシのようにテーマによって変わる値はThemeResource、テーマに関係なく固定したい値はStaticResource、と考えると整理しやすくなります。
まずはWinUI標準のテーマリソースを使う
独自のライト色・ダーク色をすぐ作る必要はありません。WinUIには標準コントロールでも使われているテーマリソースがあります。たとえば画面の背景と通常の文字色をテーマへ追従させるなら、次のように書けます。
<Grid
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel Padding="24" Spacing="12">
<TextBlock
Text="設定"
Style="{StaticResource TitleTextBlockStyle}" />
<TextBlock
Text="現在のテーマに合わせて表示されます。"
Foreground="{ThemeResource TextFillColorPrimaryBrush}" />
</StackPanel>
</Grid>
ここで背景と文字色にはThemeResourceを使っています。テーマ変更時に値が変わる可能性があるためです。一方、TitleTextBlockStyleはスタイルそのものを参照しているため、この例ではStaticResourceを使っています。スタイル内部でテーマ依存のブラシをThemeResourceとして参照していれば、その部分はテーマ変更へ追従できます。
独自色が必要ならThemeDictionariesへ分離する
製品固有のブランドカラーやカード背景など、標準リソースだけでは表現できない場合はResourceDictionary.ThemeDictionariesを使えます。Light、Dark、HighContrastで同じキーを定義しておけば、UI側はキーを1つ知っているだけで済みます。
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="CardBackgroundBrush"
Color="#FFF7F7F7" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="CardBackgroundBrush"
Color="#FF202020" />
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="CardBackgroundBrush"
Color="{StaticResource SystemColorWindowColor}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Application.Resources>
画面側では次のように参照します。
<Border
Background="{ThemeResource CardBackgroundBrush}"
Padding="16"
CornerRadius="8">
<TextBlock Text="テーマ対応カード" />
</Border>
Microsoft Learnでは、独自のテーマ辞書を作る場合、LightとDarkに加えてHighContrastも用意することが推奨されています。特にカスタムコントロールではHighContrastキーがフォールバックとして使われます。HighContrast内では、ユーザーの設定を反映できるSystemColor...系リソースを利用する方法があります。
RequestedThemeは「色を選ぶAPI」ではない
RequestedThemeは、FrameworkElementとその子要素がどのテーマのリソースを使うかを指定するプロパティです。たとえばプレビュー領域だけをライトテーマとして確認したい場合は、親要素へ指定できます。
<Border x:Name="PreviewArea"
RequestedTheme="Light"
Padding="24">
<TextBlock Text="ライトテーマのプレビュー" />
</Border>
親に指定したRequestedThemeは子要素へ継承されます。C#から実行時に切り替えることもできます。
private void DarkPreview_Click(object sender, RoutedEventArgs e)
{
PreviewArea.RequestedTheme = ElementTheme.Dark;
}
private void SystemTheme_Click(object sender, RoutedEventArgs e)
{
PreviewArea.RequestedTheme = ElementTheme.Default;
}
ここで大切なのは、RequestedThemeで個々の色を変更しているわけではないことです。「この要素ではどのテーマ辞書を参照するか」を変え、その結果としてThemeResourceが別の値へ解決されます。ThemeResourceが舞台の照明セットなら、RequestedThemeはどの照明セットを使うかを選ぶスイッチ、と考えると分かりやすいでしょう。
Application.RequestedThemeとの違いに注意する
テーマ指定にはアプリ全体のApplication.RequestedThemeもありますが、FrameworkElementのRequestedThemeとは扱いが異なります。MicrosoftのAPIドキュメントでは、FrameworkElement.RequestedThemeは実行中に変更できますが、Application.RequestedThemeをアプリ実行中に変更しようとすると例外になると説明されています。
そのため、「設定画面でライト/ダークを即時切り替える」ようなUIを作る場合、表示ツリーのルートに近いFrameworkElementのRequestedThemeを切り替える設計を検討できます。現在実際に適用されているテーマを知りたい場合はActualThemeを参照できます。
ハイコントラストではRequestedThemeよりユーザー設定が優先される
もう1つ重要な注意点があります。FrameworkElement.RequestedThemeは、ユーザーがハイコントラストモードを利用している場合には無視されます。これはアプリ側の好みより、ユーザーが必要としている視認性を優先するためです。
「アプリは必ずこの色で見せたい」と固定するより、標準テーマリソースを優先し、独自リソースが必要な部分だけLight・Dark・HighContrastを定義する方が、アクセシビリティを保ちやすくなります。
実務での使い分け
通常の業務アプリなら、まずWinUI標準のThemeResourceを利用します。ブランド固有の背景や状態色など、標準リソースでは表現できないものだけThemeDictionariesへ追加します。そしてテーマ切り替え機能やプレビュー機能が必要な場合にRequestedThemeを使う、という順番が扱いやすいでしょう。
逆に避けたいのは、ページごとに#FFFFFFや#000000を大量に書く設計です。後からダークモード対応を追加すると、どの色が背景で、どの色が文字で、どの色が状態表現なのかを一つずつ調査することになります。最初から「意味のあるリソース名」を介して色を使えば、配色変更をResourceDictionary側へ集約できます。
まとめ
WinUI 3のテーマ対応では、色を直接切り替えるより「リソースの解決先をテーマによって変える」と考えるのが基本です。実行中のテーマ変更へ追従させたい値はThemeResource、独自配色はThemeDictionaries、特定のUIツリーへテーマを指定したい場合はRequestedThemeを使います。
ライトとダークだけで完成と考えず、HighContrastも含めて設計することが重要です。WinUIが持つ標準リソースをできるだけ利用し、アプリ固有の色だけを追加する。この順番にすると、見た目とアクセシビリティの両方を保ちやすいテーマ設計になります。
参考資料
- Microsoft Learn – XAML theme resources
- Microsoft Learn – ThemeResource markup extension
- Microsoft Learn – FrameworkElement.RequestedTheme
- Microsoft Learn – ResourceDictionary.ThemeDictionaries
- GitHub – Microsoft Windows App SDK Releases
コメント