Windows中窗口和类额外字节的演变
摘要
本文追溯了Windows中窗口和类额外字节从16位到32位再到64位的演变,详细描述了随着句柄和指针的扩展,函数名称和数据大小的变化。
<p>Windows提供了一系列用于访问所谓“额外字节”的函数。额外字节分为两类:类额外字节(属于窗口类)和窗口额外字节(属于从该类创建的每个窗口)。应用程序可以在注册类时请求额外字节,这些字节从零开始递增偏移量进行访问。系统也定义了一些额外字节,它们使用负偏移量。</p>
<p>我们将探讨系统定义的偏移量。</p>
<p>在16位Windows中,以下是可用的额外字节及其读取函数:</p>
<table style="border-collapse: collapse;" border="1" cellspacing="0" cellpadding="3">
<tbody>
<tr>
<th>名称</th>
<th>大小</th>
<th>访问器</th>
<th>备注</th>
</tr>
<tr>
<td><tt>GCW_MENUNAME</tt></td>
<td>int16_t</td>
<td><tt>GetClassWord</tt></td>
<td> </td>
</tr>
<tr>
<td><tt>GCW_HBRBACKGROUND</tt></td>
<td>int16_t</td>
<td><tt>GetClassWord</tt></td>
<td> </td>
</tr>
<tr>
<td><tt>GCW_HCURSOR</tt></td>
<td>int16_t</td>
<td><tt>GetClassWord</tt></td>
<td> </td>
</tr>
<tr>
<td><tt>GCW_HICON</tt></td>
<td>int16_t</td>
<td><tt>GetClassWord</tt></td>
<td> </td>
</tr>
<tr>
<td><tt>GCW_HMODULE</tt></td>
<td>int16_t</td>
<td><tt>GetClassWord</tt></td>
<td> </td>
</tr>
<tr>
<td><tt>GCW_CBWNDEXTRA</tt></td>
<td>int16_t</td>
<td><tt>GetClassWord</tt></td>
<td> </td>
</tr>
<tr>
<td><tt>GCW_CBCLSEXTRA</tt></td>
<td>int16_t</td>
<td><tt>GetClassWord</tt></td>
<td> </td>
</tr>
<tr>
<td><tt>GCL_WNDPROC</tt></td>
<td>int32_t</td>
<td><tt>GetClassLong</tt></td>
<td> </td>
</tr>
<tr>
<td><tt>GCW_STYLE</tt></td>
<td>int16_t</td>
<td><tt>GetClassWord</tt></td>
<td> </td>
</tr>
<tr>
<td><tt>GCW_ATOM</tt></td>
<td>int16_t</td>
<td><tt>GetClassWord</tt></td>
<td>在Windows 3.1中新增</td>
</tr>
<tr>
<td><tt>GWL_WNDPROC</tt></td>
<td>int32_t</td>
<td><tt>GetWindowLong</tt></td>
<td> </td>
</tr>
<tr>
<td><tt>GWW_HINSTANCE</tt></td>
<td>int16_t</td>
<td><tt>GetWindowWord</tt></td>
<td> </td>
</tr>
<tr>
<td><tt>GWW_HWNDPARENT</tt></td>
<td>int16_t</td>
<td><tt>GetWindowWord</tt></td>
<td> </td>
</tr>
<tr>
<td><tt>GWW_ID</tt></td>
<td>int16_t</td>
<td><tt>GetWindowWord</tt></td>
<td> </td>
</tr>
<tr>
<td><tt>GWL_STYLE</tt></td>
<td>int32_t</td>
<td><tt>GetWindowLong</tt></td>
<td> </td>
</tr>
<tr>
<td><tt>GWL_EXSTYLE</tt></td>
<td>int32_t</td>
<td><tt>GetWindowLong</tt></td>
<td>在Windows 3.0中新增</td>
</tr>
<tr>
<td><tt>DWL_MSGRESULT</tt></td>
<td>int32_t</td>
<td><tt>GetWindowLong</tt></td>
<td>用于对话框窗口</td>
</tr>
<tr>
<td><tt>DWL_DLGPROC</tt></td>
<td>int32_t</td>
<td><tt>GetWindowLong</tt></td>
<td>用于对话框窗口</td>
</tr>
<tr>
<td><tt>DWL_USER</tt></td>
<td>int32_t</td>
<td><tt>GetWindowLong</tt></td>
<td>用于对话框窗口</td>
</tr>
</tbody>
</table>
<p>这里,类和窗口字节的命名模式非常明显。</p>
<p>第一个字母 <tt>G</tt> 代表 <tt>Get</tt>。第二个字母 <tt>C</tt> 或 <tt>W</tt> 代表 <tt>Class</tt> 或 <tt>Window</tt>。第三个字母 <tt>W</tt> 或 <tt>L</tt> 代表 <tt>Word</tt> 或 <tt>Long</tt>。¹</p>
<p>对于仅适用于对话框窗口的窗口字节,第一个字母变为 <tt>D</tt>,表示“对话框”。这些值为零或正数,因为它们实际上只是注册到标准对话框类的额外字节。</p>
<p>现在,在16位Windows中,句柄是16位值,但在32位Windows中,它们扩展到32位值,因此32位Windows将函数从 <code>GetSomething<span style="border: solid 1px currentcolor;">Word</span></code> 改为 <code>GetSomething<span style="border: solid 1px currentcolor;">Long</span></code>,前缀也相应地从 <tt>W</tt> 变为 <tt>L</tt>。因此,我们的表格现在如下所示:</p>
<table style="border-collapse: collapse;" border="1" cellspacing="0" cellpadding="3">
<tbody>
<tr>
<th>名称</th>
<th>16位前缀/大小</th>
<th>32位前缀/大小</th>
</tr>
<tr>
<td><tt>MENUNAME</tt></td>
<td><tt>GCW_</tt> int16_t</td>
<td><tt>GCL_</tt> int32_t ◱</td>
</tr>
<tr>
<td><tt>HBRBACKGROUND</tt></td>
<td><tt>GCW_</tt> int16_t</td>
<td><tt>GCL_</tt> int32_t ◱</td>
</tr>
<tr>
<td><tt>HCURSOR</tt></td>
<td><tt>GCW_</tt> int16_t</td>
<td><tt>GCL_</tt> int32_t ◱</td>
</tr>
<tr>
<td><tt>HICON</tt></td>
<td><tt>GCW_</tt> int16_t</td>
<td><tt>GCL_</tt> int32_t ◱</td>
</tr>
<tr>
<td><tt>HMODULE</tt></td>
<td><tt>GCW_</tt> int16_t</td>
<td><tt>GCL_</tt> int32_t ◱</td>
</tr>
<tr>
<td><tt>CBWNDEXTRA</tt></td>
<td><tt>GCW_</tt> int16_t</td>
<td><tt>GCL_</tt> int32_t ◱</td>
</tr>
<tr>
<td><tt>CBCLSEXTRA</tt></td>
<td><tt>GCW_</tt> int16_t</td>
<td><tt>GCL_</tt> int32_t ◱</td>
</tr>
<tr>
<td><tt>WNDPROC</tt></td>
<td><tt>GCL_</tt> int32_t</td>
<td><tt>GCL_</tt> int32_t ◱</td>
</tr>
<tr>
<td><tt>STYLE</tt></td>
<td><tt>GCW_</tt> int16_t</td>
<td><tt>GCL_</tt> int32_t ◱</td>
</tr>
<tr>
<td><tt>ATOM</tt></td>
<td><tt>GCW_</tt> int16_t</td>
<td><tt>GCW_</tt> int16_t</td>
</tr>
<tr>
<td><tt>HICONSM</tt></td>
<td> </td>
<td><tt>GCL_</tt> int32_t <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a5.png" alt="💥" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
</tr>
<tr>
<td><tt>WNDPROC</tt></td>
<td><tt>GWL_</tt> int32_t</td>
<td><tt>GWL_</tt> int32_t ◱</td>
</tr>
<tr>
<td><tt>HWNDPARENT</tt></td>
<td><tt>GWW_</tt> int16_t</td>
<td><tt>GWL_</tt> int32_t ◱</td>
</tr>
<tr>
<td><tt>ID</tt></td>
<td><tt>GWW_</tt> int16_t</td>
<td><tt>GWL_</tt> int32_t ◱</td>
</tr>
<tr>
<td><tt>STYLE</tt></td>
<td><tt>GWL_</tt> int32_t</td>
<td><tt>GWL_</tt> int32_t</td>
</tr>
<tr>
<td><tt>EXSTYLE</tt></td>
<td><tt>GWL_</tt> int32_t</td>
<td><tt>GWL_</tt> int32_t</td>
</tr>
<tr>
<td><tt>USERDATA</tt></td>
<td> </td>
<td><tt>GWL_</tt> int32_t <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a5.png" alt="💥" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
</tr>
<tr>
<td><tt>MSGRESULT</tt></td>
<td><tt>DWL_</tt> int32_t</td>
<td><tt>DWL_</tt> int32_t</td>
</tr>
<tr>
<td><tt>DLGPROC</tt></td>
<td><tt>DWL_</tt> int32_t</td>
<td><tt>DWL_</tt> int32_t</td>
</tr>
<tr>
<td><tt>USER</tt></td>
<td><tt>DWL_</tt> int32_t</td>
<td><tt>DWL_</tt> int32_t</td>
</tr>
</tbody>
</table>
<p>◱ 符号表示值变大了,而 <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a5.png" alt="💥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 符号表示在16位Windows中不存在的值。</p>
<p>尽管控件ID通常是小整数,但它们的空间从16位扩展到了32位,因为<a title="为什么计时器ID和对话框控件ID在64位Windows上是64位值?你真的认为人们会创建超过40亿个计时器或对话框控件吗?" href="https://devblogs.microsoft.com/oldnewthing/20191010-00/?p=102978">有些人用它来存储指针或句柄</a>。(一种创建进程内唯一编号的方法是<a title="一个窗口不能有两个相同ID的计时器,那么如何分配一个无人使用的ID?" href="https://devblogs.microsoft.com/oldnewthing/20191009-00/?p=102974">分配内存并使用其地址</a>。)</p>
<p>额外字节演变的下一步是从32位到64位Windows的转换。在64位Windows上,指针和句柄扩展到64位值,因此所有用于(或可能用于)保存句柄或指针的额外字节都扩展到了64位版本。</p>
<p>为了能够编写同时针对32位和64位Windows的代码,64位Windows的设计没有像32位Windows与16位Windows那样进行硬性断裂。相反,他们引入了接受指针大小整数的新函数,这些整数在32位Windows上是32位值,在64位Windows上是64位值。这样,您只需在所有地方使用这些新函数</p>
查看缓存全文
缓存时间: 2026/06/30 05:16
# Windows 中窗口类和窗口额外字节的演变 - 旧事新说
来源:https://devblogs.microsoft.com/oldnewthing/20260629-00?p=112484
Windows 提供了一组函数用于访问所谓的“额外字节”。额外字节分为两类:类额外字节(属于窗口类)和窗口额外字节(属于从该类创建的每个窗口)。应用程序可以在注册类时申请额外字节,并通过从零开始的递增偏移量进行访问。系统也定义了一些额外字节,这些字节使用负偏移量。
我们将重点研究系统定义的偏移量。
在 16 位 Windows 中,可用的额外字节及其读取函数如下:
| 名称 | 大小 | 访问函数 | 备注 |
|------|------|----------|------|
| GCW_MENUNAME | int16_t | GetClassWord | |
| GCW_HBRBACKGROUND | int16_t | GetClassWord | |
| GCW_HCURSOR | int16_t | GetClassWord | |
| GCW_HICON | int16_t | GetClassWord | |
| GCW_HMODULE | int16_t | GetClassWord | |
| GCW_CBWNDEXTRA | int16_t | GetClassWord | |
| GCW_CBCLSEXTRA | int16_t | GetClassWord | |
| GCL_WNDPROC | int32_t | GetClassLong | |
| GCW_STYLE | int16_t | GetClassWord | |
| GCW_ATOM | int16_t | GetClassWord | 在 Windows 3.1 中添加 |
| GWL_WNDPROC | int32_t | GetWindowLong | |
| GWW_HINSTANCE | int16_t | GetWindowWord | |
| GWW_HWNDPARENT | int16_t | GetWindowWord | |
| GWW_ID | int16_t | GetWindowWord | |
| GWL_STYLE | int32_t | GetWindowLong | |
| GWL_EXSTYLE | int32_t | GetWindowLong | 在 Windows 3.0 中添加 |
| DWL_MSGRESULT | int32_t | GetWindowLong | 用于对话框窗口 |
| DWL_DLGPROC | int32_t | GetWindowLong | 用于对话框窗口 |
| DWL_USER | int32_t | GetWindowLong | 用于对话框窗口 |
这里明显存在类字节和窗口字节的命名模式。
首字母 `G` 代表 `Get`(获取)。第二个字母 `C` 或 `W` 代表 `Class`(类)或 `Window`(窗口)。第三个字母 `W` 或 `L` 代表 `Word`(字)或 `Long`(长整型)[^1]。
仅适用于对话框窗口的窗口字节,首字母改为 `D`(代表“对话框”)。这些值为零或正数,因为它们实际上是注册到标准对话框类的额外字节。
在 16 位 Windows 中,句柄是 16 位值,但在 32 位 Windows 中,它们扩展为 32 位值,因此 32 位 Windows 将函数从 `GetSomethingWord` 改为 `GetSomethingLong`,相应的前缀也从 `W` 改为 `L`。于是表格变为:
| 名称 | 16 位前缀/大小 | 32 位前缀/大小 |
|------|----------------|----------------|
| MENUNAME | GCW_ int16_t | GCL_ int32_t ◱ |
| HBRBACKGROUND | GCW_ int16_t | GCL_ int32_t ◱ |
| HCURSOR | GCW_ int16_t | GCL_ int32_t ◱ |
| HICON | GCW_ int16_t | GCL_ int32_t ◱ |
| HMODULE | GCW_ int16_t | GCL_ int32_t ◱ |
| CBWNDEXTRA | GCW_ int16_t | GCL_ int32_t ◱ |
| CBCLSEXTRA | GCW_ int16_t | GCL_ int32_t ◱ |
| WNDPROC | GCL_ int32_t | GCL_ int32_t ◱ |
| STYLE | GCW_ int16_t | GCL_ int32_t ◱ |
| ATOM | GCW_ int16_t | GCW_ int16_t |
| HICONSM | - | GCL_ int32_t 💥 |
| WNDPROC | GWL_ int32_t | GWL_ int32_t ◱ |
| HWNDPARENT | GWW_ int16_t | GWL_ int32_t ◱ |
| ID | GWW_ int16_t | GWL_ int32_t ◱ |
| STYLE | GWL_ int32_t | GWL_ int32_t |
| EXSTYLE | GWL_ int32_t | GWL_ int32_t |
| USERDATA | - | GWL_ int32_t 💥 |
| MSGRESULT | DWL_ int32_t | DWL_ int32_t |
| DLGPROC | DWL_ int32_t | DWL_ int32_t |
| USER | DWL_ int32_t | DWL_ int32_t |
其中 ◱ 符号代表值变大,💥 符号代表这些值在 16 位 Windows 中不存在。
尽管控件 ID 通常是较小的整数,但存储它们的空间从 16 位扩展到了 32 位,因为有人用它们来存放指针或句柄[^2]。(创建进程范围内唯一编号的一种方法是分配内存并使用其地址[^3]。)
额外字节演变的下一步是从 32 位 Windows 迁移到 64 位 Windows。在 64 位 Windows 中,指针和句柄扩展为 64 位值,因此所有用于(或可能用于)保存句柄或指针的额外字节都被扩展为 64 位版本。
为了使代码能同时面向 32 位和 64 位 Windows,64 位 Windows 的设计并未像 32 位 Windows 与 16 位 Windows 那样硬性割裂。相反,它们引入了新函数,这些函数接受指针大小的整数——在 32 位 Windows 上是 32 位值,在 64 位 Windows 上是 64 位值。这样,你可以在任何地方使用这些新函数,它们在 64 位系统上会扩展,在 32 位系统上保持不变。
新函数名称类似于 `GetWindowLongPtr`,相应的前缀改为 `GWLP_` 等。
| 名称 | 16 位前缀/大小 | 32 位前缀/大小 | 32/64 位前缀/大小 |
|------|----------------|----------------|-------------------|
| MENUNAME | GCW_ int16_t | GCL_ int32_t ◱ | GCLP_ intptr_t ◱ |
| HBRBACKGROUND | GCW_ int16_t | GCL_ int32_t ◱ | GCLP_ intptr_t ◱ |
| HCURSOR | GCW_ int16_t | GCL_ int32_t ◱ | GCLP_ intptr_t ◱ |
| HICON | GCW_ int16_t | GCL_ int32_t ◱ | GCLP_ intptr_t ◱ |
| HMODULE | GCW_ int16_t | GCL_ int32_t ◱ | GCLP_ intptr_t ◱ |
| CBWNDEXTRA | GCW_ int16_t | GCL_ int32_t ◱ | GCL_ int32_t |
| CBCLSEXTRA | GCW_ int16_t | GCL_ int32_t ◱ | GCL_ int32_t |
| WNDPROC | GCL_ int32_t | GCL_ int32_t ◱ | GCLP_ intptr_t ◱ |
| STYLE | GCW_ int16_t | GCL_ int32_t ◱ | GCL_ int32_t |
| ATOM | GCW_ int16_t | GCW_ int16_t | GCW_ int16_t |
| HICONSM | - | GCL_ int32_t 💥 | GCLP_ intptr_t ◱ |
| WNDPROC | GWL_ int32_t | GWL_ int32_t ◱ | GWLP_ intptr_t ◱ |
| HWNDPARENT | GWW_ int16_t | GWL_ int32_t ◱ | GWLP_ intptr_t ◱ |
| ID | GWW_ int16_t | GWL_ int32_t ◱ | GWLP_ intptr_t ◱ |
| STYLE | GWL_ int32_t | GWL_ int32_t | GWL_ int32_t |
| EXSTYLE | GWL_ int32_t | GWL_ int32_t | GWL_ int32_t |
| USERDATA | - | GWL_ int32_t 💥 | GWLP_ intptr_t ◱ |
| MSGRESULT | DWL_ int32_t | DWL_ int32_t | DWLP_ intptr_t ◱ |
| DLGPROC | DWL_ int32_t | DWL_ int32_t | DWLP_ intptr_t ◱ |
| USER | DWL_ int32_t | DWL_ int32_t | DWLP_ intptr_t ◱ |
从额外字节名称的前缀可以看出它应配合哪个函数使用。
| 前缀 | 对应函数 |
|------|----------|
| GCW_ ↔ | GetClassWord |
| GWW_ ↔ | GetWindowWord |
| GCL_ ↔ | GetClassLong |
| GWL_ ↔ | GetWindowLong |
| GCLP_ ↔ | GetClassLongPtr |
| GWLP_ ↔ | GetWindowLongPtr |
比较特殊的是 `DWLP_`,因为它需要同时编码可使用的窗口类型(`D` = 对话框)以及对应的函数名(`WindowLongPtr`)。
作为让步,Windows 允许你将 `GCL_` 和 `GWL_` 值分别传递给 `GetClassLongPtr` 和 `GetWindowLongPtr`,即便它们本意是与 `GetClassLong` 和 `GetWindowLong` 配合使用。若这样做,你会得到对应的 32 位值,必要时会零扩展为指针大小[^2]。这种情况主要体现在 `GWL_ID` 上,因为多数人不会用到全部 ID 范围,所以如果你愿意局限于 32 位子集,就可以假装这些值不是指针大小[^3]。
“为什么非要更改所有前缀?这难道不是给从 32 位代码移植到 64 位代码的人制造大量额外工作吗?”
是的,但这是有益的额外工作。目的是在需要修改的地方强制产生构建中断,因为你必须调用访问指针大小整数的函数,而不是 32 位整数;否则就会遭遇整数截断错误。
[^1]: 这是经典 Win32 常见的命名约定。例如,`ShowWindow` 的操作参数前缀为 `SW_`;`SetWindowPos` 的标志前缀为 `SWP_`;`GetWindow` 的关系参数前缀为 `GW_`。
[^2]: 将 `GWL_` 值与 `SetWindowLongPtr` 一起使用时问题稍多。看起来你存储的是指针大小的整数,但实际上只有低 32 位被保留。
[^3]: `ID` 比较特殊,它同时定义为 `GWL_ID` 和 `GWLP_ID`。其他所有值只定义了一个前缀。
## 分类
## 主题
## 作者
**Raymond Chen**
Raymond 参与 Windows 的演变已有 30 余年。2003 年,他创办了一个名为“旧事新说(The Old New Thing)”的网站,其受欢迎程度远远超出了他最大胆的想象,这一发展至今仍让他感到不安。该网站还衍生出一本书,巧合的是书名也叫《The Old New Thing》(Addison Wesley 2007)。他偶尔会出现在 Windows 开发文档的 Twitter 账号上,讲述一些毫无用处的故事。
相似文章
关于滥用Windows窗口类额外字节的兼容性说明
Raymond Chen 讨论了一个历史性的 Windows 兼容性问题,其中一些 16 位程序滥用窗口类额外字节来存储私有数据,以及微软如何在保持向后兼容性的同时,对 32 位和 64 位程序堵住了这个漏洞。
Let's Decode the Mystery Bytes [video]
本视频通过反汇编和WinDbg调试工具,揭示了在x86环境下calloc分配内存时出现的8个神秘字节实际上是Windows堆管理器的条目头部,包含当前块大小、前一块大小等信息。
为Windows 3.1改造WM_COPYDATA消息
本文解释如何利用共享地址空间,将针对32位Windows引入的WM_COPYDATA消息改造到16位Windows 3.1上,使得16位程序间的通信无需任何更改。
为什么32位x86系统上的Windows客户端版本人为地将内存限制为4 GB?
解释了32位Windows客户端版本人为将内存限制为4 GB的历史原因:物理地址扩展(PAE)和数据执行保护(DEP)存在驱动兼容性问题,而非出于任何恶意动机。
Windows UI 演变:点击未关联的文件
历史回顾:从 Windows 386 到 Windows XP,Windows 操作系统打开未关联文件的用户界面如何演变,展示了对话框的演进以及 Web 服务提示的引入。