2011/02/10

魔法使いの弟子(その6-1)

※ 今日は C++ と Windows プログラミングのお話だよ。

ぼくが C++ 言語で GUI プログラムを作るときには、なるべくユーザインタフェースと中核処理のコードを分離したいなと思っている。その理由は、ライブラリへの依存度をなるべく下げて他の環境への移植性を高める為と、処理そのものの安定性を保証する為である。

たとえば Windows API を積極的に用いて作成したプログラムは、当然ながら Windows 環境で動作させる事が前提となり、移植性は考慮の対象外になりがちだ。また、サードパーティ製ライブラリがプログラム全域に亘ってかっちり食い込んでいると、ライブラリの仕様変更に伴って厖大な修正作業が必要になる事がある(生産性も低下する)。もしかしたら、ライブラリに潜在するバグが処理に何らかの影響を及ぼす事だってあるかも知れない。

だが、本質的な処理をライブラリ非依存で書く事ができれば、当然ながら“本質を損なわずに” C++ の処理系を有する他環境へ移植する事が容易になるだろう。何らかの不具合が生じたときにも、その責任の所在も突き止めやすくなる。

では、これを実現するにはどうすればよいだろうか。

実はそれほど面倒な事ではない。ライブラリが独自に拡張したデータ型と、言語仕様で定義されている型を相互に変換する処理を実装すれば、(ライブラリ非依存の)中核処理の結果を Windows API から利用したり、あるいはその逆を行ったりする事ができるようになる。

Windows API の内部では、typedef を駆使しておびただしい量の型が定義されており、必要に応じてこれらを C++ で用意されている標準的な型に変換する必要がある。今回はその一例として、 Windows で定数文字列を表すためによく用いられる LPCTSTR 型から、 std::string 型への橋渡しを考えてみよう。

場合によっては、 Adapter パターンを適用した本格的なクラス設計が必要になる事もあるが、今回は変換用の関数を作るだけで充分だ。



【ミッション: 文字列の型を変換したい】

/* LPCTSTRからstd::stringへの変換 */

std::string WideCharsToString(LPCTSTR src) {
    std::string str;
    #ifdef UNICODE
        int bufSize = WideCharToMultiByte(CP_ACP, 0, src, -1, NULL, NULL, NULL, NULL);
        char* buf = new char[bufSize];
        if (WideCharToMultiByte(CP_ACP, 0, src, -1, buf, bufSize, NULL, NULL) == 0) return NULL;
        str = buf;
        delete[] buf;
    #else
        str = src;
    #endif
    return str;
}

こんなものが何の役に立つのか。

たとえば、ファイルを開く処理を考えてみよう。一般的な GUI アプリケーションでは、通常、ファイルを選択するためのダイアログボックスが表示される。 Windows プログラミングにおいて、この処理は Windows API の役割だ( OpenFileDialog() 関数参照)。

では、ファイルを読み込む処理はどうだろうか。これには、 Windows API を用いる方法と、 C++ の標準関数を用いる方法が存在する。前述の理由から、ぼくは後者が好きなので、
  • Windows API は、ファイルを開くダイアログボックスを表示するためだけに用いる。
  • ユーザが選んだファイルのパス( TCHAR[] 型)を、 C++ が標準でサポートしている std::string 型に変換
  • std::string 型のファイルパスを、 C++ の標準関数に渡してファイルを読み込む。
という方針を好んで実装する。

このときの『ファイルパスの型変換』のために、先ほど定義した WideCharsToString() 関数を使うのだ。

/* ファイルを開くダイアログを表示する */

TCHAR szFile[MAX_PATH];          // パスを含むファイル名
TCHAR szFileTitle[MAX_PATH];     // パスを含まないファイル名

int OpenFileDialog(HWND hEdit) {
    OPENFILENAME ofn;
    memset(&ofn, 0, sizeof(OPENFILENAME));
    ofn.lStructSize = sizeof(OPENFILENAME);
    ofn.hwndOwner = hEdit;
    ofn.lpstrFilter = TEXT("text(*.txt)\0*.txt\0ALL files(*.*)\0*.*\0\0");
    ofn.lpstrFile = szFile;
    ofn.lpstrFileTitle = szFileTitle;
    ofn.nMaxFile = MAX_PATH;
    ofn.nMaxFileTitle = MAX_PATH;
    ofn.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;
    ofn.lpstrDefExt = TEXT("txt");
    ofn.lpstrTitle = TEXT("ファイルを開く");

    // ファイルを開くダイアログの表示
    if(GetOpenFileName(&ofn) == 0) return -1;
    return 0;
}

この関数を実行して 0 が返ったとき、グローバル変数 szFile にはユーザが指定したファイルの有効なフルパスが格納されている。このパスを std::string に変換すれば、そこから string::c_str() を利用して const char* 型の文字列を得る事も可能だ。これで、標準の fopen() 関数や ifstream を用いてファイルを読み出す事ができるようになる(テキストファイルを読み込む処理は、どの C++ のテキストにも書いてあるので省略)。

グローバル変数を使うのが少々気持ち悪い場合は、それらをシングルトンオブジェクトにしてしまうか、あるいは OpenFileDialog() 関数もろとも一つのクラスにカプセル化してしまうという方法が考えられるだろう。



以上で述べた方針には、“本質的業務を窓口に配さない”という特徴がある。両者が分離可能である事は、一方の変更が他方に及ぼす影響を最小限にとどめられる事を意味する。これは、局所的なバグが全体に波及する事を抑制するといった意味でも有効であるし、あるいはその発生源を突き止める事も容易になる。