Tutorial 27: Tooltip Control

今回のチュートリアルでは、ツールチップコントロールについて説明する。
 メインソース   ヘッダファイル   リソース   実行結果 

Theory:

ツールチップとは小さな矩形ウィンドウのことで、マウスポインタが指定した領域に重なったときに表示される。 ツールチップウィンドウには文字列も表示できる。 この点で言えば、ツールチップはステータスウィンドウと同じような役割なのだが、 ユーザがマウスをクリックしたり、他の場所へマウスポインタを移動させると消えるようになる。 このツールチップはきっとほとんどのアプリケーションでツールバーのボタンで使用しているので、きっとよく使ったことがあるだろう。 それらのツールチップはツールバーコントロールにより提供される便利なしろものだ。 もし他のウィンドウやコントロールでツールチップを使いたければ、自分独自のツールチップコントロールを作成する必要がある。

では、ツールチップがどんなものかわかったところで、それの使い方と作り方の説明に移ろう。 概要は以下のようになっている。

  1. CreateWindowEx関数でツールチップコントロールを作成
  2. マウスポンタの移動を監視する領域を定義する
  3. ツールチップコントロールの領域を指定する
  4. その指定した領域におけるマウスメッセージとツールチップコントロールをマッピングさせる(この部分はメッセージのマッピングの仕方のよってはもっと早くなるかもしれない)

では詳細に移ろう。

●ツールチップの作成

ツールチップコントロールはコモンコントロールの一つなので、MASMに自分のプログラムがcomctl32.dllと暗黙的にリンクさせるためにInitCommonControls関数をCALLする必要がある。 その後、CreateWindowEx関数をCALLしてツールチップコントロールを作成する。 典型的なコードは以下のようなものだ。

.data
TooltipClassName db "Tooltips_class32",0
.code
.....
invoke InitCommonControls
invoke CreateWindowEx, NULL, addr TooltipClassName, NULL, TIS_ALWAYSTIP,
       CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
       NULL, NULL, hInstance, NULL

ウィンドウスタイルTIS_ALWAYSTIPに注目しよう。このスタイルを指定することにより、 マウスポインタが指定した領域に重なれば、ウィンドウの状態によらずツールチップ表示されるようになる。 簡単に言うと、このフラグを使用すれば、ツールチップコントロールを登録した領域にマウスが重なれば、 例えウィンドウが非アクティブな状態でもツールチップウィンドウが表示される。

CreateWindowEx関数の引数には他にも注意点があり、WS_POPUPWS_EX_TOOLWINDOWを指定しなくてもよい。 というのも、ツールチップコントロールのウィンドウプロシージャにより自動的につけられるからだ。 また、座標値や幅、高さも指定する必要は無い。ツールチップコントロールは表示される際に自動的に調節して表示されるようになっているからである。 なので、4つの引数はCW_USEDEFAULTでよくなっている。 残りの引数は特に注意することはない。

●toolの指定

以上でツールチップコントロールは作成されるが、すぐには表示されない。 我々の期待としては、ある領域にマウスが重なったときにツールチップウィンドウが表示されて欲しい。 そのような領域を「tool」と呼ぶことにする。toolはクライアントウィンドウの矩形領域で、ツールチップコントロールはマウスポインタを監視している。 もしマウスポインタがtoolに重なれば、ツールチップウィンドウが表示される。 その矩形領域はクライアント領域全体、もしくはその一部、という指定ができる。

なので、toolには2つのタイプがあることになる。 1つはウィンドウそのものとして、もう1つはあるウィンドウのクライアントエリアの矩形領域として実装される。 クライアント領域全体をtool領域とするのは、ほとんどの場合ボタンやエディットコントロールなどのようなコントロールと一緒に使用するときである。 その場合、toolの座標値、大きさを指定する必要はない。 ウィンドウのクライアントエリア全体と認識されるからだ。
チャイルドウィンドウを使用せず、クライアントエリアをいくつかの領域に分割したいときには、 クライアントエリアの矩形領域として実装することになる。 このタイプの場合、toolの座標値と大きさを指定しなければならない。

toolを指定する際に必要となるTOOLINFO構造体は以下のように定義されている。

TOOLINFO STRUCT
 cbSize            DWORD     ?
 uFlags            DWORD     ?
 hWnd              DWORD     ?
 uId               DWORD     ?
 rect              RECT     <>
 hInst             DWORD     ?
 lpszText          DWORD     ?
 lParam            LPARAM    ?
