INIファイル

初期化ファイル

アプリケーションによっては設定をファイルに保存して、次回の起動時にファイルを読み込み前回の状態を復元できるようにしているものがあります。
設定ファイルの形式は色々考えられますが、Windowsでは伝統的なファイル形式にINIファイルという方法が使用されています。
(INIは「Initialization」(初期化)の略で、初期化ファイルとも呼ばれます)
これは一定のフォーマットに沿って記述されたテキストファイルで、拡張子は「.ini」です。
単なるテキストファイルなので通常のファイル関数でも読み書き可能ですが、Windows APIには専用の関数が用意されています。

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

まずはINIファイルのフォーマットを簡単に説明します。

パラメータ

INIファイルに保存されるデータは「キー」と「」の組み合わせで表されます。
これをパラメータといいます。


name=value

キーと値の間は「=」で結合します。
これは変数と同じようなものと考えるとわかりやすいでしょう。
プログラムから「name」にアクセスすれば「value」という値が取得できることになります。

セクション

パラーメータはセクションによってグループ化することができます。


[section1]
key1=value1
key2=value2
[section2]
key1=value1
key2=value2

セクションは[]記号によって記述します。
ひとつのセクションは次のセクションが現れるまでのパラメータをグループ化します。
それぞれのセクションは独立しているので、異なるセクションであれば重複するキー名を使用することができます。

INIファイルは基本的にこの二つだけ知っておけば十分です。

INIファイルの作成および書き込み

WritePrivateProfileString関数

INIファイルはWritePrivateProfileString関数で作成、およびパラメータを設定することができます。

BOOL WritePrivateProfileStringW(
 LPCWSTR lpAppName,
 LPCWSTR lpKeyName,
 LPCWSTR lpString,
 LPCWSTR lpFileName
);
INIファイルlpFileNameのセクションlpAppNameに、キーlpKeyNameと値lpStringのパラメーターを設定する。
成功した場合は0以外を、失敗した場合は0を返す。

第一引数lpAppNameはセクション名です。
セクション名は大文字と小文字を区別しません。

第二引数lpKeyNameはキー名です。
NULLを指定すると、そのセクションのすべてのパラメーターとセクション自体を削除します。

第三引数lpStringはキーの値です。
NULLを指定すると、そのキーを削除します。

第四引数lpFileNameはINIファイル名です。
ファイルが存在しない場合は作成します。
ここにはフルパス(絶対パス)を指定する必要があります。
ファイル名だけを記述すると相対パスでの指定にはならず、Windowsフォルダ内からファイルを検索、および作成しようとします。
(書き込み権限がないと関数は失敗します)


#pragma comment(lib, "Pathcch.lib")
#include <windows.h>
#include <pathcch.h>

//途中省略

WCHAR path[MAX_PATH];

//カレントディレクトリの取得
GetCurrentDirectory(MAX_PATH, path);

//保存先パスの作成
//カレントディレクトリ\test.ini
PathCchCombine(path, MAX_PATH, path, L"test.ini");

//iniファイルの書き込み(なければ作成)
WritePrivateProfileString(
	L"section1",
	L"key1",
	L"value1",
	path
);

上記コードはカレントディレクトリ下に以下のINIファイルを作成します。


[section1]
key1=value1

すでに同名のキーが指定のセクションに存在する場合は値が上書きされます。

なお、決まりがあるわけではありませんが、アプリケーションの全般的な設定を保存するINIファイルの名前は「(アプリケーション名).ini」にするのが一般的です。

WritePrivateProfileSection関数

INIファイルはWritePrivateProfileSection関数で作成および書き込みすることもできます。

BOOL WritePrivateProfileSectionW(
 LPCWSTR lpAppName,
 LPCWSTR lpString,
 LPCWSTR lpFileName
);
INIファイルlpFileNameのセクションlpAppNameにパラメーターlpStringを設定する。
成功した場合は0以外を、失敗した場合は0を返す。

第一引数lpAppNameはセクション名です。

第二引数lpStringはキーと値の組み合わせの文字列です。
これは複数のパラメータを同時に指定できます。
各パラメータはNULL文字で区切ります。
文字列の最後はダブルNULLで終了します。
(ダブルNULLについてはダブルNULL終端文字列を参照)
この文字列は最大65,535バイトまでに制限されています。

