mcpField
M
maui-theming

maui-theming

github.com/dotnetデザイン・UI5.8k3.4k 回インストール1 日前に更新
リポジトリ

maui-theming について

.NET MAUI アプリの開発者向けに、システムのライト/ダークモードとカスタムテーマの設定方法を解説します。実行時の切り替え、システムテーマへの対応、設定の保存も扱います。

主な機能

  1. 01システムのライト/ダークテーマ設定
  2. 02カスタム ResourceDictionary テーマの作成
  3. 03実行時のテーマ切り替え
  4. 04システムテーマ変更の検知
  5. 05テーマ設定の保存と復元

活用シーン

  1. 01.NET MAUI アプリへのライト/ダークモード追加
  2. 02ブランドテーマの選択肢の提供
  3. 03OS のテーマ変更への対応
  4. 04アプリ起動後もテーマ設定を維持

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dotnet/skills --skill maui-theming

インストール後、Claude Code で/maui-themingを入力して読み込まれたか確認します。

同梱ファイルreferences/theming-api.md

詳細

.NET MAUI のテーマ設定

AppThemeBinding、ResourceDictionary の切り替え、システムテーマ検出 API を使って、.NET MAUI アプリでライト/ダークモード、カスタムブランドテーマ、実行時のテーマ切り替えに対応します。

使用する場面

  • .NET MAUI アプリにライトモードとダークモードを追加する
  • ResourceDictionary を使ってカスタムブランドテーマを作成する
  • 実行時にシステムテーマの変更を検出して対応する
  • ユーザーが優先テーマ(ライト、ダーク、またはシステムの既定値)を選べるようにする
  • OS によるテーマ変更への対応とカスタムカラーパレットを組み合わせる

使用しない場面

入力

  • .NET 8 以降をターゲットとする .NET MAUI プロジェクト
  • テーマに応じたスタイル設定が必要な XAML ページまたは C# の UI コード

ワークフロー

  1. プロジェクトで現在使われているテーマ設定方法(AppThemeBinding、ResourceDictionary、または未使用)を確認します。
  2. 適切な方法を選択します。シンプルなライト/ダークテーマには AppThemeBinding、カスタムテーマや複数のテーマには ResourceDictionary の切り替え、または両方を組み合わせます。
  3. テーマリソースを定義します。インラインの AppThemeBinding 値、または対応するキーを持つ個別の ResourceDictionary ファイルを使用します。
  4. XAML ページ全体でハードコードされた色を DynamicResource バインディング(または AppThemeBinding マークアップ)に置き換えます。
  5. Application.Current.RequestedTheme と RequestedThemeChanged イベントを使って、システムテーマの検出を追加します。
  6. Preferences.Set/Preferences.Get を使ってユーザー設定を保存し、起動時に適用します。
  7. テーマ変更時のアクティビティ再起動を避けるため、Android の ConfigChanges.UiMode が MainActivity に設定されていることを確認します。
  8. 少なくとも 1 つのターゲットプラットフォームでライトテーマとダークテーマをテストし、すべての UI 要素が正しく反応することを確認します。

回答を変えるルール

次のルールをユーザーのシナリオに照らして確認し、質問に影響するものだけを適用してください。UiMode とディクショナリの切り替えが関係するのは、実行時のテーマ切り替えの場合です。AppThemeBinding の設定に関する質問では不要です。

質問の範囲に絞りつつ、完全に回答してください。 完全な回答とは、推奨した内容を実装するコードを示すことです。関連するだけの話題を追加することではありません。DynamicResource を推奨する場合は、値が更新されるようにするディクショナリの切り替えも示してください。ユーザーが C# でライト/ダークの色を質問した場合は、SetAppThemeColor(色)と汎用の SetAppTheme<T>(任意のバインド可能なプロパティ型)の両方を示し、散在するハードコードされた色よりもリソースキーを優先してください。質問にないプラットフォーム設定を付け加えないでください。