TOOLINFO ENDS

メンバ 説明
cbSize TOOLINFO構造体のサイズで、必ずセットしなければならない。Windowsにはこの値が適切にせっとされているかどうかはわからないので、エラーは出ないが、奇怪な現象にさいなまれることになる。
uFlags toolの性質を決めるフラグ。以下のフラグを組み合わせて使用する。
  • TTF_IDISHWND (= "IDはhWnd")
    このフラグにより、ウィンドウのクライアント領域全体をtoolとして使用することを意味する(上の説明の最初に説明したタイプ)。もしこのフラグを指定した場合、必ずこの構造体のuIdメンバにウィンドウハンドルを指定しなければならない。このフラグを使用しないのなら、2番目のタイプを使用することを意味し、つまりクライアントエリアの矩形領域としてtoolを使用する。この場合、rect構造体に矩形領域をセットしなければならない。
  • TTF_CENTERTIP
    通常、ツールチップウィンドウはマウスポインタの右下に表示されるが、このフラグを指定すれば、ツールチップウィンドウはマウスポインタの位置に関係なくtoolの真下に表示される。
  • TTF_RTLREADING
    アラビア語やヘブライ語のシステムを使用する場合でなければこのフラグを忘れてもよい。これは文字の方向が右から左に並ぶようになる。他のシステムでは動作しない。
  • TTF_SUBCLASS
    このフラグにより、ツールチップコントロールがtoolウィンドウをサブクラス化するということを意味する。その結果、ウィンドウに送られてくるマウスメッセージを途中で捕まえることができる。このフラグは非常に便利だが、、もしこれを使用するのなら、ツールチップコントロールにマウスメッセージをリレーするというさらなる作業をしなければならない。
hWnd toolを含むウィンドウハンドル。TTF_IDISHWNDフラグを指定していれば、WindowsはuIdをウィンドウハンドルとして使用するのでこのフィールドは無視される。なので、もし以下の条件に当てはまる場合はセットする必要がある。
  • TTF_IDISHWNDフラグを使用しないとき(つまり、矩形領域toolを使用するとき)
  • lpszTextにLPSTR_TEXTCALLBACKを指定するとき。これにより、ツールチップコントロールが表示される際にどんな文字列を表示すればいいのかを要求してくるようになる。これは動的に文字列を更新するツールチップの一種で、実行時に文字列を変更したければ、lpszTextにLPSTR_TEXTCALLBACKを指定する必要がある。ツールチップコントロールはhWndフィールドのウィンドウにTTN_NEEDTEXT通知メッセージを送ることになる。
uId このフィールドは2通りの意味があり、uFlagsメンバにTTF_IDISHWNDが指定されているかどうかで変わることになる。
  • TTF_IDISHWNDフラグが指定されていなければアプリケーションにより定義されるツールIDとなる。つまり、クライアント領域の一部でしか使用しないツールなので、1つのクライアントエリア上で、いくつものツールを(重なり合うことなしに)定義することができる。ツールチップコントロールはそれらを区別しなければならないのだが、同じウィンドウ上にあるので、hWndメンバのウィンドウハンドルだけでは不可能である。
    よって、アプリケーションで使用するIDが必要になってくるのである。ちなみにこれらのIDは重複しなければどんな値でもよい。
  • TTF_IDISHWNDフラグが指定されていれば、toolとして使用するクライアントエリアを持つウィンドウのハンドルとなる。ただ、hWndフィールドがすでにあるのに、どうしてまた同じフィールドがあるのかと疑問に思うかもしれない。その答えはこうだ。
    lpszTextメンバにLPSTR_TEXTCALLBACKが指定されていれば、hWndメンバが既にセットされていることになっている。そして、ツールチップテキストを表示するウィンドウと、tool領域を含むウィンドウとは同じでは無いかもしれないのだ。
    (もちろん、1つのウィンドウで2つの役割を果たすことはできるが、これは非常に限定的なものとなっている。この場合、Microsoftにさらに自由度の高い方法が提供されている。拍手喝采)
