这个问题在 SO 上有很多重复和几乎重复的问题,但我见过的几乎没有一个答案探讨了他们选择的解决方案的陷阱。
有多种方法可以将任意数据指针与窗口关联起来,并且需要考虑两种不同的情况。根据不同的情况,可能性也不同。
情况 1 是当您编写窗口类时。这意味着您正在实施WNDPROC
,并且您的意图是让其他人在他们的应用程序中使用您的窗口类。您通常不知道谁将使用您的窗口类以及用途。
情况 2 是当您使用自己的应用程序中已存在的窗口类时。一般来说,您无权访问窗口类源代码,也无法对其进行修改。
我假设这个问题isn't将数据指针放入WNDPROC
最初(这只是通过CREATESTRUCT
与lpParam
参数输入CreateWindow[ExW]
),而是如何存储它以供后续调用。
方法一:cbWndExtra
当 Windows 创建窗口实例时,它会在内部分配一个WND
结构。该结构体具有一定的大小,包含各种与窗口相关的内容,例如它的位置、窗口类和当前的 WNDPROC。在此结构的末尾,Windows 有选择地分配属于该结构的许多附加字节。该数字指定于WNDCLASSEX.cbWndExtra
,它用于RegisterWindowClassEx
.
这意味着只有当您是注册窗口类的人时才能使用此方法,即您是编写窗口类.
应用程序无法直接访问WND
结构。相反,使用GetWindowLong[Ptr]
。非负索引访问结构末尾额外字节内的内存。 “0”将访问第一个额外字节。
如果您正在编写窗口类,那么这是一种干净、快速的方法。大多数Windows内部控件似乎都使用这种方法。
不幸的是,这种方法在对话框中表现不佳(DialogBox
家庭)。除了提供对话框模板之外,您还将拥有一个对话框窗口类,这可能会变得维护起来很麻烦(除非您出于其他原因需要这样做)。如果您确实想在对话框中使用它,则必须在对话框模板中指定窗口类名称,并确保在显示对话框之前已注册该窗口类,并且您需要实现一个WNDPROC
对于对话框(或使用DefDlgProc
)。此外,所有对话框都已经在其中保留了一定数量的字节cbWndExtra
使对话管理器正常工作。需要的额外字节数是DLGWINDOWEXTRA
持续的。这意味着your东西需要来after对话框已经保留的额外字节。将所有对额外内存的访问偏移量DLGWINDOWEXTRA
(包括值cbWndExtra
您在窗口类中指定的)。
另请参阅下面的对话框专用的额外方法。
方法2:GWLP_USERDATA
之前所提WND
struct 恰好包含一个指针大小的字段,系统不使用该字段。它是使用以下方式访问的GetWindowLongPtr
具有负索引(即GWLP_USERDATA
)。负索引将访问内部的字段WND
结构。请注意,根据this https://winterdom.com/dev/ui/wnd/,负索引似乎并不代表内存偏移量,而是任意的。
问题在于GWLP_USERDATA
就是不清楚,过去也一直不清楚,什么exactly该字段的目的就是该字段的所有者是谁。也可以看看这个问题 https://stackoverflow.com/questions/41521809/who-is-owner-of-gwlp-userdata-cell. 普遍的共识是没有共识。可能就是这样GWLP_USERDATA
本来是要被使用的窗口的用户, 并不是窗口类的作者。这意味着在 WNDPROC 内部使用它是完全不正确的,因为 WNDPROC 始终由窗口类作者提供。
我个人确信这是工程师们提出的意图GWLP_USERDATA
仅仅因为如果这是真的,那么整个 API 就是健全的、可扩展的且面向未来的。但如果不是这样,那么 API 就不是这两个,而且它会是多余的cbWndExtra
.
我所知道的所有标准 Windows 控件(例如BUTTON
, EDIT
等)遵守这一点并且不要使用GWLP_USERDATA
在内部,将其保留给窗口uses这些控制。问题是有太多的例子,包括在 MSDN 和 SO 上,它们打破了这个规则并使用GWLP_USERDATA
用于实现窗口类。这有效地消除了最干净、最简单的控制方法user将上下文指针与它相关联,只是因为太多人做“错误”(根据我对“错误”的定义)。最坏的情况是,用户代码不知道GWLP_USERDATA
被占用,并且可能会覆盖它,这可能会导致应用程序崩溃。
由于长期存在的所有权争议GWLP_USERDATA
,一般来说使用它并不安全。如果你是创作一个窗口类,无论如何你可能永远都不应该使用它。如果你是using一个窗口,只有当您确定它不被窗口类使用时才应该这样做。
方法三:设置属性
The SetProp
函数族实现对属性表的访问。每个窗口都有其自己独立的属性。该表的键在 API 表面级别是一个字符串,但在内部它实际上是一个 ATOM。
SetProp
可以被窗口类使用authors,和窗口users,它也有问题,但它们不同于GWLP_USERDATA
。您必须确保用作属性键的字符串不会发生冲突。窗口用户可能不一定知道窗口类作者在内部使用什么字符串。尽管冲突不太可能发生,但您可以通过使用 GUID 作为字符串等方式完全避免冲突。从全局 ATOM 表的内容可以明显看出,许多程序都以这种方式使用 GUID。
SetProp
必须小心使用。大多数资源并没有解释这个函数的缺陷。在内部,它使用GlobalAddAtom
。这有几个含义,使用此函数时需要考虑这些含义:
-
打电话时SetProp
(或任何其他使用全局 ATOM 表的 API),您可以使用ATOM
,当您注册一个新字符串时得到的GlobalAddAtom
。 ATOM 只是一个整数,它引用 ATOM 表中的一个条目。这将提高性能;SetProp
内部总是使用ATOM
s 作为属性键,而不是字符串。传递字符串会导致SetProp
以及类似的函数,首先在 ATOM 表中内部搜索匹配项。通过一个ATOM
直接跳过在全局原子表中搜索字符串。
-
全局原子表中可能的字符串原子数量在系统范围内限制为 16384。这是因为原子是 16 位 uint,范围从0xC000
to 0xFFFF
(以下所有值0xC000
是指向固定字符串的伪原子(使用起来完全没问题,但你不能保证没有其他人在使用它们))。使用许多不同的属性名称不是一个好主意,更不用说这些名称是在运行时动态生成的了。相反,您可以使用单个属性来存储指向包含您需要的所有数据的结构的指针。
-
如果您使用 GUID,则可以安全地使用相同的 GUIDevery您正在使用的窗口,甚至跨不同的软件项目,因为每个窗口都有自己的属性。这样,你所有的软件最多只会用完two全局原子表中的条目(作为窗口类作者,您最多需要一个 GUID,作为窗口类用户,您最多需要一个 GUID)。事实上,定义两个事实上的标准 GUID 可能是有意义的,每个人都可以将其用作上下文指针(实际上不会发生)。
-
因为属性使用GlobalAddAtom
,您必须确保原子未注册。当进程存在时,全局原子不会被清理,并且会堵塞全局原子表,直到操作系统重新启动。为此,您必须确保RemoveProp
叫做。这样做的好地方通常是WM_NCDESTROY
.
-
全局原子是引用计数的。这意味着计数器可能在某个时刻溢出。为了防止溢出,一旦原子的引用计数达到65536,该原子将永远保留在原子表中,并且无论再多GlobalDeleteAtom
可以摆脱它。在这种情况下,必须重新启动操作系统才能释放原子表。
如果要使用,请避免使用许多不同的原子名称SetProp
。除此之外,SetProp
/GetProp
这是一种非常干净和防御性的方法。如果开发人员同意为所有窗口使用相同的 2 个原子名称,原子泄漏的危险可能会大大减轻,但这不会发生。
方法4:设置窗口子类
SetWindowSubclass
是为了允许覆盖WNDPROC
特定窗口的,以便您可以在自己的回调中处理一些消息,并将其余消息委托给原始窗口WNDPROC
。例如,这可以用于监听特定的组合键EDIT
控制,同时将其余消息留给其原始实现。
一个方便的副作用SetWindowSubclass
那是new, 替代品WNDPROC
实际上不是一个WNDPROC
, but a SUBCLASSPROC
.
SUBCLASSPROC
有 2 个附加参数,其中之一是DWORD_PTR dwRefData
。这是任意指针大小的数据。数据来自你,通过最后一个参数来SetWindowSubclass
。然后数据被传递到每次调用替换的SUBCLASSPROC
。要是every WNDPROC
有了这个参数,我们就不会陷入这种可怕的境地了!
This method only helps the window class author.(1) During the initial creation of the window (e.g. WM_CREATE
), the window subclasses itself (it can allocate memory for the dwRefData
right there if that's appropriate). Deallocation probably best in WM_NCDESTROY
. The rest of the code that would normally go in WNDPROC
is moved to the replacement SUBCLASSPROC
instead.
它甚至可以在对话框自己的中使用WM_INITDIALOG
信息。如果对话框显示为DialogParamW
,最后一个参数可以用作dwRefData
in a SetWindowSubclass
调用WM_INITDIALOG
信息。然后,所有其余的对话逻辑都进入新的SUBCLASSPROC
,它将收到这个dwRefData
对于每条消息。请注意,这会稍微改变语义。您现在正在对话框的级别上编写window过程,而不是对话过程。
在内部,SetWindowSubclass
使用属性(使用SetProp
) 其原子名称为UxSubclassInfo
。每个实例SetWindowSubclass
使用这个名称,因此它实际上已经存在于任何系统的全局原子表中。它取代了原来的窗口WNDPROC
with a WNDPROC
called MasterSubclassProc
。该函数使用中的数据UxSubclassInfo
属性来获得dwRefData
并致电所有已登记的SUBCLASSPROC
功能。这也意味着您可能不应该使用UxSubclassInfo
作为您自己的任何事物的属性名称。
方法5:Thunk
thunk 是一个小函数,其机器代码在运行时在内存中动态生成。它的目的是调用另一个函数,但带有似乎神奇地突然出现的附加参数。
这可以让你定义一个函数,就像WNDPROC
,但它还有一个附加参数。该参数可以等效于“this”指针。然后,在创建窗口时,替换原始存根WNDPROC
伴随着一声重击,调用真实的、伪的——WNDPROC
带有附加参数。
其工作原理是,当创建 thunk 时,它会在内存中为加载指令生成机器代码,将额外参数的值加载为constant,然后是跳转到通常需要附加参数的函数地址的指令。然后可以像常规的一样调用 thunk 本身WNDPROC
.
该方法可供窗口类作者使用,并且速度极快。然而,实施并非易事。这AtlThunk https://learn.microsoft.com/en-us/windows/win32/api/atlthunk/nf-atlthunk-atlthunk_initdata函数族实现了这一点,但有一个怪癖。它不添加extra范围。相反,它replaces the HWND
的参数WNDPROC
使用您的任意数据(指针大小)。但是,这并不是一个大问题,因为您的任意数据可能是指向包含以下内容的结构的指针HWND
窗户的。
类似地SetWindowSubclass
方法,您可以在创建窗口期间使用任意数据指针创建 thunk。然后,更换窗户WNDPROC
随着一声重响。所有真正的工作都在新的、伪的中进行WNDPROC
这是 thunk 的目标。
thunk 根本不会扰乱全局原子表,也没有字符串唯一性的考虑。然而,就像堆内存中分配的所有其他内容一样,它们必须被释放,之后,可能不再调用 thunk。自从WM_NCDESTROY
是窗口收到的最后一条消息,这是执行此操作的地方。否则,您必须确保重新安装原始版本WNDPROC
当释放 thunk 时。
请注意,这种将“this”指针走私到回调函数中的方法实际上在许多生态系统中都很普遍,包括 C# 与本机 C 函数的互操作。
方法六:全局查表
不需要长篇大论的解释。在您的应用程序中,实现一个全局表来存储HWND
s 作为键,上下文数据作为值。您负责清理桌子,并在需要时使其足够快。
窗口类作者可以使用私有表来实现,窗口用户可以使用自己的表来存储特定于应用程序的信息。不用担心原子或字符串的唯一性。
底线
如果你是这样的人,这些方法就有效窗口类作者:
cbWndExtra、(GWLP_USERDATA)、SetProp、SetWindowSubclass、Thunk、全局查找表。
窗口类作者意味着您正在编写WNDPROC
功能。例如,您可能正在实现一个自定义图片框控件,它允许用户平移和缩放。您可能需要额外的数据来存储平移/缩放数据(例如作为 2D 变换矩阵),以便您可以实现您的WM_PAINT
正确编码。
建议:避免使用 GWLP_USERDATA,因为用户代码可能依赖它;如果可能,请使用 cbWndExtra。
如果你是这样的人,这些方法就有效窗口用户:
GWLP_USERDATA、SetProp、全局查找表。
窗口用户意味着您正在创建一个或多个窗口并在您自己的应用程序中使用它们。例如,您可能会动态创建数量可变的按钮,每个按钮都与单击时相关的不同数据相关联。
建议:如果它是标准 Windows 控件,请使用 GWLP_USERDATA,或者您确定该控件不会在内部使用它。否则,SetProp
.
使用对话框时额外提及
默认情况下,对话框使用具有以下功能的窗口类cbWndExtra
set to DLGWINDOWEXTRA
。可以为对话框定义您自己的窗口类,您可以在其中分配,例如,DLGWINDOWEXTRA + sizeof(void*)
,然后访问GetWindowLongPtrW(hDlg, DLGWINDOWEXTRA)
。但这样做时,您会发现自己必须回答您不喜欢的问题。例如,哪个WNDPROC
你用吗(答案:你可以用DefDlgProc
),或者您使用哪种类样式(默认对话框碰巧使用CS_SAVEBITS | CS_DBLCLKS
,但祝你好运找到权威的参考)。
内DLGWINDOEXTRA
字节,对话框恰好保留了一个指针大小的字段,可以使用GetWindowLongPtr
带索引DWLP_USER
。这是一种额外的GWLP_USERDATA
,并且理论上也存在同样的问题。在实践中,我只见过它在内部使用过DLGPROC
最终被传递给DialogBox[Param]
。毕竟,窗口用户仍然有GWLP_USERDATA
。因此,使用它可能是安全的窗口类的实现几乎在所有情况下。