ルールすることしないこと理由
実行時に切り替える値は動的にする{DynamicResource Key}{StaticResource Key}StaticResource は読み込み時に一度だけ解決され、ディクショナリを切り替えても更新されません。
Android では UiMode を宣言する (実行時/システムテーマの切り替えに限る)MainActivity の ConfigurationChanges リストに ConfigChanges.UiMode を含める省略する設定しないと、テーマ変更時に Android がアクティビティを再起動するため、ナビゲーション状態が失われ、クラッシュしたように見えます。静的な AppThemeBinding の設定には関係ありません
UserAppTheme でテーマを強制するApplication.Current.UserAppTheme = AppTheme.Dark色を手動で再割り当てするUserAppTheme は OS のテーマ設定を上書きします。AppTheme.Unspecified にすると、システムテーマに従う状態に戻ります。

ユーザーが別の方法の利点(2 つを超えるテーマ、またはユーザーが選択できるテーマ)を必要としていない限り、動作している AppThemeBinding の設定を ResourceDictionary の切り替えに置き換えたり、その逆を行ったりしないでください。

アプローチの選択

アプローチ最適な用途制限事項
AppThemeBindingOS に合わせたライト/ダークの自動切り替え — コードを最小限に抑えられますテーマは 2 種類(ライト+ダーク)のみです
ResourceDictionary の切り替え独自ブランドのテーマ、2 種類を超えるテーマ、ユーザー設定設定が増えます。すべての箇所で DynamicResource を使用する必要があります
両方の組み合わせOS に応じた切り替えと、独自のテーマカラー最も柔軟ですが、最も複雑です

AppThemeBinding(OS のライト/ダーク)

AppThemeBinding は、現在のシステムテーマに応じて値を選択します。Light、Dark、および任意のフォールバックとしての Default をサポートします。

パレットは一度だけ定義し、あちこちにリテラルを散在させない

すべての要素に {AppThemeBinding Light=#333333, Dark=#FFFFFF} を記述するのは、テーマ設定で最もよくある失敗です。パレットが数十個のファイルに重複し、一か所で変更できなくなります。最終的な回答では、インラインのリテラルではなく、以下の形式を推奨してください。


<Application.Resources>
    <ResourceDictionary>

        
        <Color x:Key="LightPageBackground">#FFFFFF</Color>
        <Color x:Key="DarkPageBackground">#1E1E1E</Color>
        <Color x:Key="LightPrimaryText">#333333</Color>
        <Color x:Key="DarkPrimaryText">#E0E0E0</Color>

        
        <Style TargetType="ContentPage" ApplyToDerivedTypes="True">
            <Setter Property="BackgroundColor"
                    Value="{AppThemeBinding Light={StaticResource LightPageBackground},
                                            Dark={StaticResource DarkPageBackground}}" />
        </Style>

        <Style TargetType="Label">
            <Setter Property="TextColor"
                    Value="{AppThemeBinding Light={StaticResource LightPrimaryText},
                                            Dark={StaticResource DarkPrimaryText}}" />
        </Style>

    </ResourceDictionary>
</Application.Resources>

ページ側にテーマ設定のマークアップは一切必要ありません。スタイルが暗黙的に適用されます。インラインの AppThemeBinding は、本当に個別対応が必要な箇所に限って使用し、その場合もリテラルな 16 進数ではなく {StaticResource} キーを参照してください。

XAML(個別対応向けのインライン形式)
<Label Text="Themed text"
       TextColor="{AppThemeBinding Light=Green, Dark=Red}"
       BackgroundColor="{AppThemeBinding Light=White, Dark=Black}" />


<Label TextColor="{AppThemeBinding Light={StaticResource LightPrimary},
                                   Dark={StaticResource DarkPrimary}}" />
C# 拡張メソッド

「C# でライト/ダークの色を指定する方法」について回答する場合は、両方を示してください。SetAppThemeColor は Color プロパティに、SetAppTheme<T> はそれ以外のすべてに対応します。

var label = new Label();

// Color-specific helper
label.SetAppThemeColor(Label.TextColorProperty, Colors.Green, Colors.Red);

// Generic helper — works for any bindable property type, not just Color
label.SetAppTheme<Color>(Label.TextColorProperty, Colors.Green, Colors.Red);
label.SetAppTheme<double>(Label.FontSizeProperty, 14, 16);

// The BindableProperty must belong to the object you call it on —
// Image.SourceProperty goes on an Image, not a Label.
var image = new Image();
image.SetAppTheme<ImageSource>(Image.SourceProperty,
    ImageSource.FromFile("logo_light.png"),
    ImageSource.FromFile("logo_dark.png"));

