WPF で Fluent デザインを追加ライブラリなしで適用する方法

追加ライブラリなしで WPF に Fluent の外観を適用する方法。App.xaml でのテーマの設定、ライトとダークに追従する Fluent テーマのブラシ、SystemColors がダークモードに追従しないことを、実測した図と表で示す。

概要

本記事では、WPF アプリに Fluent デザインの要素を取り入れる方法を扱う。
対象は「追加ライブラリを導入しない」構成であり、WPF 標準のテンプレート、余白設計、角丸、階層表現と、Fluent テーマ自身のブラシで、ライトとダークに追従する一貫した外観を構築する。SystemColors が何に追従し、何に追従しないかも示す。


前提・対象環境

本記事の実測は、上記の環境で、Windows をダークモードにした PC で行った。
3 つの表は順に、各 SystemColors キーが返す色とその相対輝度、アプリケーションリソースを差し替えた前後の参照値、ThemeMode の Light と Dark でのブラシのキーの値を読み出している。
画面の図は、記事の XAML を表示したものである。
この環境で確認しているのは次の点である。


問題

既定の WPF テーマは長期運用で安定している一方、余白、配色、角丸、情報階層の表現が現行の Windows UI と乖離しやすい。
特に複数画面を持つ業務アプリでは、コントロールを既定スタイルのまま配置すると視覚的な密度が高くなり、操作対象の優先度が判別しにくくなる。

既定テーマの WPF 画面。見出し、説明文、四角い枠のボタンが背景と同じ面の上に並んでいる。
既定テーマ(Aero2)のまま配置した画面。カード面と背景が分離しておらず、ボタンの角も直角で、どこが主要な操作かが読み取りにくい。

原因・背景

WPF は柔軟な描画基盤を持つが、Fluent 固有の外観は標準で自動適用されない。
したがって、Fluent 的な印象は次の要素を明示的に定義して初めて成立する。

標準で使える色は 2 系統ある。Fluent テーマ自身のブラシのキー(ApplicationBackgroundBrush など)は、テーマに合わせて切り替わる。SystemColors は固定色の代わりに Windows のシステムカラーを返すが、後述の実測のとおり、ダークモードには追従しない。


SystemColors が実際に返す色は、読み出して確かめられる。

キー 値 相対輝度
Windows のアプリのモード(AppsUseLightTheme) ダーク(0) -
SystemColors.WindowColor #FFFFFFFF 1.00
SystemColors.WindowTextColor #FF000000 0.00
SystemColors.ControlColor #FFF0F0F0 0.87
SystemColors.HighlightColor #FF0078D7 0.18
SystemColors.HighlightTextColor #FFFFFFFF 1.00
SystemColors.AccentColor #FFE2241A 0.18
SystemColors.GrayTextColor #FF6D6D6D 0.15

.NET 10 / Windows 11で、Windows をダークモードにした PC(表の 1 行目)で SystemColors の各キーを読み出した結果。「相対輝度」の列は WCAG の相対輝度で、前景と背景のコントラストを見積もるために併記している。

この PC はダークモードだが、WindowColor と WindowTextColor の輝度は 1.00 と 0.00 で、ライトの値のままである。SystemColors は Windows のダークモードに追従しなかった。

HighlightColor は選択項目のハイライト色である。ユーザーが個人用設定で選ぶアクセント色は別のキーの AccentColor で、両者は混同されやすい。
撮影した環境ではアクセント色を赤にしてあるため、表でも HighlightColor が #FF0078D7、AccentColor が #FFE2241A と別の値になっている。アクセントを固定色で書くと、この設定と食い違う。

ただし、これらのキーを読み取るだけでは、後からの差し替えに追随しない。
SystemColors.WindowColor のように色を直接読み取ると、読み取った時点の値がそのまま焼き込まれる。

色の参照のしかた 置き換える前 置き換えた後
new SolidColorBrush(SystemColors.WindowColor) #FFFFFFFF #FFFFFFFF
DynamicResource SystemColors.WindowBrushKey #FFFFFFFF #FF102030

アプリケーションのリソースにある SystemColors.WindowBrushKey を差し替え、その前後で両者の値を読み取った結果。測っているのはこの差し替えへの追随であり、OS のテーマ切り替えそのものは測っていない。

直接読み取った側は差し替え後も値が変わらず、リソースキーを DynamicResource で参照した側だけが新しい色になっている。
差し替えに追随させるには、色ではなく SystemColors.WindowBrushKey のようなリソースキーを DynamicResource で参照する必要がある。

