见招拆招Windows程序设计(六)
相关的例子:下载>>> 作者:Zoologist 于2008-3-16上传 

鼠标

         鼠标是有一个或多个键的定位设备。虽然也可以使用诸如触摸画面和光笔之类的输入设备,但是只有鼠标以及常用在笔记本上的轨迹球等才是渗透了PC市场的唯一输入设备。

情况并非总是如此。当然,Windows的早期开发人员认为他们不应该要求使用者为了执行其产品而必须买只鼠标。因此,他们将鼠标作为一种选择性的附加设备,而为Windows中的所有操作以及applet提供一种键盘接口(例如,查看Windows计算器程序的在线说明信息,可以看到每个按钮都提供了一个同等功效的键盘操作方式)。第三方软件开发人员使用键盘接口来提供与鼠标操作相同的功能,这本书以前的版本也是这么做的。

理论上来说,现在的Windows需要鼠标。至少,一些消息框是这样讲的。当然,您也可以拔下鼠标,而且Windows仍然可以执行良好(提醒一句,如果你使用的不是USB鼠标,最好不要尝试将鼠标再插回去,这样做可能会烧毁你的主板哦)。试图不用鼠标来使用Windows就像用脚趾来弹钢琴一样(至少在最初的一段时间里是这样),但您依然可以这样做。正因为如此,我还是喜欢为鼠标功能提供键盘操作。打字员尤其喜欢让他们的手保持在键盘上,并且我认为每个人都有在杂乱的桌上找不到鼠标,或者鼠标移动不灵敏的经验。使用键盘通常不需要花费更多的精力和努力,并且为喜欢使用键盘的人提供更多的功能。或者说大量使用键盘的通常给人很牛X的感觉。

我们通常认为,键盘便于输入和操作文字数据,而鼠标则便于画图和操作图形对象。实际上,本章大多数的范例程序都画了一些图形,并且用到了我们前面所学到的知识。

鼠标基础

Windows 98/2000/XP能支持单键、双键或者三键鼠标,也可以使用摇杆或者光笔来仿真单键鼠标。早期,由于许多使用者都有单键鼠标,所以Windows应用程序总是避免使用双键或三键鼠标。不过,由于双键鼠标已经成为事实上的标准,因此不使用第二个键的传统已经不再合理了。当然,第二个鼠标按键是用于启动一个「快捷菜单」,亦即出现在普通菜单列之外的窗口中菜单,或者用于特殊的拖曳操作(拖曳将在后面加以解释)。然而,程序不能依赖双键鼠标。(题外话,大多数的宣传都在说Windows操作系统是“抄袭”自Apple。不过,在鼠标方面,Apple的机器却整整花费了20年才考虑给他们的用户提供双键鼠标。以至于很多Apple的用户不得不在购买机器后马上购买一个第三方鼠标。)

理论上,您可以用我们的老朋友GetSystemMetrics函数来确认鼠标是否存在:

fMouse = GetSystemMetrics (SM_MOUSEPRESENT) ;
        

如果已经安装了鼠标,fMouse将传回TRUE(非0);如果没有安装,则传回0。然而,在Windows 98中,不论鼠标是否安装,此函数都将传回TRUE 。在Microsoft Windows NT中,它可以正常工作。

要确定所安装鼠标其上按键的个数,可使用

cButtons = GetSystemMetrics (SM_CMOUSEBUTTONS) ;
        

如果没有安装鼠标,那么函数将传回0。然而,在Windows 98下,如果没有安装鼠标,此函数将传回2。

习惯用左手的使用者可以使用Windows的「控制面板」来切换鼠标按键。虽然应用程序可以通过在GetSystemMetrics中使用SM_SWAPBUTTON参数来确定是否进行了这种切换,但通常没有这个必要。由食指触发的键被认为是左键,即使事实上是位于鼠标的右边。不过,在一个教育训练程序中,您可能想在屏幕上画一个鼠标,在这种情况下,您可能想知道鼠标按键是否被切换过了。

您可以在「控制面板」中设定鼠标的其它参数,例如双击速度。从Windows应用程序,通过使用SystemParametersInfo函数可以设定或获得此项信息。

一些简单的定义

当Windows使用者移动鼠标时,Windows在显示器上移动一个称为「鼠标光标」的小位图。鼠标光标有一个指向显示器上精确位置的单图素「热点」。当我提到鼠标光标在屏幕上的位置时,指的是热点的位置。

Windows支持几种预先定义的鼠标光标,程序可以使用这些光标。最常见的是称为IDC_ARROW的斜箭头(在WINUSER.H中定义)。热点在箭头的顶端。IDC_CROSS光标(在后面的BLOKOUT程序中有用到)的热点在十字交叉线的中心。IDC_WAIT光标是一个沙漏,通常用于指示程序正在执行。程序写作者也可以设计自己的光标。我们将在后面学习设计方法。在定义窗口类别结构时指定特定窗口的内定光标,例如:

wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
        

下面是一些描述鼠标按键动作的术语:

对三键鼠标来说,三个键分别称为左键、中键、右键。在Windows表头文件中定义的与鼠标有关的标识符使用缩写LBUTTON、MBUTTON和RBUTTON。双键鼠标只有左键与右键,单键鼠标只有一个左键。

鼠标(Mouse)的复数

现在,为了展现我的勇气,我将面对输入设备最难辩的争论话题:什么是「mouse」的复数。虽然每个人都知道多只啮齿动物称为mice,似乎没有人对该如何称呼多个输入设备有最后的答案。不管「mice」或「mouse」听起来都不对劲。我惯常参考的《American Heritage Dictionary of the English Language》第三版则只字未提。

《Wired style:Principles of English Usage in the Digital Age》(HardWired, 1996)指出「mouse」比较好,以避免与啮齿动物搞混。在1964发明鼠标的Doug Engelbart对此争议也帮不上忙。我曾经问过他mouse的复数是什么,他说我不知道。

最后,高权威的Microsoft Manual of Style for Technical Publications告诉我们「避免使用复数mice。假如你必须提到多只mouse,使用mouse devices」。这听起来像是在逃避问题,但当一切听起来都不对劲时,它确实是个明智的忠告了。事实上,大部分需要mouse复数的句子都能重新修改来避开。例如,试着说"People use the almost as much as keyboard",而不是"Pople use mice almost as much as keyboards"。

显示区域鼠标消息

在前一期中您已经看到,Windows只把键盘消息发送给拥有输入焦点的窗口。鼠标消息与此不同:只要鼠标跨越窗口或者在某窗口中按下鼠标按键,那么窗口消息处理程序就会收到鼠标消息,而不管该窗口是否活动或者是否拥有输入焦点。Windows为鼠标定义了21种消息,不过,其中有11个消息和显示区域无关(下面称之为「非显示区域」消息),Windows程序经常忽略这些消息。

当鼠标移过窗口的显示区域时,窗口消息处理程序收到WM_MOUSEMOVE消息。当在窗口的显示区域中按下或者释放一个鼠标按键时,窗口消息处理程序会接收到下面这些消息:

表7-1

按下

释放

按下(双键)

WM_LBUTTONDOWN WM_LBUTTONUP WM_LBUTTONDBLCLK
WM_MBUTTONDOWN WM_MBUTTONUP WM_MBUTTONDBLCLK
WM_RBUTTONDOWN WM_RBUTTONUP WM_RBUTTONDBLCLK

只有对三键鼠标,窗口消息处理程序才会收到MBUTTON消息;只有对双键或者三键鼠标,才会接收到RBUTTON消息。只有当定义的窗口类别能接收DBLCLK(双击)消息,窗口消息处理程序才能接收到这些消息(请参见本期「双击鼠标按键」)。

对于所有这些消息来说,其lParam值均含有鼠标的位置:低字组为x坐标,高字组为y坐标,这两个坐标是相对于窗口显示区域左上角的位置。您可以用LOWORD和HIWORD宏来提取这些值:

x = LOWORD (lParam) ;
        
y = HIWORD (lParam) ;
        

wParam的值指示鼠标按键以及Shift和Ctrl键的状态。您可以使用表头文件WINUSER.H中定义的位屏蔽来测试wParam。MK前缀代表「鼠标按键」。

MK_LBUTTON 按下左键
MK_MBUTTON 按下中键
MK_RBUTTON 按下右键
MK_SHIFT 按下Shift键
MK_CONTROL 按下Ctrl键

例如,如果收到了WM_LBUTTONDOWN消息,而且值

wparam & MK_SHIFT
        

是TRUE(非0),您就知道当左键按下时也按下了Shift键。

当您把鼠标移过窗口的显示区域时,Windows并不为鼠标的每个可能的图素位置都产生一个WM_MOUSEMOVE消息。您的程序接收到WM_MOUSEMOVE消息的次数,依赖于鼠标硬件,以及您的窗口消息处理程序在处理鼠标移动消息时的速度。换句话说,Windows不能用未处理的WM_MOUSEMOVE消息来填入消息队列。当您执行下面将描述的CONNECT程序时,您将会更了解WM_MOUSEMOVE消息处理的速率。