第三引数はlpFileNameINIファイル名です。
注意点はWritePrivateProfileString関数と同じです。


//pathの作成等は省略

WritePrivateProfileSection(
	L"section1",
	L"key1=abc\0"		//←末尾にコンマが無いことに注意
	L"key2=あいうえお\0",
	path
);

上記コードは以下のINIファイルを作成します。


[section1]
key1=abc
key2=あいうえお

上記コードは四つの引数を指定しているように見えますが、5行目終端にコンマがない点に注意してください。
文字列リテラルを連続して記述するとひとつの文字列リテラルを記述したものとみなされます。
このとき、文字列リテラル同士の間は空白や改行の挿入が可能です。

INIファイルの読み込み

GetPrivateProfileString関数

INIファイルの読み込みにはGetPrivateProfileString関数を使用します。

DWORD GetPrivateProfileStringW(
 LPCWSTR lpAppName,
 LPCWSTR lpKeyName,
 LPCWSTR lpDefault,
 LPWSTR lpReturnedString,
 DWORD nSize,
 LPCWSTR lpFileName
);
INIファイルlpFileNameのセクションlpAppNameから、キーlpKeyNameの値をバッファlpReturnedStringに格納する。
戻り値はバッファに格納した文字列の文字数。

第一引数lpAppNameはセクション名です。
NULLを指定すると、INIファイル内のすべてのセクション名を取得します。

第二引数lpKeyNameはキー名です。
NULLを指定すると、指定のセクションにあるすべてのキー名を取得します。

第三引数lpDefaultは、指定のキー名が存在しない場合に取得する文字列です。
第二引数で指定したキーが見つからなくてもエラーなはならずに、ここで指定した値をデフォルト値として取得します。
NULLを指定すると、空文字が使用されます。

第四引数lpReturnedStringは取得する文字列を格納するバッファです。

第五引数nSizeは第四引数のバッファのサイズ(文字数)です。

第六引数lpFileNameはINIファイル名です。
WritePrivateProfileString関数と同じく、フルパスで指定する必要があります。

戻り値は第四引数lpReturnedStringに格納された文字列の文字数です。
(NULL文字は含まない)
関数が正常終了した場合でも0を返すことがあります。
ファイルが見つからないなどで関数が失敗した場合、GetLastError関数はNO_ERROR以外の値を返します。


#pragma comment(lib, "Pathcch.lib")
#include <windows.h>
#include <pathcch.h>

#define BUFFERSIZE 32

//途中省略

WCHAR path[MAX_PATH];

//カレントディレクトリの取得
GetCurrentDirectory(MAX_PATH, path);

//保存先パスの作成
//カレントディレクトリ\test.ini
PathCchCombine(path, MAX_PATH, path, L"test.ini");

WCHAR buf[BUFFERSIZE];

GetPrivateProfileString(
	L"section1",
	L"key1",
	NULL,
	buf,
	BUFFERSIZE,
	path
);

MessageBox(NULL, buf, L"情報", MB_OK);

このコードはカレントディレクトリ内の「test.ini」ファイルから「section1」というセクション内の「key1」キーの値を取得します。
存在しない場合は空文字がバッファに格納されます。

バッファサイズはここでは「32」を指定していますが、これを超える文字数の場合は途中で切り捨てられます。
その場合でも文字列の終端にはNULL文字が付加されます。
つまり取得可能な文字数は31文字までです。

なお、値が引用符で囲われている場合は引用符を削除した上で値が取得されます。


[section]
key1="value1"
key2='value2'

上記のINIファイルの場合、key1もkey2も値を囲っている引用符(シングル/ダブルクォーテーション)はバッファには格納されません。

全てのセクション名の取得

第一引数lpAppNameNULLを指定すると、INIファイル内のセクション名を全て取得します。

ここでは以下のINIファイルを読み込みます。


[section1]
key1=value1

[section2]
key1=value1

[section3]
key1=value1

#define BUFFERSIZE 32

//パスの作成等は省略

WCHAR buf[BUFFERSIZE];

GetPrivateProfileString(
	NULL,
	NULL,
	NULL,
	buf,
	BUFFERSIZE,
	path
);

