Tutorial 18: Common Controls

このチュートリアルではコモンコントロールの使い方について説明するが、 クリックリファレンスになると思うが、ご容赦いただきたい。
 ソース   実行結果 

Theory:

Windows95はWindows3.1Xのユーザインターフェイスから拡張したものとなっており、 よりGUI表現が豊かなものとなっている。 それらのうち、ステータスバーやツールバーといったものは、 Windows95が出回る前によく使用されていたのだが、 それらはプログラマが自前で書かなければならなかった。 ところが今は、Windows95、NTに既に含まれているので、今回はそれらについて学んでいこう。

以下のものが新しいコントロールだ。

たくさんあるので、全てをメモリにロードするのはリソースの無駄使いである。 リッチエディットコントロール以外は comctl32.dll に格納されており、 コントロールを使いたいときにアプリケーションがロードしてくるのである。 リッチエディットコントロールは richedXX.dll に格納されている。 それは、リッチエディットコントロールが非常に複雑なため、 コードが大きいからである。

コントロールが格納されているcomctl32.dllをロードするには、InitCommonControls関数の呼び出しを記述すればよい。 InitCommonControls関数はcomctl32.dllにある関数で、その関数への参照がプログラム中のどこかにありさえすれば、 PEローダがプログラム実行時にcomctl32.dllをロードしてくれる。 ただし、この関数を実行する必要は無く、ただ単にソースコード中のどこかにあればよい。 というのも、この関数は何もせず、「ret命令」だけしかない。 唯一の目的は、インポートセクションにcomctl32.dllへの参照を埋め込むことで、 その結果、PEローダはプログラムがロードされたら常にcomctl32.dllをロードするようになる。 実際に意味のある関数はDLLがロードされたときに、 全てのコモンコントロールクラスを登録するDLLのエントリポイント関数である。 コモンコントロールはコモンコントロールクラスをベースにして作成されており、 これはちょうど、チャイルドウィンドウコントロールのエディットボックスや、 リストボックスなどと同様である。

リッチエディットは今までとは全く違っており、もし使いたければ、 LoadLibrary関数をコールして明示的にロードしなければならず、 アンロードもFreeLibrary関数をCALLして自分で行わなければならない。

では、どうやったら作成できるかを説明しよう。 まず、リソースエディタでダイアログボックスにそれらを貼り付けたり、 自分でリソーススクリプトを作成する。 ほとんどのコモンコントロールは、 コントロールクラス名を引数にしてCreateWindowEx関数かCreateWindow関数をCALLすることによって作成する。 中には、独自の関数を呼ばなければならないものもあるが、 それらは単に、CreateWindowEx関数を使いやすくラップしたものである。 それらの関数を以下にリストアップする。

コモンコントロールを作成するために、クラス名を知っておかなければならない。 対応表は以下のとおりだ。

Class NameCommon Control
ToolbarWindow32Toolbar
tooltips_class32Tooltip
msctls_statusbar32Status bar
SysTreeView32Tree view
SysListView32List view
SysAnimate32Animation
SysHeader32Header
msctls_hotkey32Hot-key
msctls_progress32Progress bar
RICHEDITRich edit
msctls_updown32Up-down
SysTabControl32Tab

プロパティシートとプロパティページ、イメージリストは独自の作成関数があり、 ドラッグリストコントロールはリストボックスを拡張したものなので、専用のクラス名はない。 この対応表はVC++のリソースエディタで作成したリソーススクリプトで確認したものである。 BorlandやCharles Petzold'sとはクラス名が異なっているかもしれないが、この表は正確なものである。

それらのコモンコントロールはWS_CHILDなどの一般的なウィンドウスタイルを使用できるが、 ツリービューコントロール専用のTVS_XXXXとか、リストビューコントロール専用のLVS_XXXXなどといった独自のスタイルも指定できる。 Win32APIリファレンスが非常に参考になるだろう。

では次に、コモンコントロールと親ウィンドウとの間でコミュニケーションをとる方法を説明しよう。 チャイルドウィンドウコントロールと違って、WM_COMMANDメッセージでは通信できない。 その代わり、コモンコントロールに何かイベントが発生すれば、 WM_NOTIFYメッセージを送信することになっている。 親ウィンドウは、コモンコントロールにメッセージを送って制御するわけだが、 個々のコモンコントロールにつき、独自のメッセージがあるので、詳しく知りたければ、 Win32APIリファレンスを参照せよ。

次のサンプルでは、プログレスバーとステータスバーを作成している。

Sample code:

.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

.const
IDC_PROGRESS equ 1           ; control IDs
IDC_STATUS equ 2
IDC_TIMER equ 3

.data
ClassName db "CommonControlWinClass",0
AppName   db "Common Control Demo",0
ProgressClass db "msctls_progress32",0      ; the class name of the progress bar
Message db "Finished!",0
TimerID dd 0