如果您在非活动窗口的显示区域中按下鼠标左键,Windows将把活动窗口改为在其中按下鼠标按键的窗口,然后把WM_LBUTTONDOWN消息送到该窗口消息处理程序。当窗口消息处理程序得到WM_LBUTTONDOWN消息时,您的程序就可以安全地假定该窗口是活动化的了。不过,您的窗口消息处理程序可能在未接收到WM_LBUTTONDOWN消息的情况下先接收到了WM_LBUTTONUP的消息。如果在一个窗口中按下鼠标按键,然后移动到使用者窗口释放它,就会出现这种情况。类似的情况,当鼠标按键在另一个窗口中被释放时,窗口消息处理程序只能接收到WM_LBUTTONDOWN消息,而没有相应的WM_LBUTTONUP消息。

这些规则有两个例外:

简单的鼠标处理:一个例子

程序7-1中所示的CONNECT程序能作一些简单的鼠标处理,使您对Windows如何向您的程序发送鼠标消息有一些体会。

程序7-1 CONNECT

        
CONNECT.ASM
        
;MASMPlus 代码模板 - 普通的 Windows 程序代码

.386
.Model Flat, StdCall
Option Casemap :None

Include windows.inc
Include user32.inc
Include kernel32.inc
Include gdi32.inc
Include libc.inc

includelib gdi32.lib
IncludeLib user32.lib
IncludeLib kernel32.lib
IncludeLib msvcrt.lib
include macro.asm
	
	WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
	WndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD
	
MAXPOINTS equ	1000

.DATA
	szAppName	DB		"Connect",0
.DATA?
	hInstance	DD		?
	pt 			POINT MAXPOINTS DUP (<?>)
	iCount		DD		?
	szBuffer		db		100 dup (?)

.CODE

START:   ;从这里开始执行
	invoke   GetModuleHandle,NULL
	mov 		hInstance,eax
	invoke   WinMain,hInstance,NULL,NULL,SW_SHOWDEFAULT
	invoke   ExitProcess,0

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,iCmdShow:DWORD
	LOCAL wndclass :WNDCLASSEX
	LOCAL msg  		:MSG
	local hWnd 		:HWND
	mov wndclass.cbSize,sizeof WNDCLASSEX	
	mov wndclass.style,CS_HREDRAW or CS_VREDRAW	
	mov wndclass.lpfnWndProc,offset WndProc

	mov wndclass.cbClsExtra,0
	mov wndclass.cbWndExtra,0
	
	push hInst
	pop wndclass.hInstance
	
	invoke LoadIcon,NULL,IDI_APPLICATION
	mov wndclass.hIcon,eax	
	
	invoke LoadCursor,NULL,IDC_ARROW
	mov wndclass.hCursor,eax	
	
	invoke GetStockObject,WHITE_BRUSH
	mov wndclass.hbrBackground,EAX
	
	mov wndclass.lpszMenuName,NULL
	mov wndclass.lpszClassName,offset szAppName

	mov wndclass.hIconSm,0
	
	invoke RegisterClassEx, ADDR wndclass
	.if (EAX==0)
		 invoke MessageBox,NULL,CTXT("This program requires Windows NT!"),addr szAppName,MB_ICONERROR 		
		 ret
	.endif
        
	invoke CreateWindowEx,
					NULL,
					ADDR szAppName, 	;window class name
					CTXT("Connect"), ;window caption
					WS_OVERLAPPEDWINDOW,	;window style
					CW_USEDEFAULT,	;initial x position
					CW_USEDEFAULT,	;initial y position
					CW_USEDEFAULT, 	;initial x size
					CW_USEDEFAULT,	;initial y size
					NULL,	;parent window handle
					NULL,	;window menu handle
					hInstance,	;program instance handle
					NULL	;creation parameters
	mov hWnd,eax
	
	invoke ShowWindow,hWnd,iCmdShow
	invoke UpdateWindow,hWnd
	
	StartLoop:
		invoke GetMessage,ADDR msg,NULL,0,0
			cmp eax, 0
			je ExitLoop
				invoke TranslateMessage, ADDR msg
				invoke DispatchMessage,  ADDR msg
			jmp StartLoop
	ExitLoop:
	
	mov eax,msg.wParam
	ret
WinMain endp

WndProc proc hwnd:DWORD,message:DWORD,wParam :DWORD,lParam :DWORD
	LOCAL hdc 				:HDC
   LOCAL i, j			   :DWORD
	LOCAL ps  				:PAINTSTRUCT 

	.if message == WM_LBUTTONDOWN
		 xor			eax,eax
		 mov			iCount,eax
       invoke     InvalidateRect,hwnd, NULL, TRUE
       ret
   .elseif message == WM_MOUSEMOVE  
      	mov	eax,wParam
      	test	eax,MK_LBUTTON
      	jz		@f
      	and	eax,iCount
      	cmp	eax,1000
      	jae	@f
      	mov	ebx,lParam
      	lea	esi,pt
      	mov	eax,iCount
      	shl	eax,3	;iCount * 8
      	add	esi,eax
      	
      	push	ebx	; pt[iCount  ].x = LOWORD (lParam)
      	shl	ebx,16
      	shr	ebx,16
      	mov   [esi],ebx
      	pop	ebx
      	
      	shr	ebx,16	; pt[iCount++].y = HIWORD (lParam) 
      	add	esi,4
      	mov [esi],ebx

			inc	iCount
         invoke GetDC,hwnd
         mov   hdc,eax
         mov	ebx,lParam
         mov	eax,ebx
         shr	eax,16
         
         push	0
         and	eax,0FFFFh	;HIWORD(lParam)
         push	eax
         and	ebx,0FFFFh	;LOWORD(lParam)
         push	ebx
         push	hdc
         call SetPixel
                                   
         invoke ReleaseDC,hwnd, hdc

		@@:	
		
			ret    	
		.elseif message == WM_LBUTTONUP
			invoke InvalidateRect,hwnd, NULL, FALSE
         ret	
		.elseif message == WM_PAINT
		invoke	BeginPaint,hwnd,addr ps
		mov		hdc,eax

		invoke	LoadCursor,NULL,IDC_WAIT
		invoke	SetCursor,eax
      invoke   ShowCursor,TRUE
       
		xor		eax,eax
		mov		i,eax
	Loopi:
		mov		eax,i
		inc		eax
		mov		j,eax
	  Loopj:
	  	
	  	lea		esi,pt
	  	mov		eax,i
	  	shl		eax,3
	  	add		esi,eax
	  	mov		eax,NULL
	  	push		eax
	  	mov		eax,[esi+4] ;pt[i].y
	  	push		eax
	  	mov		eax,[esi]	;pt[i].x
	  	push		eax
	  	push		hdc
	  	call		MoveToEx
	  	lea		esi,pt
	  	mov		eax,j
	  	shl		eax,3
	  	add		esi,eax	  	
	  	mov		eax,[esi+4]	;pt[j].y
	  	push		eax
	  	mov		eax,[esi]	;pt[j].x
	  	push		eax
	  	push		hdc
	  	call		LineTo
	  	inc		j
	  	mov		eax,j
	  	cmp		eax,iCount 
	  	jb			Loopj
      ;invoke	wsprintf,addr szBuffer,CTEXT("oo") ,NULL
	;invoke	MessageBox,hwnd,addr szBuffer,NULL,NULL	  	
		inc		i
	   mov		eax,i
	   inc		eax
	   cmp		eax,iCount
	   jb			Loopi
		
      invoke   ShowCursor,FALSE

		invoke	LoadCursor,NULL,IDC_ARROW
		invoke	SetCursor,eax
      invoke   ShowCursor,TRUE
               
      invoke	EndPaint,hwnd, addr ps
        
      ret
	.elseif message == WM_DESTROY
		invoke PostQuitMessage,NULL		
	.endif
UseDefWindowProc:
	invoke DefWindowProc,hwnd, message, wParam, lParam
	ret
WndProc endp

END START

        

CONNECT处理三个鼠标消息:


 

wpe1.jpg (32912 字节)

图7-1 CONNECT的屏幕显示

CONNECT的使用方法:把鼠标光标移动到显示区域中,按下左键,移动一下位置,释放左键。对几个构成曲线的点,CONNECT能处理得很好,方法是按住左键,快速移动鼠标,这样就可以绘制出该曲线图案。

CONNECT使用了三个简单的图形设备接口(GDI)函数。当鼠标左键按下时,SetPixel为每个WM_MOUSEMOVE消息绘制一个黑图素。画直线需要MoveToEx和LineTo函数。

如果您在释放鼠标按键之前把鼠标光标移到显示区域之外,那么CONNECT就不会连接这些点,因为它没有收到WM_LBUTTONUP消息。如果您把鼠标移回显示区域内并按下左键,那么CONNECT将清除显示区域。如果想在显示区域外释放左键后还继续进行画图,那么可以在显示区域外按下鼠标再移回显示区域中。

