2011年11月22日火曜日

.net framework : フリーソフトの設定ファイルはどこに作るべきか

PCを買い換えてOSがWindowsXPからWindows7に変わったとき、program filesのアクセス権管理が厳格になったことに気付きました。 Vista以降で厳格になったようです。 それについて解説してあるページはいくつかあります。 たとえば、

セキュリティを考慮してこうなったようですね。

フリーソフトを作る場合、設定ファイルの保存先に気を配ることになりそうです。 WindowsXPまでなら実行ファイルと同じフォルダに設定ファイルを作っておいて、

「アンインストールするにはフォルダごと削除してください。レジストリは使用しておりません。」

で済みました。 Vista以降でprogram filesにソフトを置かれた場合はそれは止めておいた方がよさそうです。 アクセス権のせいで設定ファイルが作れなかったり、VirtualStoreに設定ファイルが作られてしまいます。

VirtualStoreがやっかいですね。 program filesに書き込む代わりに別のフォルダに書き込まれてしまいます。 VirtualStoreに書き込まれると、アンインストールしたときに「消したはずなのにゴミが残る」という1番嫌われるパターンになってしまいます。 それは避けたいです。

では、どうするかというと、

  1. ユーザーがソフトをprogram files以外のフォルダに置いた場合、同じフォルダに設定ファイルを作る。
  2. program filesにソフトを置かれた場合、または同じフォルダに設定ファイルを作れない場合、アプリケーションデータフォルダに設定ファイルを作る。

というようにします。 ユーザーに選択してもらえるようにするのが重要。 作る側がマイクロソフトの仕様に納得していなくても、ユーザーが「それでいい」と思っているならそちらが優先されるべきですしね。 ソフトの置き場所とかはこだわっている人も多いでしょう。 セキュリティが絡んでこうなった以上は、昔の習慣を押し付けるのは考えなければならないかと。

もうちょっと凝るなら、program files以外のフォルダにソフトを置かれた場合でもパスを選べるようにすると良いかもしれません。 実行ファイルと同じフォルダに「useAppData」みたいな名前のファイル(空でよい)があるかをチェックして、あったらアプリケーションデータフォルダを使う、無かったら実行ファイルと同じフォルダを使う、というような感じで。 インストーラを作るなら、そこでuseAppDataを作るかどうか決めればいいでしょう。 インストーラなしでも、設定ファイルの位置にこだわるような人はreadmeも読むでしょうから、アプリケーションデータフォルダを使いたい人には管理者権限で手動で作ってもらえばいいでしょう。

まぁそんな方針で設定ファイルの入出力をするコードはこんな感じ。

public const string FILE_EXTENSION = ".conf";
public const string FILE_FOLDER_SWITCH = "useAppData";

private static void GetFilePath(out string pathExe, out string pathAppData)
{
    FileInfo fileInfo = new FileInfo(Environment.GetCommandLineArgs()[0]);
    string fileExt = fileInfo.Extension;
    string fileName = fileInfo.Name;
    string exeName = fileName.Substring(0, fileName.Length - fileExt.Length);

    pathExe = fileInfo.Directory.FullName;
    if (File.Exists(pathExe + "\\" + FILE_FOLDER_SWITCH)
        || pathExe.StartsWith(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86))
        || pathExe.StartsWith(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)))
    {
        pathExe = null;
    }
    else
    {
        pathExe += "\\" + exeName + FILE_EXTENSION;
    }

    pathAppData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)
                + "\\" + exeName + "\\" + exeName + FILE_EXTENSION;
}

// 例外処理は呼び出し元で
public static void Save()
{
    if (FilePath != null)
    {
        // ロードしたパス or 最後に書き込んだパスを使用。
        SaveActually(FilePath);
    }
    else
    {
        string pathExe, pathAppData;
        GetFilePath(out pathExe, out pathAppData);

        // ロードしたパス or 最後に書き込んだパスがない場合
        // path1 → path2の順に書き込んでみて
        // 実際に書き込めたパスをFilePathに残す。
        try
        {
            SaveActually(pathExe);
            FilePath = pathExe;
        }
        catch
        {
            SaveActually(pathAppData);
            FilePath = pathAppData;
        }
    }
}