コードベース全体にハードコードされた色を散在させるのではなく、値をリソースキーとして定義し、参照することをおすすめします。

ResourceDictionary によるテーマ設定(カスタムテーマ)

キーをそろえた個別の ResourceDictionary ファイルでテーマを定義し、実行時に切り替えます。

ステップ 1 — テーマディクショナリを定義する

以下の例のように x:Class を使ってコンパイル済み XAML を使用する場合、各ディクショナリには InitializeComponent() を呼び出すコードビハインドが必要です。x:Class を使用せずに Source 経由で読み込むディクショナリには、コードビハインドは必要ありません。

LightTheme.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                    x:Class="MyApp.Themes.LightTheme">
    <Color x:Key="PageBackgroundColor">White</Color>
    <Color x:Key="PrimaryTextColor">#333333</Color>
    <Color x:Key="AccentColor">#2196F3</Color>
</ResourceDictionary>

LightTheme.xaml.cs

namespace MyApp.Themes;

public partial class LightTheme : ResourceDictionary
{
    public LightTheme() => InitializeComponent();
}

同じキーを使い、値を変えた DarkTheme.xaml / DarkTheme.xaml.cs を作成します。

ステップ 2 — DynamicResource で使用する

実行時にディクショナリを切り替えた際に値が更新されるよう、DynamicResource を使用します。

<ContentPage BackgroundColor="{DynamicResource PageBackgroundColor}">
    <Label Text="Hello"
           TextColor="{DynamicResource PrimaryTextColor}" />
    <Button Text="Action"
            BackgroundColor="{DynamicResource AccentColor}" />
</ContentPage>
ステップ 3 — 実行時にテーマを切り替える

🚨 テーマを切り替えるために、MergedDictionaries.Clear() を呼び出してはいけません。 MAUI の既定のテンプレートでは、Resources/Styles/Colors.xaml と Styles.xaml が Application.Resources にマージされます。Clear() はそれらも削除するため、アプリ内の暗黙的なスタイル、ブラシ、色がすべてひそかに失われ、ボタン、入力欄、ラベルなどがスタイル未適用の既定状態に戻ります。検証済みです。Clear() の後、MergedDictionaries は 2 個から 1 個に減り、テンプレートの Primary 色が解決されなくなります。

追加したテーマだけを削除し、それ以外には手を加えないでください。

static ResourceDictionary? _currentTheme;

void ApplyTheme(ResourceDictionary theme)
{
    var merged = Application.Current!.Resources.MergedDictionaries;

    // ✅ Remove ONLY the previous theme — Colors.xaml / Styles.xaml survive
    if (_currentTheme is not null)
        merged.Remove(_currentTheme);

    merged.Add(theme);
    _currentTheme = theme;
}

// Usage
ApplyTheme(new DarkTheme());
// ❌ Destroys the app's Colors.xaml and Styles.xaml along with the old theme
var merged = Application.Current!.Resources.MergedDictionaries;
merged.Clear();
merged.Add(theme);

システムテーマの検出

現在のテーマを取得する
AppTheme currentTheme = Application.Current!.RequestedTheme;
// Returns AppTheme.Light, AppTheme.Dark, or AppTheme.Unspecified
システムテーマを上書きする
// Force dark mode regardless of OS setting
Application.Current!.UserAppTheme = AppTheme.Dark;

// Reset to follow system theme
Application.Current!.UserAppTheme = AppTheme.Unspecified;
テーマの変更に対応する
Application.Current!.RequestedThemeChanged += (s, e) =>
{
    AppTheme newTheme = e.RequestedTheme;
    // Update UI or switch ResourceDictionaries
};

両方のアプローチを組み合わせる

最大限の柔軟性を得るには、DynamicResource の値とともに AppThemeBinding を使用します。ネストされた DynamicResource は有効なままなので、ディクショナリを切り替えると値が更新され、OS のライト/ダーク切り替えも引き続き反映されます。

<Label TextColor="{AppThemeBinding
    Light={DynamicResource LightPrimary},
    Dark={DynamicResource DarkPrimary}}" />

または、システムの変更に対応して、ディクショナリ全体を切り替えます。

Application.Current!.RequestedThemeChanged += (s, e) =>
{
    ApplyTheme(e.RequestedTheme == AppTheme.Dark
        ? new DarkTheme()
        : new LightTheme());
};

