第27课 工具提示控件


我们将学习工具提示控件:它是什么如何创建和使用.下载例子

理论:

工具提示是当鼠标在某特定区域上停留时显示的一个矩形窗口.工具提示窗口包含一些编程者想要显示的文本.在这点上,工具提示同状态栏的作用是一样的,所不同的是工具提示当单击或者远离指定区域的时候就会消逝,你可能熟悉与工具栏相关联的工具提示,那些"提示"是工具栏控件提供的便利.如果你想要在其它窗口、控件中显示工具提示的话,就不得不自己创建他们.

既然已经了解了什么是工具提示,就让我们来看看如何创建他们.大致步骤如下:

  1. 用CreateWindowEx函数创建工具提示控件.
  2. 定义一个工具提示控件将要监视鼠标移动的区域.
  3. 将区域传递给工具提示控件
  4. 将传递区域的鼠标消息转送给工具提示控件.(这步或许更早,具体依据转播消息的方法)
下面我们就来详细的讨论每一步.

工具提示控件的创建

工具提示控件是一种通用控件.同样,要在源代码某处调用InitCommonControls以便MASM能够将你的程序和comctl32.dll连接. 用CreateWindowEx创建工具提示控件,典型代码如下:
.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风格,因为工具提示处理过程会自动加上,你也不必指定工具提示窗口的坐标和宽高,控件会依据要显示的文字自动调节.四个参数,均使用CW_USEDEFAULT ,其余的参数都不太重要.

指定工具

工具提示控件创建了但还没有显示,我们想要当鼠标指针在某个区域之上时显示工具提示窗口.现在需要指定这个区域.我们称这样的区域为"工具",“工具”就是工具提示控件监视鼠标指针是否移过的位于窗口客户区的一个方形区域.如果鼠标指针移过"工具",工具提示窗口就显示."工具"可覆盖整个客户区或者仅仅是它的一部分.因此我们把"工具"分成两种类型,一种是作为一个窗口,另一种则是某窗口客户区的一部分.两种各有所用.覆盖整个客户区的"工具"通常用于按钮、编辑控件等,你不必指定焦点域的坐标和大小:它被假定为窗口的整个客户区.仅覆盖窗口客户区一部分的"工具"在你想把窗口客户区分成几个部分但又不想使用子窗口时特别有用,但需要指定左上角的坐标和宽高.

使用如下的 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 指定焦点域的属性,可以是如下标志的联合:
  • TTF_IDISHWND  "ID is hWnd".如果你指定了这个标志,就意味着你要使用覆盖整个客户区的"工具" (上面第一种"工具"). 如果你使用了这个标志,你必须用你要使用的窗口句柄填充uId成员,如果你不指定这个成员,就意味着你要使用第二种"工具"、客户区窗口的一方形区域.在这种情况下,你就必须以方形区域的大小填充rect成员.
  • TTF_CENTERTIP  通常工具提示窗口显示在鼠标的右下方,如果你指定了这个标志,不管鼠标的位置如何,工具提示总显示在焦点域总的中下方.
  • TTF_RTLREADING  .如果你的程序不是为阿拉伯或者希伯来语系统设计的,你完全可以不理它,它使得提示文本以从右至左的顺序显示,在其它系统中无效.
  • TTF_SUBCLASS  如果你使用了这个标志,工具提示控件将子类化"工具"所在窗口以便截取发送给它的的鼠标消息,这个标志非常有用,否则你将不得不做更多的工作来向工具提示控件转发消息.
hWnd 包含"工具"的窗口句柄,如果你指定了TTF_IDISHWND标志,Windows将忽略该值,而使用uId成员的值作为窗口句柄.你需要填充这个域域如果:
  • 你不使用 TTF_IDISHWND标志 (换句话说,你使用局部"工具")
  • 你在 lpszText 成员中指定了LPSTR_TEXTCALLBACK .这个值告诉工具提示控件当需要显示提示窗口时,必须向包含"工具"的窗口查询应该显示什么. 这是一种实时的控件文本更新.如果你需要动态改变提示文本,你应当在 lpszText成员中指定LPSTR_TEXTCALLBACK值,控件就会向hWnd指定的窗口发送TTN_NEEDTEXT 消息.
uId 这个域的值可能有两种含义,依 uFlags 是否包含TTF_IDISHWND.
  • 如果TTF_IDISHWND标志没有被指定就代表应用程序定义的"工具"ID,由于这意味着你使用仅覆盖客户区一部分的"工具",逻辑的推出一个客户区可能存在多个同样的焦点域(不存在交迭),Hwnd成员的一个窗口句柄就不够了,应用程序定义ID以区分他们因此而显得必要,只要唯一ID可以是任何值.
  • 如果TTF_IDISHWND标志被指定就表示整个客户区都作为焦点域的窗口句柄,你或许会奇怪为什么不用上面提到的hWnd成员的值来储存窗口句柄.答案是:如果lpszText指定为LPSTR_TEXTCALLBACK,Hwnd 可能已经被填充了.还有提供提示文本的窗口和包含"工具"的窗口可能不是同一个!(你可以设计一个提供两种服务的窗口程序,但太严格了,在这点上,微软为我们提供了更大的自由,欢呼吧!)
