ウィンドウプロシージャをどうラップするか?

まだメモ書き段階.

Win32 APIGUI プログラミングをする場合,WndProc と呼ばれるコールバック関数に処理が集中してしまうため,注意しておかないとこの関数がカオスになります.この問題への対策として最初に思い浮かぶのが,WM_XXX 毎に関数を分けると言う方法で,これに関してはウィンドウメッセージクラッカーと言うマクロ群が Microsoft から提供されているようです.

例えば上記のコードは、メッセージクラッカを使うと次のようにかけます。

LRESULT CALLBACK WindowProc ( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) {
    switch ( uMsg ) {
    HANDLE_MSG (hwnd, WM_CREATE, OnCreate);
    HANDLE_MSG (hwnd, WM_COMMAND, OnCommand);
    }
    return DefWindowProc (hwnd, uMsg, wParam, lParam);
}

BOOL Cls_OnCreate (HWND hwnd, LPCREATESTRUCT lpCreateStruct) {
    // WM_CREATE の処理
    return TRUE;
}

void Cls_OnCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) {
    // WM_CREATE の処理
}

注目するところは、(1) WindowProc の switch 文が HANDLE_MSG の並びに変わっているところ、もうひとつはそれぞれのメッセージ (ここでは WM_CREATE と WM_COMMAND) の処理がそれぞれ異なるプロシージャで行われていることです。

メッセージクラッカ - Web/DB プログラミング徹底解説

これである程度は関数を分離する事ができる訳ですが,何らかの状態を保持する必要が出てきた場合に苦しくなります.状態の保持に関しては,Win32 API だとウィンドウ生成時に LPARAM と言う引数に任意のデータを渡せるので,この引数に適当な構造体なりクラス(へのポインタ)を指定して,そこに状態を保存すると言う方法がよく取られるようです.

ただし,この方法を用いた場合,WM_CREATE (もしくは WM_INITDIALOG) で渡された任意のユーザデータをどこかに記憶しておく必要があり,何も考えずにやると関数内の static 変数に記憶すると言う形になってしまいます.モーダルダイアログ等,「同時には一つのウィンドウしか開かない」と言う前提があるようなケースではこれでも良いのですが,そうではない場合には問題になってきます.

この問題への回避策としては,HWND とユーザデータ (構造体やクラスへのポインタ) のマップを保持できる static 変数を関数内に持って,そのマップへ適切なタイミングで追加/削除を行う事で回避すると言うパターンが多いようです.

これをもう少し推し進めて,1つのゲートウェイ的な (static な) WndProc 関数と,インスタンス毎の (static ではない) WndProc を用意して,ユーザ(プログラマ)から見ると WndProc が自分のウィンドウクラスの通常のメンバ関数の一つであるように振舞わせると,記述が楽に書けるようになると予想されます(下記の例はモーダルダイアログの場合).

class CommonDialog {
public:
    virtual ~CommonDialog() {}
    
    /* --------------------------------------------------------------------- */
    //  ShowDialog
    /* --------------------------------------------------------------------- */
    int ShowDialog(HWND owner, const TCHAR* resource_name) {
        return ::DialogBoxParam(::GetModuleHandle(NULL), resource_name, owner, CommonDialog::StaticWndProc, reinterpret_cast<LPARAM>(this));
    }
    
    /* --------------------------------------------------------------------- */
    //  Handle
    /* --------------------------------------------------------------------- */
    HWND& Handle() { return handle_; }
    const HWND& Handle() const { return handle_; }

protected:
    /* --------------------------------------------------------------------- */
    /*
     *  Close
     *
     *  EndDialog を呼び出す際に (HWND, CommonDialog*) のマップから
     *  削除する必要があるので,代わりにこちらを使用する.
     */
    /* --------------------------------------------------------------------- */
    void Close(int result) {
        GetInstanceMap().erase(this->Handle());
        EndDialog(this->Handle(), result);
    }
    
    /* --------------------------------------------------------------------- */
    /*
     *  WndProc
     *
     *  メッセージ毎に対応するメンバ関数へ分岐させる.
     */
    /* --------------------------------------------------------------------- */
    virtual BOOL WndProc(UINT uMsg, WPARAM wParam, LPARAM lParam) {
        switch (uMsg) {
        case WM_INITDIALOG: return this->OnCreate(wParam, lParam);
        case WM_COMMAND:    return this->OnCommand(wParam, lParam);
        // ...
        default: braek;
        }
    }
    
    virtual BOOL OnCreate(WPARAM wParam, LPARAM lParam) { ... }
    virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) { ... }
    // ...
    
private:
    /* --------------------------------------------------------------------- */
    /*
     *  GetInstanceMap
     *
     *  HWND と CommonDialog クラスのインスタンスの対応関係を保持するため
     *  のマップ.StaticWndProc 関数がこのマップを利用して,該当する
     *  クラスの WndProc へと分岐させる.
     *
    /* --------------------------------------------------------------------- */
    typedef std::map<HWND, EncodingDialog*> instance_map_type;
    static instance_map_type& GetInstanceMap() {
        static instance_map_type v_;
        return v_;
    }
    
    /* --------------------------------------------------------------------- */
    //  StaticWndProc
    /* --------------------------------------------------------------------- */
    static BOOL CALLBACK StaticWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
        instance_map_type& v = GetInstanceMap();
        if (uMsg == WM_INITDIALOG) {
            assert(v.find(hWnd) == v.end());
            
            CommonDialog* instance = reinterpret_cast<CommonDialog*>(lParam);
            instance->Handle() = hWnd;
            v.insert(std::make_pair(hWnd, instance));
            break;
        }
        
        instance_map_type::iterator pos = v.find(hWnd);
        if (pos != v.end()) return pos->second->WndProc(uMsg, wParam, lParam);
        return FALSE;
    }
};

こう言った基底クラスをあらかじめ定義しておき,ユーザはこの基底クラスを継承して必要な OnXXX メンバ関数をオーバーライドすると言う方法を取ることで,WndProc 関数がカオスになると言う状況を回避できる事が期待できます.既にある実装だと Win32++ と言うプロジェクトがこの方針を基に各種ウィンドウ/ダイアログクラスを定義しているようです.使う機会がなかったのでちょっと追っていませんが,MFC もこの方針で実装されていたのでしょうか.

問題としては,WM_COMMAND に必要な処理が集中すると言うケースが多いため,この方針で関数に分離していっても OnCommand() メンバ関数がカオスになりがちです.この辺りは,ユーザ (継承したクラスを書く人)が注意しながら書く位しか今のところは思いつきません.