2026年7月14日火曜日

WPF/C#で jsonを使ったAppSettingsの実装

1.パッケージ追加

microsoft.extensions.configuration.json

JSON形式の設定ファイル(appsettings.jsonなど)を読み込むための構成プロバイダー

パッケージのインストール

Visual Studioツール>NuGetマネージャー>パッケージ マネージャー コンソール以下を実行
PM> Install-Package Microsoft.Extensions.Configuration.Json

microsoft.extensions.hosting

依存関係の注入 (DI)構成管理 (Configuration)などを統合的に管理するためのライブラリ(汎用ホスト)

パッケージのインストール
Visual Studioのツール>NuGetマネージャー>ソリューションのNuGet パッケージの管理
Microsoft.Extensions.Hostingを検索>選択>プロジェクトにチェック>インストール

2.appsettiongs.json追加
 (ファイル名は他と干渉しなければ任意)

・ソリューションエクスプローラー>プロジェクト選択>追加>JavaScript JSON 構成ファイル>名前入力>追加

・追加されたappsettiongs.jsonを右クリック>プロパティー>ビルドアクション>ドロップダウンで[コンテンツ]に変更

・出力ディレクトリにコピー>ドロップダウンで[新しい場合はコピーする]に変更

・JSONの例{

  "AppName": "MyApp",
  "MainWin": {
    "Title": "MainWindow", "Width": 800, "Height": 500, "Left": -877, "Top": 202 }

3.Jsonに対応するクラス追加

appsettingsをいくつかに分割する前提で、全体を管理するクラスを作る。名前は任意。
分割した個々の要素(Section)に対応するクラスを作り、そのインスタンスをプロパティーとして設定する。

    public class AppSettings
    {
public string AppName { get; set; } = string.Empty;
public MainWinSettings MainWin { get; set; } = new(); }

appsettings.jsonのセクション名と一致する名前でプロパティーを作る。
上記の例ではappsettings.jsonの中に”MainWin
"というセクションがある。

4.JSONとクラスの依存関係設定

JSONとクラスの依存関係設定

・プロジェクトダブルクリック>csprojに次のようなtemGroup追加

 	<ItemGroup>
<Compile Update="AppSettings.cs">
<DependentUpon>appsettings.json</DependentUpon>
</Compile>
</ItemGroup>

この設定を行うことで、ビルド時に実行ファイル(.exe)と同じフォルダにJSONファイルが出力され、DIコンテナ(Microsoft.Extensions.Configuration など)から正しく読み込めるようになる。

App.xaml.csのようにAppコンストラクタでServiceを構成することで、入出力先を変更できる。

5.App.xaml変更

StartupUriの設定を削除。

    StartupUri="MainWindow.xaml">

6.App.xaml.cs変更

コンストラクタでDIから受け取るServiceを登録

        public static IHost? AppHost { get; private set; }
        /// <summary>
        /// ホスト(Host)を構築し、設定ファイルとウインドウをDIコンテナに登録します。
        /// </summary>
        public App()
        {
            AppHost = Host.CreateDefaultBuilder()
                .ConfigureAppConfiguration((context, config) =>
                {
                    // 1. 環境(Debug/Release)に応じて読み込み元を決定
                    string jsonFileName = AppSettings.FileName; // "appsettings.json";
                    string targetPath = StorageService.GetSettingsFilePath(jsonFileName, true);
                    config.AddJsonFile(targetPath, optional: true, reloadOnChange: true);
                })
                .ConfigureServices((context, services) =>
                {
                    services.AddSingleton<StorageService>();
                    services.Configure<AppSettings>(context.Configuration);

                    var rootSettings = context.Configuration.Get<AppSettings>() ?? new AppSettings();
                    services.PostConfigure<AppSettings>(settings => { settings.PostConfigure(); });
                    // MainWindow や他の設定クラスに渡す AppSettings は、Options パターン(IOptions)を経由して取得したものをシングルトン登録する
                    services.AddSingleton<AppSettings>(provider =>
                    {
                        // ここで .Value を要求した瞬間に、AppSettingsが読み込まれ、PostConfigureが実行される
                        var options = provider.GetRequiredService<Microsoft.Extensions.Options.IOptions<AppSettings>>();
                        return options.Value;
                    });
                    // 画面をDIコンテナに登録
                    services.AddTransient<MainWindow>();
                })
                .Build();
        }
        protected override async void OnStartup(StartupEventArgs e)
        {
            // ホストの起動
            await AppHost.StartAsync();

            // DIコンテナからMainWindowを取得(自動的にAppSettingsが注入される)
            var mainWindow = AppHost.Services.GetRequiredService<MainWindow>();
            mainWindow.Show();

            base.OnStartup(e);
        }

        protected override async void OnExit(ExitEventArgs e)
        {
            // アプリ終了時にホストをクリーンアップ
if (AppHost != null)
{
await AppHost.StopAsync();
}
AppHost.Dispose(); base.OnExit(e); }

7.MainWindow.xaml.cs変更

コンストラクタでDIから登録したServiceを受け取る。
MainWindowの設定に関わるものがある場合は
InitializeComponentの前に呼ぶ。

        public MainWindow(AppSettings settings)
        {
            this.DataContext = settings.MainWin;
InitializeComponent(); }


8.StorageServiceの実装例

        const string brandName = "MyComp";
        const string appName = "MyApp";

         /// <summary>
        /// AppData/Local/MyComp/MyApp 配下の指定されたファイル名のパスを返す。
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="copyIfNotExistsInAppData"></param>
        /// <returns></returns>
        public static string GetSettingsFilePath(string fileName, bool copyIfNotExistsInAppData)
        {
            #if DEBUG
                    // Debug時はexeと同じフォルダのファイルを返す
                    return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
            #else
            //Release(インストール後)はAppData配下の専用フォルダを返す
            string appDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); // Local配下に作る場合はこちら
            //string appDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); // Roaming配下に作る場合はこちら
            appDataFolder = Path.Combine((Path.Combine(appDataFolder, Path.Combine(brandName, appName))));
            // フォルダがなければ自動作成
            if (!Directory.Exists(appDataFolder))
            {
                Directory.CreateDirectory(appDataFolder);
            }
            string targetPath = Path.Combine(appDataFolder, fileName);
            // 初回起動時など、AppData にファイルがなければ exe 横からコピーする
            if (!File.Exists(targetPath) && copyIfNotExistsInAppData)
            {
                string sourcePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
                if (File.Exists(sourcePath))
                {
                    File.Copy(sourcePath, targetPath);
                }
            }
            return targetPath;
            #endif
        }