CONNECT最多可以保存1000个点。设点数为P,则CONNECT画的线数就等于P × (P - 1) / 2。如果有1000个点,则要绘制50万条直线,大约需要几分钟才能画完(时间的长短取决于您的硬设备)。由于Windows是一种优先权式多任务环境,因此您可以在这一段时间切换到别的程序中。但是,当程序正在忙的时候,您将无法对CONNECT程序做任何事(诸如移动或者缩放等)。在后面的一期中,我们将讨论解决这一问题的方法。

因为CONNECT可能会花一些时间来绘制直线,因此在处理WM_PAINT消息时它将切换到沙漏光标,然后再恢复原状。这要求使用两个现有光标来呼叫SetCursor。CONNECT还呼叫两次ShowCursor,一次用TRUE参数,另一次用FALSE参数。我将在本期的后面,"使用键盘仿真鼠标"一节中更详细地讨论这些调用。

有时,我们使用「跟踪」这个词代表程序处理鼠标移动的方法。但是,跟踪并不意味着,程序在窗口消息处理程序中的某个循环里,不断跟随鼠标在显示器上的运动。实际上,窗口消息处理程序处理每条鼠标消息,然后迅速退出。

读者将会发现,上面的程序很难画出一个如同7-1一样“干净”的图像。很大程度上,这是因为你的电脑太快

了。如果您能够将上面的程序完全读懂,在某一个地方加上几条语句,会让作图变得容易一些。

处理Shift键

当CONNECT接收到一个WM_MOUSEMOVE消息时,它把wParam和MK_LBUTTON进行位与(AND)运算,来确定是否按下了左键。wParam也可以用于确定Shift键的状态。例如,如果处理必须依赖于Shift和Ctrl键的状态,那么您可以使用如下所示的方法:

if (wParam & MK_SHIFT)
        
