Tutorial 20: Window Subclassing

このチュートリアルでは、ウィンドウサブクラス化について学ぶ。
 ソース   実行結果 

Theory:

Windowsアプリを作っていると、あなたが欲しい機能を「ある程度」備えたウィンドウは用意されているが、「完全に」合致したウィンドウは無いときがある。 例えば、入力してほしくない文字をフィルタリングしてくれるような特別版のエディットコントロールがほしくなったりしたことはないだろうか?
単刀直入に言うと、ウィンドウを自作すればいいのだが、非常にハードな仕事になるので、 そういう時はウィンドウサブクラス化を行えばよい。 サブクラス化というのは、サブクラス化されたウィンドウを「支配」できるようになる。

簡単にわかるような例を出そう。 16進数で表される文字しか受け付けないテキストボックスがほしかったとしよう。 単純にエディットコントロールを使う場合、ユーザがテキストボックスに16進数以外の文字を入力したらなんであれ拒否できない。 つまり、ユーザが"zb+q*"と入力したら、ユーザが入力したあと、何が入力されたかを解析しないといけない。 これはプロのやることではない。 本当は、そもそもユーザが入力するときにチェックを行う必要がある。

これをやろうと思うと、ユーザが入力するときに、WindwosはエディットコントロールのウィンドウプロシージャにWM_CHARメッセージを送信するのだが、 このウィンドウプロシージャはWindows内にあるので、我々は手出しできない。 しかし、自分で作成したウィンドウプロシージャにメッセージをリダイレクトするこができる。 なので、Windowsがエディットコントロールに送信するメッセージを自分のウィンドウプロシージャが先に横取りできる。 もし、自分のウィンドウプロシージャがそのメッセージに対してアクションをとろうと思えばできる。 でも、何もしたくなければ、デフォルトのウィンドウプロシージャにスルーすることもできる。
以下のように、自分のウィンドウプロシージャをWindowsとエディットコントロールの間に置くことができる。


 Before Subclassing   Windows ==> エディットコントロールウィンドウプロシージャ 
 After Subclassing   Windows ==> 自作ウィンドウプロシージャ ==> エディットコントロールウィンドウプロシージャ 

では今度はどうやってサブクラスを行えばいいのかの説明に移ろう。 まず、サブクラスはコントロールだけにとどまらず、どんなウィンドウに対しても適用できることを念頭におくこと。 さて、ではどうやってWindowsはエディットコントロールのウィンドウプロシージャがどこにあるのかを知るのだろう?
... 思い出してみよう。WNDCLASSEX構造体にlpfnWndProcというメンバがあったはずだ。 もしこのメンバ変数の値を自作したウィンドウプロシージャに置き換えることができれば、 Windowsは置き換えたプロシージャにメッセージを送ることになるだろう。

これをするには、SetWindowLong関数をCALLすればよい。

SetWindowLong PROTO hWnd:DWORD, nIndex:DWORD, dwNewLong:DWORD

みればわかるように、我々のやることは簡単である。エディットコントロール用のメッセージを受け付けるウィンドウプロシージャを書き、 2番目の引数にGWL_WNDPROCを、3番目の引数にそのウィンドウプロシージャをセットしてSetWindowLong関数をCALLすればよい。 関数が成功すれば、戻り値として、前にセットされていた32ビットの整数値が返される。 今回の場合、もともとのウィンドウプロシージャのアドレスである。 このアドレスはプログラムで使用するのできちんとどこかに保持していなければならない。

扱いたいメッセージがあると同時に、扱いたくないメッセージもあるだろう。 そのような扱いたくないメッセージはCallWindowProc関数をCALLすることにより、 もともとのウィンドウプロシージャに任せることができる。

CallWindowProc PROTO lpPrevWndFunc:DWORD, \
                     hWnd         :DWORD, \
                     Msg          :DWORD, \
                     wParam       :DWORD, \
                     lParam       :DWORD

Code Sample:

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

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "SubclassWinClass",0
AppName   db "Subclassing Demo",0
EditClass db "EDIT",0
Message db "You pressed Enter in the text box!",0

.data?
hInstance HINSTANCE ?
hwndEdit dd ?
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,ADDR ClassName,ADDR AppName,\
 WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
          CW_USEDEFAULT,350,200,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 hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
   .if uMsg==WM_CREATE
       invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR EditClass,NULL,\
           WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
           20,300,25,hWnd,NULL,\
           hInstance,NULL
       mov hwndEdit,eax
       invoke SetFocus,eax
       ;-----------------------------------------
       ; Subclass it!
       ;-----------------------------------------
       invoke SetWindowLong,hwndEdit,GWL_WNDPROC,addr EditWndProc
       mov OldWndProc,eax
   .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
       .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:

invoke SetWindowLong,hwndEdit,GWL_WNDPROC,addr EditWndProc
mov OldWndProc,eax

エディットコントロールを作成したら、SetWindowLong関数をCALLしてサブクラス化を行う。 これにより、もともとのウィンドウプロシージャと自作のウィンドウプロシージャのアドレスが置き換わるわけだが、 CallWindowProc関数をCALLしてもともとのウィンドウプロシージャを使用するので、 もともとのウィンドウプロシージャのアドレスを保管しておかなければならない。 また、EditWndProcはごく普通のウィンドウプロシージャだ。

.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

EditWndProc関数内では、WM_CHARメッセージに対して処理を行っている。 もし入力された文字が0-9もしくはa-fであれば、もともとのウィンドウプロシージャにそのままスルーする。 このとき、入力された文字が小文字なら0x20を加えて大文字にしている。 ここでもし、入力して欲しくない文字が入力されたら、もともとのウィンドウプロシージャにはスルーしないようにする。 これにより、ユーザが0-9かa-f以外の文字を入力してもエディットボックスには現れてこないことになる。

.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
  .else
    invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
    ret
  .end

このサンプルでは、エンターキーをトラップすることにより、サブクラス化のさらなるパワーを披露している。 EditWndProc関数ではWM_KEYDOWNメッセージが送信されたときに、VK_RETURN(リターンキーを表すキーコード)かどうかをチェックしている。 もしリターンキーが押されたら、メッセージボックスを表示させ、 エンターキーでなければ、もともとのウィンドウプロシージャにメッセージを送信する。

このように、サブクラス化により他のウィンドウを支配することができる。 是非ともこの強力なテクニックを「もの」にして欲しい。


[戻る]