本文转自:http://www.vckbase.com/index.php/wv/1645
Raymond Chen是微软最早会讲故事的工程师,我在美国的时候有幸能够跟他请教窗口管理器,这在我的编程生涯中起到了很大的作用,之后我将交谈的内容仔细的整理后,才写出这篇文章。在讨论窗口管理之前,我们先讨论窗口管理器中的一些基本设计要点,然后再通过一些代码来说明各种类型的功能,最后,利用Windows模态循环的设计思想来实现一些漂亮的技巧。
1、为什么会收到伪WM_MOUSEMOVE消息
我们知道,当鼠标移动的时候,鼠标硬件会报告一个中断,然后Windows来确定将由哪个窗口来接受这个消息。同时在这个线程的输入队列中设置一个标记量,用来标记“鼠标已经移动”。当然,同时还会发生其他的动作,但是我们现在暂时不考虑。
当线程调用GetMessage函数,并且标记量的位置也已经被设置,这时候Windows就会去找该由哪个窗口来接受消息。这时候就会有一个问题:当鼠标快速移动的时候,程序会获取到所有的鼠标移动消息么?
当调用GetMessage比较慢,那么,可能在此之前就已经有很多次鼠标移动消息被发送过来了,但是Windows接收这个消息的操作仅仅是在队列中设置一个标记量,那么,从第一次接收消息之后,每一次接收都是只做 “将标记量赋值”的操作。因此,从第一次接收消息后的所有消息都可以算是“丢失”了。
好了,现在我们回到最开始的问题“为什么会接收到伪WM_MOUSEMOVE消息?”
注意,当鼠标没有移动的时候,Windows也会进行一些工作,而这些工作通常被认为是鼠标移动的一部分。最典型的例子就是:当一个窗口被显示或者隐藏的时候,鼠标正好覆盖在窗口上面,而动作发生时鼠标的光标将会被重新计算(可能原来鼠标光标是箭头,当窗口被显示或被隐藏之后就变成了手形)。此时,Windows就人为地设置了一次鼠标移动消息,其中就包括了伪WM_MOUSEMOVE消息。因此,如果程序想要检测鼠标是否移动了,就需要在WM_MOUSEMOVE响应函数中再加上一个判断,即判断鼠标位置与上一次接收消息的位置是否相同。
2、为什么没有WM_MOUSEENTER;
在Windows中有WM_MOUSEMOVE和WM_MOUSELEAVE,为什么却没有WM_MOUSEENTER?
其实很简单:当收到一个WM_MOUSELEAVE的消息时,你可以设置一个标记量用来标记鼠标是否移动到窗口外部。当接收WM_MOUSEMOVE消息时,只要检测标记量发生改变,就表示鼠标移动到了窗口内部,然后在这个判断中写入鼠标进入窗口相应的代码。
3、GetDesktopWindow()有什么特殊的地方
由GetDesktopWindow()函数返回的窗口是很特殊的,但是很多程序中都不恰当地用到了这一句话。
如果将这个函数的返回值作为某个窗口(例如m_MyDlg)的父窗口,当m_MyDlg弹出一个模态对话框时(例如MessageBox),此时所有的窗口都将无法激活。因为任何应用程序窗口都是GetDesktopWindow()的子窗口或者孙窗口,这就把所有的窗口关联到了一起。但是实际上却没有这种错误,为什么?
对话框管理器会检测你是否使用GetDesktopWindow()的返回值作为一个窗口的父窗口。如果是,就会改成把NULL赋值过去。这样以来就是创建一个没有父窗口的窗口。
4、禁用,激活窗口的顺序
如果你想手动显示一个模态对话框,而不实用MessageBox之类的函数,那么窗口禁用,激活的顺序就显得很重要了。
首先,必须禁用模态对话框的父窗口,然后激活模态对话框,当模态对话框显示完毕时,这时候你有可能会按照这样的方式来进行清理工作:
禁用模态对话框。
激活模态对话框的父窗口。
如果真的这么做了,你将发现前台焦点会变得混乱,哪怕是禁用后手动设定焦点在模态对话框的父窗口上也不行,获得焦点的窗口依然是随机窗口。为什么?
其实,当销毁模态对话框的时候,系统将焦点交还给模态窗口的父窗口,但此时父窗口是被禁用状态,系统之后绕开父窗口再找其他未被禁用的窗口。
所以,销毁模态对话框的顺序应该是:
激活父窗口。
销毁模态对话框。
5、界面模态与代码模态
从用户的角度来看,模态指的是这种情况:当用户开始一个任务时,无法进行其他任何操作,直到完成或取消这个任务。
从程序的角度来看,模态可以被视为一个函数:这个函数执行某些包含界面的操作,直到这个操作完成才返回。换句话说:模态是一个内嵌的消息循环,直到满足某个条件才会停止消息响应。
你可以创建某个界面模态,而在内部代码编写时使用非模态的函数。例如:
01.
#include < MYDLG.H >
02.
HWND
g_wnd;
03.
UINT
g_uMsgXXX;
04.
Void CreateMyDlg(
HWND
hwnd)
05.
{
06.
……
07.
If(g_wnd)
08.
{
09.
hwnd.EnableWindow(FALSE);
10.
}
11.
{
12.
Void OnFinishTask(Hwnd hwnd)
13.
{
14.
……
15.
hwnd.EnableWindow(TRUE);
16.
DestroyWindow(g_hwnd);
17.
g_hwnd = NULL;
18.
}
运行这个程序后,除了响应OnFinishTask函数之外或者关闭MyDlg对话框才能与主窗口进行交互。从用户角度来开,g_hwnd窗口是模态的,但是从代码的角度来看,对话框是按照非模态的方式组织的:对话框没有自己的消息循环,而是由主窗口循环在必要的时候派发对话框的消息。
6、为模态窗口设置正确的父窗口
当你准备显示某个模态窗口时,很重要的一点就是给这个模态窗口设置一个正确的父窗口。如果你忽略了这条规则,那么可能会得到这样的结果:
01.
void
OnChar(
HWND
hwnd,
TCHAR
ch,
int
cRepeat)
02.
{
03.
Switch(ch)
04.
{
05.
case
‘ ‘:
06.
MessageBox(NULL, TEXT(”Title”), MB_OK);
07.
if
(!IsWindow(hwnd)) MessageBeep(-1);
08.
break
;
09.
}
10.
}
运行这个程序,按下空格弹出一个对话框,不要关闭这个对话框,而是点击主窗口的 关闭按钮,在程序退出之前会有一个“嘟嘟”声。这是因为MessageBeep函数告诉我们遇到了一个无效的窗口句柄。在实际程序中,窗口的状态保存在每个窗口实例变量中,这种情况下,因为窗口被销毁时,所有的窗口实例变量都消失了。在上面的程序中,窗口是在一个嵌套的模态循环中被销毁的。因此程序控制返回到调用者的时候,就是在调用一个已经被销毁的对象的方法。
当你清理窗口的时候,通常要销毁所有窗口中的数据。不过要注意,OnChar函数依然需要使用到这些正在被释放的数据。最终当程序的控制权回到OnChar函数时,它所使用的是一个无效的实例指针。这是由于没有为模态窗口设置正确的父窗口造成的。在窗口并不希望改变状态时,用户却可以与主窗口进行交互。
下面是一个非常简单的修正:
1.
MessageBox(hwnd, TEXT(“Message”),TEXT(“Title”), MB_OK);
因为MessageBox是模态窗口,当它显示时,父窗口将被禁用,这就防止了用户销毁窗口或者改变父窗口的状态。这就是为什么那些可能需要显示界面的函数会将窗口句柄作为函数的参数之一。函数需要知道是哪个窗口可以被用作为对话框的父窗口或其他的模态操作。
如果你需要在一个模态窗口中显示另外一个模态窗口,那么第二个模态窗口初始化时,应该传递第一个模态窗口句柄而不是主窗口句柄。如果错误的传递了主窗口的句柄,那么用户可以关闭第一个窗口,但是第二个窗口仍然在显示。
7、WM_QUIT消息与模态
使用模态的技巧是:
(1)、在调用一个模态函数时,消息分发的工作将有模态函数完成而不是主程序。因此如果你自定义了主程序的消息泵,当你把控制权交给模态循环时,这些自定义的东西会消失。
(2)、如果调用PeekMessage或者GetMessage函数得到一个WM_QUIT消息,就必须重新生成一个WM_QUIT消息(通过PostQuitMessage函数)并抛给外层消息层。否则,程序看上去像是无法执行到关闭代码。
以下代码演示了如何在模态循环中重新生成WM_QUIT消息并发送给外层消息层:
01.
BOOL
Function(
void
)
02.
{
03.
MSG msg;
04.
BOOL
fResuit = TRUE;
//假设模态循环已经开始工作
05.
while
(!SomethingFinished())
06.
{
07.
If(GetMessage(&msg, NULL, 0, 0)
08.
{
09.
TranslateMessage(&msg);
10.
DispatchMessage(&msg);
11.
}
12.
else
13.
{
14.
PostQuitMessage(msg.wparam);
15.
fResult = FALSE;
16.
break
;
17.
}
18.
}
19.
return
fResult;
20.
}
如果在Function与程序之间还有其他模态循环消息层,那么所有这些消息层都会收到WM_QUIT消息,并执行各自的清理工作,最后在退出循环之前再抛出WM_QUIT消息。在这种方式中,WM_QUIT消息将从一个模态循环传递到另外一个模态循环,直到达到最外层消息循环,最后退出程序。
当然,如果你能控制程序中的每一个模态循环,就可以不用这么麻烦,只需要定义一个全局变量g_fQuit用来标记是否退出程序。但事实上你做不到。例如:当调用DialogBox函数时,对话框将运行自己私有的模态循环来显示对话框界面。只有当调用EndDialog函数时,对话框才会结束,此外当用户点击程序菜单时,Windows将运行私有的模态循环来显示菜单上的界面。
显然,Windows并不知道在你程序中的g_fQuit变量,因此当你退出程序时,Windows可能并不知道。而WM_QUIT消息的作用就是协调系统中各个独立部分的退出操作。