C#でINIファイル操作を属性(Attribute)使ってやってみた

# C#でINIファイル操作

## はじめにのはじめに

2021年あけましておめでとうございます。

極力今年はこまめにコードを書いていきたく、小ネタの集まりになりますがよろしくお願いいたします。

では2021年最初のネタです。

---

## はじめに

- App.Configを利用し環境設定を保持するのが普通？の使い方と思われます。
- ですがまだまだ以前通りの使い方をしたいユーザのニーズがありなかなかXML環境に移行するのは・・・が実情です。
- それと多くのユーザが使っていると、なぜか一部のユーザからApp.Configを参照できないケースの報告があり（記述にヌケがある？）、個人的には枯れた技術であるINIを使うケースもあります。

---

## 使い方

- 単純にWindowsAPIを使います。
- 以下のコードをクラス内で定義して呼び出すだけです。

### 定義は以下の通り

- ※対象を文字列のみにしています。
- 私の使い方は数値を扱うAPIは使わず、文字列を読み取ってコード側で変換しています。（深い理由もないですが。。。）

```CSharp
#region INIファイル操作
    /// <summary>
    /// INIファイルから文字列の取得
    /// </summary>
    /// <param name="lpAppName">アプリケーション名</param>
    /// <param name="lpKeyName">キー</param>
    /// <param name="lpDefault">デフォルト値</param>
    /// <param name="lpReturnedString">戻り値</param>
    /// <param name="nSize">最大文字数</param>
    /// <param name="lpFileName">ファイル名</param>
    /// <returns>読み取りバイト数</returns>
    [DllImport("KERNEL32.DLL")]
    public static extern uint GetPrivateProfileString(
        string lpAppName,
        string lpKeyName,
        string lpDefault,
        StringBuilder lpReturnedString,
        uint nSize,
        string lpFileName);

    /// <summary>
    /// INIファイルへ文字列の書き込み
    /// </summary>
    /// <param name="lpAppName">アプリケーション名</param>
    /// <param name="lpKeyName">キー</param>
    /// <param name="lpString">書き込み文字数</param>
    /// <param name="lpFileName">ファイル名</param>
    /// <returns></returns>
    [DllImport("KERNEL32.DLL")]
    public static extern uint WritePrivateProfileString(
        string lpAppName,
        string lpKeyName,
        string lpString,
        string lpFileName);
#endregion
```

### 使い方

以下のように使います。

```CSharp
string appName = @"アプリケーション名";
string keyName = @"キー名";
string defaultValue = @"戻り値のデフォルト";
StringBuilder sb = new StringBuilder(256);  // 戻り値
string iniFileName = Path.Combine(Application.StartupPath,"SETTING.INI");

uint res = GetPrivateProfileString(appName, keyName, defaultValue, sb, 256, iniFileName);
```

> SETTING.INIの記述

```ini
[アプリケーション名]
キー名=値
```

- 結果、

resに"値"のバイト数が入り、```sb.ToString()```は```値```を返します。

---

## この使い方は

- 正直に言うとこのままでは、値が増えるたびにコードを追加し続ける必要があるので面倒です。
- 同じコードを繰り返すのもなんだか・・・ですね。

---

## 属性を使ってコントロールする

- C#にはせっかくAttributeがあるので、活用します。

### サンプルのINIファイルは以下の通りとします。

- ファイルのパスはテスト用の実行ファイルと同じフォルダーにあるものとします。

> SETTING.INI

```ini
[app1]
data1=データ1
data2=データ2

[app2]
data3=データ3
```

- INIのデータを受け取るクラスを作ります。

```CSharp
/// <summary>
/// 設定保存用クラス
/// </summary>
public class SettingClass
{
    /// <summary>
    /// データ(アプリケーション名：app1 / キー名：data1)
    /// </summary>
    public string data1 { get; set; }
    /// <summary>
    /// データ(アプリケーション名：app1 / キー名：data2)
    /// </summary>
    public string data2 { get; set; }
    /// <summary>
    /// データ(アプリケーション名：app2 / キー名：data3 / デフォルト値：def3)
    /// </summary>
    public string data3 { get; set; }
}
```

