見出し画像

[C#]iniファイルの読み書きクラスを作る

こんにちは。

やっと。
やっと暖かくなってきたので、ノートを書くパワーが湧きます。

はじめに

本題ですが、私はとても深くてどうにも変え難い事情で、
アプリケーションの設定をiniファイルで記憶させておく必要があります。

個人で使ったり開発者自身のためのアプリケーションであればiniファイルはまず使う必要性が無さそうですが、
例えば使用者に知見がなく、もしもの時にメモ帳で編集出来るというメリットがあるかもしれません。

作ってみるとどちらかというと、iniファイルよりもジェネリック関数の勉強になりました。
もしジェネリック関数に聞き馴染みない方はご参考ください。

前提条件

まずは、前提条件は下記となります。
・権限の範囲: 親クラスからのみ読み書き出来る事
・コーディング: コーディングする際、極力手間がかからない事(キーの数だけコピペして済ませたい)
・その他: 変数的に扱える事

サンプルコード

サンプルコードは下記となります。

using System;
using System.Text;
using System.Runtime.InteropServices;
using System.IO;
using System.ComponentModel;

namespace TestSystemIni
{
    public class SystemIni
    {
        // ===== DLL Import =====
        ///  <summary>iniファイル書込み</summary>
        /// <param name="section_name">セクション名</param>
        /// <param name="key_name">キー名</param>
        /// <param name="write_value">書き込む文字列</param>
        /// <param name="file_path">iniファイルPATH</param>
        /// <returns>0以外: 正常終了, 0: 異常終了</returns>
        [DllImport("KERNEL32.DLL")]
        public static extern uint WritePrivateProfileString(string section_name, string key_name, string write_value, string file_path);

        ///  <summary>iniファイル読込み(文字列)</summary>
        /// <param name="section_name">セクション名</param>
        /// <param name="key_name">キー名</param>
        /// <param name="default_value">値が取得できなかった場合に返される値</param>
        /// <param name="read_value">格納先</param>
        /// <param name="read_size">格納サイズ</param>
        /// <param name="file_path">iniファイルPATH</param>
        /// <returns>0以外: 正常終了, 0: 異常終了</returns>
        [DllImport("KERNEL32.DLL")]
        public static extern uint GetPrivateProfileString(string section_name, string key_name, string default_value, StringBuilder read_value, uint read_size, string file_path);

        // ===== コンストラクタ =====
        /// <remarks>引数指定無しの場合、".\System.ini"(実行場所)のPATHを読み書きする</remarks>
        public SystemIni() : this(Path.GetFullPath(@".\System.ini")) { }

        /// <param name="ini_file_path">iniファイルPATH</param>
        public SystemIni(string ini_file_path)
        {
            if (File.Exists(ini_file_path))
            {
                file_path = ini_file_path;
            }
            else// ファイルが存在しない場合, ".\System.ini"(実行場所)のPATHを格納する
            {
                file_path = Path.GetFullPath(@".\System.ini");
            }
        }

        // ===== グローバル変数 =====
        /// <summary>iniファイルPATH</summary>
        public string FilePath => file_path;
        private static string file_path = "";

        // ===== ローカル関数 =====
        ///  <summary>iniファイルから値を取得する</summary>
        /// <typeparam name="T">int,double,bool,string</typeparam>
        /// <param name="section_name">セクション名</param>
        /// <param name="key_name">キー名</param>
        /// <returns>取得した値</returns>
        private static T Read<T>(string section_name, string key_name)
        {
            // iniファイルから読込し、失敗時はTの型のデフォルト値を返す
            StringBuilder read_str_builder = new StringBuilder(1024);
            uint error_code = GetPrivateProfileString(section_name, key_name, "", read_str_builder, (uint)read_str_builder.Capacity, file_path);
            if (error_code == 0 || string.IsNullOrWhiteSpace(read_str_builder.ToString()))
            {
                return default(T);
            }

            // Tの型を取得し、未指定時はTの型のデフォルト値を返す
            TypeConverter type_converter = TypeDescriptor.GetConverter(typeof(T));
            if (type_converter == null)
            {
                return default(T);
            }

            // 読み込んだ文字列をTの型に変換し返す
            try
            {
                return (T)type_converter.ConvertFromString(read_str_builder.ToString());
            }
            catch (NotSupportedException)
            {
                return default(T);
            }
            catch (FormatException)
            {
                return default(T);
            }
        }

        ///  <summary>iniファイルに値を格納する</summary>
        /// <typeparam name="T">int,double,bool,string</typeparam>
        /// <param name="section_name">セクション名</param>
        /// <param name="key_name">キー名</param>
        /// <param name="value">格納する値</param>
        private static void Write<T>(string section_name, string key_name, T value)
        {
            WritePrivateProfileString(section_name, key_name, value.ToString(), file_path);
        }

        /// <summary>キー取得の構造体</summary>
        /// <typeparam name="T">int,double,bool,string</typeparam>
        public struct Key<T>
        {
            private string section_name;
            private string key_name;

            /// <param name="section_name">セクション名</param>
            /// <param name="key_name">キー名</param>
            public Key(string section_name, string key_name)
            {
                this.section_name = section_name;
                this.key_name = key_name;
            }
            /// <summary>設定値プロパティ</summary>
            public T Value
            {
                get => Read<T>(section_name, key_name);
                set => Write(section_name, key_name, value);
            }
        }

        // ===== iniファイルの項目 =====
        ///  <summary>設定項目</summary>
        public Me ME = new Me();
        public class Me
        {
            /// <summary>Key1</summary>
            public Key<string> NAME = new Key<string>(nameof(ME), typeof(Me).GetFields()[0].Name);
            /// <summary>Key2</summary>
            public Key<int> AGE = new Key<int>(nameof(ME), typeof(Me).GetFields()[1].Name);
            /// <summary>Key3</summary>
            public Key<double> HEIGHT = new Key<double>(nameof(ME), typeof(Me).GetFields()[2].Name);
            /// <summary>Key4</summary>
            public Key<bool> MARRIED = new Key<bool>(nameof(ME), typeof(Me).GetFields()[3].Name);
        }
    }
}

サンプルのiniファイル

上記のサンプルコードに対応するiniファイルの内容は下記

;::::::::::::::::::::::::::::::
[ME]
NAME=ひむらさん
AGE=31
HEIGHT=172.2
MARRIED=true
;::::::::::::::::::::::::::::::

解説その1

サンプルコードを上から順に、詳細説明載せておきます。

// ===== DLL Import =====
///  <summary>iniファイル書込み</summary>
/// <param name="section_name">セクション名</param>
/// <param name="key_name">キー名</param>
/// <param name="write_value">書き込む文字列</param>
/// <param name="file_path">iniファイルPATH</param>
/// <returns>0以外: 正常終了, 0: 異常終了</returns>
[DllImport("KERNEL32.DLL")]
public static extern uint WritePrivateProfileString(string section_name, string key_name, string write_value, string file_path);

///  <summary>iniファイル読込み(文字列)</summary>
/// <param name="section_name">セクション名</param>
/// <param name="key_name">キー名</param>
/// <param name="default_value">値が取得できなかった場合に返される値</param>
/// <param name="read_value">格納先</param>
/// <param name="read_size">格納サイズ</param>
/// <param name="file_path">iniファイルPATH</param>
/// <returns>0以外: 正常終了, 0: 異常終了</returns>
[DllImport("KERNEL32.DLL")]
public static extern uint GetPrivateProfileString(string section_name, string key_name, string default_value, StringBuilder read_value, uint read_size, string file_path);

ここはあまり説明いらないかと思いますが、
iniファイルへの読み書きはWritePrivateProfileStringとGetPrivateProfileStringを使用します。
"using System.Runtime.InteropServices;"を忘れずに。

解説その2

// ===== コンストラクタ =====
/// <remarks>引数指定無しの場合、".\System.ini"(実行場所)のPATHを読み書きする</remarks>
public SystemIni() : this(Path.GetFullPath(@".\System.ini")) { }

/// <param name="ini_file_path">iniファイルPATH</param>
public SystemIni(string ini_file_path)
{
      if (File.Exists(ini_file_path))
      {
          file_path = ini_file_path;
      }
      else// ファイルが存在しない場合, ".\System.ini"(実行場所)のPATHを格納する
      {
          file_path = Path.GetFullPath(@".\System.ini");
      }
}

// ===== グローバル変数 =====
/// <summary>iniファイルPATH</summary>
public string FilePath => file_path;
private static string file_path = "";

ここは悩みの種その1です。
iniファイルに伴う可変の値のうち1つが「ファイルPATH」になります。
これはアプリケーションによっては下記2点が変わってきます。
・実行ファイルと同じディレクトリ or 異なるディレクトリ
・任意のファイル名

なので、ファイルPATHはコンストラクタで引数に渡すようにしました。
このiniファイルクラスを親クラスが定義しようとする時に、
ファイルPATHを決定することとなります。

解説その3

// ===== ローカル関数 =====
///  <summary>iniファイルから値を取得する</summary>
/// <typeparam name="T">int,double,bool,string</typeparam>
/// <param name="section_name">セクション名</param>
/// <param name="key_name">キー名</param>
/// <returns>取得した値</returns>
private static T Read<T>(string section_name, string key_name)
{
      // iniファイルから読込し、失敗時はTの型のデフォルト値を返す
      StringBuilder read_str_builder = new StringBuilder(1024);
      uint error_code = GetPrivateProfileString(section_name, key_name, "", read_str_builder, (uint)read_str_builder.Capacity, file_path);
      if (error_code == 0 || string.IsNullOrWhiteSpace(read_str_builder.ToString()))
      {
          return default(T);
      }

      // Tの型を取得し、未指定時はTの型のデフォルト値を返す
      TypeConverter type_converter = TypeDescriptor.GetConverter(typeof(T));
      if (type_converter == null)
      {
          return default(T);
      }

      // 読み込んだ文字列をTの型に変換し返す
      try
      {
          return (T)type_converter.ConvertFromString(read_str_builder.ToString());
      }
      catch (NotSupportedException)
      {
          return default(T);
      }
      catch (FormatException)
      {
          return default(T);
      }
}

///  <summary>iniファイルに値を格納する</summary>
/// <typeparam name="T">int,double,bool,string</typeparam>
/// <param name="section_name">セクション名</param>
/// <param name="key_name">キー名</param>
/// <param name="value">格納する値</param>
private static void Write<T>(string section_name, string key_name, T value)
{
      WritePrivateProfileString(section_name, key_name, value.ToString(), file_path);
}

/// <summary>キー取得の構造体</summary>
/// <typeparam name="T">int,double,bool,string</typeparam>
public struct Key<T>
{
      private string section_name;
      private string key_name;

      /// <param name="section_name">セクション名</param>
      /// <param name="key_name">キー名</param>
      public Key(string section_name, string key_name)
      {
          this.section_name = section_name;
          this.key_name = key_name;
      }
      /// <summary>設定値プロパティ</summary>
      public T Value
      {
          get => Read<T>(section_name, key_name);
          set => Write(section_name, key_name, value);
      }
}

ここがiniファイルクラスの要と思います。
iniファイルから値を読み込もうとする時、
設定項目によっていくつかの型で読み込みたい場面があります。
int、doubleであったり、boolであったり。

シンプルに考えると、読み書き時には文字列扱いで考えて、アプリケーションに任意の型に変換するかと思います。

それでどうしたかと言うと、
ジェネリック関数Readとジェネリック関数Writeを用意しました。

関数名の隣にある"<T>"や引数の型にあるTがそれで、
型を指定していない、という意味合いで良いかと思います。

少し脱線すると、Tをobjectに置き換えて近しい事が可能ですが、
このTとobjectの細かい違いを調べてみたのですが、少々深そうでしたのでここは別途勉強したいと思います。

話を戻して、
Writeは気にせずT型の変数をToStringで文字列に変換すれば良いですが、
Readは前提条件に書いた「変数的に扱える事」を考慮すると、
Tの型を取得して、その型に変換してreturnする必要があります。

その結果が先ほどのコードとなります。
そして、ジェネリック構造体のKey<T>ですが、
これも同様に、この後のiniファイルの項目を定義する際に、
任意の型で定義するためのものとなります。

解説その4

// ===== iniファイルの項目 =====
///  <summary>設定項目</summary>
public Me ME = new Me();
public class Me
{
      /// <summary>Key1</summary>
      public Key<string> NAME = new Key<string>(nameof(ME), typeof(Me).GetFields()[0].Name);
      /// <summary>Key2</summary>
      public Key<int> AGE = new Key<int>(nameof(ME), typeof(Me).GetFields()[1].Name);
      /// <summary>Key3</summary>
      public Key<double> HEIGHT = new Key<double>(nameof(ME), typeof(Me).GetFields()[2].Name);
      /// <summary>Key4</summary>
      public Key<bool> MARRIED = new Key<bool>(nameof(ME), typeof(Me).GetFields()[3].Name);
}

ここは使用時の注意となる場所です。
まず、public Me ME = new Me()の"ME"の部分はセクション名です。
なぜこうかはこの後の使用例を見ると分かりやすいかと思いますが、
親クラスで書く時に、iniファイル内のキー名「ME」になるようにしたい意図です。

次に、public Key<string> NAME = new Key<string>(nameof(ME), typeof(Me).GetFields()[0].Name)ですが、"NAME"部分がキー名です。
これもセクション名同様に、親クラスで書く時にME.NAMEとなるようにしたい意図です。

そして、悩みの種その2になる冗長的な引数です。
"nameof(ME), typeof(Me).GetFields()[0].Name"と書きましたが、
出来る限りコピペで済まそうと思った場合、セクション名・キー名を定数的に扱いたいのです。

ただ視認性が悪いので、改善の余地はあるように思います。
これについては、シンプルにセクション名・キー名を別途定数定義してしまった方がいいかもしれませんね。
あえてこの場合のメリットを上げるのではあれば、コーディングミスがある場合コンパイルエラーで気付きやすい事かと思います。

解説その5

最後に、親クラスから読みだした場合は以下のようになります。

string file_path= System.IO.Path.GetFullPath(@".\System.ini");
SystemIni system_ini = new SystemIni(file_path);

string name = system_ini.ME.NAME.Value;
int age = system_ini.ME.AGE.Value;
double height = system_ini.ME.HEIGHT.Value;
bool is_married = system_ini.ME.MARRIED.Value;


長々と書いたところ、活用の余地があるんでしょうか。
と思いましたが、どちらかと言えばジェネリック関数への理解が深まったところとなりました。

ちなみに最近こんな暑かったのに明日から寒いらしいぞ。

この記事が気に入ったらサポートをしてみませんか?