ただし、DynamicResource でも、値そのものが変わらなければ追従のしようがない。ThemeMode を Light と Dark で切り替えると、測った Fluent のブラシのキー 5 つはすべて変わったが、測った SystemColors のキー 3 つ(WindowBrushKey・ControlTextBrushKey・AccentColorBrushKey)はどれも変わらなかった。

リソースキー ThemeMode=Light ThemeMode=Dark
ApplicationBackgroundBrush #FFFAFAFA #FF202020
CardBackgroundFillColorDefaultBrush #B3FFFFFF #0DFFFFFF
TextFillColorPrimaryBrush #E4000000 #FFFFFFFF
TextFillColorSecondaryBrush #9E000000 #C5FFFFFF
AccentFillColorDefaultBrush #FFC21C14 #FFF38064
SystemColors.WindowBrushKey #FFFFFFFF #FFFFFFFF
SystemColors.ControlTextBrushKey #FF000000 #FF000000
SystemColors.AccentColorBrushKey #FFE2241A #FFE2241A

.NET 10 / Windows 11 で、それぞれの ThemeMode のウィンドウを表示し、そのウィンドウから各キーを引いた結果。

そのため、テーマに追従させたい背景・面・文字の色は、Fluent のブラシのキーから取る必要がある。SystemColors は Windows 自身から来る色に向く。たとえば SystemColors.AccentColorBrushKey は、どちらのテーマでも個人用設定のアクセント色を返した。


解決方法

外部ライブラリを使わず、以下の 4 点を組み合わせる。

特に .NET 9 の Fluent テーマをアプリ全体に反映する場合、App.xaml の設定が実質的な必須手順となる。
Window 単位の設定だけでは、画面ごとにテーマ適用が分散し、運用時の整合性が崩れやすい。
この構成により、WPF でも手軽に Fluent の設計思想に近い UI を実現できる。


実装例

1. App.xaml で Fluent テーマを適用する

アプリ全体で Fluent テーマを有効化するには、App.xaml にテーマ設定を記述する。
.NET 9 では ThemeMode を使う方法と、Fluent リソースディクショナリをマージする方法のどちらかで適用できる。
ThemeMode を使う場合は次のように記述する。

<Application x:Class="Sample.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.Resources>
    <ResourceDictionary />
  </Application.Resources>
</Application>

Fluent リソースディクショナリを使う場合は次のように記述する。

<Application x:Class="Sample.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml">
  <Application.Resources>
    <ResourceDictionary>
      <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="pack://application:,,,/PresentationFramework.Fluent;component/Themes/Fluent.xaml" />
      </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
  </Application.Resources>
</Application>

ただし、2 つの方法は同等ではない。Application.ThemeMode プロパティのリファレンスによれば、ThemeMode はウィンドウの背景とダークモードも制御する。また、ThemeMode を設定したうえで Fluent のディクショナリを手動でもマージすると、手動のほうが優先されるため、併用は勧められていない。
なお、同じリファレンスによれば ThemeMode は .NET 10 でも実験的な API(Experimental("WPF0001"))であり、将来のバージョンで削除される可能性がある。コードから使うには診断 WPF0001 を抑える必要がある。長く保守するアプリケーションでは、この点を踏まえて選ぶ。次の手順の例は ThemeMode を使う。
どちらか一方を先に入れることで、各 Window 側ではコントロールのローカル調整に集中できる。
App.xaml の設定がない場合、Fluent テーマの適用範囲が局所化し、画面間で見た目が揃わない。

2. Window 側で Fluent テーマのブラシを使って配色と操作感を整える

次に、画面全体とカード領域、ボタンのスタイルを定義する。
次の例は、背景・カード・文字の色を Fluent テーマ自身のブラシのキーから、ボタンの色をアクセントのキーから、いずれも DynamicResource で参照する。そのため、ライトとダークのテーマに合わせて切り替わる。

