Tutorial 9: Child Window Controls

このチュートリアルではとても重要な入出力デバイスである、チャイルドウィンドウコントロールについて。
   ソース      リソース     実行結果  

Theory:

Windowsにはすぐに使用できるウィンドウクラスをいくつか既に定義している。 ほとんどの場合、それらはダイアログボックスコンポーネントとして使用するので、 よく、チャイルドウィンドウコントロール(以下「チャイコン」)と呼ばれている。 そのチャイコンは、自分に送られてきたマウスやキーボードメッセージを処理し、 それらのチャイコンの状態が変化すれば、親ウィンドウに通知がいくようになっており、 我らプログラマーにとっては、非常に便利なものなので、できるだけ使用するべきだ。 実際にはダイアログボックスに貼り付けることになっているのだが、 このチュートリアルではとりあえず単なるサンプルとして、 標準のウィンドウにチャイコンを貼り付けてみる。

チャイコンを使用するためには、CreateWindow関数もしくはCreateWindowEx関数を使用してまずは作成しないといけない。 もうすでにそれらのチャイコンはWindowsが登録してあるので、あなたが改めて登録する必要は無い。 クラス名は、Windowsに既に定義されているクラス名を絶対に使用しなければならない。 つまり、ボタンを作りたければ、CreateWindowEx関数が必要とするクラス名として、"button"と記述しなければならない。 他のパラメータは、親ウィンドウのハンドルとチャイコンのコントロールIDだ。 コントロールIDは一意なIDでなければならず、数あるコントロールの中から識別できるようにしなければならない。

チャイコンを作成したら、そのチャイコンの状態が変化したら親ウィンドウにメッセージが飛んでくるようになる。 通常、チャイルドウィンドウを作成するタイミングでWM_CREATEメッセージが送られてくる。 また、チャイルドウィンドウは親ウィンドウにWM_COMMANDメッセージを送るのだが、 そのとき、wParamの下位ワードには自身のコントロールIDが、上位ワードには告知コード(notification code のこと イイ訳が思いつかなかった・・・)が入り、 lParamはウィンドウハンドルとなっている。 各チャイコンは、それぞれ異なる告知コードとなっているのだが、詳細はWin32APIリファレンスを参考のこと。

親ウィンドウはSendMessage関数によりチャイルドウィンドウにコマンドを送ることもできる。 SendMessage関数は指定したウィンドウハンドルを持つウィンドウに対してメッセージを送信するとともに、 wParam、lParamを介して何らかの値を渡すこともできる。 この関数は非常に便利な関数で、ウィンドウハンドルさえ知っていればどんなウィンドウにもメッセージを送ることができる。

結局親ウィンドウは、チャイルドウィンドウを作成し終わったら、 そのチャイルドウィンドウの告知コードを受け取るために、WM_COMMANDメッセージを処理しなければならない。

Example:

この例ではエディットコントロールとプッシュボタンのあるウィンドウを作成する。 ボタンをクリックしたら、エディットボックスに記述した文字列をメッセージボックスで表示する。 メニューもあり、4つのメニューアイテムがある。

  1. Say Hello
    エディットボックスに文字列を表示する
  2. Clear Edit Box
    エディットボックスの文字列をクリアする
  3. Get Text
    エディットボックスにある文字列をメッセージボックスで表示する
  4. Exit
    終了する

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

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

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MenuName db "FirstMenu",0
ButtonClassName db "button",0
ButtonText db "My First Button",0
EditClassName db "edit",0
TestString db "Wow! I'm in an edit box now",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwndButton HWND ?
hwndEdit HWND ?
buffer db 512 dup(?)                   ; buffer to store the text retrieved from the edit box

.const
ButtonID equ 1                               ; The control ID of the button control
EditID equ 2                                   ; The control ID of the edit control
IDM_HELLO equ 1
IDM_CLEAR equ 2
IDM_GETTEXT equ 3
IDM_EXIT equ 4

