第3章GetWindowText的秘密
GetWindowText函数远比你想象的要复杂。
在GetWindowText函数帮助文档中试图通过简短的文字来解释这个函数的复杂性,如果你无法理解一些长篇大论的文字,那么这种做法无疑是很好的,但简短的文字同样意味着整个内容会变得有些晦涩难懂。
下面,我们就来讲述GetWindowText函数的完整内容。
3.1窗口如何来管理文本
在窗口类中可以通过两种方法来管理文本:既可以让窗口自己进行管理,也可以让系统进行管理。默认的情况是由系统进行管理。
如果窗口类让系统来管理文本,那么系统会进行以下这些工作:
·对WM_NCCREATE消息进行默认的处理:将传递给CreateWindow/Ex函数的参数lpWindowName提取出来,并将这个字符串保存在某个“特殊的位置”。
·对WM_GETTEXT消息进行默认的处理:从“特殊的位置”上提取字符串。
·对WM_SETTEXT消息进行默认的处理:将字符串复制到“特殊的位置”。
如果是由窗口类自己来管理窗口中的文本,那么系统将不会做任何特殊的处理,而是由窗口类来负责响应WM_GETTEXT/WM_SETTEXT消息,并且直接返回/保存字符串。
框架窗口(Frame Windows)通常是由系统来管理窗口中的文本,而自定义控件(Custom control)通常是由它们自己来管理窗口中的文本。
3.2 深入GetWindowText函数
在GetWindowText函数中有一个要求:函数需要迅速地得到窗口文本并且不会被挂起。FindWindow函数需要通过窗口文本来查找窗口。而任务切换(Taskswitching)程序也需要获得窗口文本,以便在切换器(switcher)窗口中显示窗口的标题。某个挂起的程序阻塞其他应用程序的情况是不应该发生的。这就是任务切换程序所要面对的实际情况。
这就要求不应该发送WM_GETTEXT消息,因为WM_GETTEXT的目标窗口可能被挂起。此时,GetWindowText应该从“特殊的位置”上获取文本,因为这种做法不会受到挂起程序的影响。
而另一方面,GetWindowText也用于从对话框的控件中提取文本,而这些控件通常使用的是自定义的文本管理机制。因此,这又要求应该发送WM_GETTEXT消息,因为这是获得自定义管理文本的唯一方法。
于是,在GetWindowText函数中采取了一种折中的方法:
·如果是从同一进程的窗口中得到窗口文本,那么GetWindowText将发送WM_GETTEXT消息。
·如果是从另外一个进程的窗口中得到窗口文本,那么GetWindowText将会在“特殊的位置”上获取字符串,而不是发送消息。
根据第一条原则,如果你想要获得自己进程中的窗口文本,而这个窗口被挂起了,那么GetWindowText函数也会被挂起。不过,因为这个窗口是属于你自己的进程,所以函数被挂起是你自己犯的错误,并且你应该为此负责。发送WM_GETTEXT消息将确保我们能够正确地得到那些使用自定义文本管理方式的窗口(通常是自定义控件)中的文本。
根据第二条规则,如果想要获得另一个进程中的窗口文本,那么GetWindowText将不会发送消息,而只是在“特殊的位置”上获取字符串。通常,使用最多的方式是获取另一个进程中的框架窗口文本,而在框架窗口中一般不会使用自定义的窗口文本管理方式,因此往往能够获得正确的字符串。
而在GetWindowText的帮助文档中将上述讨论内容简化为“GetWindowText无法从另一个应用程序的窗口中得到文本”。
3.3如果不喜欢这些规则,该怎么办
如果你不喜欢第二条规则,例如希望得到另一个进程中自定义控件的文本,那么可以自己发送WM_GETTEXT消息。此时,由于没有使用GetWindowText函数,因此就不受这条规则的约束。
注意,如果目标窗口被挂起,那么你的应用程序将同样被挂起,因为SendMessage函数只有当目标窗口处理完这条消息时才会返回。
同样需要注意的是,由于WM_GETTEXT是在系统消息范围之内(0到WM_USER-1),因此像把当前进程中的缓冲区传送到目标进程以及从目标进程将结果字符串返回到当前进程中等这些操作[这个过程也被称之为列集(marshalling)],就不需要你自己进行特殊的处理。事实上,无论你采取什么样的特殊处理,最终都将是错误的。窗口管理器将自动为你完成列集操作。
3.4能否给出一个说明这种差异的示例
我们来考虑下面这个控件:
SampleWndProc(…)
{
case WM_GETTEXT:
Lstrcpyn((LPTSTR)lParam, TEXT(“Booga!, (int)wParam);
Return lstrlen((LPTSTR)lParam);
case WM_GETTEXTLENGTH: return 7; //lstrlen(“Booga!) + null
…
}
在应用程序A中,我们进行了以下的操作:
Hwnd = CreateWindow(“Sample”, “Frappy”, …);
现在来考虑进程B,这个进程得到了应用程序A创建的窗口句柄(无论通过什么方法):
TCHAR szBuf[80];
GetWindowText(hwnd, szBuf, 80);
上面这段代码将会返回szBuf=“Frappy”,因为这是从“特殊的位置”上获得窗口文本。然而,下面的代码:
SendMessage(hwnd, WM_GETTEXT, 80, (LPARAM)szBuf);
将会返回szBuf=“Booga!”。
3.5为什么GetWindowText的规则如此奇怪
让我们把时光机器(wayback machine)倒回1983年,当时个人电脑中的配置通常是8086的处理器,主频47MHz,两个360K的51/4英寸软盘驱动器(如果你很富有的话,也可能是一个软盘驱动器和一个10MB的硬盘驱动器),以及256KB内存。
这就是Windows 1.0在当时所处的情况。
Windows 1.0是一个协作式多任务(cooperatively multitasked)操作系统,并没有抢先式多任务(preemptive multitasking)的概念。如果程序得到了系统的控制权,那么它可以想占有多久,就占有多久。只有当你调用像PeekMessage或者GetMessage这样的函数时,才会将控制权转交给其他的应用程序。
这是很重要的,因为在缺少硬件内存管理器的情况下,你需要特别确保不会在内存使用上出现问题。
在协作式多任务操作系统中,很重要的一点是,如果你的程序正在运行,那么你就不仅要知道没有其他的程序在运行,而且还要知道每个窗口都会响应消息。为什么?因为如果这些窗口挂起了,那么你将无法得到系统控制权!
这意味着发送消息将总是安全的。你永远都不用担心将消息发送给一个挂起的窗口,因为你很清楚不会存在挂起的窗口。
因此,在Windows 1.0这个简单的系统中,GetWindowText函数是非常直接了当的:
int WINAPI GetWindowText(HWND hwnd, LPSTR pchBuf, int cch)
{
//在简单时期的做法
return SendMessage(hwnd, WM_GETTEXT, (WPARAM)cch, (LPARAM)pchBuff));
}
对于所有的窗口,以上代码无论在什么时候都是有效的。对于不同进程中的窗口并没有进行特殊处理。
当操作系统过渡到Win32时,抢先式多任务的方式强行改变了这个规则,此时,你想要进行通信的窗口可能不会对消息进行响应。
现在,你就遇到了一个向后兼容性的问题。正如前面的描述,系统的许多组件和程序都要求在获取窗口文本时不会遇到挂起的情况。因此,如何才能够实现在获取窗口文本时不会遇到挂起的情况,而同时又可以让一些控件(例如编辑控件等)能够进行自定义的文本管理工作?
在Win32中GetWindowText函数的规则正是为了解决这个相互矛盾的目标而制定的。