从Win32消息值反推出名称串:自动代码生成

来源:互联网 发布:冰川网络有手游吗 编辑:程序博客网 时间:2024/05/17 11:04

     些天刚开始学习win32 API,出于对未知的不安,我试图用打印出所有爬过系统的消息的详细资料。其中有个简单的需求:从消息值反推出名称串,类似:

     在MSDN上搜了下,貌似没有API;也没怎么找到网上的资源,只好自己动手了。

 

 


 

     我们现在就要获得一个完整的、C++程序能识别的消息值-消息名的表。Win32的大部分消息在 WinUser.h 中定义的,形如:

     考虑到消息种类非常多,手工完成这个表很累也很容易出错。这里我想起一种有趣的技术:我们可以用C++写一个自动程序,扫描 WinUser.h 文件,自动完成所需的C++代码。碰巧逻辑也不难。以最重要的 WM_前缀消息为例,只要将所有以 "#define WM_” 开头的行提取出来,分别读出字符串 "WM_ PAINT" 以及剩下的串"0x000F",就可以转化为类似下的 C/C++ 代码:

     这样算还相当稳健,规避了棘手的表达式解析。鉴于后面将提到的条件编译问题,保留表达式反倒比解出来更准确。
 

     这里共享一个简化的版本,包括 VS2008 的 WinUser.h 中所有 WM_ 前缀的消息(该文件可以编译通过): 

     这种“自动生成代码的代码”的设计,被语法、工具卡住不妨试试这方面。

 


 

     试图编一小块代码来全自动生成大堆代码的时候,自然会遇到很多例外杂项。不过同时也有好处:为了安全和Debug,你必须得写些防御性的测试代码,反过来这些冷酷的代码顺便也能给研究对象盖棺定论。(事实证明这话说得太早)

  • 绝大部分WM_ 开头的消息都在 0~ 0x400 之间且不冲突,我们本可以用一个容量为 0x401 的数组来完成这个映射(其中有些格子留空)。 可惜消息值域方面有一个例外: WM_APP = 0x8000,严重超出正常范围,这迫使我使用 std::map;值域独立性上也有一个唯一例外:有一行 #define WM_SETTINGCHANGE  WM_WININICHANGE,显然这两个名称对应同一个值。这一行的输出是手动修改的。
  • 处理一对多的映射。
    实际上,除了WM_系列以外,WinUser.h 中包含很多系统控件对应的消息(MSDN 的 About Messages and Message Queues 一文给出了他们详细的前缀列表)。颇为糟糕的是,他们的消息值不再唯一对应名字。例如 0x6 同时对应 SB_TOP/SB_LEFT/WM_ACTIVATE, 具体消息取决于上下文。 map 中一个键只能对应一项;我这里的解决方案是使用 std::multimap。相对于把所有冲突的名字拼成一个string(或者更土的只保留一项),multimap 保留了所有项,我们就可以单独搜索比如WM_前缀的消息。

    补:好吧我SB了,前缀这个东西并不靠谱:刚才说的 SB_TOP 和 SB_LEFT ( SB = ScrollBar ) 就不是消息;问题是他们的前缀正好和StatusBar 消息系列相同; 类似还有LB_OK等。根据名字没法判断他是否是消息。

    另经初步检查(没有查到文档证据),系统消息目前还是保持了唯一性,WinUser.h 中暂时未发现 { 控件消息 + WM_ } 中任何两个“实际的”消息值冲突。 重名仍然存在,就是助记名 ( 例如 WM_KEYFIRST 、 WM_KEYLAST  ) 和实际名称的冲突问题。
  • 处理条件编译。
    宏常量并不总是固定的。例如CB_MSGMAX ,我们会有:

    于是,CB_MSGMAX 的值悲剧的取决于 windows SDK版本,我们输出代码的时候怎么知道他是 0x165还是 0x163?

    一个简单的解决方案是,在解析的时候,顺便将输出所有条件编译语句到生成代码中。这样基本能再现最初的条件编译路线。稍微不美观的是,会出现很多不包含消息定义的 #if #endif 对。 我们需要栈和研究宏的 if else镶嵌规则... 茫然... 既然他们无害那就先留着吧。
  • 拿着锤子也只能把所有东西当钉子
    用C++解析文本的时候不得不生造很多辅助函数,差点写了个完整的表达式求值器。用正则表达式大概会方便很多吧? 该学学 C# 啊脚本语言之类的了。

后来换了关键词,找到一篇较全面的获取消息名的文章

《获取Windows消息名称(含VCL消息)-1》,后面还有 -2 和 -3。

 

消息大抵上有三类:系统消息;普通的自定义的消息——他们甚至根本没有字符串名称;以及使用 RegisterWindowMessage 注册的用于跨进程通信的消息。在系统运行时,该函数对输入同一个字符串总是返回相同的消息号,用于跨进程消息通信。遗憾的是,在MSDN文档中,该函数却没有直接配对的逆向函数。《获取Windows消息名称(含VCL消息)-3》中使用 GetClipboardFormatName。这是一个非官方的、未文档的方法。这里有两篇文章做了解释:

 

[CSDN]《FAQ: 如何得到用RegisterWindowMessage注册的消息的字符串名称》

[MSDN] Inverse of RegisterWindowMessage (GetClipboardFormatName) 

 

So, basically, it seems that RegisterWindowMessage is just an "alias" for RegisterClipboardFormat...

 

未文档的特性往往潜藏着兼容性问题,因为原则上开发者没有义务去维护他们。无论是C++还是Window开发都是如此。推荐读读《Windows编程启示录》,作者在这个方面的感受应该是刻骨铭心的。

 

顺便,对于获得窗口标题这个主题,也有很多有趣的文章。

 

首先,我打算在 WinProc 中打印每个消息的详细清单,其中需要打印接收消息的窗口对应的标题名。但是要获得这个名称就要使用 GetWindowText  或者 WM_GETTEXT,这又产生了新的消息形成死循环。解决方案是,有一个不太正式的函数 InternalGetWindowText,他绕开消息机制从HWND对应的窗口结构块中直接读取标题,从XP SP1起可以直接用但不保证兼容性。

 

GetWindowText 本身就有些复杂,涉及到 SendMessage 跨线程死锁的问题。简而言之,如果你 SendMessage 发出一个消息,而对方不处理它,你就会这么永远挂起来等回信。这里有篇文章,摘录了《WINDOWS编程启示录》上的一篇详实而有趣的解释。按照编程启示录的说法,在windows1.0时代是没有挂起这个概念的。然而时光之轮滚入了Win32时代,面对一堆老代码(包括大量系统组件),最终Windows API开发团队决定绕开挂起问题:在对本进程GetWindowText 的时候发送WM_GETTEXT消息(挂起是你自己的责任);跨进程的时候使用 InternalGetWindowText 直接读标题。

 

另外,鉴于程序可以自己处理 WM_GETTEXT,你总不能保证他不出错。比如微软的输入法经常带的 MSCTFIME UI,我这里遍历时打印他的名称就出错(XP SP3)。原来他的名字只有一个字符 "M",但是却忘了加 /0 结尾。 这篇《细说窗口文字》提到了这个,以及一系列令人崩溃的灾难。

原创粉丝点击