- クラスとファイル名を関連付けしてみます。

- まずは属性用のクラスを作成します。

``` CSharp
/// <summary>
/// INIファイル名属性
/// </summary>
public class IniFileNameAttribute
{
    /// <summary>
    /// ファイル名
    /// </summary>
    public string FileName { get; set; }
}
```

- 当然ですがこのままでは属性として認識されません。
- ```System.Attribute```を継承します。

``` CSharp
/// <summary>
/// INIファイル名属性
/// </summary>
public class IniFileNameAttribute : System.Attribute
{
    /// <summary>
    /// ファイル名
    /// </summary>
    public string FileName { get; set; }
}
```

- これがクラスや構造体で使えると宣言します。
- 宣言自体も属性で付与します。

``` CSharp
/// <summary>
/// INIファイル名属性
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct)]
public class IniFileNameAttribute : System.Attribute
{
    /// <summary>
    /// ファイル名
    /// </summary>
    public string FileName { get; set; }
}
```

- INIのデータを受け取るクラスに属性を付与してみます。

```CSharp
/// <summary>
/// 設定保存用クラス
/// </summary>
[IniFileName(FileName = "SETTINGS.INI")]
public class SettingClass
{
    {省略}
```

- 属性の名称とクラス名は```Attribute```の部分が省略されています。

引数名を書かなければならないため、ちょっと冗長な感じですね。。。
属性にコンストラクターをつけます。

``` CSharp
/// <summary>
/// INIファイル名属性
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct)]
public class IniFileNameAttribute : System.Attribute
{
    /// <summary>
    /// ファイル名
    /// </summary>
    public string FileName { get; set; }

    /// <summary>
    /// INIファイルに対するファイル名属性
    /// </summary>
    /// <param name="filename">ファイル名</param>
    public IniFileNameAttribute(string filename)
    {
        FileName = filename;
    }
}
```

データ受け取りクラスも以下のようになります。

```CSharp
/// <summary>
/// 設定保存用クラス
/// </summary>
[IniFileName("SETTINGS.INI")]
public class SettingClass
{
    {省略}
```

気持ち、ですがすっきりしました。

では、要素にも属性をつけましょう。

- 要素は、プロパティやフィールドに付与できるよう属性をつけます。
- 与えるのは、アプリケーション名、キー名、デフォルト値です。
- コンストラクターではアプリケーション名とキー名のみ強制し、デフォルト値を任意にします。

``` CSharp
/// <summary>
/// INIファイルに対する属性値
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Property | AttributeTargets.Field)]
public class IniFileAttribute : System.Attribute
{
    /// <summary>
    /// アプリケーション名
    /// </summary>
    public string Application { get; set; }
    /// <summary>
    /// キー
    /// </summary>
    public string Key { get; set; }
    /// <summary>
    /// デフォルト値
    /// </summary>
    public string DefaultValue { get; set; }

    /// <summary>
    /// INIに対する連携用属性
    /// </summary>
    /// <param name="application">アプリケーション名</param>
    /// <param name="key">キー</param>
    public IniFileAttribute(string application, string key)
    {
        Application = application;
        Key = key;
        DefaultValue = string.Empty;
    }
}
```

属性を付与した形は以下になります。

```CSharp
/// <summary>
/// 設定保存用クラス
/// </summary>
[IniFileName("SETTINGS.INI")]
public class SettingClass
{
    /// <summary>
    /// データ(アプリケーション名：app1 / キー名：data1)
    /// </summary>
    [IniFile("app1","data1")]
    public string data1 { get; set; }
    /// <summary>
    /// データ(アプリケーション名：app1 / キー名：data2)
    /// </summary>
    [IniFile("app1","data2")]
    public string data2 { get; set; }
    /// <summary>
    /// データ(アプリケーション名：app2 / キー名：data3 / デフォルト値：def3)
    /// </summary>
    [IniFile("app2","data3", DefaultValue="def3")]
    public string data3 { get; set; }
}
```

こうなると、コメントは冗長な気がしてきます。

