WIN98特殊窗口的动态拖动
宋立波
WIN98中常居顶层的无标题条窗口是一种特殊的窗口,典型实例有IME输入法应用程序、UCWIN平台、各种浮动工具箱、OFFICE桌面工具栏等。
一、命令检测与光标动态提示
这种窗口拖动一般分为两种:特定客户命令区域和非特定客户命令区域。特定客户命令区域是指利用"RECT"定义的特定子矩形区域;非特定客户命令区域是指没有明确定义的窗口客户区域部分,即所有特定客户命令区域之外的部分。实现该功能的首要问题是如何检测和处理特定客户命令区域和非特定客户命令区域内的鼠标命令,以及如何利用鼠标光标来动态提示用户何时可以进行窗口的拖动操作。
1、特定客户命令区域检测鼠标命令
要在窗口中设置实现拖动功能的图标命令按钮,就必须在资源文件中定义命令按钮的特定客户区域,该区域一般也就是命令按钮图标的矩形区域,这个区域的定义方法为"RECT DragRT",其中DragRT为定义的检测鼠标命令的矩形区域,它用DragRT.LEFT、DragRT.TOP、DragRT.RIGHT和DragRT.BOTTOM四个参数来描述矩形区域相对于窗口客户区域左上角的相对坐标值。也可用"SETRECT"函数填充。
窗口函数在处理鼠标消息WM_LBUTTONDOWN时,当收到系统传递的鼠标位置参数lParam后,通过MAKEPOINT()函数将其转换为窗口坐标值,然后利用判断某坐标点是否位于特定矩形区域内的函数PtInRect(),判断出鼠标指针是否点击在拖动命令按钮之内。
2、非特定客户命令区域检测鼠标命令
当窗口应用程序中采取了非特定客户命令区域拖动方法时,必须在资源文件中事先确定各个特定客户命令区域的矩形坐标,这时非特定客户命令区域是不规则的区域,它需要根据实际的应用程序窗口及各个命令按钮矩形区域来确定,也就是各个命令按钮矩形区域相对于窗口矩形区域的“非”子集。窗口函数在处理鼠标消息WM_LBUTTONDOWN时,首先利用函数PtInRect()判断当前鼠标指针是否点击在各个命令按钮矩形区域内,如果未点击在任何命令按钮区域内,则可确定鼠标点击在非特定客户命令区域内,从而启动窗口拖动功能。
3、窗口拖动功能的光标动态提示
实现鼠标光标动态提示功能前需要定制鼠标光标形状,窗口拖动功能的动态提示光标形状一般为四箭头图案,这可以利用微软公司的SDK、FPT3.0和VC++4.1等高级开发软件中的资源编辑器"IMAGE EDIT"等来实现。光标资源文件一般为32X32的2色或16色.CUR图形文件。建立起自己的鼠标光标资源文件后,首先需要在应用程序的资源文件中定义鼠标光标,然后在应用程序中利用LoadCursor()调入,并在类中进行设置:wc.hCursor=hCurm。
当鼠标光标需要在窗口的特定客户命令按钮区域内或非特定客户命令区域内进行动态提示时,不能使用上述定义方法,而需要在窗口函数处理WM_MOUSEMOVE消息时进行特殊处理:首先判断鼠标光标指针的当前位置是否在拖动命令按钮或非特定客户命令区域内,如果鼠标指针位置满足拖动窗口功能区域的要求,则利用API函数SETCURSOR()改变鼠标光标图案,提示用户此时可以进行窗口拖动操作,并将鼠标输入控制权交给当前窗口,同时设置改变后的鼠标光标标志;当鼠标指针移出拖动窗口命令启动区域时,恢复原来鼠标光标图案同时释放鼠标输入焦点控制权,并清除鼠标光标动态提示标志单元。
二、动态拖动框的定制
窗口拖动前的关键问题是鼠标拖动窗口过程中的拖动框显示与擦除功能实现。窗口拖动虚框就是在WINDOWS 整个屏幕区域内显示被拖动窗口大小的线框,它的大小需要根据被拖动窗口的矩形区域大小和实际需要来具体确定,一般为被拖动窗口的矩形区域大小。
WINDOWS系统中的绘图方法是通过显示设备描述表实现的,绘图操作需要占用一定的GDI资源。WINDOWS 95中的GDI资源要比WINDOWS3.X中的GDI资源大得多,所以系统可以为窗口、菜单、对话框、字体和各种绘图函数分配足够的GDI资源。WINDOWS中有两种使用显示设备描述符表的方法:更新窗口显示客户区域和直接操作窗口显示客户区域。更新窗口显示客户区域是直接针对应用程序窗口矩形区域而言的,在窗口函数响应WM_PAINT消息时利用图形操作命令进行窗口更新处理:
InvalidateRect(hWnd,&WinRECT,TRUE);//WinRECT为要更新区域
UpdateWindow(hWnd);
窗口初始建立时默认为更新窗口的全部区域,当要更新的矩形区域为NULL时表示更新窗口所有矩形区域。函数UpdateWindow()通知系统向要更新矩形区域的窗口发送WM_PAINT消息,窗口函数接收到WM_PAINT消息后首先利用BeginPaint()函数取得设备描述符表,然后利用图形命令直接对显示设备进行更新操作,最后利用EndPaint()函数通知系统更新操作结束。
更新窗口矩形区域直接使用窗口类中定义的屏幕画刷,即使利用SelectObject()函数选择相应屏幕画刷也无效,而且更新矩形区域范围是通过InvalidateRect()函数累加的,由UpdateWindow()函数通知系统开始进行窗口更新操作。整个过程是由系统来调度的,因此使用这种方法无法实现窗口的拖动虚框绘制和实时操作。
直接操作窗口客户区域的方法是利用GetDC()函数直接取得显示设备句柄,利用各种图形操作命令直接对显示设备进行绘图。它使用屏幕当前设置的画笔和画刷来实现各种图形绘制操作,无须向系统传递任何消息应用程序就可以实时地对屏幕窗口进行更新和绘图操作。其操作过程是首先取得显示设备描述符句柄hDC=GetDC(hWnd),当hWnd 参数为NULL时取得的是整个屏幕的设备描述符表句柄,然后利用SelectObject()函数设置当前屏幕的画笔和画刷,利用各种画图函数完成屏幕的绘图操作,最后利用ReleaseDC()函数释放获取的设备描述表。如果利用画矩形函数Rectangle()实现虚框,那么在设置当前屏幕画笔的同时必须使用SelectObject(hDC,GetStockObject(NULL_BRUSH))屏蔽掉任何屏幕画刷,否则WINDOWS程序会很快吞筮掉所有GDI资源,相当于在屏幕设备资源中增加了无数矩形区域。
对于窗口拖动框的擦除操作,只需在拖动框绘制函数中将屏幕的图形画笔操作方式设置为R2_XORPEN异或方式,即SetROP2(hDC2,R2_XORPEN)。在拖动框绘制结束时注意恢复,在窗口拖动框移动到下一个位置前,在原屏幕位置重新调用绘制函数一次将原来拖动框擦除(见程序1)。
//利用画矩形函数实现拖动实框(由于篇幅画线、画点函数省略)
void DrawMoveRect(int xx1,int yy1,int xx2,int yy2,int xy)
{ HDC hDC;
int oldrop2,m,k;
hDC = GetDC(NULL); //取得全屏幕设备描述句柄
oldrop2= GetROP2(hDC); //取得原来屏幕画图方式
SetROP2(hDC,R2_XORPEN); //设置异或屏幕画图方式
SelectObject(hDC,GetStockObject(NULL_BRUSH));//屏蔽画刷
SelectObject(hDC2,GetStockObject(WHITE_PEN));//选择画笔
for (k=0;k xx1-=1;xx2+=1;
yy1-=1;yy2+=1;
Rectangle(hDC2,xx1,yy1,xx2,yy2);
}
SetROP2(hDC2,oldrop2); //恢复原来作图方式
ReleaseDC(NULL,hDC2); //释放设备描述符表
}
(程序1)
三、动态拖动窗口“三步曲”
完成了对WINDOWS 高级窗口的客户区域拖动命令判断、拖动功能的鼠标光标动态提示和定制窗口拖动框函数之后,就需要实现整个拖动方案中的拖动过程启动、窗口拖动框移动和拖动结束处理的三步曲过程。于是必须在窗口函数中直接处理WM_LBUTTONDOWN、WM_MOUSEMOVE和WM_LBUTTONUP消息。下面来具体处理上述三个步骤中的细节问题。
第一步,在窗口函数中对鼠标点击消息WM_LBUTTONDOWN进行判断,以处理用户通过鼠标光标动态提示功能获取满足窗口拖动条件时,按下鼠标左键产生的启动拖动过程消息。其功能性代码如下:
case WM_LBUTTONDOWN:
pt = MAKEPOINT(lParam);
if(PtInRect(&DragRT,pt)){
DragBegin((LPRECT)&WinRT,lParam,hWnd,2);//启动过程
} else {//进行其它处理;}
上述DragBegin()函数为笔者开发的窗口拖动启动函数。由于一个高级窗口应用程序中往往存在很多窗口,所以将其作为一个单独函数处理。其中WinRT为高级窗口矩形区域,这里作为拖动框矩形区域参数来传递,lParam为鼠标光标指针长整数,hWnd为当前被拖动窗口的句柄,2 为拖动框宽度。这时,需要同时将鼠标控制权交给当前被拖动窗口、设置拖动窗口标志单元、保存当前鼠标在屏幕上的位置并显示被拖动窗口的拖动框。拖动功能启动函数的源代码如下:
void DragBegin(LPRECT WinRect, //拖动框的矩形区域
LPARAM lParam, //鼠标光标当前指针
HWND hwnd, //当前窗口句柄
unsigned int kk) //拖动框显示的宽度
{SetCapture(hwnd); //拖动时窗口必须具有鼠标输入权
MoveFlag=TRUE; //设置拖动标志
oldmx=LOWORD(lParam);//记录当前鼠标屏幕坐标X
oldmy=HIWORD(lParam);//记录当前鼠标屏幕坐标Y
DrawMoveRect(WinRect->left,WinRect->top,//显示拖动框
WinRect->right,WinRect->bottom,kk);
}
第二步,需要处理鼠标拖动窗口时的拖动框移动过程,这需要在窗口函数中进行WM_MOUSEMOVE消息处理。拖动框的移动包括上次显示拖动框的清除和本次拖动框的显示两步。由于拖动框绘制函数中对当前的绘制方式进行了重新设置,异或方式使得只要重新在原屏幕坐标位置处调用一次该函数即可清除拖动框,因此,在鼠标拖动窗口移动过程中显示和清除拖动框只需要调用两次拖动框绘制函数即可。另外,拖动框在屏幕上位置的计算方法也非常简单,就是将当前取得的屏幕位置坐标值减去保存的前次屏幕位置坐标值所得鼠标移动偏移量,再用原来窗口屏幕左上角坐标值加上这个偏移量,就可以确定被拖动窗口和拖动框新的屏幕位置坐标值。
case WM_MOUSEMOVE:
if(MoveFlag==TRUE){
DragMove((LPRECT)&WinRT,WinWT,WinHi,lParam,2);
} else {//进行其它处理;}
鉴于高级窗口应用程序一般为多个子窗口,所以将拖动框移动处理过程单独编制成函数,并且对鼠标拖动窗口过程中窗口不能完全位于屏幕可见区域内进行了特殊处理。开发者可根据需要自行调整其位置,以便被拖动的窗口能够完全被显示于屏幕可视区域内。其拖动过程函数源代码部分如下:
void DragMove(LPRECT rcwin, //拖动框矩形区域
unsigned int wi, //被拖动窗口宽度
unsigned int hi, //被拖动窗口高度
LPARAM lParam, //鼠标位置指针
unsigned int kk) //拖动框边框宽度
{ DrawMoveRect(rcwin->left,rcwin->top,
rcwin->right,rcwin->bottom,kk);//清除上次画拖动框
rcwin->left+=LOWORD(lParam)-sImeG.oldmx;//计算窗口
rcwin->top+=HIWORD(lParam)-sImeG.oldmy; //新位置
sImeG.oldmx=LOWORD(lParam); //保存当前坐标值
sImeG.oldmy=HIWORD(lParam);
if (rcwin->left<0) rcwin->left=0;//对窗口超越屏幕
if (rcwin->left>sImeG.xScrWi-wi) //可视区域处理
rcwin->left=sImeG.xScrWi-wi;
ii=sImeG.yScrHi-hi-(sImeG.WinVer<0x35f ? 0:BOTOFF);
if (rcwin->top<0) rcwin->top=0; //对WIN95进行底部
if (rcwin->top>ii) rcwin->top=ii;//特殊保留处理
rcwin->right =rcwin->left+wi-1;
rcwin->bottom=rcwin->top+hi-1;
DrawMoveRect(rcwin->left,rcwin->top,
rcwin->right,rcwin->bottom,kk);//画新位置拖动框
}
第三步,在鼠标拖动窗口结束时需要进行窗口的实际移动处理,这就需要在处理WM_LBUTTONUP消息时利用MOVEWINDOW()命令进行实际移动处理。同样鉴于多窗口原因仍然需要将这个处理过程单独形成一个函数,而且在移动窗口前还需要利用绘制函数清除屏幕上所画的拖动框。如果窗口未完全位于屏幕的可见位置,还必须进行适当调整使被拖动的窗口能够完全位于屏幕可视区内,同时释放鼠标控制权并清除拖动窗口标志单元。结束过程的描述性代码部分如下:
case WM_LBUTTONUP:
if (sImeG.MoveFlag==TRUE){//拖动标志有效
DragEnd((LPRECT)&WinRT,WinWT,WinHI,hWnd);}
拖动结束处理函数的源代码部分如下:
void DragEnd(LPRECT rcwin,//拖动框矩形区域
unsigned int wi, //被拖动窗口宽度
unsigned int hi, //被拖动窗口高度
unsigned int kk) //拖动框边框宽度
{ DrawMoveRect(rcwin->left,rcwin->top,
rcwin->right,rcwin->bottom,1); //清除拖动框
if (rcwin->left<0) rcwin->left=0;//对窗口超越屏幕
if (rcwin->left>sImeG.xScrWi-wi) //可视区域处理
rcwin->left=sImeG.xScrWi-wi;
ii=sImeG.yScrHi-hi-(sImeG.WinVer<0x35f ? 0:BOTOFF);
if (rcwin->top<0) rcwin->top=0; //对WIN95进行底部
if (rcwin->top>ii) rcwin->top=ii;//特殊保留处理
rcwin->right =rcwin->left+wi-1;
rcwin->bottom=rcwin->top+hi-1;
MoveWindow(hwnd,rcwin->left,rcwin->top,
wi,hi,TRUE); //将窗口实际移到新位置
sImeG.MoveFlag=FALSE; //清除拖动标志单元
ReleaseCapture(); //释放鼠标控制权
}
至此,整个窗口的动态拖动过程就完成了。有兴趣的读者可以去"bobosong.yeah.net"主页下载推荐的软件来看看这一功能的实现。
(作者地址:辽宁省铁岭市委办公室 112000 收稿日期:1999.2.1)