単純に保存すると日本語がのエスケープ付きバイナリ値(UTF16?)になる。次のようにするとUTF8で保存できる。

        public void SaveJson<T>(T data, string fileName){
            var options = new JsonSerializerOptions
            {
                Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
                WriteIndented = true
            };
            string filePath = GetSettingsFilePath(fileName, false);
            var json = JsonSerializer.Serialize(data, options);
            File.WriteAllText(filePath, json, Encoding.UTF8);
        }

WPF/C# マルチディスプレイで一方が未接続時にウインドウが表示範囲外に出てしまうときの対応

アプリ起動時にウインドウを前回終了時の位置に表示する場合、 マルチディスプレイの一方が未接続だとにウインドウがスクリーンの表示範囲外に出てしまうことがある。その場合、メインディスプレイに表示させるようにする。

1. スクリーンサイズを取得するために using System.Windows.Forms を有効にする。

プロジェクトをダブルクリック
csprojのXMLの<PropertyGroup>に次の行を追加

<UseWindowsForms>true</UseWindowsForms>

これだけだと全てのクラスに暗黙的に using System.Windows.Forms が適用され、MessageBoxのライブラリ間の競合が発生する。

    'MessageBox' は、'System.Windows.Forms.MessageBox' と  'System.Windows.MessageBox' 間のあいまいな参照です