各セクション名の区切り文字にはNULL文字が使用されます。
また、文字列の終端にはさらにNULL文字が付加されます。
(ダブルNULL)
つまりこのコードの場合は、文字列バッファbufには「section1\0section2\0section3\0\0」という文字列が格納されます。
途中にNULL文字が現れるため、そのままでは最初のセクション名までしか文字列として有効でないことに注意してください。

取得する文字列の長さがバッファの長さを超える場合でも終端は必ずダブルNULLに置き換えられます。
つまり今回の場合は「32-2」で30文字まで取得できます。
(一文字も取得されない場合はNULL文字ひとつだけが格納されます)

各セクションの取り出し

取得したバッファ文字列から各セクション名にアクセスするには、「NULL文字の次の文字」のポインタを取得します。
ただしNULL文字が連続する場合は文字列の終端です。


#pragma comment(lib, "Pathcch.lib")
#include <windows.h>
#include <pathcch.h>

int SplitNullDelimitedString(const WCHAR*, size_t, WCHAR**, size_t);

#define BUFFERSIZE 256

//NULL文字で区切られた、長さsrcLengthの文字列srcから
//各文字列の先頭のポインタをdstに格納する
//戻り値はdstに格納した文字列の数
//dstにNULLを指定すると有効な文字列の数を返す
int SplitNullDelimitedString(const WCHAR* src, size_t srcLength, WCHAR** dst, size_t dstCount)
{
	WCHAR* p = src;
	WCHAR* pNext = NULL;
	int count = 0;

	//dstがNULLの場合は有効な文字列の数を返す
	if (!dst)
	{
		while (srcLength--) //検索がsrcLengthを超えたら終了
		{
			if (*p == L'\0') {
				//NULL文字が連続したら終了
				if (p == pNext) {
					break;
				}
				count++;
				//NULL文字の次の文字のポインタを保存
				pNext = ++p;
			}
			else {
				++p;
			}
		}
		return count;
	}

	if (dstCount == 0)
		return 0;

	//先頭文字列のアドレスを保存
	dst[count++] = p;

	if (dstCount == 1)
		return count;

	while (srcLength--)
	{
		if (*p == L'\0') {
			if (p == pNext) {
				break;
			}
			dst[count++] = ++p;
			if (count >= dstCount)
				break;
			pNext = p;
		}
		else {
			++p;
		}
	}

	return count;
}

int APIENTRY wWinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPWSTR lpCmdLine,
	int nCmdShow)
{
	WCHAR path[MAX_PATH];

	GetCurrentDirectory(MAX_PATH, path);
	PathCchCombine(path, MAX_PATH, path, L"test.ini");

	WCHAR buf[BUFFERSIZE];

	//バッファに格納された文字列の長さを保存しておく
	int len = GetPrivateProfileString(
		NULL,
		NULL,
		NULL,
		buf,
		BUFFERSIZE,
		path
	);

	//第三引数にNULLを指定して
	//有効な文字列の数を取得
	int count = SplitNullDelimitedString(buf, len, NULL, 0);

	//各文字列の先頭ポインタを格納するメモリ領域を動的確保
	//ポインタのポインタ(ダブルポインタ)であることに注意
	WCHAR** strs = malloc(sizeof(WCHAR*) * count);
	if (!strs) //メモリ確保失敗
		return 0;

	//確保した領域に実際に格納
	int count2 = SplitNullDelimitedString(buf, len, strs, count);

	for (int n = 0; n < count2; n++)
	{
		MessageBox(NULL, strs[n], L"情報", MB_OK);
	}

	//メモリ解放
	free(strs);
	return 0;
}

自作関数SplitNullDelimitedStringは、NULL文字で区切られた文字列から各文字列の先頭のポインタを取得する関数です。

文字列の数(今回はセクションの数)が事前にわからない場合はmalloc関数でメモリを動的確保します。
malloc関数は確保したメモリ領域へのポインタ(void*型)を返します。
このメモリ領域に保存するのは各文字列の先頭へのポインタ(WCHAR*型)です。
つまり、malloc関数の戻り値を受け取る変数はダブルポインタ(ポインタのポインタ)になります。
(WCHAR**型)
これは二次元配列と同じような構造と考えると良いでしょう。


