使用C#实现移动无边框窗体 2007年05月09日 15:20:04 作者: david_zha0
其实这是个让人说过无数次的内容,但是最近在写一个测试小程序的时候发现了一个问题,今天没什么事做,就做个小的总结。
通过拖动窗体的客户区来移动一个窗体并不是很新鲜的内容,很多的程序都用到了这一点,尤其是一些可以换肤的程序。
这篇文章并不打算详细论述如何在C#下实现这一功能,因为它的代码实在是简单得不能再简单。这里简单说一下实现的原理:
首先说一个概念——窗体的客户区,窗体的客户区指的是一个窗体除了标题栏和边框以外的部分。
当我们的鼠标在窗体中移动的时候,会触发WM_NCHITTEST系统消息,MSDN中对这个消息的说明为:The WM_NCHITTEST message is sent to a window when the cursor moves, or when a mouse button is pressed or released. If the mouse is not captured, the message is sent to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. (当光标移动或一个鼠标键被按下或释放时,WM_NCHITTEST消息会被发送到一个窗口中,如果光标没有被捕获,这个消息被送到光标下的窗口。否则这个消息被送到捕获了光标的窗口。)
这个消息被默认的(请注意是“默认的”)窗口过程(窗口过程这个概念后面再说)处理后,会根据触发这个消息时鼠标的位置返回一个值,例如当鼠标在窗口的标题栏上时,返回HTCAPTION;当鼠标在一个窗口的客户区中时,返回HTCLIENT;如果鼠标指向某个窗口的字窗口的“关闭”按钮或系统菜单(就是点击窗口图标后出现的那个菜单),就返回HTSYSMENU。
所以我们要做的就是骗!我们要欺骗Windows,当我们的鼠标在窗体的客户区中移动时,默认的窗口过程处理后会返回HTCLIENT,Windows系统根据这个值进行相应的操作,把适当的消息插入到应用程序的消息队列(这个概念同样在后面讨论)中。这时如果我们做一些改变,人为地修改窗口过程的返回值,把HTCLIENT修改为HTCAPTION并返回给系统,系统就会认为鼠标这时在窗体的标题栏中,而拖动标题栏可以移动一个窗体,所以当我们在一个被这样修改后的应用程序的客户区按下鼠标并拖动时,Windows会认我们在拖动一个窗体的标题栏,于是它把一个移动窗体的消息插入到程序的消息队列中,再经过窗口过程的处理,就实现了我们需要的功能——拖动窗体的客户区移动窗体。
于是就有了下面的代码:
protected override WndProc(ref message m)
{
switch (m.Msg)
{
case WM_NCHITTEST: //如果鼠标移动或单击
base.WndProc(ref m);//调用基类的窗口过程——WndProc方法处理这个消息
if (m.Result == (IntPtr)HTCLIENT)//如果返回的是HTCLIENT
{
m.Result = (IntPtr)HTCAPTION;//把它改为HTCAPTION
return;//直接返回退出方法
}
break;
}
base.WndProc(ref m);//如果不是鼠标移动或单击消息就调用基类的窗口过程进行处理
}
这也是MSDN上的一个例子,重写窗体的WndProc方法,判断消息然后返回。
我本来也是想这样就完事了,可是后来测试的时候却发现有个大问题:双击窗体的时候,窗体会最大化。
那天脑子有点迷糊,也没多想就到网上问了一下,一个高手提醒我说可能是那个重写的窗口过程的问题,于是一下子明白了。我们向Windows传递了假信息,Windows我们在窗体的客户区双击鼠标在Windows看来是双击了窗体的标题栏,理所当然的Windows会告诉窗口过程窗体需要最大化,窗口过程又理所当然的照做了,于是就出现了这样的现象。
刚开始的时候还不是很确定这样想对不对,不过后来出现的另一个现象证实了这个推断是正确的:
因为窗体没有标题栏,每次退出程序都要按Alt+F4实在是比较不爽,我就给窗体添加了一个ContextMenuStrip,只有一个菜单项用来关闭窗体退出程序。我发现当我修改了窗体的默认窗口过程后,这个右键菜单就显示不出来了。出现这个现象的原因只能有一个——Windows真的把窗体的客户区当成标题栏处理了,而默认ContextMenuStrip只能用在窗体的客户区或控件上。
后来我又跟那位高手讨论了一下,他给我提供了另一种方法,这个方法需要两个API函数:
LRESULT SendMessage(
HWND hWnd,// handle of destination window
UINT Msg,// message to send
WPARAM wParam,// first message parameter
LPARAM lParam // second message parameter
);
BOOL ReleaseCapture(VOID)
SendMessage函数想必不用多说了,无数个木马制作教学贴里都有详细的描述,这个函数用来向指定的窗体发送Windows消息,功能的确十分强大^_^
ReleaseCapture函数相对陌生一些,这个函数用来释放被当前线程中某个窗口捕获的光标。
他给出的代码是这样的:
private void Form1_MouseDown(object sender, MouseEventArgs e)
{
ReleaseCapture();
SendMessage(this.Handle, WM_SYSCOMMAND, SC_MOVE + HTCAPTION, 0);
}
也就是说,在窗体的MouseDown(鼠标按下)事件中,首先释放光标,然后向窗口过程发送WM_SYSCOMMAND消息。
说实话这个方式的SendMessage函数调用我也是第一次遇到,刚开始怎么也不明白为什么要把它们(SC_MOVE 和 HTCAPTION)用一个加号连接起来。
这个WM_SYSCOMMAND消息包含了一些信息表示用户要进行的操作,消息的wParam字段表示用户要最大化、最小化或移动窗体。
我去掉HTCAPTION试了一下,每当我在窗体上按下鼠标左键时,鼠标就会移动到窗体的最顶端——原来标题栏的位置,与我们在系统菜单中选择“移动”菜单项的效果一样。于是查阅MSDN,在WM_SYSCOMMAND的帮助中查到这样一句话:In WM_SYSCOMMAND messages, the four low-order bits of the uCmdType parameter are used internally by Windows. To obtain the correct result when testing the value of uCmdType, an application must combine the value 0xFFF0 with the uCmdType value by using the bitwise AND operator. (在WM_SYSCOMMAND消息中,uCmdType的低四位是Windows内部使用的。如果测试时要获得uCmdType的正确结果,应用程序必须把0xFFF0这个值与uCmdType的值通过位相加的方式合并在一起。)于是豁然开朗:这个所谓的“Windows内部使用的值”在移动这个窗体的时候其实就是表示“窗体的标题栏”,所以我们使用SC_MOVE + HTCAPTION表示窗体将被移动且目前鼠标正在窗体的标题栏中。
至于这里为什么要先释放光标再发送消息,根据前面的WM_NCHITTEST消息的说明,如果光标被当前窗口捕获,那么当鼠标移动时,就会发送WM_NCHITTEST到我们的窗体,而由于我们这时没有重写窗口过程,这个消息会被按照默认的方式处理,Windows会发现鼠标在窗体的客户区移动,SendMessage发过去的HTCAPTION被WM_NCHITTEST消息中的HTCLIENT覆盖,这样窗体就不会有任何动作。
还有两个名词解释:
窗口过程:窗口过程是一个应用程序定义的、用来处理Windows消息的回调(CallBack)函数,当有消息发生时,窗口过程负责处理消息。
消息队列:Windows会把有关一个应用程序的全部消息放到一块内存区域中,这个区域使用一种先进先出(FIFO)的方式工作,就是说最先进入这个区域的消息会第一个离开这个区域,故称消息队列。应用程序从消息队列中取出一条消息,进行相应的处理,返回一定的信息,然后再取出下一条处理。
PS:我手头只有一份Win32 Developer's References,是以前从一张Delphi的碟里扒下来的。里面的说明不是很详细,有份Platform SDK就好多了,55555~~~~