これを避けるため、csproj次の設定を追加する
  <ItemGroup>
	<!-- WinFormsの自動usingから、衝突の原因になるSystem.Windows.Formsを削除 -->
	<Using Remove="System.Windows.Forms" />
  </ItemGroup>

スクリーンサイズを扱うクラスに using を追加
using WinForms = System.Windows.Forms;

2. ウィンドウがスクリーンに表示されているかの判断

//Windowの矩形領域
var windowRect = new Rectangle(Left, Top, Width, Height);
bool isVisible = false;
foreach (var screen in System.Windows.Forms.Screen.AllScreens)
{
// IntersectsWith を使い、現在のディスプレイの有効範囲(WorkingArea)と
// ウィンドウの矩形領域が重なっているか判定
if (screen.WorkingArea.IntersectsWith(windowRect))
{
isVisible = true;
break;
}
}
// どのディスプレイの範囲内にも入っていない、メインディスプレイの左上に配置、または画面右上あたりへ
if (!isVisible)
{
this.Left = 100;
this.Top = 100;
}

2026年7月12日日曜日

MSBuild プロジェクトビルドの出力の警告レベルを変更する手順

「発行」でエラーが発生し、原因を調べるためにビルド警告レベルを「詳細」に替えましたが、その後もとに戻そうとして設定方法を忘れて困ったので、メモしておきます。
ほとんどAIの回答のままです。

Visual Studio の「プロジェクトのプロパティ」ではなく、「全体のオプション」 画面にあります。

  • 設定画面の開き方と項目[ツール] メニュー を開きます。
  • [オプション] を選択します。
  • 左側のリストから [プロジェクトおよびソリューション] を展開します。
  • [ビルドおよび実行] を選択します。
  • 右側にある以下の 2つの項目 を変更します。
    「MSBuild プロジェクトビルドの出力の記述詳細度」
    「MSBuild プロジェクトビルドのログファイルの記述詳細度」
設定する値通常は「最小」や「標準」になっています。
エラーの詳しい原因を突き止めるために、これを 「詳細 (Detailed)」 または 「診断 (Diagnostic)」 に切り替えてください。
設定後に再度「発行」を試行すると、画面下の 「出力」ウィンドウ に、どのファイルや処理でエラーが起きたのかが非常に細かく分析・表示されるようになります。

WPFで単一exeのアプリを作成する方法

WPFアプリを単純にビルドするとすごい数のDLLが含まれる。

発行のオプションに「単一ファイルの作成」があるが、これだけだとまだいくつかDLLの残る。それらも全て含めてひとつのexeを作る手順。


ソリューションエクスプローラー > プロジェクト右クリック > 発行

> ターゲット/フォルダ

> 特定のターゲット/フォルダ > 場所 > 完了

>「公開準備が完了しました。」

> [すべての設定を表示]をクリック

【重要】 [配置モード]は[フレームワーク依存]にすること

 [ターゲットランタイム]を[移動可能]から特定環境指定に変更

> [ファイル公開オプション]が表示される

> 展開 > [単一ファイルの作成] をチェック

> 発行

次の警告が出る場合は、メッセージとおりプロジェクトのcsprojの<PropertyGroup>にメッセージにある設定を追加する。

2>'インターセプター' 機能は、この名前空間では有効になっていません。
プロジェクトに
'<InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.Extensions.Configuration.Binder.SourceGeneration</InterceptorsNamespaces>'

 を追加します。


これだけだといくつかdllが出力される。これもexeに含める場合は、ソリューションエクスプローラーでFolderProfile.pubxml に次の一行を追加   

Project>Properties>PublishProfiles>FolderProfile.pubxml 

<IncludeNativeLibrariesForSelfContained>true</IncludeNativeLibrariesForSelfContained>

(cs.projのPropertyGroupに追加してもよい。)