Windowsメッセージハンドラの実装方法を考える

公開:2012-01-15 12:39
更新:2020-02-15 04:37
カテゴリ:windows,dawもどきの作成,c++,audio

本格的にUI作りに取り組むにあたり自前のWindowラッパを改良しようとしているところ。

ウィンドウメッセージのハンドリングについて検討している。

多くのWindowsラッパではhwndをインスタンス変数として種々のAPIをメンバ関数としてラップするのが普通である。HiloにしてもDarkWaveStudioにしても、MFCにしてもWTL(ATL)にしても同じである。hwndに対するメッセージをC++クラスインスタンスで捕まえて、必要であればhwndメンバを引数にしてAPIを呼び出す。これはhwndがWindowのインスタンスであるのでごく自然な考え方であるといえる。少し考えなくてはいけないのはWindowsメッセージを直接C++インスタンスのメンバ関数で捕まえる方法である。

APIでのWindowの生成方法

ここで少しAPIでのWindow生成方法について書いておく。

APIでWindowを作る方法は下記の通りである。

  1. RegisterClass APIでウィンドウプロシージャを登録(定義)する。この時クラス名およびウィンドウプロシージャの関数ポインタをセットする。その他の設定パラメータもあるがここでは省略する。
  2. CreateWindow APIで引数にクラス名を指定してウィンドウを生成する。APIを呼び出すと1.で登録したウィンドウプロシージャにメッセージがコールバックされる。

C++クラスインスタンスとウィンドウインスタンス(hwnd)を結びつける上での問題点

RegisterClassにセットするウィンドウプロシージャは静的メンバ関数かフリー関数のアドレスしかセットできないため、C++クラスインスタンスのメンバ関数を直接呼び出すことができない。これが問題点であり、これをどうするかが私のような初学者が悩むところなのである。

解決策

解決策として3通りほどある。

C++クラスインスタンスをグローバル変数で持つ

C++クラスインスタンスをグローバル変数で持ち、ウィンドウプロシージャから呼び出す。登録したウィンドウクラスがアプリケーションでただ1つしか作られrない場合はこの方法が使える。しかしあまりいいお作法ではない。ボタンクラスなどアプリケーションで複数使用するクラスであった場合は使えない方法である。

ウィンドウメモリ内にC++クラスインスタンスを格納する。

ウィンドウインスタンスには拡張ウィンドウメモリというのがあり、その中にユーザデータ格納領域がありポインタの格納・取り出しができるようになっている。そこにC++クラスインスタンスを格納するのがこのアイデアである。格納/取り出しは SetWindowLongPtr/GetWindowLongPtrで行う。このデータをウィンドウプロシージャ内で取り出せばC++クラスインスタンスのメンバ関数を呼び出すことができる。この方法はSDKサンプル、Hiloでも使用されているて最も一般的な方法である。実装例は下記の通り(コードはSDK サンプルを引用)。

  1. CreateWindowの最後の引数にthisポインタをセットする。

    
    
    m_hwnd = CreateWindow(
        L"D2DDemoApp",
        L"Direct2D Demo Application",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        static_cast<UINT>(ceil(640.f * dpiX / 96.f)),
        static_cast<UINT>(ceil(480.f * dpiY / 96.f)),
        NULL,
        NULL,
        HINST_THISCOMPONENT,
        this
        );
    

  2. ウィンドウプロシージャのWM_CREATEもしくはWM_NCCREATEでthisポインタを取り出して、SetWindowLongPtrでユーザデータ領域にセットする。

    
    
    if (message == WM_CREATE)
    {
        LPCREATESTRUCT pcs = (LPCREATESTRUCT)lParam;
        DemoApp *pDemoApp = (DemoApp *)pcs->lpCreateParams;
    
        ::SetWindowLongPtrW(
            hwnd,
            GWLP_USERDATA,
            PtrToUlong(pDemoApp)
            );
    
        result = 1;
    }
    

  3. WM_CREATE/WM_NCCREATEメッセージ以降はGetWindowLongPtrWでthisポインタを取り出す。

    
    
    DemoApp *pDemoApp = reinterpret_cast<DemoApp *>(static_cast<LONG_PTR>(
        ::GetWindowLongPtrW(
            hwnd,
            GWLP_USERDATA
            )));
    // このあとこのポインタを使ってイベント処理を呼び出す。。
    

サンクを使ってthisポインタとhwndと結びつける。

これは私のラッパやWTL(ATL)で使用している方法。WTL(ATL)のサンクについては下記サイトが詳しい。

http://hp.vector.co.jp/authors/VA022575/c/msgmap.html

私はこのアイデアを少し簡略化して利用している。簡略化するためにウィンドウクラスとC++クラスインスタンスは1:1に対応づけている。つまり全く同じ処理をするウィンドウでもクラス名は異なる。C++クラスインスタンス生成時に必ずウィンドウクラスの登録とウィンドウの生成を行うようにしている(つもりだが実装は怪しい感じもする)。

S.F.Traker(372) – thunk

thunkをいじる

thunkを使いOSからダイレクトにメンバ関数を呼び出させる。

メッセージのハンドル方法

C++クラスインスタンスとウィンドウインスタンスの結びつけができるようになると次に来るのがメッセージのハンドリング方法である。

これはメッセージごとに仮想関数で実装するのが普通だろう。HiloやAPIサンプルでも同様である。しかしすべてのメッセージ用の仮想関数を用意するとかなり大きなクラスとなってしまうのが問題だ。基本的なクラスを用意して仮想関数は最低限必要なものだけ用意し、後は派生側の実装に任せたい。そこでまず考えるのがウィンドウメッセージをディスパッチするメンバ関数を仮想化する方法である。でもこの方法だと派生側で1つハンドラを追加するとディスパッチする仮想関数を1から作らなくてはいけなくなる。

今考えているのは。基本クラスのウィンドウメッセージディスパッチ関数の最後にother_message_proc()なんていうものを用意して、基本クラスでディスパッチしないメッセージはこの仮想関数に任せるようにしておけば、派生側で他のメッセージをディスパッチする部分だけ書けばよくなる。基本クラスが捕まえるメッセージについて派生側で実装の変更したいのであれば基本クラスの仮想関数をオーバーライドすれば良い。多段に継承すると少しother_message_proc()の実装がややこしくなりそうだけれども、このアイデアで行こうかなと考えている。