今回のチュートリアルでは、ツールチップコントロールについて説明する。
メインソース | ヘッダファイル | リソース | 実行結果 |
|
ツールチップとは小さな矩形ウィンドウのことで、マウスポインタが指定した領域に重なったときに表示される。 ツールチップウィンドウには文字列も表示できる。 この点で言えば、ツールチップはステータスウィンドウと同じような役割なのだが、 ユーザがマウスをクリックしたり、他の場所へマウスポインタを移動させると消えるようになる。 このツールチップはきっとほとんどのアプリケーションでツールバーのボタンで使用しているので、きっとよく使ったことがあるだろう。 それらのツールチップはツールバーコントロールにより提供される便利なしろものだ。 もし他のウィンドウやコントロールでツールチップを使いたければ、自分独自のツールチップコントロールを作成する必要がある。
では、ツールチップがどんなものかわかったところで、それの使い方と作り方の説明に移ろう。 概要は以下のようになっている。
- CreateWindowEx関数でツールチップコントロールを作成
- マウスポンタの移動を監視する領域を定義する
- ツールチップコントロールの領域を指定する
- その指定した領域におけるマウスメッセージとツールチップコントロールをマッピングさせる(この部分はメッセージのマッピングの仕方のよってはもっと早くなるかもしれない)
では詳細に移ろう。
●ツールチップの作成
ツールチップコントロールはコモンコントロールの一つなので、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_POPUPとWS_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は使用しないが、lParamにTOOLINFO構造体のポインタを指定しなければならない。
.data? ti TOOLINFO <> ....... .code ....... [fill the TOOLINFO structure] ....... invoke SendMessage, hwndTooltip, TTM_ADDTOOL, NULL, addr tiツールチップコントロールへの登録が成功すれば、SendMessage関数はTRUEを返し、失敗すればFALSEを返す。 登録を抹消するにはTTM_DELTOOLメッセージを送信すればよい。
●ツールチップコントロールへマウスメッセージのリレー
今までのステップが完了すれば、ツールチップコントロールはどの領域のマウスメッセージを監視すればよいか、またツールチップウィンドウにどんな文字列を表示するかを理解している。 後残っているのは、その動作の引き金となるものである。 ツールチップコントロールは、ツール領域上にマウスポインタが位置する時間を計測し、 ある指定した時間ずっとマウスポインタが位置していれば、ツールチップウィンドウを表示する、という動作を行っている。
ツールによって記述された領域が他のウィンドウだったとしたら、ツールチップコントロールはどうやってマウスメッセージを横取りすればいいのだろうか。
それには2つの方法があり、ツール領域を含むウィンドウと連携する方法と連携しない方法だ。
ツールを含むウィンドウは、TTM_RELAYEVENTメッセージをツールチップコントロールに送信してメッセージをリレーしなければならない。このメッセージのlParamにはツールチップコントロールがリレーすべきMSG構造体へのポインタを指定しなければならない。ツールチップコントロールは以下のマウスメッセージだけを処理することになっている。
- WM_LBUTTONDOWN
- WM_MOUSEMOVE
- WM_LBUTTONUP
- WM_RBUTTONDOWN
- WM_MBUTTONDOWN
- WM_RBUTTONUP
- WM_MBUTTONUP
これ以外のメッセージは全て無視される。なので、ウィンドウプロシージャには次のようなswitch文を含むことになる。
WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD ....... if uMsg==WM_CREATE ............. elseif uMsg==WM_LBUTTONDOWN || uMsg==WM_MOUSEMOVE || uMsg==WM_LBUTTONUP || uMsg==WM_RBUTTONDOWN || uMsg==WM_MBUTTONDOWN || uMsg==WM_RBUTTONUP || uMsg==WM_MBUTTONUP invoke SendMessage, hwndTooltip, TTM_RELAYEVENT, NULL, addr msg ..........- TOOLINFO構造体のuFlagsメンバにTTF_SUBCLASSフラグを指定できる。このフラグを指定すれば、ツールチップコントロールがツールを含むウィンドウをサブクラス化することができるようになる。
なので、ウィンドウ間で協調せずともマウスメッセージを処理できるようになる。この方法は比較的簡単で、TTF_SUBCLASSフラグを指定すればいいだけである。そうすればツールチップコントロールは全てのメッセージを扱うようになる。これまでの説明でツールチップコントロールはちゃんと動くようになるだろう。以下で、便利なメッセージリレー方法をいくつか紹介しよう。
- TTM_ACTIVATE
ツールチップコントロールのenable/disableを動的に操作したいなら、このメッセージを使用すればよい。wParamにTRUEを指定すれば、ツールチップはenable状態になり、FALSEを指定すればdisable状態となる。ちなみにツールチップコントロールを作成した時にはenable状態となっている。- TTM_GETTOOLINFO, TTM_SETTOOLINFO
TOOLINFO構造体の値を取得、変更したければこれらのメッセージを使用する。その際には正しいuIdとhWndを指定しなければならない。rectメンバを変更したければ、TTM_NEWTOOLRECTメッセージを、ツールチップテキストを変えたければTTM_UPDATETIPTEXTを使用する。- TTM_SETDELAYTIME
このメッセージで、ツールチップテキストを表示するまでの時間を指定できる。
|
以下のコードは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
|
メインダイアログを作成した後、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 rectTOOLINFO構造体を初期化する。ここでは、クライアントエリアを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:DWORDhWndは親ウィンドウのハンドルで、lpEnumFuncは全てのコントロールに対してCALLするEnumChildProc関数へのポインタだ。 lParamはアプリケーション固有に定義する値で、EnumChildProc関数へ渡される。EnumChildProc関数は以下のようになっている。
EnumChildProc proto hwndChild:DWORD, lParam:DWORDhwndChildは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を指定しなければならない。