.code
start:
   invoke GetModuleHandle, NULL
   mov   hInstance,eax
   invoke GetCommandLine
    mov CommandLine,eax
   invoke WinMain, hInstance,NULL,CommandLine, 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_BTNFACE+1
   mov  wc.lpszMenuName,OFFSET MenuName
   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_OVERLAPPEDWINDOW,\
                       CW_USEDEFAULT, CW_USEDEFAULT,\
                       300,200,NULL,NULL, hInst,NULL
   mov  hwnd,eax
   invoke ShowWindow, hwnd,SW_SHOWNORMAL
   invoke UpdateWindow, hwnd
   .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_DESTROY
       invoke PostQuitMessage,NULL
   .ELSEIF uMsg==WM_CREATE
       invoke CreateWindowEx,WS_EX_CLIENTEDGE, ADDR EditClassName,NULL,\
                       WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT or\
                       ES_AUTOHSCROLL,\
                       50,35,200,25,hWnd,8,hInstance,NULL
       mov hwndEdit,eax
       invoke SetFocus, hwndEdit
       invoke CreateWindowEx,NULL, ADDR ButtonClassName,ADDR ButtonText,\
                       WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
                       75,70,140,25,hWnd,ButtonID,hInstance,NULL
       mov hwndButton,eax
   .ELSEIF uMsg==WM_COMMAND
       mov eax,wParam
       .IF lParam==0
           .IF ax==IDM_HELLO
               invoke SetWindowText,hwndEdit,ADDR TestString
           .ELSEIF ax==IDM_CLEAR
               invoke SetWindowText,hwndEdit,NULL
           .ELSEIF ax==IDM_GETTEXT
               invoke GetWindowText,hwndEdit,ADDR buffer,512
               invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
           .ELSE
               invoke DestroyWindow,hWnd
           .ENDIF
       .ELSE
           .IF ax==ButtonID
               shr eax,16
               .IF ax==BN_CLICKED
                   invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0
               .ENDIF
           .ENDIF
       .ENDIF
   .ELSE
       invoke DefWindowProc,hWnd,uMsg,wParam,lParam
       ret
   .ENDIF
    xor   eax,eax
   ret
WndProc endp
end start

Analysis:

では詳しく見ていこう。

.ELSEIF uMsg==WM_CREATE
    invoke CreateWindowEx,WS_EX_CLIENTEDGE, \
                    ADDR EditClassName,NULL,\
                    WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT\
                    or ES_AUTOHSCROLL,\
                    50,35,200,25,hWnd,EditID,hInstance,NULL
    mov hwndEdit,eax
    invoke SetFocus, hwndEdit
    invoke CreateWindowEx,NULL, ADDR ButtonClassName,\
                    ADDR ButtonText,\
                    WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
                    75,70,140,25,hWnd,ButtonID,hInstance,NULL
    mov hwndButton,eax

WM_CREATEメッセージを処理している間にコントロールが作られる。 エクストラウィンドウスタイルとして、WS_EX_CLIENTEDGE スタイルを指定してCreateWindowEx関数をCALLすると、 クライアントエリアが沈みこんだようなウィンドウになる。 それぞれのコントロール名は既に定義されたもので、"edit" はエディットコントロール、 "button" はボタンコントロールとなっている。 次に、チャイルドウィンドウのスタイルを決定するのだが、 各コントロールは通常のウィンドウスタイルに加えてエクストラスタイルも指定する。 例えば、ボタンスタイルだったら、"button style" の略となる、"BS_???" というスタイル、 エディットスタイルなら、"edit style" の略となる "ES_???" というスタイルを指定できる。 詳しくは、Win32APIリファレンスを参照しよう。 ただ、気をつけなければならないのは、メニューハンドルの替わりにコントロールIDを使用しなければならないことだ。 このことは、チャイコンにメニューはないので、何の害もなさない。

