ウィンドウのサブクラス化

今回は、新しいウィンドウプロシージャの関数コードを作成して、ウィンドウのサブクラス化を行なってみることにしましょう。

まずは、新しいウィンドウプロシージャを SubProc という名前で作成します(名前は何でもいいのですが)。ウィンドウプロシージャの関数と同じ形式なので、以下のように定義されます。

LRESULT CALLBACK SubProc(
    HWND hWnd,              // ウィンドウハンドル
    UINT message,           // メッセージコード
    WPARAM wParam,          // wParamパラメータ
    LPARAM lParam           // lParamパラメータ
);

今回は、ドラッグアンドドロップされたファイルを取得するのに、 WM_DROPFILES メッセージを取得するようにしてみます。ドラッグアンドドロップファイルの取得については、 hsgetmsg.dll を使った方法が『HSPの裏技??』で紹介されているので、そちらも参照しておきましょう。

今回は関数コード中に情報を埋め込む必要があります。今回必要な情報は以下の3つになります。

これらの情報を含む構造体を以下のように定義してみます。

typedef struct {
    LRESULT (WINAPI *pfnCallWindowProc)(WNDPROC,HWND,UINT,WPARAM,LPARAM);
    WNDPROC pfnOrgProc;
    int *phDrop;
} SUBPROC_DATA;

ウィンドウのサブクラス化は、以下の手順で行ないます。

まず、 GetWindowLong 関数に GWL_WNDPROC を渡して、現在のウィンドウプロシージャのアドレスを取得し、それを保存しておきます。

次に、 SetWindowLong 関数に GWL_WNDPROC と新しいウィンドウプロシージャのアドレスを渡して、ウィンドウプロシージャを新しいものに置き換えます。

GetWindowLong 関数や SetWindowLong 関数の呼び出しは setwndlong 命令で行なうことができます。

新しいウィンドウプロシージャでは、受け取ったメッセージに対して必要な処理を行なった後、 CallWindowProc 関数を呼び出して、メッセージ情報をそのまま古いウィンドウプロシージャに送ります。そして、 CallWindowProc 関数の戻り値を、ウィンドウプロシージャの戻り値として返します。

ここで使われる CallWindowProc 関数は、ウィンドウプロシージャを明示的に呼び出すための関数です。新しいウィンドウプロシージャの中から古いウィンドウプロシージャを呼び出すために、この関数を使用します。この関数は KERNEL32.DLL によって提供されています。

LRESULT CallWindowProcA(
  WNDPROC pWndProc,       // ウィンドウプロシージャのアドレス
  HWND hWnd,              // ウィンドウハンドル
  UINT message,           // メッセージコード
  WPARAM wParam,          // wParamパラメータ
  LPARAM lParam           // lParamパラメータ
);

新しいウィンドウプロシージャのソースコードです。

ソースファイル subproc.c

#include <windows.h>

typedef struct {
    LRESULT (WINAPI *pfnCallWindowProc)(WNDPROC,HWND,UINT,WPARAM,LPARAM);
    WNDPROC pfnOrgProc;
    int *phDrop;
} SUBPROC_DATA;

LRESULT CALLBACK SubProc(
    HWND hWnd,
    UINT message,
    WPARAM wParam,
    LPARAM lParam)
{
    SUBPROC_DATA *volatile pTemp = (SUBPROC_DATA *)0x11111111;
    SUBPROC_DATA *pProcData = pTemp;

    // WM_DROPFILESのとき、ドロップ情報のハンドル(wParam)を変数に格納
    if (message == WM_DROPFILES) {
        pProcData->phDrop[0] = (int)wParam;
    }

    // メッセージ情報を古いウィンドウプロシージャに渡す
    return pProcData->pfnCallWindowProc(pProcData->pfnOrgProc,
        hWnd, message, wParam, lParam);
}

埋め込まれる情報は、始めに定義した内部構造体(SUBPROC_DATA 構造体)のアドレス1つだけです。後でわかりやすいように、アドレスを 0x11111111 としておきます。また、変数の最適化の防止を行なう修飾子 volatile を付けておきます。

ところで、上のソースプログラムを見て気づかれたかと思いますが、 SUBPROC_DATA 構造体のアドレスをいったん一時的なポインタに格納していますよね。もちろん、今までどおりに、

SUBPROC_DATA *volatile pProcData = (SUBPROC_DATA *)0x11111111;