rect 指定"工具"大小的rect结构.这个结构定义了一个以hWnd指定窗口客户区左上角为基点的方形大小,简言之,如果你想指定客户区的一部分作为"工具"就得填充这个结构,如果你指定了TTF_IDISHWND标志 ,控件就会忽略这个值.(你已经选择整个客户区作为"工具")
hInst 如果lpszText指定了字符串资源的标识,包含将作为工具提文本字符串资源的实例句柄.听起来有点费解,阅读一下lpszText的说明就可以明白这个域是干什么用的了.若lpszText不包含字符串资源标识,控件会忽略这个域.
lpszText 这个域可以有如下几个值:
  • 如果指定为LPSTR_TEXTCALLBACK, 工具提示控件就会向HWnd窗口发送TTN_NEEDTEXT消息以获得将要显示的字符串.提示文本的动态更新方法:每次显示提示窗口都改变提示文本.
  • 如果在这个域中指定字符串资源标识,当控件要在提示窗口中显示提示文本时,就搜索hInst成员标识的实例的字符串资源列表.由于字符串资源列表标识总是16位值,这个域的高字节将永远为0,这种方法在你想移植程序时非常有用,由于字符串资源以脚本形式定义,你不必修改源代码.只需要修改字符串列表提示文本就会相应改变,而不必担心引进bugs.
  • 如果这个域的值不是LPSTR_TEXTCALLBACK并且高字节不为零, 控件截取这个值作为提示文本的指针,这是最简单的方法但也最不稳定.通过检查高字节区分字符串资源标识.

 

总言之,你需要将TOOLINFO结构传递给工具提示控件之前填充填充好,它描述了你期望的"工具"属性.

向工具提示控件注册"工具"

填充完TOOLINFO结构后, 必须将其传递给控件 . 一个工具提示控件可以控制很多"工具",因此不必为一个窗口创建很多控件,为了注册"工具",向控件发送TTM_ADDTOOL消息 wParam不使用,lParam必须包含要注册的TOOLINFO结构的指针
.data?
ti TOOLINFO <>
.......
.code
.......
<fill the TOOLINFO structure>
.......
invoke SendMessage, hwndTooltip, TTM_ADDTOOL, NULL, addr ti
成功返回 TRUE,否则返回 FALSE.
发送 TTM_DELTOOL消息取消注册.

向工具提示控件转发鼠标消息

以上步骤完毕之后,控件知道了应当监视那一块区域和应该在提示窗口显示什么.唯一缺乏的就是激发机制. 想想看:"工具"指定的其它窗口的客户区的区域.控件如何截取发送向该窗口的消息呢?实际中需要截取消息以便了解鼠标停留了多长时间,当指定时间流逝以后,控件显示提示窗口.有两种方法: 一种需要包含"工具"窗口的合作,另一种则不需要. 就是这些了,到这步为止,控件已经全功能了.还有几个你应当知道的相关消息.

例子:

例子是一个有两个按钮的对话框,对话框的客户区分为4部分:左上、右上、左下、右下.每个区域都指定为有自己提示文本的"工具",两个按钮也有自己的提示文本.

.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
  

之后,我们继续定义对话框四个角作为焦点域.

mov id,0        ; 焦点域ID
    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

我们初始化TOOLINFO结构. 注意我们要把客户区分成4个焦点域,因此我们需要知道客户区的大小,所以调用GetWindowRect.因为我们不想自己向控件转发消息,因此指定TIF_SUBCLASS 标志.
SetDlgToolArea 是计算焦点域矩形范围的并向控件注册的函数,我不详细解释计算过程.只说明它把对话框分成4个焦点域.然后向控件发送TTM_ADDTOOL 消息, 在lParam参数中传递TOOLINFO结构的地址.

    invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti

在四个控件注册之后,我们来看看对话框的按钮,我们可以用ID来处理每个按钮,但是实在太乏味了.我们使用EnumChildWindows函数列举对话框上的所有控件并把他们注册给控件,EnumChildWindows原型如下:

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

hWnd 是父窗口句柄.

lpEnumFunc 是每个控件将调用的EnumChildProc函数地址.lParam 是应用程序定义的要传给EnumChildProc 函数的值. EnumChildProc 函数定义如下:

EnumChildProc proto hwndChild:DWORD, lParam:DWORD
hwndChild是EnumChildWindows函数枚举的句柄. lParam 就是你传递给EnumChildWindows函数的同一个lParam.
在例子中.我们如此调用 EnumChildWindows 函数:
invoke EnumChildWindows,hDlg,addr EnumChild,addr ti
我们把TOOLINFO结构的地址放在lParam参数中传递,是因为我们要在EnumChild函数中注册每个子控件.如果我们不使用这种方法,就需要将ti声明为全局变量,但这可能会引入很多bug.
当我们调用 EnumChildWindows时, Windows会枚举出对话框上所有的子控件并为每个子控件调用一次EnumChild f函数. 这样如果我们的对话框有两个控件,EnumChild将被调用两次.
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标志.


This article come from Iczelion's asm page, Welcom to http://asm.yeah.net/