```CSharp
/// <summary>
/// 設定保存用クラス
/// </summary>
[IniFileName("SETTINGS.INI")]
public class SettingClass
{
    /// <summary>
    /// データ1
    /// </summary>
    [IniFile("app1","data1")]
    public string data1 { get; set; }
    /// <summary>
    /// データ2
    /// </summary>
    [IniFile("app1","data2")]
    public string data2 { get; set; }
    /// <summary>
    /// データ3
    /// </summary>
    [IniFile("app2","data3", DefaultValue="def3")]
    public string data3 { get; set; }
}
```

すっきりしました。

---

では、肝心な読み取り部分を作成します。

### クラスが持つ属性の取得方法

- Attributeの値を取得するには、以下の手順で取得します。

```CSharp
// まずは型を取得
Type t = typeof(SettingClass);

// 型が持つ属性を（この場合はIniFileAttribute属性）を取得
var att = (IniFileNameAttribute) Attribute.GetCustomAttribute(t, typeof(IniFileNameAttribute));
// 取得できなければ処理を抜ける
if (att == null) return null;

// 属性値、FileNameを取得
string iniFileName = att.FileName;
```

---

### プロパティが持つ属性の取得方法

```CSharp
// まずは型を取得
Type t = typeof(SettingClass);
// まずは型のプロパティを全て取得
var prof = t.GetProperties();
// 全て取得したプロパティをひとつづつチェックする
foreach (var prop in prof)
{
    // ----->>> 属性の有無をチェック
    var att2 = (IniFileAttribute) prop.GetCustomAttribute(typeof(IniFileAttribute));
    // ----->>> 属性がなければ無視
    if (att2 == null) continue;

    // ----->>> 属性からキーを得る
    string appName = att2.Application;
    string keyName = att2.Key;
    string defaultValue = att2.DefaultValue;
    // ----->>> 戻り値の準備
    StringBuilder sb = new StringBuilder(256);
    // ----->>> INIから取得
    uint res = GetPrivateProfileString(appName, keyName, defaultValue, sb, 256, iniFileName);
}
```

いかがですか？

クラスのプロパティに付与された属性の取得方法、意外と簡単ではないでしょうか。

後は、取得したプロパティの実体にデータを流し込むだけです。

- 以下は、```result```変数に対して、戻り値を設定しています。
- サンプルは文字列と日付型のみを扱ってみています。

```CSharp
SettingClass result = new SettingClass();
// 文字列型のプロパティの場合
if (prop.PropertyType == typeof(string))
{
    // ----->>> 文字列の場合
    prop.SetValue(result, sb.ToString());
}
// 日付型のプロパティの場合
else if (prop.PropertyType == typeof(DateTime))
{
    // ----->>> 日付型の場合
    if (DateTime.TryParse(sb.ToString(), out DateTime dmy))
        prop.SetValue(result, dmy);
}
```

データを受け付けるクラスが肥大化しても、読み込みの部分をいちいち増やす必要はありません。

（扱うプロパティの型を増やせば当然コードを増やさなければなりませんが）

- ```GetPrivateProfileString```コードを1行増やせば簡単のに、なぜこんなに手間を増やさなきゃならないの？

とかいろいろご意見はあると思います。

今回はINIの話でしたが、属性を使えば相手がデータベースでも同様にできます。

（その場合はEntityFramework使えと怒られそうですが）

いろいろな使い方ができるので、属性を使ってみようかな、と思っていただけると幸いです。

---

完全な形は以下の通りとなります。

