Tutorial 22: SuperClassing

このチュートリアルではスーパークラス化が何か、そしてその使い方について学ぶ。 またウィンドウのタブコントロールについても紹介する。
 ソース   実行結果 

Theory:

長くプログラムをしていると、自分の用途に適したコントロールがほんの少しだけ違うものが複数必要になるということがきっとあるはずだ。 例えば、数字だけしか受け付けない10個のエディットコントロールが必要になる、といったものだ。 そのためにはいくつかの方法がある。

1番目の方法はあまりにもめんどくさいやり方だ。 自分自身で全てのエディットコントロールの機能を実装しなければならないので、割に合わない。 2つ目の方法は、まだマシだが、それでも結構しんどい。 サブクラス化するコントロールが少なければ、これでもいいのだが、 1ダースのコントロールやそれ以上の数になるととてもやってられない。 スーパークラス化はこういうときに非常に役に立つテクニックだ。

スーパークラス化は特定のウィンドウクラスを「支配する」ための方法である。 「支配する」ことにより、目的に沿ったウィンドウクラスの属性を変更し、コントロールを作成する。

スーパークラス化の手順は以下の通りだ。

  1. GetClassInfoEx関数をCALLしてスーパークラス化したいウィンドウクラスの情報を取得する。GetClassInfoEx関数は、成功すれば、対象のウィンドウクラスに関する情報をセットしたWNDCLASSEX構造体へのポインタを返す。
  2. WNDCLASSEXメンバを変更する。ただ、変更しなければならないのは2つのメンバである。
    • hInstance
      プログラムのインスタンスハンドルを指定する
    • lpszClassName
      新しいクラス名へのポインタを指定する

    lpfnWndProcメンバは変更する必要がないのだが、ほとんどの場合、変更する必要がある。ただし、CallWindowProc関数により元々のウィンドウプロシージャをCALLしたければ、変更する前の元々のlpfnWndProcの値を保管しておかなければならない。
  3. WNDCLASSEX構造体を登録し、新しいウィンドウクラスを取得する。
  4. 新しいウィンドウクラスからウィンドウを作成する。

スーパークラス化は同じ特性のコントロールを複数作らなければならないときに、サブクラス化より良い方法である。

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WM_SUPERCLASS equ WM_USER+5
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "SuperclassWinClass",0
AppName   db "Superclassing Demo",0
EditClass db "EDIT",0
OurClass db "SUPEREDITCLASS",0
Message db "You pressed the Enter key in the text box!",0

.data?
hInstance dd ?
hwndEdit dd 6 dup(?)
OldWndProc dd ?

.code
start:
   invoke GetModuleHandle, NULL
   mov   hInstance,eax
   invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
   invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
   LOCAL wc:WNDCLASSEX
   LOCAL msg:MSG
   LOCAL hwnd:HWND

   mov wc.cbSize,SIZEOF WNDCLASSEX
   mov wc.style, CS_HREDRAW or CS_VREDRAW
   mov wc.lpfnWndProc, OFFSET WndProc
   mov wc.cbClsExtra,NULL
   mov wc.cbWndExtra,NULL
   push hInst
   pop wc.hInstance
   mov wc.hbrBackground,COLOR_APPWORKSPACE
   mov wc.lpszMenuName,NULL
   mov wc.lpszClassName,OFFSET ClassName
   invoke LoadIcon,NULL,IDI_APPLICATION
   mov wc.hIcon,eax
   mov wc.hIconSm,eax
   invoke LoadCursor,NULL,IDC_ARROW
   mov wc.hCursor,eax
   invoke RegisterClassEx, addr wc
   invoke CreateWindowEx,WS_EX_CLIENTEDGE+WS_EX_CONTROLPARENT,ADDR ClassName,ADDR AppName,\
       WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
          CW_USEDEFAULT,350,220,NULL,NULL,\
          hInst,NULL
   mov hwnd,eax

   .while TRUE
       invoke GetMessage, ADDR msg,NULL,0,0
       .BREAK .IF (!eax)
       invoke TranslateMessage, ADDR msg
       invoke DispatchMessage, ADDR msg
   .endw
    mov eax,msg.wParam
   ret