.data?
hInstance HINSTANCE ?
hwndProgress dd ?
hwndStatus dd ?
CurrentStep dd ?
.code
start:
   invoke GetModuleHandle, NULL
   mov   hInstance,eax
   invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
   invoke ExitProcess,eax
   invoke InitCommonControls

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,CW_USEDEFAULT,CW_USEDEFAULT,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,NULL,ADDR ProgressClass,NULL,\
           WS_CHILD+WS_VISIBLE,100,\
           200,300,20,hWnd,IDC_PROGRESS,\
           hInstance,NULL
       mov hwndProgress,eax
       mov eax,1000              ; the lParam of PBM_SETRANGE message contains the range
       mov CurrentStep,eax
       shl eax,16                  ; the high range is in the high word
       invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax
       invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0
       invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
       mov hwndStatus,eax
       invoke SetTimer,hWnd,IDC_TIMER,100,NULL       ; create a timer
       mov TimerID,eax
   .elseif uMsg==WM_DESTROY
       invoke PostQuitMessage,NULL
       .if TimerID!=0
           invoke KillTimer,hWnd,TimerID
       .endif
   .elseif uMsg==WM_TIMER       ; when a timer event occurs
       invoke SendMessage,hwndProgress,PBM_STEPIT,0,0   ; step up the progress in the progress bar
       sub CurrentStep,10
       .if CurrentStep==0
           invoke KillTimer,hWnd,TimerID
           mov TimerID,0
           invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message
           invoke MessageBox,hWnd,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
           invoke SendMessage,hwndStatus,SB_SETTEXT,0,0
           invoke SendMessage,hwndProgress,PBM_SETPOS,0,0
       .endif
   .else
       invoke DefWindowProc,hWnd,uMsg,wParam,lParam
       ret
   .endif
   xor eax,eax
   ret
WndProc endp
end start

Analysis:

invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls

意図的にExitProcess関数の後ろでInitCommonControls関数をCALLしている。 これは単に、インポートセクションにcomctl32.dllへの参照を加えるためのもので、 見てわかるとおり、たとえInitCommonControls関数が実行されなくても コモンコントロールは動作する。

.if uMsg==WM_CREATE
     invoke CreateWindowEx,NULL,ADDR ProgressClass,NULL,\
        WS_CHILD+WS_VISIBLE,100,200,300,20,hWnd,IDC_PROGRESS,hInstance,NULL
    mov hwndProgress,eax

ここでコモンコントロールを作成する。 ここでCALLするCreateWindowEx関数は親ウィンドウハンドルとしてhWndと このコントロールを識別するためのコントロールIDを引数として渡すことに注意しよう。 しかしながら、コントロールのウィンドウハンドルがあるので、 このIDは使用しない。 全てのチャイルドウィンドウコントロールはWS_CHILDスタイルになっていなければならない。

mov eax,1000
mov CurrentStep,eax
shl eax,16
invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax
invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0

プログレスバーが作成されたあとは、レンジ(幅)を設定する。 デフォルト値は0から100だ。 変更したければ、PBM_SETRANGEメッセージで変更できる。 このメッセージの時、lParamにレンジをセットするのだが、 上位ワードに最大値を、下位ワードに最小値をセットする。 PBM_SETSTEPメッセージで、ステップ(増分)も指定できる。 サンプルではこれを10にセットしている。 これにより、PBM_STEPITメッセージを送信すると、 プログレスバーのインジケータが10ずつ増加していくのである。 もちろん、インジケータを増分による相対指定だけでなく、 PBM_SETPOSメッセージにより、絶対指定も可能だ。 このメッセージを使えば、プログレスバーを思いのままに操れる。

invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
mov hwndStatus,eax
invoke SetTimer,hWnd,IDC_TIMER,100,NULL       ; create a timer
mov TimerID,eax

次に、CreateStatusWindow関数により、ステータスバーを作成する。 この関数は非常に簡単なので、言うことはない。 この例では、100ミリ秒毎にプログレスバーを更新するので、 ステータスウィンドウを作成後、タイマーを作成しないといけない。 タイマーを作成するために、SetTimer関数をCALLする。 プロトタイプは以下のとおりだ。

SetTimer PROTO hWnd:DWORD, TimerID:DWORD, TimeInterval:DWORD, lpTimerProc:DWORD

関数が成功すれば、TimerIDが返ってくるが、失敗すれば0が返る。 このため、タイマーIDを0以外の数字にしないといけない。

.elseif uMsg==WM_TIMER
    invoke SendMessage,hwndProgress,PBM_STEPIT,0,0
    sub CurrentStep,10
    .if CurrentStep==0
        invoke KillTimer,hWnd,TimerID
        mov TimerID,0
        invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message
        invoke MessageBox,hWnd,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
        invoke SendMessage,hwndStatus,SB_SETTEXT,0,0
        invoke SendMessage,hwndProgress,PBM_SETPOS,0,0
    .endif

インターバルタイムが経過すれば、タイマーはWM_TIMERメッセージを送信する。 なので、WM_TIMERセクションに実行したいコードを記述するのだが、 この例では、プログレスバーを更新し、 その結果最大値に達していないかどうかをチェックしている。 もし到達していたら、タイマーをストップし、 SB_SETTEXTメッセージを送信してステータスウィンドウに文字列をセットする。 メッセージボックスが表示され、ユーザがOKをクリックすると、 ステータスバーの文字とプログレスバーのインジケータがクリアされる。


[戻る]