各コントロールを作成し終えたら、それぞれのハンドルを保存してし、 SetFocus関数をCALLして、エディットボックスにフォーカスを移動させ、 ユーザがすぐに文字列を入力させるようにしておく。
次が非常に興味深いところになる。 全てのチャイコンはWM_COMMANDメッセージを親ウィンドウに送信するところだ。

.ELSEIF uMsg==WM_COMMAND
    mov eax,wParam
    .IF lParam==0

メニューもその状態が変化したらWM_COMMANDメッセージを送信することを思い出そう。 そうしたら、次に、どうやってメニューからのWM_COMMANDか、コントロールからのWM_COMMANDかを見分ければいいのかを疑問に思うだろう。 その答えは以下の表にある。

  Low word of wParam High word of wParam lParam
Menu Menu ID 0 0
Control Control ID Notification code Child Window Handle

lParamを注意深く見てみよう。もし0なら、そのときのWM_COMMANDメッセージはメニューからのものだ。 メニューIDとコントロールIDが同一なものかもしれないし、告知コードが0かもしれないから、 wParamはメニューかコントロールを見分ける際に使用できない。

.IF ax==IDM_HELLO
    invoke SetWindowText,hwndEdit,ADDR TestString
.ELSEIF ax==IDM_CLEAR
    invoke SetWindowText,hwndEdit,NULL
.ELSEIF ax==IDM_GETTEXT
    invoke GetWindowText,hwndEdit,ADDR buffer,512
    invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK

SetWindowText関数をCALLして、エディットボックスに文字列を記述する。 記述する文字列を空にすれば、エディットボックス内の文字をクリアできる。 SetWindowText関数はよく使われるAPI関数で、ウィンドウのキャプションや、ボタンの文字列を変更することもできる。
エディットボックス内の文字列を取得する場合は、GetWindowText関数を使用する。

.IF ax==ButtonID
    shr eax,16
    .IF ax==BN_CLICKED
        invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0
    .ENDIF
.ENDIF

上のコードはユーザがボタンを押したときの条件文だ。 まず、wParamの下位ワードがコントロールIDかどうかをチェックする。 もしそうなら、今度はwParamの上位ワードボタンが、クリックされたときに送られるBN_CLICKEDという告知コードかどうかをチェックする。

告知コードがBN_CLICKEDだった時が興味深いところだ。 エディットボックスから文字列を取得し、メッセージボックスにして表示したいわけだが、 既に .ELSEIF ax==IDM_GETTEXT の箇所でそのコードを書いているので、 ここでも全く同じ処理を記述すればそれですむのだが、それではちょっとアレだ。 なので、どうにかして同じ処理を記述しないですむような方法を考える。 簡単に考えれば、.ELSEIF ax==IDM_GETTEXT という条件文に辿り着けばいいので、 どうにかして wParamの下位ワードの値が IDM_GETTEXT となるWM_COMMANDメッセージをこのウィンドウプロシージャに送信できればいいことになる。 それを実現してくれるのが、SendMessage関数だ。 この関数はどんなウィンドウにでもあらゆるメッセージを送信し、wParam、lParamに様々な値をSETできる。 よって、同じコードを書くことなく、SendMessage関数をCALLすればよいことになる。 こうすれば、メニューから"Get Text"メニューアイテムを選択したことと同じ効果が得られる。 ユーザからは同じに見えるし、事実ウィンドウプロシージャ内でも全く同じ処理を行っていることになっている。
このようなテクニックはコードの保守性を高めるために積極的にしようすべきだ。

最後になるが、とても重要なことだが、メッセージループ内でTraslateMessage関数をCALLするのを忘れてはいけない。 というのは、エディットボックスになんらかの文字列をタイプするはずなので、 生のキーコードを文字コードに変換しないといけない。 もしこの関数を忘れてしまうと、エディットボックスに何も入力できなくなるだろう。


[戻る]