rect ツールの領域を指定するRECT構造体。これは、hWndメンバで指定されているウィンドウのクライアントエリアの左上の座標値によって決められる。つまり、クライアントエリアの一部をツール領域としたければ、この構造体をセットしなければならない。
ツールチップコントロールは、TTF_IDISHWNDフラグが指定されていれば(つまり、クライアントエリア全域をツールとして使用する場合)、このフィールドを無視することになっている。
hInst ツールチップテキストとして使用される文字列リソースを保有しているインスタンスハンドル。lpszTextメンバに文字列リソースIDを指定していれば、ツールチップテキストには文字列リソースから取得した文字列が使用される。
これは少し混乱するかもしれないので、先に、lpszTextメンバの説明を読んでみたほうがいいかもしれない。ツールチップコントロールは、lpszTextに文字列リソースIDがセットされていなければこのフィールドを無視することになっている。
lpszText このフィールドにはいくつかの意味がある。
  • LPSTR_TEXTCALLBACKを指定すれば、ツールチップコントロールはツールチップウィンドウが文字列を表示できるように、hWndで指定されているウィンドウへTTN_NEEDTEXT通知メッセージを送信する。この方法が最も動的な方法で、表示されるたびに、ツールチップ文字列を変更することができる。
  • 文字列リソースIDを指定した場合、ツールチップウィンドウに文字列を表示する必要が出てきたときに、ツールチップコントロールはhInstメンバで指定されているインスタンスの文字列テーブルから文字列を探し出す。文字列IDは16ビット値なので、ツールチップコントロールは上位ワードが 0 になっているかをチェックする。
    移植性を考慮すればこの方法は非常に便利で、文字列リソースはリソーススクリプトで定義されるので、ソースコードを修正する必要がない。プログラムにバグを入れ込む余地なく、文字列テーブルとツールチップテキストを修正できる。
  • LPSTR_TEXTCALLBACKでもなく、上位ワードが 0 でもなければ、文字列へのポインタだと認識する。最も簡単だが、柔軟性に欠けている方法である。

ツールチップコントロールを動作させる前にこのTOOLINFO構造体に有効な値をセットする必要がある。 この構造体により、どのようなツールを作成するかを指定できる。

●tooltipコントロールに対するtoolの登録

TOOLINFO構造体をセットしたら、ツールチップコントロールに知らせる必要がある。 ツールチップコントロールはいくつものツールに対応できるので、ツールチップコントロールを複数起動する必要は無い。 ツールチップコントロールにツールを登録するため、TTM_ADDTOOLメッセージをツールチップコントロールに送信する。 wParamは使用しないが、lParamTOOLINFO構造体のポインタを指定しなければならない。

.data?
ti TOOLINFO <>
.......
.code
.......
[fill the TOOLINFO structure]
.......
invoke SendMessage, hwndTooltip, TTM_ADDTOOL, NULL, addr ti

ツールチップコントロールへの登録が成功すれば、SendMessage関数はTRUEを返し、失敗すればFALSEを返す。 登録を抹消するにはTTM_DELTOOLメッセージを送信すればよい。

●ツールチップコントロールへマウスメッセージのリレー

今までのステップが完了すれば、ツールチップコントロールはどの領域のマウスメッセージを監視すればよいか、またツールチップウィンドウにどんな文字列を表示するかを理解している。 後残っているのは、その動作の引き金となるものである。 ツールチップコントロールは、ツール領域上にマウスポインタが位置する時間を計測し、 ある指定した時間ずっとマウスポインタが位置していれば、ツールチップウィンドウを表示する、という動作を行っている。

ツールによって記述された領域が他のウィンドウだったとしたら、ツールチップコントロールはどうやってマウスメッセージを横取りすればいいのだろうか。
それには2つの方法があり、ツール領域を含むウィンドウと連携する方法と連携しない方法だ。

これまでの説明でツールチップコントロールはちゃんと動くようになるだろう。以下で、便利なメッセージリレー方法をいくつか紹介しよう。

Example:

以下のコードは2つのボタンしかない単純なダイアログボックスだ。クライアント領域は、左上、右上、左下、右下、と4つに分割されている。 それぞれ、独自のツールチップテキストがあり、ツールとして記述されている。2つのボタンにもツールチップテキストがセットされている。

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
EnumChild proto :DWORD,:DWORD
SetDlgToolArea proto :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
.const
IDD_MAINDIALOG equ 101
.data
ToolTipsClassName db "Tooltips_class32",0
MainDialogText1 db "This is the upper left area of the dialog",0
MainDialogText2 db "This is the upper right area of the dialog",0
MainDialogText3 db "This is the lower left area of the dialog",0
MainDialogText4 db "This is the lower right area of the dialog",0
.data?
hwndTool dd ?
hInstance dd ?
.code
start:
   invoke GetModuleHandle,NULL
   mov hInstance,eax
   invoke DialogBoxParam,hInstance,IDD_MAINDIALOG,NULL,addr DlgProc,NULL
   invoke ExitProcess,eax

DlgProc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
   LOCAL ti:TOOLINFO
   LOCAL id:DWORD
   LOCAL rect:RECT
   .if uMsg==WM_INITDIALOG
       invoke InitCommonControls
       invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\
           TTS_ALWAYSTIP,CW_USEDEFAULT,\
           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
           hInstance,NULL
       mov hwndTool,eax
       mov id,0
       mov ti.cbSize,sizeof TOOLINFO
       mov ti.uFlags,TTF_SUBCLASS
       push hDlg
       pop ti.hWnd
       invoke GetWindowRect,hDlg,addr rect
       invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText1,id,addr rect
       inc id
       invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText2,id,addr rect
       inc id
       invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText3,id,addr rect
       inc id
       invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText4,id,addr rect
       invoke EnumChildWindows,hDlg,addr EnumChild,addr ti
   .elseif uMsg==WM_CLOSE
       invoke EndDialog,hDlg,NULL
   .else
       mov eax,FALSE
       ret
   .endif
   mov eax,TRUE
   ret
DlgProc endp

EnumChild proc uses edi hwndChild:DWORD,lParam:DWORD
   LOCAL buffer[256]:BYTE
   mov edi,lParam
   assume edi:ptr TOOLINFO
   push hwndChild
   pop [edi].uId
   or [edi].uFlags,TTF_IDISHWND
   invoke GetWindowText,hwndChild,addr buffer,255
   lea eax,buffer
   mov [edi].lpszText,eax
   invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
   assume edi:nothing
   ret
EnumChild endp

SetDlgToolArea proc uses edi esi hDlg:DWORD,lpti:DWORD,lpText:DWORD,id:DWORD,lprect:DWORD
   mov edi,lpti
   mov esi,lprect
   assume esi:ptr RECT
   assume edi:ptr TOOLINFO
   .if id==0
       mov [edi].rect.left,0
       mov [edi].rect.top,0
       mov eax,[esi].right
       sub eax,[esi].left
       shr eax,1
       mov [edi].rect.right,eax
       mov eax,[esi].bottom
       sub eax,[esi].top
       shr eax,1
       mov [edi].rect.bottom,eax
   .elseif id==1
       mov eax,[esi].right
       sub eax,[esi].left
       shr eax,1
       inc eax
       mov [edi].rect.left,eax
       mov [edi].rect.top,0
       mov eax,[esi].right
       sub eax,[esi].left
       mov [edi].rect.right,eax
       mov eax,[esi].bottom
       sub eax,[esi].top
       mov [edi].rect.bottom,eax
   .elseif id==2
       mov [edi].rect.left,0
       mov eax,[esi].bottom
       sub eax,[esi].top
       shr eax,1
       inc eax
       mov [edi].rect.top,eax
       mov eax,[esi].right
       sub eax,[esi].left
       shr eax,1
       mov [edi].rect.right,eax
       mov eax,[esi].bottom
       sub eax,[esi].top
       mov [edi].rect.bottom,eax
   .else
       mov eax,[esi].right
       sub eax,[esi].left
       shr eax,1
       inc eax
       mov [edi].rect.left,eax
       mov eax,[esi].bottom
       sub eax,[esi].top
       shr eax,1
       inc eax
       mov [edi].rect.top,eax
       mov eax,[esi].right
       sub eax,[esi].left
       mov [edi].rect.right,eax
       mov eax,[esi].bottom
       sub eax,[esi].top
       mov [edi].rect.bottom,eax
   .endif
   push lpText
   pop [edi].lpszText
   invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti
   assume edi:nothing
   assume esi:nothing
   ret
SetDlgToolArea endp
end start

Analysis:

メインダイアログを作成した後、CreateWindowEx関数でツールチップコントロールを作成する。