{
        
    if (wParam & MK_CONTROL)
        
           {
        
            //按下了Shift和Ctrl键
        
    }
        
    else
        
    {
        
                           //按下了Shift键
        
            }
        
    {
        
    else
        
    {
        
            if (wParam & MK_CONTROL)
        
            {
        
                           //按下了Ctrl键
        
            }
        
            else
        
            {
        
                           //Shift和Ctrl键均未按下
        
            }
        
}
        

如果您想在程序中同时使用左右键,同时如果您还希望只有单键鼠标的使用者也能使用您的程序,那么您可以这样来写作程序:Shift与左键的组合使用等效于右键。在这种情况下,对鼠标按键的处理可以采用如下所示的方法:

caseWM_LBUTTONDOWN:
        
    if (!(wParam & MK_SHIFT))
        
    {
        
                           //处理左键
        
                           return 0 ;
        
    }
        
                           // Fall through
        
case WM_RBUTTONDOWN:
        
    //处理右键
        
    return 0 ;
        

Windows函数GetKeyState可以使用虚拟键码VK_LBUTTON、VK_RBUTTON、VK_MBUTTON、VK_SHIFT和VK_CONTROL来传回鼠标按键与Shift键的状态。如果GetKeyState传回负值,则说明已按下了鼠标按键或者Shift键。因为GetKeyState传回目前正在处理的鼠标按键或者Shift键的状态,所以全部状态信息与相应的消息都是同步的。但是,正如不能把GetKeyState用于尚未按下的键一样,您也不能为尚未按下的鼠标按键呼叫GetKeyState。请不要这样做:

while (GetKeyState (VK_LBUTTON) >= 0) ;  // WRONG !!!
        

只有在您呼叫GetKeyState期间处理消息时,而左键已经按下,才会报告键已经按下的消息。

双击鼠标按键

双击鼠标按键是指在短时间内单击两次。要确定为双击,则这两次单击必须发生在其相距的实际位置十分接近的状况下(内定范围是一个平均系统字体字符的宽,半个字符的高),并且发生在指定的时间间隔(称为「双击速度」)内。您可以在「控制面板」中改变时间间隔。

如果希望您的窗口消息处理程序能够收到双按键的鼠标消息,那么在呼叫RegisterClass初始化窗口类别结构时,必须在窗口风格中包含CS_DBLCLKS标识符:

wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS ;
        

如果在窗口风格中未包含CS_DBLCLKS,而使用者在短时间内双击了鼠标按键,那么窗口消息处理程序会接收到下面这些消息:

WM_LBUTTONDOWN

WM_LBUTTONUP

WM_LBUTTONDOWN

WM_LBUTTONUP

窗口消息处理程序可能在这些键的消息之前还收到了其它消息。如果您想实作自己的双击处理,那么您可以使用Windows函数GetMessageTime取得WM_LBUTTONDOWN消息之间的相对时间。在后面的某期中将更详细地讨论这个函数。

如果您的窗口类别风格中包含了CS_DBLCLKS,那么双击时窗口消息处理程序将收到如下消息:

WM_LBUTTONDOWN

WM_LBUTTONUP

WM_LBUTTONDBLCLK

WM_LBUTTONUP

WM_LBUTTONDBLCLK消息简单地替换了第二个WM_LBUTTONDOWN消息。

如果双击中的第一次键操作完成单击的功能,那么双击这一消息是很容易处理的。第二次按键(WM_LBUTTONDBLCLK消息)则完成第一次按键以外的事情。例如,看看Windows Explorer中是如何用鼠标来操作文件列表的。按一次键将选中文件,Windows Explorer用反白显示列指出被选择文件的位置。双击则实作两个功能:第一次是单击那个选中文件;第二次则指向Windows Explorer以打开该文件。执行方式相当简单,如果双击中的第一次按键不执行单击功能,那么鼠标处理方式会变得非常复杂。

非显示区域鼠标消息

在窗口的显示区域内移动或按下鼠标按键时,将产生10种消息。如果鼠标在窗口的显示区域之外但还在窗口内,Windows就给窗口消息处理程序发送一条「非显示区域」鼠标消息。窗口非显示区域包括标题列、菜单和窗口滚动条。

通常,您不需要处理非显示区域鼠标消息,而是将这些消息传给DefWindowProc,从而使Windows执行系统功能。就这方面来说,非显示区域鼠标消息类似于系统键盘消息WM_SYSKEYDOWN、WM_SYSKEYUP和WM_SYSCHAR。

非显示区域鼠标消息几乎完全与显示区域鼠标消息相对应。消息中含有字母「NC」以表示是非显示区域消息。如果鼠标在窗口的非显示区域中移动,那么窗口消息处理程序会接收到WM_NCMOUSEMOVE消息。鼠标按键产生如表7-2所示的消息。

表7-2

按下

释放

按下(双击)

WM_NCLBUTTONDOWN WM_NCLBUTTONUP WM_NCLBUTTONDBLCLK
WM_NCMBUTTONDOWN WM_NCMBUTTONUP WM_NCMBUTTONDBLCLK
WM_NCRBUTTONDOWN WM_NCRBUTTONUP WM_NCRBUTTONDBLCLK

对非显示区域鼠标消息,wParam和lParam参数与显示区域鼠标消息的wParam和lParam参数不同。wParam参数指明移动或者按鼠标按键的非显示区域。它设定为WINUSER.H中定义的以HT开头的标识符之一(HT表示「命中测试」)。

lParam参数的低位word为x坐标,高位word为y坐标,但是,它们是屏幕坐标,而不是像显示区域鼠标消息那样指的是显示区域坐标。对屏幕坐标,显示器左上角的x和y的值为0。当往右移时x的值增加,往下移时y的值增加(见图7-2)。

您可以用两个Windows函数将屏幕坐标转换为显示区域坐标或者反之:

ScreenToClient (hwnd, &pt) ;
        
ClientToScreen (hwnd, &pt) ;
        

这里pt是POINT结构。这两个函数转换了保存在结构中的值,而且没有保留以前的值。注意,如果屏幕坐标点在窗口显示区域的上面或者左边,显示区域坐标x或y值就是负值。


 

ss3.JPG (27743 字节)

图7-2 屏幕坐标与客户显示区域坐标

命中测试消息

如果您数一下,就可以知道我们已经介绍了21个鼠标消息中的20个,最后一个消息是WM_NCHITTEST,它代表「非显示区域命中测试」。此消息优先于所有其它的显示区域和非显示区域鼠标消息。lParam参数含有鼠标位置的x和y屏幕坐标,wParam参数没有用。

Windows应用程序通常把这个消息传送给DefWindowProc,然后Windows用WM_NCHITTEST消息产生与鼠标位置相关的所有其它鼠标消息。对于非显示区域鼠标消息,在处理WM_NCHITTEST时,从DefWindowProc传回的值将成为鼠标消息中的wParam参数,这个值可以是任意非显示区域鼠标消息的wParam值再加上以下内容:

HTCLIENT

HTNOWHERE

HTTRANSPARENT

HTERROR

显示区域

不在窗口中

窗口由另一个窗口覆盖

使DefWindowProc产生警示用的哔声

如果DefWindowProc在其处理WM_NCHITTEST消息后传回HTCLIENT,那么Windows将把屏幕坐标转换为显示区域坐标并产生显示区域鼠标消息。

如果您还记得我们如何通过拦截WM_SYSKEYDOWN消息来停用所有的系统键盘功能,那么您可能会想我们可否通过拦截鼠标消息完成类似的事情。完全可以!只要您在窗口消息处理程序中包含以下几条叙述:

case WM_NCHITTEST:
        
  return (LRESULT) HTNOWHERE ;
        

就可以有效地禁用您窗口中的所有显示区域和非显示区域鼠标消息。这样一来,当鼠标在您的窗口(包括系统菜单图标、缩放按钮以及关闭按钮)中时,鼠标按键将会失效。

从消息产生消息

Windows用WM_NCHITTEST消息产生所有其它鼠标消息,这种由消息引出其它消息的想法在Windows中是很普遍的。让我们来举个例子。您知道,如果您在一个Windows程序的系统菜单图标上双击一下,那么程序将会终止。双击产生一系列的WM_NCHITTEST消息。由于鼠标定位在系统菜单图标上,因此DefWindowProc将传回HTSYSMENU的值,并且Windows把wParam等于HTSYSMENU的WM_NCLBUTTONDBLCLK消息放在消息队列中。

窗口消息处理程序通常把鼠标消息传递给DefWindowProc,当DefWindowProc接收到wParam参数等于HTSYSMENU的WM_NCLBUTTONDBLCLK消息时,它就把wParam参数等于SC_CLOSE的WM_SYSCOMMAND消息放入消息队列中(这个WM_SYSCOMMAND消息是在使用者从系统菜单中选择「Close」时产生的)。同样地,窗口消息处理程序也把这个消息传给DefWindowProc。DefWindowProc通过给窗口消息处理程序发送WM_CLOSE消息来处理该消息。

如果一个程序在终止之前要求来自使用者的确认,那么窗口消息处理程序就需要拦截WM_CLOSE,否则,DefWindowProc呼叫DestroyWindow函数来处理WM_CLOSE。除了其它处理,DestroyWindow还给窗口消息处理程序发送一个WM_DESTROY消息。窗口消息处理程序通常用下列程序代码来处理WM_DESTROY消息:

caseWM_DESTROY:
        
    PostQuitMessage (0) ;
        
    return 0 ;
        

PostQuitMessage使得Windows把WM_QUIT消息放入消息队列中,此消息永远不会到达窗口消息处理程序,因为它使GetMessage传回0,并终止消息循环,从而也终止了程序。

程序中的命中测试

我在前面讨论了Windows Explorer如何响应鼠标的单击和双击。显然,程序(或者更精确的说,如同Windows Explorer般使用list view control)必须确定使用者鼠标所指向的是哪一个文件。

这叫做「命中测试」。正如DefWindowProc在处理WM_NCHITTEST消息时做一些命中测试一样,窗口消息处理程序经常必须在显示区域中进行一些命中测试。一般来说,命中测试中会使用x和y坐标值,它们由传到窗口消息处理程序的鼠标消息的lParam参数给出。

一个假想的例子

有这样一个例子。假设您的程序需要显示几列按字母排列的文件。通常,您可以使用list view control,他会帮您由于要做全部的命中测试工作。但我们假设您由于某种原因而不能使用,这时就需要自己来做了。让我们假定文件名保存在称为szFileNames的已排序字符串指针数组中。

让我们也假定文件列表开始于显示区域的顶端,显示区域为cxClient图素宽,cyClient图素高,每列为cxColWidth图素宽,每个字符高度为cyChar图素高。那么每栏可填入的文件数就是:

iNumInCol = cyClient / cyChar ;
        

接收到一个鼠标单击消息后,您就能从lParam获得cxMouse和cyMouse坐标。然后可以用下面的公式来计算使用者所指的是哪一列的文件名:

iColumn = cxMouse / cxColWidth ;
        

相对于列顶端的文件名位置为:

iFromTop = cyMouse / cyChar ;
        

现在您就可以计算szFileNames数组的下标:

iIndex = iColumn * iNumInCol + iFromTop ;
        

如果iIndex超过了数组中的文件数,则表示使用者是在显示器的空白区域内按鼠标按键。

在许多情况下,命中测试要比本例更加复杂。在显示一幅包含许多小图形的图像时,您必须决定要显示的每个小图形的坐标。在命中计算中,您必须从坐标找到对象。但这将在使用不确定字体大小的字处理程序中变得非常凌乱,因为您必须找到字符在字符串中的位置。

范例程序

程序7-2所示的CHECKER1程序展示了一些简单的命中测试,此程序把显示区域分为5×5的25个矩形。如果您在某个矩形中按下鼠标按键,那么在该矩形中将出现一个「X」。如果您再按一次,那么「X」将被删除。

程序7-2 CHECKER1

        
CHECKER1.ASM
;MASMPlus 代码模板 - 普通的 Windows 程序代码

.386
.Model Flat, StdCall
Option Casemap :None

Include windows.inc
Include user32.inc
Include kernel32.inc
Include gdi32.inc
Include libc.inc

includelib gdi32.lib
IncludeLib user32.lib
IncludeLib kernel32.lib
IncludeLib msvcrt.lib
include macro.asm
	
	WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
	WndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD
	
DIVISIONS equ	5

.DATA
	szAppName	DB		"Checker1",0
.DATA?
	hInstance	DD		?
	fState		DB		DIVISIONS*DIVISIONS dup (?)
	cxBlock		DD		?
	cyBlock 		DD		?

	szBuffer		db		100 dup (?)

.CODE

START:   ;从这里开始执行
	invoke   GetModuleHandle,NULL
	mov 		hInstance,eax
	invoke   WinMain,hInstance,NULL,NULL,SW_SHOWDEFAULT
	invoke   ExitProcess,0

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,iCmdShow:DWORD
	LOCAL wndclass :WNDCLASSEX
	LOCAL msg  		:MSG
	local hWnd 		:HWND
	mov wndclass.cbSize,sizeof WNDCLASSEX	
	mov wndclass.style,CS_HREDRAW or CS_VREDRAW	
	mov wndclass.lpfnWndProc,offset WndProc

	mov wndclass.cbClsExtra,0
	mov wndclass.cbWndExtra,0
	
	push hInst
	pop wndclass.hInstance
	
	invoke LoadIcon,NULL,IDI_APPLICATION
	mov wndclass.hIcon,eax	
	
	invoke LoadCursor,NULL,IDC_ARROW
	mov wndclass.hCursor,eax	
	
	invoke GetStockObject,WHITE_BRUSH
	mov wndclass.hbrBackground,EAX
	
	mov wndclass.lpszMenuName,NULL
	mov wndclass.lpszClassName,offset szAppName

	mov wndclass.hIconSm,0
	
	invoke RegisterClassEx, ADDR wndclass
	.if (EAX==0)
		 invoke MessageBox,NULL,CTXT("This program requires Windows NT!"),addr szAppName,MB_ICONERROR 		
		 ret
	.endif
        
	invoke CreateWindowEx,
					NULL,
					ADDR szAppName, 	;window class name
					CTXT("Checker1 Mouse Hit-Test Demo"), ;window caption
					WS_OVERLAPPEDWINDOW,	;window style
					CW_USEDEFAULT,	;initial x position
					CW_USEDEFAULT,	;initial y position
					CW_USEDEFAULT, 	;initial x size
					CW_USEDEFAULT,	;initial y size
					NULL,	;parent window handle
					NULL,	;window menu handle
					hInstance,	;program instance handle
					NULL	;creation parameters
	mov hWnd,eax
	
	invoke ShowWindow,hWnd,iCmdShow
	invoke UpdateWindow,hWnd
	
	StartLoop:
		invoke GetMessage,ADDR msg,NULL,0,0
			cmp eax, 0
			je ExitLoop
				invoke TranslateMessage, ADDR msg
				invoke DispatchMessage,  ADDR msg
			jmp StartLoop
	ExitLoop:
	
	mov eax,msg.wParam
	ret
WinMain endp

WndProc proc hwnd:DWORD,message:DWORD,wParam :DWORD,lParam :DWORD
		
	LOCAL hdc 				:HDC
   LOCAL x,y			   :DWORD
	LOCAL ps  				:PAINTSTRUCT 
	LOCAL	rect				:RECT
	LOCAL xcx,ycy,x1cx,y1cy:DWORD

	.if message == WM_SIZE
		 mov		eax,lParam	;cxBlock = LOWORD (lParam) / DIVISIONS 
		 shl		eax,16
		 shr		eax,16
		 mov		ecx,DIVISIONS
		 div		ecx
		 mov		cxBlock,eax
		
		 xor		edx,edx			 
		 mov		eax,lParam	 ;cyBlock = HIWORD (lParam) / DIVISIONS
		 shr		eax,16
 	 	 mov		ecx,DIVISIONS
		 div		ecx
		 mov		cyBlock,eax
		 mov		ebx,eax		 
		 
       ret
   .elseif message == WM_LBUTTONDOWN
   	 xor		edx,edx
		 mov		eax,lParam
		 shl		eax,16
		 shr		eax,16
		 mov		ecx,cxBlock
		 div		ecx
		 mov		x,eax
		 
		 xor		edx,edx
		 mov		eax,lParam
		 shr		eax,16
		 mov		ecx,cyBlock
		 div		ecx
		 mov		y,eax
		 
		 mov		eax,x
		 mov		ebx,y
		 lea		esi,fState
	 
		 .if		(eax<DIVISIONS)&&(ebx<DIVISIONS)
	 	
		 mov		eax,y
		 mov		ecx,DIVISIONS
		 mul		ecx
		 add		esi,eax
		 add		esi,x
		 xor		BYTE ptr [esi],1
		 
;RECT STRUCT
;  left    dd      ?
;  top     dd      ?
;  right   dd      ?
;  bottom  dd      ?
;RECT ENDS

		 lea		esi,rect
		 
		 mov		eax,x
		 mov		ecx,cxBlock
		 mul		ecx
		 mov		[esi],eax

		 mov		eax,y
		 mov		ecx,cyBlock
		 mul		ecx
		 mov		[esi+4],eax

		 mov		eax,x
		 inc		eax
		 mov		ecx,cxBlock
		 mul		ecx
		 mov		[esi+8],eax

		 mov		eax,y
		 inc		eax
		 mov		ecx,cyBlock
		 mul		ecx
		 mov		[esi+12],eax
		 
		 invoke	InvalidateRect,hwnd,addr rect,FALSE
		 .elseif
		 invoke	MessageBeep,0	
		 .endif
		 ret    	
		.elseif message == WM_PAINT
		invoke	BeginPaint,hwnd,addr ps
		mov		hdc,eax
      
		xor		eax,eax
		mov		x,eax
	Loopx:
		xor		eax,eax
		mov		y,eax
	  Loopy:
	   mov		eax,y
	   inc		eax
	   mov		ecx,cyBlock
	   mul		ecx
	   mov		y1cy,eax
	   push		eax
	   
	   mov		eax,x
	   inc		eax
	   mov		ecx,cxBlock
	   mul		ecx
	   mov		x1cx,eax
	   push		eax
	   
	   mov		eax,y
	   mov		ecx,cyBlock
	   mul		ecx
	   mov		ycy,eax
	   push		eax

	   mov		eax,x
	   mov		ecx,cxBlock
	   mul		ecx
	   mov		xcx,eax
	   push		eax
	   
	   push		hdc
		call		Rectangle
		

		lea		esi,fState
  	 	mov		eax,y
		mov		ecx,DIVISIONS
		mul		ecx
		add		esi,eax
		add		esi,x
		mov		al,[esi]
		.if		(al!=0)
	;invoke	wsprintf,addr szBuffer,CTEXT("[%d][%d][%d][%d]") ,x,y,ecx,ebx
	;invoke	MessageBox,hwnd,addr szBuffer,NULL,NULL					
			invoke	MoveToEx,hdc,xcx,ycy,NULL
			invoke	LineTo,hdc,x1cx,y1cy
			invoke	MoveToEx,hdc,xcx,y1cy,NULL
			invoke	LineTo,hdc,x1cx,ycy
		.endif
		
	  	inc		y
	  	mov		eax,y
	  	cmp		eax,DIVISIONS
	  	jb			Loopy
	  	
		inc		x
	   mov		eax,x
	   cmp		eax,DIVISIONS
	   jb			Loopx
		
      invoke	EndPaint,hwnd, addr ps
        
      ret
	.elseif message == WM_DESTROY
		invoke PostQuitMessage,NULL		
	.endif
UseDefWindowProc:
	invoke DefWindowProc,hwnd, message, wParam, lParam
	ret
WndProc endp

END START

        

        

图7-3是CHECKER1的显示。程序画的25个矩形的宽度和高度均相同。这些宽度和高度保存在cxBlock和cyBlock中,当显示区域大小发生改变时,将重新对这些值进行计算。WM_LBUTTONDOWN处理过程使用鼠标坐标来确定在哪个矩形中按下了键,它在fState数组中标志目前矩形的状态,并使该矩形区域失效,从而产生WM_PAINT消息。


 

ss4.JPG (22841 字节)

图7-3 CHECKER1的屏幕显示

如果显示区域的宽度和高度不能被5整除,那么在显示区域的左边和下边将有一小条区域不能被矩形所覆盖。对于错误情况,CHECKER1通过呼叫MessageBeep响应此区域中的鼠标按键操作。

当CHECKER1收到WM_PAINT消息时,它通过GDI的Rectangle函数来重新绘制显示区域。如果设定了fState值,那么CHECKER1将使用MoveToEx和LineTo函数来绘制两条直线。在处理WM_PAINT期间,CHECKER1在重新绘制之前并不检查每个矩形区域的有效性,尽管它可以这样做。检查有效性的一种方法是在循环中为每个矩形块建立RECT结构(使用与WM_LBUTTONDOWN处理程序中相同的公式),并使用IntersectRect函数检查它是否与无效矩形(ps.rcPaint)相交。

使用键盘仿真鼠标

CHECKER1只能在装有鼠标情况下才可执行。下面我们在程序中加入键盘接口,就如同前面章节中对SYSMETS程序所做的那样。不过,即使在一个使用鼠标光标作为指向用途的程序中加入键盘接口,我们还是必须处理鼠标光标的移动和显示问题。

即使没有安装鼠标,Windows仍然可以显示一个鼠标光标。Windows为这个光标保存了一个「显示计数」。如果安装了鼠标,显示计数会被初始化为0;否则,显示计数会被初始化为-1。只有在显示计数非负时才显示鼠标光标。要增加显示计数,您可以呼叫:

ShowCursor (TRUE) ;
        

要减少显示计数,可以呼叫:

ShowCursor (FALSE) ;
        

您在使用ShowCursor之前,不需要确定是否安装了鼠标。如果您想显示鼠标光标,而不管鼠标存在与否,那么只需呼叫ShowCursor来增加显示计数。增加一次显示计数之后,如果没有安装鼠标则减少它以隐藏光标,如果安装了鼠标,则保留其显示。

即使没有安装鼠标,Windows也保留了鼠标目前的位置。如果没有安装鼠标,而您又显示鼠标光标,光标就可能出现在显示器的任意位置,直到您确实移动了它。要获得光标的位置,可以呼叫:

GetCursorPos (&pt) ;
        

其中pt是POINT结构。函数使用鼠标的x和y坐标来填入POINT字段。要设定光标位置,可以使用:

SetCursorPos (x, y) ;
        

在这两种情况下,x和y都是屏幕坐标,而不是显示区域坐标(这是很明显的,因为这些函数没有要求hwnd参数)。前面已经提到过,呼叫ScreenToClient和ClientToScreen就能做到屏幕坐标与客户坐标的相互转换。

如果您在处理鼠标消息并转换显示区域坐标时呼叫GetCursorPos ,这些坐标可能与鼠标消息的lParam参数中的坐标稍微有些不同。从GetCursorPos传回的坐标表示鼠标目前的位置。lParam中的坐标则是产生消息时鼠标的位置。

您或许想写一个键盘处理程序:使用键盘方向键来移动鼠标光标,使用Spacebar和Enter键来仿真鼠标按键。您肯定不希望每次按键只是将鼠标光标移动一个图素,如果这样做,当要把鼠标光标从显示器的一边移动到另一边时,会使用者在很长一段时间内都要按住同一个方向键。

如果您需要实作鼠标光标的键盘接口,并保持光标的精确定位能力,那么您可以采用下面的方式来处理按键消息:当按下方向键时,一开始鼠标光标移动较慢,但随后会加快。您也许还记得WM_KEYDOWN消息中的lParam参数标志着按键消息是否是重复活动的结果,这就是此参数的一个重要应用。

在CHECKER中加入键盘接口

程序7-3所示的CHECKER2程序,除了包括键盘接口外,和CHECKER1是一样的,您可以使用左、右、上和下方向键在25个矩形之间移动光标。Home键把光标移动到矩形的左上角, End键把光标移动到矩形的右下角。Spacebar和Enter键都能切换X标记。

程序7-3 CHECKER2

        
CHECKER2.ASM
        
;MASMPlus 代码模板 - 普通的 Windows 程序代码

.386
.Model Flat, StdCall
Option Casemap :None

Include windows.inc
Include user32.inc
Include kernel32.inc
Include gdi32.inc
Include libc.inc

includelib gdi32.lib
IncludeLib user32.lib
IncludeLib kernel32.lib
IncludeLib msvcrt.lib
include macro.asm
	
	WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
	WndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD
	
DIVISIONS equ	5

.DATA
	szAppName	DB		"Checker2",0
.DATA?
	hInstance	DD		?
	fState		DB		DIVISIONS*DIVISIONS dup (?)
	cxBlock		DD		?
	cyBlock 		DD		?

	szBuffer		db		100 dup (?)

.CODE

START:   ;从这里开始执行
	invoke   GetModuleHandle,NULL
	mov 		hInstance,eax
	invoke   WinMain,hInstance,NULL,NULL,SW_SHOWDEFAULT
	invoke   ExitProcess,0

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,iCmdShow:DWORD
	LOCAL wndclass :WNDCLASSEX
	LOCAL msg  		:MSG
	local hWnd 		:HWND
	mov wndclass.cbSize,sizeof WNDCLASSEX	
	mov wndclass.style,CS_HREDRAW or CS_VREDRAW	
	mov wndclass.lpfnWndProc,offset WndProc

	mov wndclass.cbClsExtra,0
	mov wndclass.cbWndExtra,0
	
	push hInst
	pop wndclass.hInstance
	
	invoke LoadIcon,NULL,IDI_APPLICATION
	mov wndclass.hIcon,eax	
	
	invoke LoadCursor,NULL,IDC_ARROW
	mov wndclass.hCursor,eax	
	
	invoke GetStockObject,WHITE_BRUSH
	mov wndclass.hbrBackground,EAX
	
	mov wndclass.lpszMenuName,NULL
	mov wndclass.lpszClassName,offset szAppName

	mov wndclass.hIconSm,0
	
	invoke RegisterClassEx, ADDR wndclass
	.if (EAX==0)
		 invoke MessageBox,NULL,CTXT("This program requires Windows NT!"),addr szAppName,MB_ICONERROR 		
		 ret
	.endif
        
	invoke CreateWindowEx,
					NULL,
					ADDR szAppName, 	;window class name
					CTXT("Checker2 Mouse Hit-Test Demo"), ;window caption
					WS_OVERLAPPEDWINDOW,	;window style
					CW_USEDEFAULT,	;initial x position
					CW_USEDEFAULT,	;initial y position
					CW_USEDEFAULT, 	;initial x size
					CW_USEDEFAULT,	;initial y size
					NULL,	;parent window handle
					NULL,	;window menu handle
					hInstance,	;program instance handle
					NULL	;creation parameters
	mov hWnd,eax
	
	invoke ShowWindow,hWnd,iCmdShow
	invoke UpdateWindow,hWnd
	
	StartLoop:
		invoke GetMessage,ADDR msg,NULL,0,0
			cmp eax, 0
			je ExitLoop
				invoke TranslateMessage, ADDR msg
				invoke DispatchMessage,  ADDR msg
			jmp StartLoop
	ExitLoop:
	
	mov eax,msg.wParam
	ret
WinMain endp

WndProc proc hwnd:DWORD,message:DWORD,wParam :DWORD,lParam :DWORD
		
	LOCAL hdc 				:HDC
   LOCAL x,y			   :DWORD
	LOCAL ps  				:PAINTSTRUCT 
	LOCAL point				:POINT
	LOCAL	rect				:RECT
	LOCAL xcx,ycy,x1cx,y1cy:DWORD

	.if message == WM_SIZE
		 mov		eax,lParam	;cxBlock = LOWORD (lParam) / DIVISIONS 
		 shl		eax,16
		 shr		eax,16
		 mov		ecx,DIVISIONS
		 div		ecx
		 mov		cxBlock,eax
		
		 xor		edx,edx			 
		 mov		eax,lParam	 ;cyBlock = HIWORD (lParam) / DIVISIONS
		 shr		eax,16
 	 	 mov		ecx,DIVISIONS
		 div		ecx
		 mov		cyBlock,eax
		 mov		ebx,eax		 
		 
       ret
   .elseif message == WM_SETFOCUS
       invoke	ShowCursor,TRUE
		 ret
	.elseif message == WM_KILLFOCUS
		 invoke  ShowCursor,FALSE
		 ret
   .elseif message == WM_KEYDOWN 
       invoke  GetCursorPos,addr point
       invoke	ScreenToClient,hwnd, addr point
      
;x = max (0, min (DIVISIONS - 1, point.x / cxBlock))
;y = max (0, min (DIVISIONS - 1, point.y / cyBlock))
		 lea		esi,point
		 mov		eax,[esi]
		 mov		ecx,cxBlock
		 xor		edx,edx
		 div		ecx
		 cmp		eax,DIVISIONS - 1  
		 jl		@f
		 mov		eax,DIVISIONS - 1 
	@@:	 
		 cmp		eax,0
		 jg		@f
		 xor		eax,eax
	@@:	 
		 mov		x,eax
		 
		 mov		eax,[esi+4]
		 mov		ecx,cyBlock
		 xor		edx,edx
		 div		ecx
		 cmp		eax,DIVISIONS - 1  
		 jl		@f
		 mov		eax,DIVISIONS - 1 
	@@:	 
		 cmp		eax,0
		 jg		@f
		 xor		eax,eax
	@@:	 
		 mov		y,eax


  		 mov		eax,wParam
		 .if		eax == VK_UP
		 			dec	y
		 .elseif	eax == VK_DOWN
		 			inc	y
     	 .elseif eax == VK_LEFT
     	 			dec	x
 		 .elseif eax ==  VK_RIGHT
 		 			inc	x
 		 .elseif eax ==  VK_HOME
 		 			xor	eax,eax
 		 			mov	x,eax
 		 			mov	y,eax
 		 .elseif eax ==  VK_END
 		 			mov	eax,DIVISIONS - 1 
 		 			mov	x,eax
 		 			mov	y,eax
 		 .elseif (eax == VK_RETURN) || (eax == VK_SPACE)
 		 	;MAKELONG宏可以将两个16位的无符号数组合成一个32位的无符号数
 		 			mov	eax,xcx
 		 			shl	eax,16
 		 			add	eax,ycy
 		 			invoke SendMessage,hwnd, WM_LBUTTONDOWN, MK_LBUTTON,eax
		.endif
		 mov		eax,x
		 add		eax,DIVISIONS
		 xor		edx,edx
		 mov		ecx,DIVISIONS
		 div		ecx
		 mov		x,edx

		 mov		eax,y
		 add		eax,DIVISIONS
		 xor		edx,edx
		 mov		ecx,DIVISIONS
		 div		ecx
		 mov		y,edx		

;point.x = x * cxBlock + cxBlock / 2 
;point.y = y * cyBlock + cyBlock / 2 

		 mov		eax,x
		 mov		ecx,cxBlock
		 mul		ecx
		 mov		xcx,eax

		 mov		eax,y
		 mov		ecx,cyBlock
		 mul		ecx
		 mov		ycy,eax
		 
	 
		 lea		esi,point
		 
		 mov		eax,cxBlock
		 shr		eax,1
		 add		eax,xcx
		 mov		[esi],eax
		 
		 mov		eax,cyBlock
		 shr		eax,1
		 add		eax,ycy
		 mov		[esi+4],eax
	 ; invoke wsprintf,addr szBuffer,CTEXT("[%d] [%d]"),xcx,ycy
  ;invoke MessageBox,hwnd, addr szBuffer, NULL, NULL		 
	;ClientToScreen (hwnd, &point)
	;SetCursorPos (point.x, point.y)
		 invoke	ClientToScreen,hwnd,addr point
		 lea		esi,point
		 mov		eax,[esi]
		 mov		ebx,[esi+4]
		 invoke	SetCursorPos,eax,ebx

   .elseif message == WM_LBUTTONDOWN
   	 xor		edx,edx
		 mov		eax,lParam
		 shl		eax,16
		 shr		eax,16
		 mov		ecx,cxBlock
		 div		ecx
		 mov		x,eax
		 
		 xor		edx,edx
		 mov		eax,lParam
		 shr		eax,16
		 mov		ecx,cyBlock
		 div		ecx
		 mov		y,eax
		 
		 mov		eax,x
		 mov		ebx,y
		 lea		esi,fState
	 
		 .if		(eax<DIVISIONS)&&(ebx<DIVISIONS)
	 	
		 mov		eax,y
		 mov		ecx,DIVISIONS
		 mul		ecx
		 add		esi,eax
		 add		esi,x
		 xor		BYTE ptr [esi],1
		 
;RECT STRUCT
;  left    dd      ?
;  top     dd      ?
;  right   dd      ?
;  bottom  dd      ?
;RECT ENDS

		 lea		esi,rect
		 
		 mov		eax,x
		 mov		ecx,cxBlock
		 mul		ecx
		 mov		[esi],eax

		 mov		eax,y
		 mov		ecx,cyBlock
		 mul		ecx
		 mov		[esi+4],eax

		 mov		eax,x
		 inc		eax
		 mov		ecx,cxBlock
		 mul		ecx
		 mov		[esi+8],eax

		 mov		eax,y
		 inc		eax
		 mov		ecx,cyBlock
		 mul		ecx
		 mov		[esi+12],eax
		 
		 invoke	InvalidateRect,hwnd,addr rect,FALSE
		 .elseif
		 invoke	MessageBeep,0	
		 .endif
		 ret    	
		.elseif message == WM_PAINT
		invoke	BeginPaint,hwnd,addr ps
		mov		hdc,eax
      
		xor		eax,eax
		mov		x,eax
	Loopx:
		xor		eax,eax
		mov		y,eax
	  Loopy:
	   mov		eax,y
	   inc		eax
	   mov		ecx,cyBlock
	   mul		ecx
	   mov		y1cy,eax
	   push		eax
	   
	   mov		eax,x
	   inc		eax
	   mov		ecx,cxBlock
	   mul		ecx
	   mov		x1cx,eax
	   push		eax
	   
	   mov		eax,y
	   mov		ecx,cyBlock
	   mul		ecx
	   mov		ycy,eax
	   push		eax

	   mov		eax,x
	   mov		ecx,cxBlock
	   mul		ecx
	   mov		xcx,eax
	   push		eax
	   
	   push		hdc
		call		Rectangle
		

		lea		esi,fState
  	 	mov		eax,y
		mov		ecx,DIVISIONS
		mul		ecx
		add		esi,eax
		add		esi,x
		mov		al,[esi]
		.if		(al!=0)
	;invoke	wsprintf,addr szBuffer,CTEXT("[%d][%d][%d][%d]") ,x,y,ecx,ebx
	;invoke	MessageBox,hwnd,addr szBuffer,NULL,NULL					
			invoke	MoveToEx,hdc,xcx,ycy,NULL
			invoke	LineTo,hdc,x1cx,y1cy
			invoke	MoveToEx,hdc,xcx,y1cy,NULL
			invoke	LineTo,hdc,x1cx,ycy
		.endif
		
	  	inc		y
	  	mov		eax,y
	  	cmp		eax,DIVISIONS
	  	jb			Loopy
	  	
		inc		x
	   mov		eax,x
	   cmp		eax,DIVISIONS
	   jb			Loopx
		
      invoke	EndPaint,hwnd, addr ps
        
      ret
	.elseif message == WM_DESTROY
		invoke PostQuitMessage,NULL		
	.endif
UseDefWindowProc:
	invoke DefWindowProc,hwnd, message, wParam, lParam
	ret
WndProc endp

END START

CHECKER2中的WM_KEYDOWN的处理方式决定光标的位置(用GetCursorPos),把屏幕坐标转换为显示区域坐标(用ScreenToClient),并用矩形方块的宽度和高度来除这个坐标。这会产生指示矩形位置的x和y值(5×5数组)。当按下一个键时,鼠标光标可能在或不在显示区域中,所以x和y必须经过min和max宏处理以保证它们的范围是0到4之间。

对方向键,CHECKER2近似地增加或减少x和y。如果是Enter键或Spacebar键,那么CHECKER2使用SendMessage把WM_LBUTTONDOWN消息发送给它自身。这种技术类似于前面一章中把键盘接口加到窗口滚动条时所使用的方法。WM_KEYDOWN的处理方式是通过计算指向矩形中心的显示区域坐标,再用ClientToScreen转换成屏幕坐标,然后用SetCursorPos设定光标位置来实作的。

将子窗口用于命中测试

有些程序(例如,Windows的「画图」程序),把显示区域划分为几个小的逻辑区域。「画图」程序在其左边有一个由图示组成的工具菜单区,在底部有颜色菜单区。在这两个区做命中测试的时候,「画图」必须在使用者选中菜单项之前记住菜单的位置。

不过,也可能不需要这么做。实际上,画风经由使用子窗口简化了菜单的绘制和命中测试。子窗口把整个矩形区域划分为几个更小的矩形区,每个子窗口有自己的窗口句柄、窗口消息处理程序和显示区域,每个窗口消息处理程序接收只适用于它的子窗口的鼠标消息。鼠标消息中的lParam参数含有相当于该子窗口显示区域左上角的坐标,而不是其父窗口(那是「画图」的主应用程序窗口)显示区域左上角的坐标。

以这种方式使用子窗口有助于程序的结构化和模块化。如果子窗口使用不同的窗口类别,那么每个子窗口都有它自己的窗口消息处理程序。不同的窗口也可以定义不同的背景颜色和不同的内定光标。在第九章中,我将看到「子窗口控件」-滚动条、按钮和编辑方块等预先定义的子窗口。现在,我们说明在CHECKER程序中是如何使用子窗口的。

CHECKER中的子窗口

程序7-4所示的CHECKER3程序,这一版本建立了25个处理鼠标单击的子窗口。它没有键盘接口,但是可以按本章后面的范例的方法添加。

程序7-4 CHECKER3

        
CHECKER3.ASM

;MASMPlus 代码模板 - 普通的 Windows 程序代码

.386
.Model Flat, StdCall
Option Casemap :None

Include windows.inc
Include user32.inc
Include kernel32.inc
Include gdi32.inc
Include libc.inc

includelib gdi32.lib
IncludeLib user32.lib
IncludeLib kernel32.lib
IncludeLib msvcrt.lib
include macro.asm
	
	WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
	WndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD
	
DIVISIONS equ	5

.DATA
	szAppName	DB		"Checker3",0
	szChildClass DB	"Checker3_Child",0
.DATA?
	hInstance	DD		?
	hwndChild		DD		DIVISIONS*DIVISIONS dup (?)

	szBuffer		db		100 dup (?)

.CODE

START:   ;从这里开始执行
	invoke   GetModuleHandle,NULL
	mov 		hInstance,eax
	invoke   WinMain,hInstance,NULL,NULL,SW_SHOWDEFAULT
	invoke   ExitProcess,0

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,iCmdShow:DWORD
	LOCAL wndclass :WNDCLASSEX
	LOCAL msg  		:MSG
	local hWnd 		:HWND
	mov wndclass.cbSize,sizeof WNDCLASSEX	
	mov wndclass.style,CS_HREDRAW or CS_VREDRAW	
	mov wndclass.lpfnWndProc,offset WndProc

	mov wndclass.cbClsExtra,0
	mov wndclass.cbWndExtra,0
	
	push hInst
	pop wndclass.hInstance
	
	invoke LoadIcon,NULL,IDI_APPLICATION
	mov wndclass.hIcon,eax	
	
	invoke LoadCursor,NULL,IDC_ARROW
	mov wndclass.hCursor,eax	
	
	invoke GetStockObject,WHITE_BRUSH
	mov wndclass.hbrBackground,EAX
	
	mov wndclass.lpszMenuName,NULL
	mov wndclass.lpszClassName,offset szAppName

	mov wndclass.hIconSm,0
	
	invoke RegisterClassEx, ADDR wndclass
	.if (EAX==0)
		 invoke MessageBox,NULL,CTXT("This program requires Windows NT!"),addr szAppName,MB_ICONERROR 		
		 ret
	.endif

   mov wndclass.lpfnWndProc,offset ChildWndProc    
   mov wndclass.cbWndExtra,sizeof (LONG) 
   mov wndclass.hIcon,NULL
	mov wndclass.lpszClassName,offset szChildClass
	invoke RegisterClassEx, ADDR wndclass

	invoke CreateWindowEx,
					NULL,
					ADDR szAppName, 	;window class name
					CTXT("Checker3 Mouse Hit-Test Demo"), ;window caption
					WS_OVERLAPPEDWINDOW,	;window style
					CW_USEDEFAULT,	;initial x position
					CW_USEDEFAULT,	;initial y position
					CW_USEDEFAULT, 	;initial x size
					CW_USEDEFAULT,	;initial y size
					NULL,	;parent window handle
					NULL,	;window menu handle
					hInstance,	;program instance handle
					NULL	;creation parameters
	mov hWnd,eax
	
	invoke ShowWindow,hWnd,iCmdShow
	invoke UpdateWindow,hWnd
	

	StartLoop:
		invoke GetMessage,ADDR msg,NULL,0,0
			cmp eax, 0
			je ExitLoop
				invoke TranslateMessage, ADDR msg
				invoke DispatchMessage,  ADDR msg
			jmp StartLoop
	ExitLoop:
	
	mov eax,msg.wParam
	ret
WinMain endp

WndProc proc hwnd:DWORD,message:DWORD,wParam :DWORD,lParam :DWORD
		
	LOCAL hdc 				:HDC
   LOCAL x,y,cxBlock,cyBlock :DWORD
	LOCAL xcx,ycy:DWORD   
	.if	  message == WM_CREATE

     	 lea		esi,hwndChild
       xor		eax,eax
       mov		x,eax
     loop1x:
     	 xor		eax,eax
     	 mov		y,eax
     	loop1y:
     	 invoke	GetWindowLong,hwnd,GWL_HINSTANCE
     	 mov		ecx,eax
	;invoke	wsprintf,addr szBuffer,CTXT("%d"),sizeof (LONG) 
	;invoke	MessageBox,hwnd,addr szBuffer,NULL,NULL	     	 
     	 mov		ebx,y
     	 shl		ebx,8
     	 or		ebx,x
		 invoke	CreateWindowEx,NULL,offset szChildClass,NULL,
		 				WS_CHILDWINDOW or WS_VISIBLE,
		 				0, 0, 0, 0,
		 				hwnd,ebx,ecx,NULL
		 mov		[esi],eax
		 add		esi,4
	 
     	 inc		y
     	 mov		eax,y
     	 cmp		eax,DIVISIONS
     	 jb		loop1y
     	 
     	 inc		x
     	 mov		eax,x
     	 cmp		eax,DIVISIONS
     	 jb		loop1x
		     	 
		 ret
	.elseif message == WM_SIZE
		 mov		eax,lParam	;cxBlock = LOWORD (lParam) / DIVISIONS 
		 shl		eax,16
		 shr		eax,16
		 mov		ecx,DIVISIONS
		 xor		edx,edx
		 div		ecx
		 mov		cxBlock,eax
		
		 mov		eax,lParam	 ;cyBlock = HIWORD (lParam) / DIVISIONS
		 shr		eax,16
 	 	 mov		ecx,DIVISIONS
 	 	 xor		edx,edx
		 div		ecx
		 mov		cyBlock,eax

		 lea		esi,hwndChild                                                   
       xor		eax,eax
       mov		x,eax
     loop2x:
     	 xor		eax,eax
     	 mov		y,eax
     	loop2y:
		 mov		eax,x
		 mov		ecx,cxBlock
		 mul		ecx
		 mov		xcx,eax
		 
		 mov		eax,y
		 mov		ecx,cyBlock
		 mul		ecx
		 mov		ycy,eax
;MoveWindow (  hwndChild[x][y],x * cxBlock, y * cyBlock,cxBlock, cyBlock, TRUE)		 
		 invoke	MoveWindow,[esi],xcx,ycy,cxBlock,cyBlock,TRUE

		 ;mov		[esi],eax
		 add		esi,4
		 
     	 inc		y
     	 mov		eax,y
     	 cmp		eax,DIVISIONS
     	 jb		loop2y
     	 
     	 
     	 inc		x
     	 mov		eax,x
     	 cmp		eax,DIVISIONS
     	 jb		loop2x
     	 
       ret

   .elseif message == WM_LBUTTONDOWN
       invoke	MessageBeep,0 
      ret
	.elseif message == WM_DESTROY
		invoke PostQuitMessage,NULL		
		ret
	.endif

	invoke DefWindowProc,hwnd, message, wParam, lParam
	ret
WndProc endp

ChildWndProc proc hwnd:DWORD,message:DWORD,wParam :DWORD,lParam :DWORD
	LOCAL hdc 				:HDC
	LOCAL ps  				:PAINTSTRUCT 
	LOCAL	rect				:RECT	
	.if	  message == WM_CREATE
      invoke	SetWindowLong,hwnd, 0, 0      ; on/off flag
      ret
   .elseif message == WM_LBUTTONDOWN
   	invoke	GetWindowLong,hwnd,0
   	xor		eax,1
      invoke	SetWindowLong ,hwnd, 0, eax
      invoke   InvalidateRect,hwnd, NULL, FALSE
		ret
	
		.elseif message == WM_PAINT
		invoke	BeginPaint,hwnd,addr ps
		mov		hdc,eax
      invoke    GetClientRect,hwnd, addr rect
      lea		esi,rect
      mov		eax,[esi+8]
      mov		ebx,[esi+12]
      invoke 	 Rectangle,hdc, 0, 0, eax,ebx
		
		invoke	GetWindowLong,hwnd,0
		.if	eax != 0
      	lea		esi,rect			                                      
			invoke	MoveToEx,hdc,0,0,NULL
			mov		ebx,[esi+8]	;Right
			mov		ecx,[esi+12];Buttom
			invoke	LineTo,hdc,ebx,ecx
			mov		ecx,[esi+12];Buttom			
			invoke	MoveToEx,hdc,0,ecx,NULL
			invoke	LineTo,hdc,ebx,0
		.endif	
		invoke	  EndPaint,hwnd,addr ps
		ret
	.endif
	invoke	DefWindowProc,hwnd, message, wParam, lParam
	ret
ChildWndProc endp
END START
        

CHECKER3有两个窗口消息处理程序WndProc和ChildWndProc。WndProc还是主(或父)窗口的窗口消息处理程序。ChildWndProc是针对25个子窗口的窗口消息处理程序。这两个窗口消息处理程序都必须定义为CALLBACK函数。

因为窗口消息处理程序与特定的窗口类别结构相关联,该窗口类别结构由Windows呼叫RegisterClass函数来注册,CHECKER3需要两个窗口类别。第一个窗口类别用于主窗口,名为「Checker3」。第二个窗口类别名为「Checker3_Child」。当然,您不必选择像这样有意义的名字。

CHECKER3在WinMain函数中注册了这两个窗口类别。注册完常规的窗口类别之后,CHECKER3只是简单地重新使用wndclass结构中的大多数的字段来注册Checker3_Child类别。无论如何,有四个字段根据子窗口类别而设定为不同的值:

通常,在WinMain中,CreateWindow呼叫建立依据Checker3类别的主窗口。然而,当WndProc收到WM_CREATE消息后,它呼叫CreateWindow 25次以建立25个Checker3_Child类别的子窗口。表7-3是在WinMain中CreateWindow呼叫的参数,与在建立25个子窗口的WndProc中CreateWindow呼叫的参数间的比较。

表7-3

参数

主窗口

子窗口

窗口类别 「Checker3」 「Checker3_Child」
窗口标题 「Checker3...」 NULL
窗口样式 WS_OVERLAPPEDWINDOW WS_CHILDWINDOW | WS_VISIBLE
水平位置 CW_USEDEFAULT 0
垂直位置 CW_USEDEFAULT 0
宽度 CW_USEDEFAULT 0
高度 CW_USEDEFAULT 0
父窗口句柄 NULL hwnd
菜单句柄/子ID NULL (HMENU) (y << 8 | x)
执行实体句柄 hInstance (HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE)
额外参数 NULL NULL

一般情况下,子窗口要求有关位置和大小的参数,但是在CHECKER3中的子窗口由WndProc确定位置和大小。对于主窗口,因为它本身就是父窗口,所以它的父窗口句柄是NULL。当使用CreateWindow呼叫来建立一个子窗口时,就需要父窗口句柄了。

主窗口没有菜单,因此参数是NULL。对于子窗口,相同位置的参数称为子ID(或子窗口ID)。这是唯一代表子窗口的数字。在处理对话框的子窗口控件时,子ID显得更为重要。对于CHECKER3来说,我只是简单地将子ID设定为一个数值,该数值是每个子窗口在5×5的主窗口中的x和y位置的组合。

CreateWindow函数需要一个执行实体句柄。在WinMain中,执行实体句柄可以很容易地取得,因为它是WinMain的一个参数。在建立子窗口时, CHECKER3必须用GetWindowLong来从Windows为窗口保留的结构中取得hInstance值(相对于GetWindowLong,我也能将hInstance的值保存到整体变量,并直接使用它)。

每一个子窗口都在hwndChild数组中保存了不同的窗口句柄。当WndProc接收到一个WM_SIZE消息后,它将为这25个子窗口呼叫MoveWindow。MoveWindow的参数表示子窗口左上角相对于父窗口显示区域的坐标、子窗口的宽度和高度以及子窗口是否需要重画。

现在让我们看一下ChildWndProc。此窗口消息处理程序为所有这25个子窗口处理消息。ChildWndProc的hwnd参数是子窗口接收消息的句柄。当ChildWndProc处理WM_CREATE消息时(因为有25个子窗口,所以要发生25次),它用SetWindowWord在窗口结构保留的额外区域中储存一个0值(通过在定义窗口类别时使用的cbWndExtra来保留的空间)。ChildWndProc用此值来恢复目前矩形的状态(有X或没有X)。在子窗口中单击时,WM_LBUTTONDOWN处理例程简单地修改这个整数值(从0到1,或从1到0),并使整个子窗口无效。此区域是被单击的矩形。WM_PAINT的处理很简单,因为它所绘制的矩形与显示区域一样大。

因为CHECKER3的C原始码文件和.EXE文件比CHECKER1的大(更不用说程序的说明了),我不会试着告诉你说CHECKER3比CHECKER1更简单。但请注意,我们没有做任何的鼠标命中测试!我们所要的,就是知道CHECKER3中是否有个子窗口得到了命中窗口的WM_LBUTTONDOWN消息。

内容很多,继续看下一节>>>>


<<<上一篇
欢迎访问AoGo汇编小站:http://www.aogosoft.com
下一篇>>>