|strs[0]   |strs[1]   |strs[2]
|↓         |↓         |↓
|section1\0|section2\0|section3\0\0

strs[0][0]は's'
strs[0][1]は'e'
...
strs[0][6]は'n'
strs[0][7]は'1'
strs[0][8]は'\0'

//文字そのものではなく
//文字列中の文字へのポインタが格納されていることに注意

確保したメモリ領域は通常の配列のように添え字(インデックス)でアクセス可能ですが、Visual Studioのバージョンによっては警告が出ることがあります。
この警告は、動的確保した領域のサイズをコンパイラが知ることはできず、範囲外アクセスをしてしまう可能性があるということを知らせるものです。
警告が気になる場合は添字演算子[]ではなくポインタ演算でアクセスしてください。


MessageBox(NULL, strs[n], L"情報", MB_OK);
//↑は↓と同じ意味
MessageBox(NULL, *(strs + n), L"情報", MB_OK); //警告対策

C言語は可変長配列が扱えないため上記コードはやや面倒な処理を行っていますがが、C++ならばvectorクラスを使用することで簡単なコードで実現できます。


WCHAR buf[BUFFERSIZE];

int len = GetPrivateProfileString(
	NULL,
	NULL,
	NULL,
	buf,
	BUFFERSIZE,
	path //適切なパス
);

std::vector<WCHAR*> strs;
if (buf != NULL)
{
	strs.emplace_back(buf);
	for (int n = 1; n < len; n++) {
		if (buf[n] == L'\0') {
			if (buf[n + 1] == L'\0')
				break;
			strs.emplace_back(&buf[++n]);
		}
	}
}

for (int n = 0; n < strs.size(); n++)
{
	MessageBox(NULL, strs[n], L"情報", MB_OK);
}

セクション内の全てのキーを取得

第二引数lpKeyNameNULLを指定すると、第一引数lpAppNameで指定したセクション内にある全てのキー名を取得します。


[section1]
key1=value1
key2=value2
key3=value3

#define BUFFERSIZE 32

//パスの作成等は省略

WCHAR buf[BUFFERSIZE];

GetPrivateProfileString(
	L"section1",
	NULL,
	NULL,
	buf,
	BUFFERSIZE,
	path
);

//bufには
//"key1\0key2\0key3\0\0"
//が格納される

取得される文字列は全てのセクション名を取得する場合と同じく、各キー名はNULL文字で区切られ、ダブルNULLで終了します。

GetPrivateProfileInt関数

取得する値が整数値の場合はGetPrivateProfileInt関数を使用することができます。

UINT GetPrivateProfileInt(
 LPCTSTR lpAppName,
 LPCTSTR lpKeyName,
 INT nDefault,
 LPCTSTR lpFileName
);
INIファイルlpFileNameのセクションlpAppNameから、キーlpKeyNameの整数値を取得する。
キーが見つからない場合は整数nDefaultを返す。

戻り値はキーに関連付けられた整数値です。
キーが見つからない場合は第三引数nDefaultの値が戻り値になります。

アプリケーションの設定を保存/復元する方法にはレジストリというものを使用する方法もあります。
レジストリはWindowsシステム内のフォルダに保存されるデータベースファイルで、様々なアプリケーションの設定やWindows自体の設定もここに保存されます。

レジストリによる管理は高機能で、マイクロソフトもこちらの使用を推奨しています。
しかしWindowsのシステム内に情報を保存するので、ソフトを削除する場合には専用のアンインストーラーを用意するなどのきちんとした手順でアンインストールする必要があります。
これを怠るとシステム内に無駄な設定情報が残り続けることになります。
レジストリは手動で編集可能ですが、一般的なユーザーには難しい作業で、誤った編集をしてしまうと最悪システムが起動不能になるなどの致命的なエラーが発生する危険性があります。

INIファイルは単なるテキストファイルで、他のファイルと同じように扱うことができます。
アンインストール時も他のファイルと一緒にINIファイルを削除するだけです。
小規模なソフトであればこちらの方が簡単に扱えるでしょう。

同じテキストファイルを使用する方法としてINIファイルではなくXMLファイルを使用する方法もあります。