// 例外処理は呼び出し元で
public static void Load()
{
    string pathExe, pathAppData;
    GetFilePath(out pathExe, out pathAppData);

    // ファイルがあったら、例え読み込めなかったとしてもそれをFilePathに残す。
    if (File.Exists(pathExe))
    {
        FilePath = pathExe;
        LoadActually(pathExe);
    }
    else if (File.Exists(pathAppData))
    {
        FilePath = pathAppData;
        LoadActually(pathAppData);
    }
    else
    {
        throw new Exception("AppConfig : Load失敗");
    }
}

GetFilePathで実行ファイルと同じフォルダのパス(pathExe)とアプリケーションデータフォルダに保存する場合のパス(pathAppData)を取得しています。 「 Environment.GetCommandLineArgs()[0] 」が実行ファイル(exe)のパス、「 Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) 」がアプリケーションデータフォルダのパスです。 アプリケーションデータフォルダにはソフトごとの子フォルダを作って設定ファイルを置きます。 program filesや「useAppData」の場合分けもここでチェック。 pathExeが使えない場合は、nullを返しています。

SaveとLoadがUIから呼ばれる設定ファイル読み書きメソッドです。 エラーメッセージを表示するのはUI側の処理なので、ここでは例外処理をしてません。 SaveActuallyとLoadActuallyで実際の読み書きをするのですが、これはアプリケーションによって違うので略。

LoadとSaveではちょっとパスの決め方が違います。 Loadはアプリケーション起動直後に必ず呼ばれるという前提です。 Loadは設定ファイルがあったら書式にエラーがあろうがアクセス権が無かろうが最初に見つかった方のパスを優先します。 そのパスをFilePathプロパティに記憶。 アプリケーションの初回起動時は設定ファイルが見つからないので FilePath=null のまま、2回目以降に起動された場合は設定ファイルがあるはずなのでそれを使用という流れです。

SaveはFilePathプロパティにパスがある場合、必ずそれを使います。 nullの場合、つまり初回起動であろうという場合は、

  1. 実行ファイルと同じフォルダ
  2. アプリケーションデータフォルダ

の順に書き込んでみて、成功した方をFilePathプロパティに記憶します。 1度書き込みに成功した場合、以降はFilePathプロパティにパスが残るのでアプリケーションが終了するまで同じパスを使い続けます。

アプリケーション起動時のコードはこんな感じ。 設定ファイルが作られたとき(多分初回起動時)はそのパスを表示するようにしています。

using System.Windows;

namespace HeboPaint
{
    public partial class App : Application
    {
        // Startupのイベントハンドラ
        private void OnStartup(object sender, StartupEventArgs e)
        {
            string err = null;

            try
            {
                AppConfig.Load();
            }
            catch
            {
                try
                {
                    AppConfig.Save();
                    err = "設定ファイルが読み込めませんでした。"
                            + "新しい設定ファイルを作成しました。"
                            + "設定ファイルのパスは次のとおりです。\n\n"
                            + AppConfig.FilePath;
                }
                catch
                {
                    err = "設定ファイルが読み込めませんでした。"
                            + "新しい設定ファイルの作成にも失敗しました。";
                }
            }

            MainWindow = new MainWindow();
            MainWindow.Show(); // Loadedイベントの後に返ってくる

            if (err != null)
                MessageBox.Show(MainWindow, err, MainWindow.Title, MessageBoxButton.OK, MessageBoxImage.Information);
        }
    }
}

以降、設定が変わったときは適宜Saveメソッドを呼んで設定ファイルを更新します。

...

※捕捉 : Windows7の場合、VirtualStoreのパスは、

  • C:\Users\[ユーザー名]\AppData\Local\VirtualStore\