在 Windows 系统中,窗口文字(Window Text)这一术语针对不同类型的窗口会有不同的含义。通常来说,窗口文字是指窗口的标题文字。而对于某些系统预定义的控件而言,则需要分别对待。最具代 表性的控件是编辑框,在这里,窗口文字指的是显示在其编辑区中的内容,而不是该控件的标题。
Windows 系统对于窗口文字的操作提供了两种途径,一种是调用系统 API,另外一种是向窗口发送消息。获取窗口文字的 API 是 GetWindowText(),获取窗口文字的消息是 WM_GETTEXT;相对应的,设置窗口文字的 API 是 SetWindowText(),设置窗口文字的消息是 WM_SETTEXT。需要注意的是,这两种途径达到的效果并不完全一致。
我们先来审视一下 API 方式。根据 MSDN 文档,API 方式在目标窗口与调用者处于同一进程中的时候与对应消息的作用是等价的。但是当目标窗口处于另外的进程中的时候,情况就变得微妙起来。因此,除非特别指 出,下文中讨论的将都是针对其他进程中的窗口进行的操作测试。
首先,文档宣称,API 对于处于另外的进程中的控件是无效的,如果想对另外的进程中的控件的文字进行操作,则应该使用发送消息的方式。“控件”这个词在 Windows 世界里已经被用滥了,我本人的理解是指:经过完整封装的具有独立的可重复使用的功能的窗口,通常以子窗口的形式出现。在这里,请不要把 COM 里(或者别的什么,比如 Delphi 里)的“控件”的概念与此混淆。程序员可以开发自己的控件,就好像微软开发通用控件库(comctl32.dll)一样。不过在本文中,我们可以把控件的 涵盖范围缩小一些,尤指系统的几个预定义控件:按钮、编辑框、列表框、组合框、静态文本框,以及丰富文本(rich edit)控件。其他还有几个,但一般不涉及到文字,所以从略。其次,如果 API 要操作的目标窗口没有标题栏(WS_CAPTION 风格控制),则 API 会返回一个空串。
接下来再看一下消息。无疑,消息的功能要比 API 更强一些,不会受到进程之别的限制。不过,如果你仔细查看文档,会发现消息也有越轨的时候,对于 WM_GETTEXT 消息,文档中又特别注明,在 Windows 2000 之前的操作系统中,如果你向非文本内容的静态文本控件(这是因为静态文本控件除了可以显示静态文本之外,还可以用来显示位图、图标等其他内容)发送此消 息,则其实你会得到该控件的 ID。而且如果你使用 WM_SETTEXT 为 SS_ICON 风格的静态文本控件设置了图标,则 WM_GETTEXT 消息会得回来你所设置的图标句柄,放在你所指定的缓冲区的前四个字节里。
然而,这还不是主要的问题,因为毕竟文档中已经说的够明白了。还有一些陷阱和谬误是微软的文档中所没有提及到的。微软开发的丰富文本控件就是这样一个泥沼。一个偶然的机会,我发现我自己的一个工具在遍历系统内所有窗口的时候会崩溃,经过调试才发现,崩溃的代码恰恰是要获取一个丰富文本控件的窗口文字。
平心而论,在向指定的缓冲区中获取某些数据的规则一致性方面,微软做得还是相当棒的,通常总是有以下要求:
1、指定缓冲区的地址
2、又指定缓冲区的大小
如果是涉及到文字的操作,还会有以下附加的惯例:
3、缓冲区的大小通常是以 TCHAR 的个数来计算
4、返回值通常是 TCHAR 的个数
5、输入时指定的缓冲区大小已经将字符串结束符包含在内
6、返回的被操作文字数目不包括字符串结束符在内
可是这次让我感到了前所未有的挫折,因为我自信代码是按照上述规则写的,但却总是遇到缓冲区溢出的问题。丰富文本控件简直就好像完全不理会你传入的缓冲区大小。
预先取得该窗口的文字的长度然后分配适当大小的缓冲区看起来是一个解决方案,但是也有问题。第一个问题是当你取得文字长度后,按照该长度分配缓 冲区,如何保证在真正去取文字的这一操作发生之前目标窗口的内容不会发生变化?举例来讲,假如目标窗口是一个聊天工具的输出窗口,当取文字长度是窗口里的 内容尚且是“甲:你好。”,但极有可能取内容的时候已经变成了“甲:你好。乙:我很好。你呢?”。第二个问题是,我写的仅仅是一个小工具,而对于丰富文本 控件这种可能包含巨大数据量的控件来讲,根本没有必要把所有的内容全部复制过来。
在经过好多天的心理折磨之后,我突然注意到了那个丰富文本控件的窗口类名是 RichEdit20W,显然是一个以 Unicode 字符为基础的控件。而我的应用程序并未编译为 Unicode 版本,难道在字符串转换的过程中出了问题?抱着试试看的态度创建了一个 RichEdit20A 为类名的窗口,惊喜地发现即使缓冲区不够大,崩溃也不会发生。当然也不是完全激动人心,因为这个控件只要发现缓冲区不够,连一个字符都不会返回来。
这个测试并不能解决前面遇到的问题,因为我们不可能阻止别人去创建 RichEdit20W 类的窗口,但毕竟得到了一些解决问题的线索。功夫不负有心人,我最后想到了 SendMessageW() 函数,揣度用它发送 WM_GETTEXT 消息可能会得到正确的结果。经过测试,果然不出所料,一切正常!当然,另一件不出所料的事情是,它返回来的内容是 Unicode 形式的。不过这已经不是可以阻挡我们前进步伐的羁绊了。
为了搞清楚这个问题究竟是丰富文本控件这一个例,还是普遍行为,我又用编辑框做了测试,结果发现了同样的问题,只要是对 Unicode 窗口进行 Ansi 版本的函数调用,就会出现问题。令人疑惑的是,好像仅当存在中英文混合的时候才出现问题,但由于时间关系,我并没有深入考查。
可以得出的结论是,我们应该判断目标窗口的类型,从而相应地调用正确版本的获取窗口文字的函数。但即使如此,也还是需要万分小心。因为依然存在 别的实现上有缺陷的窗口。比如说由输入法创建的 MSCTFIME UI 类的窗口,在响应用 SendMessageW 发送的 WM_GETTEXT 消息时,又一次打破了游戏规则。该类窗口的标题通常只有一个字母 M,前述调用正确的结果应该是一个 Unicode 字符 M,以及一个 Unicode 字符的字符串结束符 \0。但是遗憾的很,该窗口的实现忽略了后者,因此,如果你对返回的字符串调用了字符串操作函数 strcpy 或者 strlen 等,那你的程序一定会把辫子翘到天上去,死的很难看。
最后的成就是本文所附的两个函数,GetWindowTextLengthEx() 和 GetWindowTextEx(),它们会自动判断目标窗口的类型,并处理像那个变态输入法那样的窗口。不过同样是由于时间关系,没有进行特别仔细的测 试。如果有谁发现了 bug,敬请告知。
在测试中的发现的另外一个与 MSDN 描述不符的事实是,其他进程内的 Button 等控件的文本其实使用 API 方式也可以得到。顺便说一下,我的测试环境是 Window XP Service Pack 2。
所有的这些,都这给我们平时的应用设置了很多细节上的障碍,会导致一系列繁琐的判断,尤其是当你要写一个通用的工具时。事实上,本文正是一个工具的副产品。但是,我想说的是:有时候,我们不得不去面对。
1、gwt.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#ifndef __GWT_H__ #define __GWT_H__ #define GWT_CALLAPI 0x00000000 #define GWT_SENDMESSAGE 0x00000001 #define GWT_AUTOCHAR 0x00000000 // #define GWT_FORCEANSI 0x00000004 // #define GWT_FORCEUNICODE 0x00000008 #define GWT_ANSICALL 0x00000010 #define GWT_UNICODECALL 0x00000020 int WINAPI GetWindowTextLengthEx(HWND hwnd, DWORD dwFlags); int WINAPI GetWindowTextEx(HWND hwnd, LPTSTR pszString, int iMaxCount, DWORD dwFlags); #endif // __GWT_H__ |
2、gwt.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
#include "stdafx.h" #include "gwt.h" typedef int (WINAPI *fnGetWindowText)(IN HWND hWnd, OUT PVOID pBuffer, IN int nMaxCount); typedef int (WINAPI *fnGetWindowTextLength)(IN HWND hWnd); typedef LRESULT (WINAPI *fnSendMessage)(IN HWND hWnd, IN UINT Msg, IN WPARAM wParam, IN LPARAM lParam); int WINAPI GetWindowTextLengthEx(HWND hwnd, DWORD dwFlags) { if(hwnd == NULL || !IsWindow(hwnd)) return 0; if((dwFlags & GWT_ANSICALL) && (dwFlags & GWT_UNICODECALL)) { SetLastError(ERROR_INVALID_PARAMETER); return 0; } static BOOL bWindows9x = (HIWORD(GetVersion()) & 0x8000); BOOL bUnicodeWindow = IsWindowUnicode(hwnd); HMODULE hModule = GetModuleHandle(TEXT("user32.dll")); static fnGetWindowTextLength pGetWindowTextLengthA = NULL; static fnGetWindowTextLength pGetWindowTextLengthW = NULL; static fnSendMessage pSendMessageA = NULL; static fnSendMessage pSendMessageW = NULL; if(pGetWindowTextLengthA == NULL) pGetWindowTextLengthA = (fnGetWindowTextLength)GetProcAddress(hModule, "GetWindowTextLengthA"); if(pGetWindowTextLengthW == NULL) pGetWindowTextLengthW = (fnGetWindowTextLength)GetProcAddress(hModule, "GetWindowTextLengthW"); if(pSendMessageA == NULL) pSendMessageA = (fnSendMessage)GetProcAddress(hModule, "SendMessageA"); if(pSendMessageW == NULL) pSendMessageW = (fnSendMessage)GetProcAddress(hModule, "SendMessageW"); BOOL bUnicodeCall = ((dwFlags & GWT_UNICODECALL) || (bUnicodeWindow && !(dwFlags & GWT_ANSICALL))); if(dwFlags & GWT_SENDMESSAGE) { fnSendMessage pMsg = bUnicodeCall ? pSendMessageW : pSendMessageA; if(pMsg) return pMsg(hwnd, WM_GETTEXTLENGTH, 0, 0); } else { fnGetWindowTextLength pApi = bUnicodeCall ? pGetWindowTextLengthW : pGetWindowTextLengthA; if(pApi) return pApi(hwnd); } SetLastError(ERROR_CALL_NOT_IMPLEMENTED); return 0; } int WINAPI GetWindowTextEx(HWND hwnd, LPTSTR pszString, int iMaxCount, DWORD dwFlags) { if(hwnd == NULL || !IsWindow(hwnd)) return 0; if(((dwFlags & GWT_ANSICALL) && (dwFlags & GWT_UNICODECALL)) || pszString == NULL || iMaxCount <= 0) { SetLastError(ERROR_INVALID_PARAMETER); return 0; } *pszString = TEXT('\0'); static BOOL bWindows9x = (HIWORD(GetVersion()) & 0x8000); BOOL bUnicodeWindow = IsWindowUnicode(hwnd); HMODULE hModule = GetModuleHandle(TEXT("user32.dll")); static fnGetWindowText pGetWindowTextA = NULL; static fnGetWindowText pGetWindowTextW = NULL; static fnSendMessage pSendMessageA = NULL; static fnSendMessage pSendMessageW = NULL; if(pGetWindowTextA == NULL) pGetWindowTextA = (fnGetWindowText)GetProcAddress(hModule, "GetWindowTextA"); if(pGetWindowTextW == NULL) pGetWindowTextW = (fnGetWindowText)GetProcAddress(hModule, "GetWindowTextW"); if(pSendMessageA == NULL) pSendMessageA = (fnSendMessage)GetProcAddress(hModule, "SendMessageA"); if(pSendMessageW == NULL) pSendMessageW = (fnSendMessage)GetProcAddress(hModule, "SendMessageW"); BOOL bUnicodeCall = ((dwFlags & GWT_UNICODECALL) || (bUnicodeWindow && !(dwFlags & GWT_ANSICALL))); #if defined(UNICODE) PVOID pBuffer = bUnicodeCall ? (PVOID)pszString : (PVOID)new CHAR[iMaxCount]; #else // UNICODE PVOID pBuffer = bUnicodeCall ? (PVOID)new WCHAR[iMaxCount] : (PVOID)pszString; #endif // UNICODE if(pBuffer == NULL) { SetLastError(ERROR_NOT_ENOUGH_MEMORY); return 0; } int iCount = 0; BOOL bCalled = FALSE; if(dwFlags & GWT_SENDMESSAGE) { fnSendMessage pMsg = bUnicodeCall ? pSendMessageW : pSendMessageA; if(pMsg) { iCount = pMsg(hwnd, WM_GETTEXT, (WPARAM)iMaxCount, (LPARAM)pBuffer); bCalled = TRUE; } } else { fnGetWindowText pApi = bUnicodeCall ? pGetWindowTextW : pGetWindowTextA; if(pApi) { iCount = pApi(hwnd, pBuffer, iMaxCount); bCalled = TRUE; } } if(!bCalled) { SetLastError(ERROR_CALL_NOT_IMPLEMENTED); return 0; } int iRet = 0; #if defined(UNICODE) if(bUnicodeCall) iRet = iCount; else { int cch = MultiByteToWideChar(CP_ACP, 0, (PSTR)pBuffer, iCount, NULL, 0); if(cch) { PWSTR pBuffer2 = new WCHAR[cch + 1]; if(pBuffer2) { iRet = MultiByteToWideChar(CP_ACP, 0, (PSTR)pBuffer, iCount, pBuffer2, cch); pBuffer2[cch] = L'\0'; lstrcpynW(pszString, pBuffer2, iMaxCount); iRet = lstrlenW(pszString); delete[] pBuffer2; } } } #else // UNICODE if(!bUnicodeCall) iRet = iCount; else { int cb = WideCharToMultiByte(CP_ACP, 0, (PWSTR)pBuffer, iCount, NULL, 0, NULL, NULL); if(cb) { PSTR pBuffer2 = new CHAR[cb + 1]; if(pBuffer2) { iRet = WideCharToMultiByte(CP_ACP, 0, (PWSTR)pBuffer, iCount, pBuffer2, cb, NULL, NULL); pBuffer2[cb] = '\0'; lstrcpynA(pszString, pBuffer2, iMaxCount); iRet = lstrlenA(pszString); delete[] pBuffer2; } } } #endif // UNICODE if(pBuffer != (PVOID)pszString) delete[] pBuffer; return iRet; } |