WinMain endp

WndProc proc uses ebx edi hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
   LOCAL wc:WNDCLASSEX
   .if uMsg==WM_CREATE
       mov wc.cbSize,sizeof WNDCLASSEX
       invoke GetClassInfoEx,NULL,addr EditClass,addr wc
       push wc.lpfnWndProc
       pop OldWndProc
       mov wc.lpfnWndProc, OFFSET EditWndProc
       push hInstance
       pop wc.hInstance
       mov wc.lpszClassName,OFFSET OurClass
       invoke RegisterClassEx, addr wc
       xor ebx,ebx
       mov edi,20
       .while ebx<6
           invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\
                WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
                edi,300,25,hWnd,ebx,\
                hInstance,NULL
           mov dword ptr [hwndEdit+4*ebx],eax
           add edi,25
           inc ebx
       .endw
       invoke SetFocus,hwndEdit
   .elseif uMsg==WM_DESTROY
       invoke PostQuitMessage,NULL
   .else
       invoke DefWindowProc,hWnd,uMsg,wParam,lParam
       ret
   .endif
   xor eax,eax
   ret
WndProc endp

EditWndProc PROC hEdit:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
   .if uMsg==WM_CHAR
       mov eax,wParam
       .if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
           .if al>="a" && al<="f"
              sub al,20h
           .endif
           invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
           ret
       .endif
   .elseif uMsg==WM_KEYDOWN
       mov eax,wParam
       .if al==VK_RETURN
           invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
           invoke SetFocus,hEdit
       .elseif al==VK_TAB
           invoke GetKeyState,VK_SHIFT
           test eax,80000000
           .if ZERO?
               invoke GetWindow,hEdit,GW_HWNDNEXT
               .if eax==NULL
                   invoke GetWindow,hEdit,GW_HWNDFIRST
               .endif
           .else
               invoke GetWindow,hEdit,GW_HWNDPREV
               .if eax==NULL
                   invoke GetWindow,hEdit,GW_HWNDLAST
               .endif
           .endif
           invoke SetFocus,eax
           xor eax,eax
           ret
       .else
           invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
           ret
       .endif
   .else
       invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
       ret
   .endif
   xor eax,eax
   ret
EditWndProc endp
end start

Analysis:

このプログラムはクライアントエリアに6つの「カスタマイズされた」エディットコントロール付きのシンプルなウィンドウを作成する。 エディットコントロール群は16進数文字列しか受け付けない。 実際には、サブクラス化の例で使用したものをスーパークラス向けに変更したものだ。 プログラムは普通にスタートするが、興味深いところは、メインウィンドウが作成されるところだ。

.if uMsg==WM_CREATE
    mov wc.cbSize,sizeof WNDCLASSEX
    invoke GetClassInfoEx,NULL,addr EditClass,addr wc

まずはじめに、スーパークラス化したいクラスのデータでWNDCLASSEX構造体をセットしなければならない。 このケースでは、EDITクラスになる。 GetClassInfoEx関数をCALLする前にWNDCLASSEXのメンバ変数cbSizeをセットすることを忘れてはならない。 さもないと、WNDCLASSEX構造体のデータを適切にセットできない。 GetClassInfoEx関数から返ってきたら、変数wcには新しいウィンドウクラスを作成するのに必要な情報がセットされている。

push wc.lpfnWndProc
pop OldWndProc
mov wc.lpfnWndProc, OFFSET EditWndProc
push hInstance
pop wc.hInstance
mov wc.lpszClassName,OFFSET OurClass