<Window x:Class="Sample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Fluent Without External Libraries"
        Width="440" Height="300"
        Background="{DynamicResource ApplicationBackgroundBrush}">

  <Window.Resources>
    <Style x:Key="CardBorderStyle" TargetType="Border">
      <Setter Property="Padding" Value="24" />
      <Setter Property="CornerRadius" Value="12" />
      <Setter Property="BorderThickness" Value="1" />
      <Setter Property="Background" Value="{DynamicResource CardBackgroundFillColorDefaultBrush}" />
      <Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorDefaultBrush}" />
    </Style>

    <Style x:Key="AccentButtonStyle" TargetType="Button">
      <Setter Property="Padding" Value="14,8" />
      <Setter Property="Margin" Value="0,12,0,0" />
      <Setter Property="HorizontalAlignment" Value="Left" />
      <Setter Property="Foreground" Value="{DynamicResource TextOnAccentFillColorPrimaryBrush}" />
      <Setter Property="Background" Value="{DynamicResource AccentFillColorDefaultBrush}" />
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="Button">
            <Border x:Name="Root"
                    Background="{TemplateBinding Background}"
                    CornerRadius="8"
                    Padding="{TemplateBinding Padding}">
              <ContentPresenter HorizontalAlignment="Center"
                                VerticalAlignment="Center" />
            </Border>
            <ControlTemplate.Triggers>
              <Trigger Property="IsMouseOver" Value="True">
                <Setter TargetName="Root" Property="Opacity" Value="0.92" />
              </Trigger>
              <Trigger Property="IsPressed" Value="True">
                <Setter TargetName="Root" Property="Opacity" Value="0.82" />
              </Trigger>
              <Trigger Property="IsEnabled" Value="False">
                <Setter TargetName="Root" Property="Opacity" Value="0.55" />
              </Trigger>
            </ControlTemplate.Triggers>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
  </Window.Resources>

  <Grid Margin="32">
    <Border Style="{StaticResource CardBorderStyle}">
      <StackPanel>
        <TextBlock FontSize="24"
                   FontWeight="SemiBold"
                   Foreground="{DynamicResource TextFillColorPrimaryBrush}"
                   Text="WPF Fluent Style" />

        <TextBlock Margin="0,10,0,0"
                   TextWrapping="Wrap"
                   Foreground="{DynamicResource TextFillColorSecondaryBrush}"
                   Text="Fluent theme brushes follow the light and dark theme." />

        <Button Style="{StaticResource AccentButtonStyle}"
                Content="Run Action" />
      </StackPanel>
    </Border>
  </Grid>
</Window>

上記の実装は、WPF 標準機能のみで視覚階層と操作フィードバックを整える構成である。
同じ XAML を ThemeMode の Light と Dark で表示すると、次のようになる。

上の XAML を ThemeMode の Light で表示した画面。明るい灰色の背景に白い角丸のカードがあり、濃い色の見出し、灰色の説明文、赤い角丸のアクセントボタンが並ぶ。
上の XAML を ThemeMode="Light" で表示した結果。追加ライブラリは使っていない。「問題」の図と比べると、カード面が背景から分離し、角丸・余白・アクセントのボタンで階層が作られている。
同じ画面を ThemeMode の Dark で表示した結果。暗い背景に少し明るいカードがあり、白い見出し、明るい灰色の説明文、暗い文字のサーモン色のアクセントボタンが並ぶ。
同じ XAML を ThemeMode="Dark" で表示した結果。すべての色がテーマのブラシのキーから来るため、画面全体が切り替わる。

同じ画面を SystemColors で組むと、ダークのテーマでは成り立たない。背景・カード・文字を SystemColors のキーから取ると、Fluent のボタンのスタイルはダークに切り替わる一方、画面は白いまま残った。

背景・カード・文字に SystemColors のキーを使った画面を ThemeMode の Dark で表示した結果。画面とカードは白と明るい灰色、文字は黒のままで、ボタンだけが暗い灰色になり文字がほとんど読めない。
背景・カード・文字を SystemColors のキーから DynamicResource で参照し、ThemeMode="Dark" で表示した結果。SystemColors の値は変わらないため、Fluent がスタイルを当てるコントロールだけが暗くなる。

注意点


代替案・比較

方法 メリット デメリット 適するケース
WPF 標準スタイル + テーマのブラシ 追加ライブラリ不要、依存が増えない、ライトとダークに追従する Fluent の高度な素材表現は限定的 長期保守重視、既存 WPF 資産を維持する場合
外部 Fluent 系ライブラリ導入 既製テーマで見た目を短時間に統一しやすい 依存関係の更新コスト、テーマ差分検証が必要 新規開発で UI 優先度が高い場合
完全カスタム描画 表現自由度が最も高い 実装・検証コストが高い ブランド要件が強く、専用 UI が必要な場合

まとめ

WPF で Fluent デザインを適用する実装は、追加ライブラリなしでも成立する。
要点は、App.xaml で Fluent テーマをアプリ全体に適用したうえで、角丸と階層表現を定義し、Fluent テーマのブラシを DynamicResource で参照してライトとダークに追従させることである。SystemColors はダークモードに追従しない。
保守性を重視する場合は WPF 標準スタイル + テーマのブラシが適し、視覚効果の優先度が高い場合のみ外部ライブラリ導入を検討するのが妥当である。


関連記事