とすることもできるのですが、これでは、このコールバック関数全体でこのポインタに対しての最適化抑止がかかってしまうので、あまり効率的とはいえません。これを改善する一つの手段として、いったん volatile を指定した一時的な変数に代入してから、その後でその一時変数の内容を本来の変数に格納するのです。すなわち、

SUBPROC_DATA *volatile pTemp = (SUBPROC_DATA *)0x11111111;
SUBPROC_DATA *pProcData = pTemp;

もしくは、同じことですが、

SUBPROC_DATA *volatile pTemp = (SUBPROC_DATA *)0x11111111, *pProcData = pTemp;

とすることで、ある程度の最適化が行なわれるようにすることができるのです。


ソースコードをコンパイルして、マシン語情報を取り出します。VC++で行なった場合には、以下のような情報が取り出せます。(取り出されるコードはコンパイラの種類や最適化設定などにより異なります。)

(…………………… 省   略 ……………………)
RAW DATA #2
  00000000: 55 8B EC 51 C7 45 FC 11 11 11 11 8B 45 FC 81 7D 
  00000010: 0C 33 02 00 00 8B 4D 10 75 05 8B 50 08 89 0A FF 
  00000020: 75 14 51 FF 75 0C FF 75 08 FF 70 04 FF 10 C9 C2 
  00000030: 10 00 

  Summary

          32 .text

この情報から、マシン語コードを生成します。

xdim fncode, 13
fncode.0 = $51ec8b55, $11fc45c7, $8b111111, $7d81fc45, $0002330c
fncode.5 = $104d8b00, $508b0575, $ff0a8908, $ff511475, $75ff0c75
fncode.10 = $0470ff08, $c2c910ff, $00000010

次に、「11 11 11 11」などとなっていた部分に正しい値(SUBPROC_DATA 構造体のアドレス)を格納しなければなりません。先頭からのオフセットを考えて、 memcpy 命令または ll_poke4 命令で格納します。上のコードの場合は、以下のようになります。

オフセット 仮の値 実際の値
7 11111111 SUBPROC_DATA 構造体のアドレス

では、スクリプトを書いてみることにしましょう。

    #include "llmod.as"
    #include "xdim.as"

    ; マシン語を変数に格納する
    xdim fncode, 13
    fncode.0 = $51ec8b55, $11fc45c7, $8b111111, $7d81fc45, $0002330c
    fncode.5 = $104d8b00, $508b0575, $ff0a8908, $ff511475, $75ff0c75
    fncode.10 = $0470ff08, $c2c910ff, $00000010

    ; マシン語コード(新しいウィンドウプロシージャ)のアドレス取得
    getptr pfn, fncode

    ; 内部構造体(SUBPROC_DATA構造体)
    ; CallWindowProc関数アドレス取得
    dll_getfunc procdata.0, "CallWindowProcA", D_USER

    ; 現在のウィンドウプロシージャアドレス取得
    mref bmscr, 67
    pm = bmscr.13, -4
    setwndlong pm, 1
    procdata.1 = stat           ; ウィンドウプロシージャアドレス

    ; ドロップ情報ハンドルを格納する変数のアドレス
    getptr procdata.2, hdrop

    ; SUBPROC_DATA構造体アドレスをマシン語コードに埋め込む
    getptr p, procdata
    memcpy fncode, p, 4, 7

    ; ウィンドウのサブクラス化
    pm = bmscr.13, -4, pfn
    setwndlong pm

    ; ドロップファイルを受け入れるように設定
    pm = bmscr.13, 1
    dllproc "DragAcceptFiles", pm, 2, D_SHELL
    mes "このウィンドウにファイルをドロップして下さい。"

*mainloop
    wait 10
    if hdrop {
        ; ファイルの数の取得
        pm = hdrop, -1, 0, 0              ; 第2パラメータを -1 に
        dllproc "DragQueryFileA", pm, 4, D_SHELL
        filenum = stat                    ; ドロップされたファイル数

        sdim filename, 260                ; ファイル名を格納するバッファ
        getptr p_filename, filename       ; バッファのアドレス

        repeat filenum                    ; ドロップされたファイルの数だけ実行
            ; ファイル名の取得
            pm = hdrop, cnt, p_filename, 260
            dllproc "DragQueryFileA", pm, 4, D_SHELL
            mes filename                  ; ファイル名を表示
        loop

        ; ドロップファイル用のメモリを解放
        pm = hdrop
        dllproc "DragFinish", pm, 1, D_SHELL

        ; 変数を必ず 0 にする
        hdrop = 0
    }
    goto *mainloop