invoke InitCommonControls
invoke CreateWindowEx,          \
       NULL,                    \
       ADDR ToolTipsClassName,  \
       NULL,                    \
       TTS_ALWAYSTIP,           \
       CW_USEDEFAULT,           \
       CW_USEDEFAULT,           \
       CW_USEDEFAULT,           \
       CW_USEDEFAULT,           \
       NULL,                    \
       NULL,                    \
       hInstance,               \
       NULL
mov hwndTool,eax

その後、ダイアログボックスの4隅に対してツールを定義する。

   mov id,0                              ; used as the tool ID
   mov ti.cbSize,sizeof TOOLINFO
   mov ti.uFlags,TTF_SUBCLASS            ; tell the tooltip control to subclass the dialog window.
   push hDlg
   pop ti.hWnd                           ; handle to the window that contains the tool
   invoke GetWindowRect,hDlg,addr rect   ; obtain the dimension of the client area
   invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText1,id,addr rect

TOOLINFO構造体を初期化する。ここでは、クライアントエリアを4つのツールに分割したいので、クライアントエリア領域を把握する必要がある。 そのため、GetWindowRect関数をCALLする。ただし、ツールチップコントロールへマウスメッセージをリレーしたくないため、TIF_SUBCLASSフラグを指定している。

SetDlgToolArea関数はツールの矩形領域を算出し、ツールをツールチップコントロールに登録する。 泥臭い計算なんかはしたくないので、均等に4つの領域を分けるだけでいいだろう。 そして、TOOLINFO構造体へのポインタをlParamに指定して、TTM_ADDTOOLメッセージをツールチップコントロールに送信する。

invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti

これで、4つのツールが登録される。次はボタンに取りかかろう。 ボタンはIDによって管理することもできるが、非常に扱いがやっかいなので、 EnumChildWindowsAPIをCALLしてダイアログボックス上のコントロールを全て列挙し、それらをツールチップコントロールに登録することにしよう。 EnumChildWindows関数は以下のようになっている。

EnumChildWindows proto hWnd:DWORD, lpEnumFunc:DWORD, lParam:DWORD

hWndは親ウィンドウのハンドルで、lpEnumFuncは全てのコントロールに対してCALLするEnumChildProc関数へのポインタだ。 lParamはアプリケーション固有に定義する値で、EnumChildProc関数へ渡される。EnumChildProc関数は以下のようになっている。

EnumChildProc proto hwndChild:DWORD, lParam:DWORD

hwndChildはEnumChildWindows関数により列挙されたウィンドウハンドルで、 lParamはEnumChildWindows関数に渡したlParamと同一のものとなっている。 今回の例では、以下のようにEnumChildWIndows関数をCALLしている。

invoke EnumChildWindows,hDlg,addr EnumChild,addr ti

ダイアログボックス上のチャイルドコントロールをEnumChild関数でツールチップコントロールとして登録したいので、lParamにTOOLINFO構造体へのポインタを渡している。 この方法を取らないとなると、tiをグローバル変数として定義しなければならず、バグを誘発する可能性が高まってしまう。

EnumChildWindows関数をCALLすると、Windowsはダイアログボックス上の全てのコントロールに対して、EnumChild関数をCALLすることになっている。 なので、ダイアログボックスに2つのアイテムがあると、EnumChild関数は2回CALLされることになる。
EnumChild関数はTOOLINFO構造体と関連のあるメンバを引数にとり、ツールチップコントロールに対応したツールを登録する。

EnumChild proc uses edi hwndChild:DWORD,lParam:DWORD
   LOCAL buffer[256]:BYTE
   mov edi,lParam
   assume edi:ptr TOOLINFO
   push hwndChild
   pop [edi].uId   ; we use the whole client area of the control as the tool
   or [edi].uFlags,TTF_IDISHWND
   invoke GetWindowText,hwndChild,addr buffer,255
   lea eax,buffer   ; use the window text as the tooltip text
   mov [edi].lpszText,eax
   invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
   assume edi:nothing
   ret
EnumChild endp

ここでは、クライアントエリア全体を覆うツールという今までとは違ったツールを使用していることに注意しよう。 なので、uIDにツールを包含するウィンドウのハンドルをセットする必要があり、 また、uFlagsメンバにTTF_IDISHWNDを指定しなければならない。



[戻る]