ユーザー設定の保存と復元

ユーザーの選択を Preferences に保存し、起動時に適用します。

// Save choice
Preferences.Set("AppTheme", "Dark");

// Restore on startup (in App constructor or CreateWindow)
var saved = Preferences.Get("AppTheme", "System");
Application.Current!.UserAppTheme = saved switch
{
    "Light" => AppTheme.Light,
    "Dark"  => AppTheme.Dark,
    _       => AppTheme.Unspecified
};

よくある問題

Android:ConfigChanges.UiMode が必要

テーマ変更イベントを発生させるには、MainActivity に ConfigChanges.UiMode を含める必要があります。含めない場合、アクティビティは変更に適切に対応できず、再起動します。

[Activity(Theme = "@style/Maui.SplashTheme",
          MainLauncher = true,
          ConfigurationChanges = ConfigChanges.ScreenSize
                               | ConfigChanges.Orientation
                               | ConfigChanges.UiMode  // ← Required for theme detection
                               | ConfigChanges.ScreenLayout
                               | ConfigChanges.SmallestScreenSize
                               | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity { }

UiMode を指定しないと、Android の設定でダークモードを切り替えた際にアクティビティが完全に再起動し、ナビゲーションの状態が失われ、クラッシュしたように見えます。指定すれば、アプリは動作を続け、RequestedThemeChanged が発生します。この修正と、テーマを再適用するハンドラーを組み合わせてください(下記を参照)。

DynamicResource と StaticResource

ResourceDictionary でテーマを切り替える場合は、必ず DynamicResource を使用してください。


<Label TextColor="{DynamicResource PrimaryTextColor}" />


<Label TextColor="{StaticResource PrimaryTextColor}" />

DynamicResource が役立つのは、実際にディクショナリが切り替わる場合に限られます。この問題を診断するときは、修正方法とあわせて、切り替え処理とシステムテーマのフックを必ず示してください。そうしないと、バインディングを修正しても更新されないままです。

static ResourceDictionary? _currentTheme;

void ApplyTheme(bool useDark)
{
    var merged = Application.Current!.Resources.MergedDictionaries;

    // Remove only the previous theme — never Clear(), which also wipes
    // the template's Colors.xaml / Styles.xaml
    if (_currentTheme is not null)
        merged.Remove(_currentTheme);

    _currentTheme = useDark ? new DarkTheme() : new LightTheme();
    merged.Add(_currentTheme);
}

// React to the OS switching light/dark
Application.Current!.RequestedThemeChanged += (s, e) =>
    ApplyTheme(e.RequestedTheme == AppTheme.Dark);
ハードコードされた色はテーマ設定を壊します

テーマに従うべき要素には、色の値をインラインで指定しないでください。


<Label TextColor="#333333" />


<Label TextColor="{DynamicResource PrimaryTextColor}" />
CSS のテーマは実行時に切り替えられません

.NET MAUI は CSS によるスタイル設定をサポートしていますが、CSS ベースのテーマは実行時に動的に切り替えられません。実行時に切り替えるには、ResourceDictionary によるテーマ設定を使用してください。

テーマキーはすべてのディクショナリで一致させる必要があります

あるテーマディクショナリで使用されているすべての x:Key は、ほかのすべてのテーマディクショナリにも存在する必要があります。キーが欠けていると、既定値にサイレントにフォールバックし、表示に一貫性がなくなります。

プラットフォームのサポート

プラットフォーム最小バージョン
iOS13+
Android10+(API 29)
macOS Catalyst10.15+
Windows10+

クイックリファレンス

  • OS のライト/ダーク → AppThemeBinding マークアップ拡張
  • C# のテーマカラー → SetAppThemeColor()、SetAppTheme<T>()
  • OS のテーマを読み取る → Application.Current.RequestedTheme
  • テーマを強制する → Application.Current.UserAppTheme = AppTheme.Dark
  • テーマの変更 → RequestedThemeChanged イベント
  • カスタム切り替え → 古いテーマを MergedDictionaries から Remove してから、新しいテーマを Add します — Clear() は決して使用しないでください
  • 実行時のバインディング → DynamicResource(StaticResource ではありません)
  • 選択内容を保持する → Preferences.Set / Preferences.Get

コメント