Windows API - CUI プログラミング編 第3回 〜ファイル入出力〜

ヒープオブジェクトと C++ アロケータクラス

ヒープオブジェクト

malloc() CRT 関数や new オペレータなどで取得することの出来るヒープ領域ですが、 Windows では、

HANDLE HeapCreate(DWORD, SIZE_T, SIZE_T);

という API 関数を用いることで、ヒープ領域を取得することの出来るプライベートヒープオブジェクトを 発行する事が出来ます。

第2引数、第3引数ともにどうせ 0 でも問題ないのですが、第1引数は、 定数 HEAP_GENERATE_EXCEPTIONS というものを指定します。0 でも構わないかもしれません。

プライベートヒープオブジェクトもまた、使用後には、

BOOL HeapDestroy(HANDLE);

という API 関数を呼び解体する必要があります。

ところで、わざわざプライベートヒープオブジェクトを作らなくても、

HANDLE GetProcessHeap(void);

という API 関数を用いると、プロセスのヒープハンドルを取得することが出来ます。

ここで発行されたヒープハンドルから malloc() CRT 関数のように記憶領域を取得するには、

LPVOID HeapAlloc(HANDLE, DWORD, DWORD);

という API 関数を用います。第1引数は型から分かるようにヒープハンドルです。 第3引数が malloc() CRT 関数の第2引数と同様に、取得するバイト数を表します。

第2引数ですが、定数 HEAP_ZERO_MEMORY を指定することで、calloc() CRT 関数のように 0 でクリアされます。

当然、free() CRT 関数のように、取得したヒープ領域に対して、

BOOL HeapFree(HANDLE, DWORD, LPVOID);

という API 関数を呼ぶ必要があります。第1引数はヒープハンドル、第2引数は 0、 第3引数が解放する記憶領域となります。

C++ アロケータクラス

C++ の STL において、メモリの管理を専門に行うクラスに、#include <memory> で 定義される std::allocator というものがあります。

このクラスには、メソッドとして、次のようなものが標準で宣言されています。

template<typename _Ty>
pointer std::allocator<_Ty>::allocate(size_type);
template<typename _Ty>
void std::allocator<_Ty>::deallocate(pointer, size_t);
template<typename _Ty>
void std::allocator<_Ty>::construct(pointer, const _Ty& value);
template<typename _Ty>
void std::allocator<_Ty>::destroy(pointer);

標準では、それぞれ、new オペレータによるヒープ領域の取得、placement new による コンストラクタ呼び出し、デストラクタの呼び出し、delete オペレータによるヒープ領域の 解放という操作に対応します。

そういうわけで、これを継承し、allocate() メソッドと destroy() メソッドを オーバーライドして、ここではプロセスのヒープオブジェクトを使うように拡張します。

template<typename _Ty>
class CHeapAllocator : public std::allocator<_Ty>{
public:
 pointer allocate(size_type count=1);
 void deallocate(pointer ptr, size_type count=0);
};

allocate() メソッドが HeapAlloc() API 関数、deallocate() メソッドが HeapFree() API 関数を 呼び出すようにオーバーロードします。

引数の count ですが、配列インスタンスの要素の数に 対応するように実装します。要するに、HeapAlloc() API 関数の第3引数は、 sizeof(_Ty)*count となります。一方で、HeapFree() API 関数にサイズは必要ないことから 分かるように、deallocate() メソッドの第2引数は使う必要がありません。

ファイル選択ダイアログ

OPENFILENAME 構造体

少し CUI プログラミングから離れることになりますが、ファイル入出力で必要となる ファイル名の取得という操作を、コモンダイアログというものを用いて実装します。

これから開こうとするファイル名に関する情報を持つ構造体が、OPENFILENAME 構造体です。 これも、他の構造体と同様、やたらと多い数のメンバを持ちます。 最低限必要なものだけ、ここでは列挙します。それ以外の全てのメンバは 0 で問題なしです。

DWORD lStructSize;
 構造体のサイズ [=sizeof(OPENFILENAME)]
HWND hwndOwner;
 オーナーウィンドウのハンドル
LPCWSTR lpstrFilter;
 ファイルのフィルタ条件
LPWSTR lpstrFile;
 ファイルのフルパス
DWORD nMaxFile;
 lpstrFile のバッファ長 [=MAX_PATH]
LPWSTR lpstrFileTitle;
 ファイル名
DWORD nMaxFileTitle;
 lpstrFileTitle のバッファ長 [=MAX_PATH]
DWORD Flags;
 OFN_OVERWRITEPROMPT ファイルの上書きの確認を行う
 OFN_FILEMUSTEXIST   対応するファイルが存在しなければならない
 OFN_READONLY        読み取り専用で開く
WORD nFileOffset;
 フルパス名に対するファイル名のオフセット
WORD nFileExtension;
 フルパス名に対する拡張子のオフセット
LPCWSTR lpstrDefExt;
 未指定時の拡張子

