【C#】INIファイルを読み書きする(DllImportなし)

INI ファイルを C# で読み書きするために DllImport で Windows の User32.dll 使うのは確かに簡単ですが汎用性に欠けると思うので、純粋な C# のみかつ、.NET 4.6 くらいからコピペで使えるライブラリを作成してみました。

ライブラリの性能

このライブラリの性能は以下の通りです。

  • DllImport は使わない, Win32APIは不使用
  • C#6.0~準拠、.NET Framework4.6以降であれば動作可能
  • iniファイルの読み取り・編集・保存ができる
  • 新規にiniファイルを作成できる
  • ★コメントや空白行, 形式がが元のまま保持される
  • ★コメントや空白行は編集・削除・変更可能
  • ★セクション・キー・コメントの並び順が変わらない

★の個所が他のライブラリと異なる点です。

ini ファイル読み書き参照できること、保存時や無効行が消えない事、設定の並び順は可能な限り元のファイルの状態を保持する性能があります。業務だとこの特徴があると助かるかもしれません。

また、利用に当たり注意事項以下の2点あるのでご注意ください

  • 読み取り・保存は非同期APIになっている
  • マルチスレッドでの動作は保証しない

コードはGithub上に公開しているので、何か不都合が各自修正も可能です。

確認環境

このライブラリは以下環境で動作確認しています。

  • .NET Framework 4.6.2(C#6.0), UnityでもOK
  • .NET 5(C#9.0)
  • VisualStudio 2019
  • Windows10

形式とサンプルファイル

INIファイルのフォーマット

INIファイルのフォーマットは以下の通りです。

[セクション]
キー=値
キー=値
;コメント行
(空白行)
[セクション2]
キー=値
キー=

ルールは概ね以下の通りです。

  • セクション名は "[" ~ "]" で囲まれている
  • 行中に = があれば左がキー、右が値
    • イコールが行中に複数ある場合一番最初の = から左をキーとして扱う、残りはすべて値
  • 行頭に ; がある場合コメントとして扱う
  • 何も記載がない空白行が存在する

使用するサンプルファイル

今回動作確認には以下のようなファイルを使用します。読み取って値を編集せずに保存した場合、以下の形式が完全に保存されることを保証しようと思います。

;;;asdf
;123
;zxcv

[aaa]
key1=aaa
key2=bbb
key3=ccc
;key4=ddd

;key5=eee
rgahwgargaga;agr=se

key6=fff=fff

[bbb]
a=1
b=2
c=3

使い方

先ずはライブラリの使い方です。

既存のファイルを読み書きする

既存のファイルを読み取って内容を編集・保存する場合以下のように記述します。

public async static Task SaveAndLoad()
{
    // .NET Core以降だけ必要が必要以下をコメントイン
    Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

    // (1) オブジェクトの作成とファイルの読み取り
    var doc = new IniDocument();
    await doc.Load(@"d:\sample.ini", Encoding.GetEncoding("Shift-JIS"));

    // (2) 既存のセクションを取得
    IniSection s1 = doc.GetSection("aaa");

    // (3) セクションを全部取得
    foreach (IniSection s in doc.GetSections())
    {
        string name = s.Name;
        // aaa
        // bbb
    }

    // (4) 値の取得 & 書き換え
    string value1 = doc.GetValue("bbb", "a");
    doc.SetValue("bbb", "a", "999");
    string value2 = doc.GetValue("bbb", "a");
    // > value1=1
    // > value1=999

    // (5) コメントの追加
    doc.SetText("aaa", "-x-x- comment -x-x-x");

    // (6) ファイル保存
    await doc.Save(@"d:\sample2.ini", Encoding.GetEncoding("Shift-JIS"));
}

新規にINIファイルを作成する

IniDocument クラスを新規に作成して INI ファイルを作成する場合、以下のように記述します。

public async static Task CreateNew()
{
    var doc = new IniDocument();

    // (1) セクションだけ追加
    doc.CreateSection("aaa");

    // (2) セクションと値の追加
    doc.SetValue("section", "b", 1);
    doc.SetValue("section", "c", "2");
    doc.SetText("section", ";This is sample key & value."); // コメントの追加
    doc.SetValue("section", "d", 4.25);
    doc.SetText("section", ""); // セクションの最後に空白行

    // (3) セクションと値の追加
    doc.SetValue("section2", "b", 1);
    doc.SetValue("section2", "c", "2");
    doc.SetText("section2", ";This is sample key & value."); // コメントの追加
    doc.SetValue("section2", "d", 4.25);
    doc.SetText("section2", ""); // セクションの最後に空白行

    // (4) ファイル保存
    await doc.Save(@"d:\sample3.ini", Encoding.GetEncoding("Shift-JIS"));
}

実装コード

すごい長くなってしまったので Github にアップロードしました。以下リポジトリ参照ください。

github.com

基本的に IniDocument クラスからファイルに対するすべての処理が行えるように実装しています。IniDocument クラス内のメソッドの説明は以下の通りです。

メソッド名 説明
Save ファイルパスか Stream に内容を書き出す
Load ファイルパスか Stream から内容を読み取る
GetSections セクションを列挙する
GetSection 名前を指定してセクションを取得する
CreateSection セクションを新規に作成する
RemoveSection 名前を指定してセクションを削除する
ChangeIndex セクションの位置を変更する
GetElements セクション内の行をすべて列挙する
SetValue キーを指定して値を変更する
SetText コメント行を追加する
InsertValue 指定した位置にキーを追加する
InsertText 指定した位置のコメントを取得する
GetValue キーに対する値を取得する
GetValue 指定した位置のコメントを取得する
RemoveValue キーを削除する
RemoveText 位置を指定してコメントを削除する

詳細はリポジトリにアップしますが IniDocumentクラスのメソッド定義を以下に記載しておきます。

public class IniDocument
{
    // -x-x- ファイルの読み書き -x-x- 

    // 指定したファイルパスに現在のオブジェクトの内容を保存する
    public async Task Save(string filePath, Encoding encoding)

    // 指定したストリームに現在のオブジェクトの内容を保存する
    public async Task Save(StreamWriter sw)

    // 指定したファイルパスを読み取って内容をロードします。
    public async Task Load(string filePath, Encoding encoding)

    // 指定したストリームを読み取って内容をロードします。
    public async Task Load(StreamReader sr)

    // -x-x- セクションの操作 -x-x-x

    // セクションを列挙する
    public IEnumerable<IniSection> GetSections()

    // セクションを取得する
    //  → 存在しない場合例外が出る
    public IniSection GetSection(string name)

    // try - parse パターンでセクションを取得する
    public bool TryGetSection(string name, out IniSection section)

    // セクションを追加する
    public IniSection CreateSection(string name)

    // セクションを削除する
    //  → 存在しないセクションでも例外でない
    public void RemoveSection(string name)

    // セクションの位置を変更する
    public void ChangeIndex(string name, int newIndex)

    // -x-x- セクション内の操作 -x-x-

    // セクション内の要素を全て取得する
    public IEnumerable<IIniElement> GetElements(string sectionName)

    // 指定した値を設定する
    //  → セクション・キーが無い場合末尾に新規作成して追加
    public void SetValue<T>(string sectionName, string key, T value)

    // コメントなどのテキストを追加する
    //  → このメソッドで key=value と入力してもこのオブジェクト中はテキスト扱いになる
    public void SetText(string sectionName, string text)

    // 途中に値を挿入する
    public void InsertValue(string sectionName, int index, string key, string value)

    // 途中にコメントなどのテキストを挿入する
    public void InsertText(string sectionName, int index, string text)

    // 指定したキーの値を取得する
    //  → 存在しない場合例外が出る
    public string GetValue(string sectionName, string key)

    // 値を try - parse パターンで取得する
    public bool TryGetValue(string sectionName, string key, out IIniElement elem)

    // 指定した位置のテキストを取得する
    //  → コメント行はキーが無いのでインデックスアクセスする
    public string GetText(string sectionName, int index)

    // 指定したキーを削除する
    public void RemoveValue(string sectionName, string key)

    // 指定したインデックスの要素を削除します
    public void RemoveText(string sectionName, int index)
}

以上