```CSharp
/// <summary>
/// INIファイル操作系
/// </summary>
public static class IniFileRWClass
{
#region INIファイル操作
    /// <summary>
    /// INIファイルから文字列の取得
    /// </summary>
    /// <param name="lpAppName">アプリケーション名</param>
    /// <param name="lpKeyName">キー</param>
    /// <param name="lpDefault">デフォルト値</param>
    /// <param name="lpReturnedString">戻り値</param>
    /// <param name="nSize">最大文字数</param>
    /// <param name="lpFileName">ファイル名</param>
    /// <returns>読み取りバイト数</returns>
    [DllImport("KERNEL32.DLL")]
    public static extern uint GetPrivateProfileString(
        string lpAppName,
        string lpKeyName,
        string lpDefault,
        StringBuilder lpReturnedString,
        uint nSize,
        string lpFileName);

    /// <summary>
    /// INIファイルへ文字列の書き込み
    /// </summary>
    /// <param name="lpAppName">アプリケーション名</param>
    /// <param name="lpKeyName">キー</param>
    /// <param name="lpString">書き込み文字数</param>
    /// <param name="lpFileName">ファイル名</param>
    /// <returns></returns>
    [DllImport("KERNEL32.DLL")]
    public static extern uint WritePrivateProfileString(
        string lpAppName,
        string lpKeyName,
        string lpString,
        string lpFileName);
#endregion

#region INI設定ファイル操作
    /// <summary>
    /// 設定をファイルから取り込む
    /// </summary>
    /// <returns></returns>
    public static SettingClass ReadIniSetting()
    {
        //! ================================================================================
        //! ----->>> 属性からINIファイル名を取得する（パスはEXEと同じフォルダにする）
        SettingClass result = new SettingClass();
        Type t = typeof(SettingClass);

        var att = (IniFileNameAttribute) Attribute.GetCustomAttribute(t, typeof(IniFileNameAttribute));
        if (att == null) return null;

        string iniFileName = att.FileName;
        string directry = Application.StartupPath;

        iniFileName = Path.Combine(directry, iniFileName);

        //! ================================================================================
        //! ----->>> プロパティを検索し、属性値からアプリケーション名とキーを取得してINIファイルから値を得る
        var prof = t.GetProperties();
        foreach (var prop in prof)
        {
            //? ----->>> 属性の有無をチェック
            var att2 = (IniFileAttribute) prop.GetCustomAttribute(typeof(IniFileAttribute));
            if (att2 == null) continue;

            //? ----->>> 属性からキーを得る
            string appName = att2.Application;
            string keyName = att2.Key;
            string defaultValue = att2.DefaultValue;
            //? ----->>> 戻り値の準備
            StringBuilder sb = new StringBuilder(256);
            //? ----->>> INIから取得
            uint res = GetPrivateProfileString(appName, keyName, defaultValue, sb, 256, iniFileName);

            if (prop.PropertyType == typeof(string))
            {
                //? ----->>> 文字列の場合
                prop.SetValue(result, sb.ToString());
            }
            else if (prop.PropertyType == typeof(DateTime))
            {
                //? ----->>> 日付型の場合
                if (DateTime.TryParse(sb.ToString(), out DateTime dmy))
                {
                    prop.SetValue(result, dmy);
                }
            }
        }

        //? ================================================================================
        //? ----->>> 戻り値を返す
        return result;
    }

    /// <summary>
    /// 設定をファイルに書き込む
    /// </summary>
    /// <param name="data"></param>
    public static void WriteIniSetting(SettingClass data)
    {
        //! ================================================================================
        //! ----->>> 属性からINIファイル名を取得する（パスはEXEと同じフォルダにする）
        Type t = typeof(SettingClass);

        var att = (IniFileNameAttribute)Attribute.GetCustomAttribute(t, typeof(IniFileNameAttribute));
        if (att == null) return;

        string iniFileName = att.FileName;
        string directry = Application.StartupPath;

        iniFileName = Path.Combine(directry, iniFileName);

        //! ================================================================================
        //! ----->>> プロパティを検索し、属性値からアプリケーション名とキーを取得してINIファイルへ値を書き込む
        var prof = t.GetProperties();
        foreach (var prop in prof)
        {
            var att2 = (IniFileAttribute)prop.GetCustomAttribute(typeof(IniFileAttribute));
            if (att2 == null) continue;

            //? ----->>> キーを取得
            string appName = att2.Application;
            string keyName = att2.Key;
            //? ----->>> 値を文字列に変換する
            string Value = prop.PropertyType == typeof(DateTime)
                ? ((DateTime) prop.GetValue(data)).ToString("yyyy/MM/dd")
                : prop.GetValue(data).ToString();

            //? ----->>> INIファイルに書き込む
            WritePrivateProfileString(appName, keyName, Value, iniFileName);
        }
    }
#endregion
}
```

最後までお付き合いいただき、誠にありがとうございました。