GetOpenFileName() API 関数

いわゆる、「ファイルを開く」ダイアログを開いてくれる API 関数です。

BOOL GetOpenFileName(LPOPENFILENAME);

引数には、型にあるように、OPENFILENAME 構造体 ofn へのポインタを渡すわけですが、 渡す前に少なくとも、lpstrFile, lpstrFileTitle は、

CHeapAllocator<WCHAR> allocator;
ofn.lpstrFile=allocator.allocate(MAX_PATH);
ofn.lpstrFileTitle=allocator.allocate(MAX_PATH);

などと初期化し、当然、nMaxFile, nMaxFileTitle も MAX_PATH で初期化します。

そして、GetOpenFileName() API 関数を呼ぶならば、最低でも Flags に、 定数 OFN_FILEMUSTEXIST を立てておきます。

後は、GetOpenFileName() API 関数を呼びさえすれば、ダイアログが開くので、 いつも通りファイルを選ぶと、ファイル名が取得できると言う寸法です。

GetSaveFileName() API 関数

これまでとの違いと言えば、タイトルが「名前を付けて保存」に変わることと、 Flags に、定数 OFN_OVERWRITEPROMPT を立てることくらいです。

いずれにせよ、アロケータを使った手前、必ず CHeapAllocator<WCHAR>::deallocate() メソッドを呼んで解放することを忘れないよう気をつける必要があります。

ファイル入出力

ファイルのオープンとクローズ

Windows では、ファイルもまたハンドルとして取り扱います。ファイルをオープンする時は、

HANDLE CreateFile(
 LPCWSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES,
 DWORD, DWORD, HANDLE
);

という API 関数を使います。引数が fopen() CRT 関数などに比べてかなり面倒になっていますが、 第1引数は先程取得したファイル名を表します。第2引数は読み取りか書き込みかを制御するもので、

GENERIC_READ  読み取りアクセス
               ["r" / std::ios::in に対応]
GENERIC_WRITE 書き込みアクセス
               ["w" / std::ios::out に対応]

の組み合わせで指定します。

第5引数は、ファイルの存在に対する振る舞いを指定します。

CREATE_NEW        新しいファイルの作成、存在時は関数が失敗
CREATE_ALWAYS     新しいファイルの作成、存在時はファイルを上書き
                  ["w" / std::ios::trunc に対応]
OPEN_EXISTING     ファイルを開く、存在しない時は関数が失敗
                  ["r" / std::ios::in に対応]
OPEN_ALWAYS       ファイルを開く、
                  存在しない時は CREATE_NEW として振る舞う
                  ["a" / std::ios::app に対応]
TRUNCATE_EXISTING ファイルを開きサイズを 0 とする、
                  存在しない時は関数が失敗

残りの引数については、第3引数は共有モードですが、0 でも問題ないと思います。 第4引数ですが、SECURITY_ATTRIBUTES 構造体へのポインタを指定します。 NULL でいいと思います。第6引数はファイルの属性ですが、 定数 FILE_ATTRIBUTE_NORMAL を指定すれば大抵のファイル操作は出来ます。 で、第7引数は NULL でいいはずです。

API 関数が成功するとファイルハンドルが発行され、失敗すると、 定数 INVALID_HANDLE_VALUE が返ります。

発行したファイルハンドルは、

BOOL CloseHandle(HANDLE);

という API 関数によりクローズされます。

シーケンシャルアクセス

ファイルを読み込む場合、fread() CRT 関数と同様に、

BOOL ReadFile(HANDLE, LPVOID, DWORD, LPDWORD, LPOVERLAPPED);

という API 関数を用います。第1引数がファイルハンドル、 第2引数がアロケータなどで取得するバッファ、 第3引数が読み取るバイト数、第4引数が実際に読み取ったバイト数を格納する DWORD 型変数へのポインタ、第5引数が NULL となります。

全く同様に、ファイルを書き込む場合、

BOOL WriteFile(HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED);

という API 関数を用います。

ランダムアクセス

fseek() CRT 関数と同様に、ファイルポインタを動かすには、

DWORD SetFilePointer(HANDLE, LONG, PLONG, DWORD);

という API 関数を用いる。第1引数がファイルハンドル、第2引数が シークバイト数の下位、第3引数がシークバイト数の上位へのポインタです。 上位が必要でないならば、 NULL で問題なしです。

第4引数は、シーク開始位置を次の定数で指定します。

FILE_CURRENT 現在位置
FILE_BEGIN   ファイル先頭
FILE_END     ファイル末尾

戻り値がファイルポインタの下位となります。逆の言い方をすれば、

SetFilePointer(hFile, 0, NULL, FILE_CURRENT);

とすると、ftell() CRT 関数と同様の挙動を示します。

これまで書いた事を、エディットコントロールに適用すれば、出来損ないのメモ帳が完成します。


文章作成 : yukki-ts (-+-twilight serenade-+- [stage])