wcのメンバのうち変更しなければならないものがある。最初のはウィンドウプロシージャへのポインタだ。 元々のウィンドウプロシージャと連携する必要があるので、元々のウィンドウプロシージャへのポインタを保存しておき、 CallWindowProc関数により元々のウィンドウプロシージャをCALLすることができる。 このテクニックはSetWindowLong関数をCALLせずとも直接WNDCLASSEX構造体を修正することを除いて、 サブクラス化のときに使用したテクニックと同じものだ。 次の2つのメンバ hInstance と lpsClassNameは必ず変更しなければならない。 でないと、新しいウィンドウクラスを登録できないだろう。 なので、自分のプログラムのhInstanceと、新しいクラス名に変更することになる。

invoke RegisterClassEx, addr wc

準備が整ったので、新しいウィンドウクラスを登録し、新しいクラスを取得する。

xor ebx,ebx
mov edi,20
.while ebx<6
    invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\
         WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
         edi,300,25,hWnd,ebx,\
         hInstance,NULL
    mov dword ptr [hwndEdit+4*ebx],eax
    add edi,25
    inc ebx
.endw
invoke SetFocus,hwndEdit

クラスを登録し、そのクラスを基にウィンドウを作成する。 上のコードは、ebxレジスタを作成するウィンドウの数を数えるために使用している。 ediレジスタはウィンドウのy座標として使用しており、 ウィンドウが作成されたときに、ハンドルは配列に格納されていく。 全てのウィンドウが作成されたら、一番最初のウィンドウにフォーカスをセットする。

この時点で、16進数文字列を受け付けるエディットコントロールを6つ作成し、 それらのウィンドウプロシージャはユーザの入力をフィルタリングしている。 実際、サブクラス化の例と同じウィンドウプロシージャで、サブクラス化するより多くの労力を払うことはない。

この例では、さらに刺激的にするために、タブコントロールを導入している。 通常、ダイアログボックスにコントロールを配置するとき、 ダイアログボックスマネージャはユーザがタブキーを押せば、次のコントロールへ、シフト+タブを押せば、前のコントロールへとフォーカスを移してくれる。 悲しいことに、そのような機能は単純なウィンドウにあるコントロールでは利用できないのである。 しかし、サブクラス化しタブキーが押されたときの処理を自前で書けば実現可能である。 この例では、スーパークラス化しているので、サブクラス化する必要は無く、 それらのコントロールを統治する「中央コントロールマネージャ」を提供すればよい。

.elseif al==VK_TAB
    invoke GetKeyState,VK_SHIFT
    test eax,80000000
    .if ZERO?
        invoke GetWindow,hEdit,GW_HWNDNEXT
        .if eax==NULL
            invoke GetWindow,hEdit,GW_HWNDFIRST
        .endif
    .else
        invoke GetWindow,hEdit,GW_HWNDPREV
        .if eax==NULL
            invoke GetWindow,hEdit,GW_HWNDLAST
        .endif
    .endif
    invoke SetFocus,eax
    xor eax,eax
    ret

上記のコードは、EditWndProcプロシージャからのもので、 ユーザがタブキーを押したかどうかをチェックし、もし押したらGetKeyState関数でシフトキーも押されているかどうかチェックする。 GetKeyState関数は指定されたキーが押されているかどうかを判定して結果をeaxレジスタに返す。 もし押されていれば、eaxレジスタの上位ビットがセットされており、押されてなければ、上位ビットはクリアされている。 なので、test命令を使用して、80000000hと比較している。もし上位ビットがセットされていれば、シフト+タブが押されており、 その場合は別の処理が必要となる。

ユーザがタブキーだけを押した場合、GetWindow関数で次のコントロールのハンドルを取得する。 その際、今のエディットコントロールhEditから次のウィンドウを取得するように、GW_HWNDNEXTフラグを使用してGetWindow関数CALLする。 この関数がNULLを返せば、それはつまり今のコントロールが一番最後のコントロールだと解釈し、 GetWindow関数をGW_HWNDFIRSTフラグを指定して最初のコントロールを取得する。 シフト+タブの場合は、タブキーが押されたときと逆に動作するので、同じようなものである。


[戻る]