本記事は、C#での設定ファイル管理とパラメータのバージョンアップの続きとして、INIファイルによる設定ファイル、パラメータファイルの扱いについて記載します。アプリケーションの設定情報を保存し、読み込む方法はいろいろあるが、その中でも一番古い方法の一つがINIファイルによる方法です。新しい設定ファイルの保存方法が出ている今でも、INIによる方法は、未だに実際の現場ではよく使っている方法でもあります(デスクトップアプリケーションを作成する場合が主な使い道です)。
Contents
INIファイルを扱うための基本インタフェース
INIファイルのフォーマット
INIファイルは、以下のフォーマットになっています。
1 2 3 |
; コメント [section] key = value |
- 1行目:コメント文です。任意の文字列です。
- 2行目:section
- section名です。言葉を変えると、データグループとも言えます。一つのINIファイルには複数のsectionを持つことができます。
- 3行目:key,value
- section内にパラメータ名(key)とパラメータ値(value)を持ちます。
- 一つのsectionには複数のkey名を持つことができます。
- 一つのsection名の中のkey名は唯一である必要があります(同じsection内に同じkeyは存在しません)。
- 違うsection名である場合は、key名が同じであっても問題ありません(あたり前ですが、属するグループが違うパラメータ扱いであるため)。
INIファイルを扱うための関数
INIファイルを扱うにはWin32APIを使ってファイルにアクセスします。C#で使うためには、Win32APIにアクセスするためにC#関数(マーシャリング関数)を定義します。
INIファイルから文字列として読み込み
1 2 3 4 5 6 7 8 |
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)] public static extern uint GetPrivateProfileString( string lpAppName, string lpKeyName, string lpDefault, StringBuilder lpReturnedString, uint nSize, string lpFileName); |
- 関数名:GetPrivateProfileString()
- 引数:
- lpAppName:section名を指定します。[]で囲まれる部分の文字列です。
INIフォーマットの例では、2行目の”section”文字列を指定します。 - lpKeyName:key名を指定します。
INIフォーマットのの例では、3行名の”key”文字列を指定します。 - lpDefault:INIファイルから読み込み失敗した場合に、このlpDefaultで指定した文字列が設定(lpReturnedString変数に設定)されます。
INIファイル読み込み失敗:ファイルがない場合、あるいは、INIファイル不備 - lpReturnedString:取得したい変数です。この変数にINIから取得した値が設定されます。
INIフォーマットのの例では、3行名の”value”文字列が取得されます。 - nSize:lpReturnedStringのBufferサイズです。
- lpFileName:INIファイルのパス名です。ローカルフォルダの場合は”.\”で始まるファイル名を指定します。
- lpAppName:section名を指定します。[]で囲まれる部分の文字列です。
- 戻り値:取得した文字列(lpReturnedString)の長さです。
INIファイルから数値として読み込み
1 2 3 4 5 6 |
[DllImport("kernel32.dll")] public static extern uint GetPrivateProfileInt( string lpAppName, string lpKeyName, int nDefault, string lpFileName); |
- 関数名:GetPrivateProfileInt()
- 引数:
- lpAppName:section名を指定します。[]で囲まれる部分の文字列です。
INIフォーマットの例では、2行目の”section”文字列を指定します。 - lpKeyName:key名を指定します。
INIフォーマットのの例では、3行名の”key”文字列を指定します。 - nDefault:INIファイルから読み込み失敗した場合に、このnDefaultで指定した数値が関数値(戻り値)になります。
INIファイル読み込み失敗:ファイルがない場合、あるいは、INIファイル不備 - lpFileName:INIファイルのパス名です。ローカルフォルダの場合は”.\”で始まるファイル名を指定します。
- lpAppName:section名を指定します。[]で囲まれる部分の文字列です。
- 戻り値:取得した数値です。
INIファイルに書き込み
1 2 3 4 5 6 7 |
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool WritePrivateProfileString( string lpAppName, string lpKeyName, string lpString, string lpFileName); |
- 関数名:WritePrivateProfileString()
- 引数:
- lpAppName:section名を指定します。[]で囲まれる部分の文字列です。
INIフォーマットの例では、2行目の”section”文字列を指定します。 - lpKeyName:key名を指定します。
INIフォーマットのの例では、3行名の”key”文字列を指定します。 - lpString:設定する値です。
INIフォーマットのの例では、3行名の”value”文字列を指定します。 - lpFileName:INIファイルのパス名です。ローカルフォルダの場合は”.\”で始まるファイル名を指定します。
- lpAppName:section名を指定します。[]で囲まれる部分の文字列です。
- 戻り値:成功すると0以外の値、失敗すると0が返ります。
INIファイル関連のその他の関数
- GetPrivateProfileSection():指定したsection内のすべてのkeyと値を取得します。
- GetPrivateProfileSectionNames():すべてのsection名を取得します。
関数の定義は以下のサイトを参考に作成できます。
PINVOKE.NET(GetPrivateProfile…)
INIファイルを実際のアプリケーションで使ってみる
INIファイルアクセスのWrapperクラス
管理者の場合は、INIファイルを使う場合が、たびたびありまして、Wrapperクラスを作成しています。基本は、このクラスを経由してINIファイルにアクセスします。INIファイルを扱うためのWin32API関数が持つクラスを作り、このクラスを経由して使っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public static class IniControl { [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] public static extern uint GetPrivateProfileString(string lpAppName,string lpKeyName,string lpDefault, StringBuilder lpReturnedString,uint nSize,string lpFileName); [DllImport("kernel32.dll")] public static extern uint GetPrivateProfileInt(string lpAppName, string lpKeyName, int nDefault, string lpFileName); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool WritePrivateProfileString(string lpAppName, string lpKeyName, string lpString, string lpFileName); } |
INIファイルとアプリケーション開発における注意点
管理者の経験上、INIだけで膨大なアプリケーションの設定情報を扱うことは非常に面倒なことです。連載で記載しますが、設定ファイルを扱う他の方法と併用することが一般的だと思います。 INIファイルを扱うにおいて、注意すべきことは、今の時代、一つのアプリケーションには膨大なモジュールとクラスがあり、INIファイル(INIファイルに限らず、設定ファイルすべてが該当します)をどのタイミングで読み込み・書き込むかのタイミングです。
管理者の経験上、INIファイルを扱うのは、アプリケーションの起動時と終了だけにまとめるのがベストだと思っています(レアなケースはあり得ると思いますが、一般的な話として)。INIファイルをLoadしたら、すぐ、アプリケーションで扱いやすいクラスに変換することです。管理者は、(10年以上の前の話ですが)INIファイル情報をクラスに変換せず、INI情報のままDictionary型のメモリに保持し、そのメモリからDictionaryのKeyでアクセスしてつかったことがあります。INIのDictionaryにアクセスするGet/Set関数を使っていましたが、システムがどんどん大きくなるにつれ、大変苦労したことがあります。INIで扱う情報量が多くなる、さらに複雑になって行くにつれ、余計な苦労になりました。 まとめると、以下のようになります。
INIファイルからクラスへの変換の例
以下、INIを使う一つの例を書いています。
- 本サンプルで使うINI
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[ParamSetting] ParamCount=3 [user_1] ID=1001 Name=Suzuki Kokugo=81 Suugaku=48 Eigo=83 [user_2] ID=1002 Name=Kato Kokugo=33 Suugaku=61 Eigo=88 [user_3] ID=1003 Name=Naruhodo Kokugo=50 Suugaku=55 Eigo=56 |
- データクラス
データクラスの持ち方(構造)は、一つの例です。アプリケーション状況(仕様)などにより変わる変わるので、説明は省略します。
1 2 3 4 5 |
public class ParamSetting { public int ParamCount { get; set; } = 0; public List<ParamUserInfo> UserInfos { get; set; } = new List<ParamUserInfo>(); } |
1 2 3 4 5 6 |
public class ParamUserInfo { public int ID { get; set; } public string Name { get; set; } public ParamUserGrades Grade { get; set; } = new ParamUserGrades(); } |
1 2 3 4 5 6 |
public class ParamUserGrades { public int Kokugo { get; set; } public int Suugaku { get; set; } public int Eigo { get; set; } } |
- INI読み込みと書き込み、INIからクラス変換
本来ならParamUserInfoControlクラスが別途存在せず、ParamSettingクラス内でParamUserInfoControlの変換処理を行うのが多いと思いますが、Conveterの意味合いで明示的に区別しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
public class ParamUserInfoControl { public const string INI_FILE = @".\UserInfo.ini"; public ParamSetting LoadParamSetting() { var param = new ParamSetting(); param.ParamCount = GetUserInfoInt(nameof(ParamSetting), nameof(param.ParamCount)); for (int i = 0; i < param.ParamCount; i++) { var user = new ParamUserInfo(); var section = $"{nameof(user)}_{(i + 1)}"; user.ID = GetUserInfoInt(section, nameof(user.ID)); user.Name = GetUserInfoString(section, nameof(user.Name)); user.Grade.Kokugo = GetUserInfoInt(section, nameof(user.Grade.Kokugo)); user.Grade.Suugaku = GetUserInfoInt(section, nameof(user.Grade.Suugaku)); user.Grade.Eigo = GetUserInfoInt(section, nameof(user.Grade.Eigo)); param.UserInfos.Add(user); } return param; } public ParamSetting WriteParamSetting(ParamSetting param) { WriteUserInfoString(nameof(ParamSetting), nameof(param.ParamCount), param.ParamCount.ToString()); for (int i = 0; i < param.ParamCount; i++) { var user = param.UserInfos[i]; var section = $"{nameof(user)}_{(i + 1)}"; WriteUserInfoString(section, nameof(user.ID), user.ID.ToString()); WriteUserInfoString(section, nameof(user.Name),user.Name); WriteUserInfoString(section, nameof(user.Grade.Kokugo),user.Grade.Kokugo.ToString()); WriteUserInfoString(section, nameof(user.Grade.Suugaku),user.Grade.Suugaku.ToString()); WriteUserInfoString(section, nameof(user.Grade.Eigo),user.Grade.Eigo.ToString()); } return param; } private string GetUserInfoString(string section,string key,string sDefault="") { StringBuilder sb = new StringBuilder(256); IniControl.GetPrivateProfileString(section, key, sDefault, sb, (uint)sb.Capacity, INI_FILE); return sb.ToString(); } private int GetUserInfoInt(string section, string key, int nDefault = 0) { StringBuilder sb = new StringBuilder(256); var ret = (int)IniControl.GetPrivateProfileInt(section, key, nDefault, INI_FILE); return ret; } private bool WriteUserInfoString(string section, string key, string nValue) { StringBuilder sb = new StringBuilder(256); var ret = IniControl.WritePrivateProfileString(section, key, nValue, INI_FILE); return ret; } } |
管理者の場合、40行:GetUserInfoString()、47行:GetUserInfoInt()、53行:WriteUserInfoString()のようなWrapper関数を使い、INIファイルへのアクセスを行います。
- 呼び出し側
1 2 3 4 5 6 7 8 9 10 11 12 |
static void Main(string[] args) { Console.WriteLine("Hello World!"); var usrcont = new ParamUserInfoControl(); //起動時の処理 var paramLoaded = usrcont.LoadParamSetting(); //GUIなどの画面に表示にparamLoadedを渡す:コードは省略 //終了時の処理 usrcont.WriteParamSetting(paramLoaded); } |
パラメータバージョンアップにおけるINI管理による影響
アプリケーションは、必ず、バージョンアップがあり、同時にアプリケーションに必要なパラメータのバージョンアップも同時に必要になると思うべきでしょう。アプリケーション開発においてパラメータのバージョンアップによる設定ファイルのバージョンアップは悩みの多いところです。ただ、INIファイルを使った時の長所でも言えますが、INIファイルを使って設定ファイルを管理した場合は、パラメータのバージョンアップはシンプルです。 普通、パラメータバージョンアップ処理は、src.2.3.4の LoadParamSetting()の中で行います。普通は、旧バージョンのParamSettingクラスと新バージョンのParamSetting*を用意し、INIから読み込んで新ParamSettingに変換すべきですが、INIの場合は、クラス情報を保存しているのではなく、INIフォーマットに変換して保存しるため、パラメータバージョンアップの際も、クラスメンバなどを追加したParamSetting(クラス自体はParamSettingでも、他の名称でも影響を受けない)に設定すればよいです。 もちろん、例外的なことはあり得ます。たとえば、バージョンアップによって、パラメータの意味が変わってしまった場合は、src.2.3.4の LoadParamSetting()の中で変換処理を行うとよいでしょう。
まとめ
INIファイルによるパラメータ管理は、簡単なプログラムではあり得ると思います。しかし、膨大なアプリケーション(膨大になる可能性がある)では、なるべくINIの方法を避けるか、INIからクラスへの変換を行うことを勧めします。
- アプリケーション起動時:INIから読み込む、クラスへ変換する。
- アプリケーション終了時:クラスからINIへ書き込む