存储此指针以便在 WndProc 中使用的最佳方法
我有兴趣知道存储 this
指针以供在 WndProc
中使用的最佳/常见方法。 我知道有几种方法,但据我了解,每种方法都有自己的缺点。 我的问题是:
有哪些不同的方法可以生成这种代码:
CWindow::WndProc(UINT msg, WPARAM wParam, LPARAM)
{
this->DoSomething();
}
我可以想到 Thunk、HashMap、线程本地存储和 Window 用户数据结构。
这些方法的优点/缺点是什么?
代码示例和建议可获得积分。
这纯粹是出于好奇心。 使用 MFC 之后,我只是想知道它是如何工作的,然后开始考虑 ATL 等。
编辑: 我最早可以在中有效使用 HWND
的地方是什么?窗口过程? 它被记录为WM_NCCREATE
- 但如果您实际进行实验,这不是发送到窗口的第一条消息。
编辑: ATL 使用 thunk 来访问 this 指针。 MFC 使用 HWND
的哈希表查找。
I'm interested to know the best / common way of storing a this
pointer for use in the WndProc
. I know of several approaches, but each as I understand it have their own drawbacks. My questions are:
What different ways are there of producing this kind of code:
CWindow::WndProc(UINT msg, WPARAM wParam, LPARAM)
{
this->DoSomething();
}
I can think of Thunks, HashMaps, Thread Local Storage and the Window User Data struct.
What are the pros / cons of each of these approaches?
Points awarded for code examples and recommendations.
This is purely for curiosities sake. After using MFC I've just been wondering how that works and then got to thinking about ATL etc.
Edit: What is the earliest place I can validly use the HWND
in the window proc? It is documented as WM_NCCREATE
- but if you actually experiment, that's not the first message to be sent to a window.
Edit: ATL uses a thunk for accessing the this pointer. MFC uses a hashtable lookup of HWND
s.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(11)
这个问题在 SO 上有很多重复和几乎重复的问题,但我见过的几乎没有一个答案探讨了他们选择的解决方案的陷阱。
有多种方法可以将任意数据指针与窗口关联起来,并且需要考虑两种不同的情况。 根据具体情况,可能性也不同。
情况 1 是当您编写窗口类时。 这意味着您正在实现
WNDPROC
,并且您的意图是让其他人在他们的应用程序中使用您的窗口类。 您通常不知道谁将使用您的窗口类以及用途。情况 2 是当您使用自己的应用程序中已存在的窗口类时。 一般来说,您无权访问窗口类源代码,也无法对其进行修改。
我假设问题不是最初将数据指针放入
WNDPROC
中(这只是通过CREATESTRUCT
与CreateWindow[ExW]
中的lpParam
参数),而是如何存储它以供后续调用。方法 1:cbWndExtra
当 Windows 创建窗口实例时,它会在内部分配一个
WND
结构体。 这个结构体有一定的大小,包含各种与窗口相关的东西,比如它的位置、它的窗口类和它的当前 WNDPROC。 在此结构的末尾,Windows 有选择地分配属于该结构的许多附加字节。 该编号在WNDCLASSEX.cbWndExtra
中指定,并在RegisterWindowClassEx
中使用。这意味着只有当您是注册窗口类的人(即您正在创作窗口类)时才能使用此方法。
应用程序无法直接访问
WND
结构。 请改用GetWindowLong[Ptr]
。 非负索引访问结构末尾额外字节内的内存。 “0”将访问第一个额外字节。如果您正在编写窗口类,那么这是一种干净、快速的方法。 大多数Windows内部控件似乎都使用这种方法。
不幸的是,这种方法在对话框(
DialogBox
系列)中表现不佳。 除了提供对话框模板之外,您还将拥有一个对话框窗口类,这可能会变得维护起来很麻烦(除非您出于其他原因需要这样做)。 如果您确实想在对话框中使用它,则必须在对话框模板中指定窗口类名称,并确保在显示对话框之前注册该窗口类,并且您需要为该对话框实现一个WNDPROC
对话框(或使用DefDlgProc
)。 此外,所有对话框都已在cbWndExtra
中保留一定数量的字节,以便对话框管理器正常运行。 所需的额外字节数是 DLGWINDOWEXTRA 常量。 这意味着您的内容需要位于对话框已保留的额外字节之后。 将所有对额外内存的访问偏移DLGWINDOWEXTRA
(包括您在窗口类中指定的cbWndExtra
的值)。另请参阅下面的对话框专用的额外方法。
方法2:GWLP_USERDATA
前面提到的
WND
结构体恰好包含一个指针大小的字段,该字段未被系统使用。 使用带有负索引(即GWLP_USERDATA
)的GetWindowLongPtr
来访问它。 负索引将访问 WND 结构内的字段。 请注意,根据 this,负索引似乎并不代表内存偏移量,而是任意的。GWLP_USERDATA
的问题在于,过去也不清楚该字段的具体用途是什么,以及所有者是谁。这个领域的。 另请参阅此问题。 普遍的共识是没有达成共识。GWLP_USERDATA
可能是供窗口用户使用的,而不是< em>窗口类的作者。 这意味着在 WNDPROC 内部使用它是完全不正确的,因为 WNDPROC 始终由窗口类作者提供。我个人相信,这是提出
GWLP_USERDATA
的工程师的意图,因为如果这是真的,那么整个 API 就是健全的、可扩展的且面向未来的。 但如果不是这样,那么该 API 就不是这两个 API,并且对于cbWndExtra
来说它将是多余的。我所知道的所有标准 Windows 控件(例如
BUTTON
、EDIT
等)都遵循这一点,并且不在内部使用GWLP_USERDATA
,从而使对于使用这些控件的窗口来说,它是免费的。 问题是有太多的例子,包括在 MSDN 和 SO 上,它们打破了这个规则并使用 GWLP_USERDATA 来实现窗口类。 这实际上剥夺了控件用户将上下文指针与其关联的最干净、最简单的方法,仅仅是因为太多人做“错误”(根据我对“错误”的定义) 。 最坏的情况是,用户代码不知道GWLP_USERDATA
已被占用,并且可能会覆盖它,这可能会导致应用程序崩溃。由于关于 GWLP_USERDATA 所有权的长期争议,使用它通常并不安全。 如果您正在创作一个窗口类,那么您可能永远都不应该使用它。 如果您使用窗口,则只有在确定窗口类不使用该窗口时才应该这样做。
方法 3:SetProp
SetProp
系列函数实现对属性表的访问。 每个窗口都有其自己独立的属性。 该表的键在 API 表面级别是一个字符串,但在内部它实际上是一个 ATOM。SetProp
可以由窗口类authors和窗口用户使用,它也有问题,但它们与GWLP_USERDATA< /代码>。 您必须确保用作属性键的字符串不会发生冲突。 窗口用户可能不一定知道窗口类作者在内部使用什么字符串。 尽管冲突不太可能发生,但您可以通过使用 GUID 作为字符串等方式来完全避免冲突。 从全局 ATOM 表的内容可以明显看出,许多程序都以这种方式使用 GUID。
SetProp
必须谨慎使用。 大多数资源并没有解释这个函数的缺陷。 在内部,它使用 GlobalAddAtom。 这有几个含义,在使用此函数时需要考虑:当调用
SetProp
(或任何其他使用全局 ATOM 表的 API)时,您可以使用ATOM
,当您注册新字符串GlobalAddAtom
时获得。 ATOM 只是一个整数,它引用 ATOM 表中的一个条目。 这将提高性能;SetProp
在内部始终使用ATOM
作为属性键,而不是字符串。 传递字符串会导致SetProp
和类似函数首先在 ATOM 表内部搜索匹配项。 直接传递ATOM
会跳过在全局原子表中搜索字符串。全局原子表中可能的字符串原子数量在系统范围内限制为 16384。 这是因为原子是范围从
0xC000
到0xFFFF
的 16 位 uint(0xC000
以下的所有值都是指向固定字符串的伪原子(完全可以使用,但你不能保证没有其他人在使用它们))。 使用许多不同的属性名称不是一个好主意,更不用说这些名称是在运行时动态生成的。 相反,您可以使用单个属性来存储指向包含您需要的所有数据的结构的指针。如果您使用 GUID,则可以安全地为您正在使用的每个窗口使用相同的 GUID,即使是在不同的软件项目中也是如此,因为每个窗口都有其自己的属性。 这样,您的所有软件最多只会使用全局原子表中的两个条目(您最多需要一个 GUID 作为窗口类作者,最多需要一个 GUID 作为窗口类用户)。 事实上,定义两个事实上的标准 GUID 可能是有意义的,每个人都可以将其用作上下文指针(实际上不会发生)。
由于属性使用
GlobalAddAtom
,因此您必须确保原子未注册。 当进程存在时,全局原子不会被清理,并且会堵塞全局原子表,直到操作系统重新启动。 为此,您必须确保调用RemoveProp
。 这样做的好地方通常是WM_NCDESTROY
。全局原子是引用计数的。 这意味着计数器可能在某个时刻溢出。 为了防止溢出,一旦原子的引用计数达到65536,该原子将永远保留在原子表中,并且再多的
GlobalDeleteAtom
也无法删除它。 在这种情况下,必须重新启动操作系统才能释放原子表。如果您想使用
SetProp
,请避免使用许多不同的原子名称。 除此之外,SetProp
/GetProp
是一种非常干净和防御性的方法。 如果开发人员同意为所有窗口使用相同的 2 个原子名称,原子泄漏的危险可能会大大减轻,但这不会发生。方法 4:SetWindowSubclass
SetWindowSubclass
旨在允许覆盖特定窗口的WNDPROC
,以便您可以在自己的回调中处理一些消息,并委托其余消息到原始的WNDPROC
。 例如,这可用于侦听 EDIT 控件中的特定按键组合,同时将其余消息保留为其原始实现。SetWindowSubclass
的一个方便的副作用是 new,替换WNDPROC
实际上不是WNDPROC
,而是一个 <代码>SUBCLASSPROC。SUBCLASSPROC
有 2 个附加参数,其中之一是DWORD_PTR dwRefData
。 这是任意指针大小的数据。 数据来自您,通过SetWindowSubclass
的最后一个参数。 然后,数据会传递到替换SUBCLASSPROC
的每次调用。 如果只有 everyWNDPROC
有这个参数,那么我们就不会处于这种可怕的情况了!此方法仅对窗口类作者有帮助。(1) 在窗口初始创建期间(例如
WM_CREATE
),窗口会对其自身进行子类化(它可以为< code>dwRefData 就在那里,如果合适的话)。 释放可能在WM_NCDESTROY
中效果最好。 通常放入WNDPROC
的其余代码被移至替换的SUBCLASSPROC
中。它甚至可以用在对话框自己的
WM_INITDIALOG
消息中。 如果对话框使用DialogParamW
显示,则最后一个参数可用作WM_INITDIALOG
中SetWindowSubclass
调用中的dwRefData
> 消息。 然后,所有其余的对话逻辑都进入新的SUBCLASSPROC
中,它将为每条消息接收此dwRefData
。 请注意,这会稍微改变语义。 您现在是在对话框的窗口 过程级别编写,而不是在对话框过程级别。在内部,
SetWindowSubclass
使用一个原子名称为UxSubclassInfo
的属性(使用SetProp
)。SetWindowSubclass
的每个实例都使用此名称,因此它几乎已经存在于任何系统上的全局原子表中。 它用名为MasterSubclassProc
的WNDPROC
替换了窗口的原始WNDPROC
。 该函数使用UxSubclassInfo
属性中的数据来获取dwRefData
并调用所有注册的SUBCLASSPROC
函数。 这也意味着您可能不应该使用UxSubclassInfo
作为您自己的任何属性名称。方法5:Thunk
thunk 是一个小函数,其机器代码在运行时在内存中动态生成。 它的目的是调用另一个函数,但带有似乎神奇地突然出现的附加参数。
这可以让您定义一个类似于 WNDPROC 的函数,但它有一个附加参数。 该参数可以等效于“this”指针。 然后,在创建窗口时,您将原始存根
WNDPROC
替换为一个 thunk,该 thunk 通过附加参数调用真正的伪WNDPROC
。其工作原理是,当创建 thunk 时,它会在内存中生成加载指令的机器代码,将额外参数的值加载为常量,然后跳转到地址的跳转指令通常需要附加参数的函数。 然后可以像常规
WNDPROC
一样调用 thunk 本身。该方法可供窗口类作者使用,并且速度非常快。 然而,实施并非易事。
AtlThunk
系列函数实现了这一点,但有一个怪癖。 它不添加额外参数。 相反,它用您的任意数据(指针大小)替换WNDPROC
的HWND
参数。 然而,这并不是一个大问题,因为您的任意数据可能是指向包含窗口的HWND
的结构的指针。与
SetWindowSubclass
方法类似,您可以在窗口创建期间使用任意数据指针创建 thunk。 然后,用 thunk 替换窗口的WNDPROC
。 所有实际工作都在新的伪WNDPROC
中进行,它是 thunk 的目标。thunk 根本不会扰乱全局原子表,也没有字符串唯一性的考虑。 然而,就像堆内存中分配的所有其他内容一样,它们必须被释放,之后,可能不再调用 thunk。 由于 WM_NCDESTROY 是窗口收到的最后一条消息,因此可以在此处执行此操作。 否则,您必须确保在释放 thunk 时重新安装原始的
WNDPROC
。请注意,这种将“this”指针走私到回调函数中的方法实际上在许多生态系统中都很普遍,包括 C# 与本机 C 函数的互操作。
方法6:全局查找表
不需要长解释。 在您的应用程序中,实现一个全局表,在其中将
HWND
存储为键,将上下文数据存储为值。 您负责清理桌子,并在需要时使其足够快。窗口类作者可以使用私有表来实现,窗口用户可以使用自己的表来存储特定于应用程序的信息。 不用担心原子或字符串的唯一性。
底线
如果您是窗口类作者,这些方法就有效:
cbWndExtra、(GWLP_USERDATA)、SetProp、SetWindowSubclass、Thunk、全局查找表。
Window Class Author 意味着您正在编写
WNDPROC
函数。 例如,您可能正在实现一个自定义图片框控件,它允许用户平移和缩放。 您可能需要额外的数据来存储平移/缩放数据(例如作为 2D 转换矩阵),以便您可以正确实现您的WM_PAINT
代码。建议:避免使用 GWLP_USERDATA,因为用户代码可能依赖它; 如果可能,请使用 cbWndExtra。
如果您是窗口用户,这些方法就有效:
GWLP_USERDATA、SetProp、全局查找表。
窗口用户意味着您正在创建一个或多个窗口并在您自己的应用程序中使用它们。 例如,您可能动态创建数量可变的按钮,每个按钮都与单击时相关的不同数据相关联。
建议:如果它是标准 Windows 控件,请使用 GWLP_USERDATA,或者您确定该控件不会在内部使用它。 否则,
SetProp
。使用对话框时需要额外提及
默认情况下,对话框使用将
cbWndExtra
设置为DLGWINDOWEXTRA
的窗口类。 可以为对话框定义您自己的窗口类,例如在其中分配DLGWINDOWEXTRA + sizeof(void*)
,然后访问GetWindowLongPtrW(hDlg, DLGWINDOWEXTRA)
。 但这样做时,您会发现自己必须回答您不喜欢的问题。 例如,您使用哪种WNDPROC
(答案:您可以使用DefDlgProc
),或者您使用哪种类样式(默认对话框恰好使用CS_SAVEBITS | CS_DBLCLKS,但祝你好运找到权威的参考)。
在 DLGWINDOEXTRA 字节内,对话框恰好保留了一个指针大小的字段,可以使用索引为 DWLP_USER 的 GetWindowLongPtr 来访问该字段。 这是一种额外的
GWLP_USERDATA
,并且理论上具有相同的问题。 在实践中,我只见过它在DLGPROC
中使用,最终被传递给DialogBox[Param]
。 毕竟,窗口用户仍然拥有GWLP_USERDATA
。 因此,在几乎所有情况下,使用窗口类实现可能都是安全的。This question has many duplicates and almost-duplicates on SO, yet almost none of the answers I've seen explore the pitfalls of their chosen solutions.
There are several ways how to associate an arbitrary data pointer with a window, and there are 2 different situations to consider. Depending on the situation, the possibilities are different.
Situation 1 is when you are authoring the window class. This means you are implementing the
WNDPROC
, and it is your intention that other people use your window class in their applications. You generally do not know who will use your window class, and for what.Situation 2 is when you are using a window class that already exists in your own application. In general, you do not have access to the window class source code, and you cannot modify it.
I'm assuming that the problem isn't getting the data pointer into the
WNDPROC
initially (that would just be the through theCREATESTRUCT
with thelpParam
parameter inCreateWindow[ExW]
), but rather, how to store it for subsequent calls.Method 1: cbWndExtra
When Windows creates an instance of a window, it internally allocates a
WND
struct. This struct has a certain size, contains all sorts of window-related things, like its position, its window class, and its current WNDPROC. At the end of this struct, Windows optionally allocates a number of additional bytes that belong to the struct. The number is specified inWNDCLASSEX.cbWndExtra
, which is used inRegisterWindowClassEx
.This implies that this method can only be used if you are the person who registers the window class, i.e. you are authoring the window class.
Applications cannot directly access the
WND
struct. Instead, useGetWindowLong[Ptr]
. Non-negative indices access memory inside the extra bytes at the end of the struct. "0" will access the first extra bytes.This is a clean, and fast way of doing it, if you are authoring the window class. Most Windows internal controls seem to use this method.
Unfortunately, this method does not play so well with dialogs (
DialogBox
family). You would have a dialog window class in addition to providing the dialog template, which can become cumbersome to maintain (unless you need to do so for other reasons anyway). If you do want to use it with dialogs, you must specify the window class name in the dialog template, make sure this window class is registered before showing the dialog, and you need to implement aWNDPROC
for the dialog (or useDefDlgProc
). Furthermore, all dialogs already reserve a certain amount of bytes incbWndExtra
for the dialog manager to function properly. The number of extra bytes needed is theDLGWINDOWEXTRA
constant. This means your stuff needs to come after the extra bytes which are already reserved by the dialog. Offset all accesses to the extra memory byDLGWINDOWEXTRA
(including the value ofcbWndExtra
which you specify in your window class).See also below for an extra method exclusive to dialogs.
Method 2: GWLP_USERDATA
The aforementioned
WND
struct happens to contain one pointer-sized field, which is not used by the system. It is accessed usingGetWindowLongPtr
with a negative index (namely,GWLP_USERDATA
). A negative index will access fields inside theWND
structure. Note that according to this, the negative indices do not seem to represent memory offsets, but are arbitrary.The problem with
GWLP_USERDATA
is that it is not clear, and it has not been clear in the past, what exactly the purpose of this field is, and hence, who the owner of this field is. See also this question. The general consensus is that there is no consensus. It is likely thatGWLP_USERDATA
was meant to be used by users of the window, and not authors of the window class. This implies that using it inside of the WNDPROC is strictly incorrect, as the WNDPROC is always provided by the window class author.I am personally convinced that this is the intention of the engineers that came up with
GWLP_USERDATA
simply because if it is true, then the API as a whole is sound, extensible, and future-proof. But if it is not true, then the API is neither of those, and it would be redundant withcbWndExtra
.All standard windows controls that I am aware of (e.g.
BUTTON
,EDIT
, etc.) adhere to this and do not useGWLP_USERDATA
internally, leaving it free for the window which uses these controls. The problem is that there are WAY too many examples, including on MSDN and on SO, which break this rule and useGWLP_USERDATA
for implementation of the window class. This effectively takes away the cleanest and simplest method for a control user to associate a context pointer with it, simply because way too many people are doing it "wrong" (according to my definition of "wrong"). At worst, the user code does not know thatGWLP_USERDATA
is occupied, and may overwrite it, which would likely crash the application.Because of this longstanding dispute about the ownership of
GWLP_USERDATA
, it is not generally safe to use it. If you are authoring a window class, you probably never should have used it anyway. If you are using a window, you should only do so if you are certain that it is not used by the window class.Method 3: SetProp
The
SetProp
family of functions implements access to a property table. Each window has its own, independent properties. The key of this table is a string at API surface level, but internally it is really an ATOM.SetProp
can be used by window class authors, and window users, and it has issues too, but they are different fromGWLP_USERDATA
. You must make sure that the strings used as the property keys do not collide. The window user may not necessarily know what strings the window class author is using internally. Even though conflicts are unlikely, you can avoid them entirely by using a GUID as string, for example. As is evident when looking at the contents of the global ATOM table, many programs use GUIDs this way.SetProp
must be used with care. Most resources do not explain the pitfalls of this function. Internally, it usesGlobalAddAtom
. This has several implications, which need to be considered when using this function:When calling
SetProp
(or any other API that uses the global ATOM table), instead of a string, you can use anATOM
, which you get when you register a new stringGlobalAddAtom
. An ATOM is just an integer which refers to one entry in the ATOM table. This will improve performance;SetProp
internally always usesATOM
s as property keys, never strings. Passing a string causesSetProp
and similar functions to internally search the ATOM table for a match first. Passing anATOM
directly skips searching the string in the global atom table.The number of possible string atoms in the global atom table is limited to 16384, system-wide. This is because atoms are 16-bit uints ranging from
0xC000
to0xFFFF
(all values below0xC000
are pseudo-atoms pointing to fixed strings (which are perfectly fine to use, but you cannot guarantee that nobody else is using them)). It is a bad idea to use many different property names, let alone if those names are dynamically generated at runtime. Instead, you can use a single property to store a pointer to a structure that contains all the data you need.If you are using a GUID, it is safe to use the same GUID for every window you are working with, even across different software projects, since every window has its own properties. This way, all of your software will only use up at most two entries in the global atom table (you'll need at most one GUID as a window class author, and at most one GUID as a window class user). In fact, it might make sense to define two de-facto standard GUIDs everyone can use for their context pointers (realistically not going to happen).
Because properties use
GlobalAddAtom
, you must make sure that the atoms are unregistered. Global atoms are not cleaned up when the process exists and will clog up the global atom table until the operating system is restarted. To do this, you must make sure thatRemoveProp
is called. A good place for this is usuallyWM_NCDESTROY
.Global atoms are reference-counted. This implies that the counter can overflow at some point. To protect against overflows, once the reference count of an atom reaches 65536, the atom will stay in the atom table forever, and no amount of
GlobalDeleteAtom
can get rid of it. The operating system must be restarted to free the atom table in this case.Avoid having many different atom names if you want to use
SetProp
. Other than that,SetProp
/GetProp
is a very clean and defensive approach. The dangers of atom leaks could be greatly mitigated if developers agreed upon using the same 2 atom names for all windows, but that is not going to happen.Method 4: SetWindowSubclass
SetWindowSubclass
is meant to allow overriding theWNDPROC
of a specific window, so that you can handle some messages in your own callback, and delegate the rest of the messages to the originalWNDPROC
. For example, this can be used to listen for specific key combinations in anEDIT
control, while leaving the rest of the messages to its original implementation.A convenient side effect of
SetWindowSubclass
is that the new, replacementWNDPROC
is not actually aWNDPROC
, but aSUBCLASSPROC
.SUBCLASSPROC
has 2 additional parameters, one of them isDWORD_PTR dwRefData
. This is arbitrary pointer-sized data. The data comes from you, through the last parameter toSetWindowSubclass
. The data is then passed to every invocation of the replacementSUBCLASSPROC
. If only everyWNDPROC
had this parameter, then we wouldn't be in this horrible situation!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 thedwRefData
right there if that's appropriate). Deallocation probably best inWM_NCDESTROY
. The rest of the code that would normally go inWNDPROC
is moved to the replacementSUBCLASSPROC
instead.It can even be used in a dialog's own
WM_INITDIALOG
message. If the dialog is shown withDialogParamW
, the last parameter can be used asdwRefData
in aSetWindowSubclass
call in theWM_INITDIALOG
message. Then, all the rest of the dialog logic goes in the newSUBCLASSPROC
, which will receive thisdwRefData
for every message. Note that this changes semantics slightly. You are now writing at the level of the dialog's window procedure, not the dialog procedure.Internally,
SetWindowSubclass
uses a property (usingSetProp
) whose atom name isUxSubclassInfo
. Every instance ofSetWindowSubclass
uses this name, so it will already be in the global atom table on practically any system. It replaces the window's originalWNDPROC
with aWNDPROC
calledMasterSubclassProc
. That function uses the data in theUxSubclassInfo
property to get thedwRefData
and call all registeredSUBCLASSPROC
functions. This also implies that you should probably not useUxSubclassInfo
as your own property name for anything.Method 5: Thunk
A thunk is a small function whose machine code is dynamically generated at run-time in memory. Its purpose is to call another function, but with additional parameters that seem to magically come out of nowhere.
This would let you define a function which is like
WNDPROC
, but it has one additional parameter. This parameter could be the equivalent of a "this" pointer. Then, when creating the window, you replace the original stubWNDPROC
with a thunk that calls the real, pseudo-WNDPROC
with an additional parameter.The way this works is that when the thunk is created, it generates machine code in memory for a load instruction, loading the value of the extra parameter as a constant, and then a jump instruction to the address of the function which would normally require an additional parameter. The thunk itself can then be called as if it were a regular
WNDPROC
.This method can be used by window class authors and is extremely fast. However, the implementation is not trivial. The
AtlThunk
family of functions implements this, but with a quirk. It does not add an extra parameter. Instead, it replaces theHWND
parameter ofWNDPROC
with your arbitrary piece of data (pointer-sized). However, that is not a big problem since your arbitrary data may be a pointer to a struct containing theHWND
of the window.Similarly to the
SetWindowSubclass
method, you would create the thunk during window creation, using an arbitrary data pointer. Then, replace the window'sWNDPROC
with the thunk. All the real work goes in the new, pseudo-WNDPROC
which is targeted by the thunk.Thunks do not mess with the global atom table at all, and there are no string uniqueness considerations either. However, like everything else that is allocated in heap memory, they must be freed, and after that, the thunk may no longer be called. Since
WM_NCDESTROY
is the last message a window receives, this is the place to do that. Otherwise, you must make sure to reinstall the originalWNDPROC
when freeing the thunk.Note that this method of smuggling a "this" pointer into a callback function is practically ubiquitous in many ecosystems, including C# interop with native C functions.
Method 6: Global lookup table
No long explanation needed. In your application, implement a global table where you store
HWND
s as keys and context data as values. You are responsible for cleaning up the table, and, if needed, to make it sufficiently fast.Window class authors can use private tables for their implementations, and window users can use their own tables to store application-specific information. There are no concerns about atoms or string uniqueness.
Bottom line
These methods work if you are the Window Class Author:
cbWndExtra, (GWLP_USERDATA), SetProp, SetWindowSubclass, Thunk, Global lookup table.
Window Class Author means that you are writing the
WNDPROC
function. For example, you may be implementing a custom picture box control, which allows the user to pan and zoom. You may need additional data to store pan/zoom data (e.g. as a 2D transformation matrix), so that you can implement yourWM_PAINT
code correctly.Recommendation: Avoid GWLP_USERDATA because the user code may rely on it; use cbWndExtra if possible.
These methods work if you are the Window User:
GWLP_USERDATA, SetProp, Global lookup table.
Window User means you are creating one or more of the windows and use them in your own application. For example, you may be creating a variable number of buttons dynamically, and each of them is associated with a different piece of data that is relevant when it is being clicked.
Recommendation: Use GWLP_USERDATA if it's a standard Windows control, or you are sure that the control doesn't use it internally. Otherwise,
SetProp
.Extra mention when using dialogs
Dialogs, by default, use a window class that has
cbWndExtra
set toDLGWINDOWEXTRA
. It is possible to define your own window class for a dialog, where you allocate, say,DLGWINDOWEXTRA + sizeof(void*)
, and then accessGetWindowLongPtrW(hDlg, DLGWINDOWEXTRA)
. But while doing so you will find yourself having to answer questions you won't like. For example, whichWNDPROC
do you use (answer: you can useDefDlgProc
), or which class styles do you use (the default dialogs happen to useCS_SAVEBITS | CS_DBLCLKS
, but good luck finding an authoritative reference).Within the
DLGWINDOEXTRA
bytes, dialogs happen to reserve a pointer-sized field, which can be accessed usingGetWindowLongPtr
with indexDWLP_USER
. This is kind of an additionalGWLP_USERDATA
, and, in theory, has the same problems. In practice I have only ever seen this used inside theDLGPROC
which ends up being passed toDialogBox[Param]
. After all, the window user still hasGWLP_USERDATA
. So it is probably safe to use for the window class implementation in practically every situation.虽然使用 SetWindowLongPtr 和 GetWindowLongPtr 访问 GWL_USERDATA 可能听起来是个好主意,但我强烈建议不要使用这种方法。
这正是 Zeus 编辑器所使用的方法,近年来它除了带来痛苦之外什么也没有造成。 。
我认为发生的情况是第三方Windows消息被发送到Zeus,它也有他们的GWL_USERDATA 值设置。 其中一个特别的应用程序是 Microsoft 工具,它提供了在任何 Windows 应用程序中输入亚洲字符的替代方法(即某种软件键盘实用程序)。
问题是 Zeus 总是假设 GWL_USERDATA 数据是由它设置的,并且尝试将数据用作 this 指针,这会导致崩溃。
如果我要用我现在所知道的再次做这一切,我会采用缓存哈希查找方法,其中窗口句柄用作键。
While using the SetWindowLongPtr and GetWindowLongPtr to access the GWL_USERDATA might sound like a good idea, I would strongly recommend not using this approach.
This is the exactly the approached used by the Zeus editor and in recent years it has caused nothing but pain.
I think what happens is third party windows messages are sent to Zeus that also have their GWL_USERDATA value set. One application in particular was a Microsoft tool that provied an alternative way to enter Asian characters in any windows application (i.e. some sort of software keyboard utility).
The problem is Zeus always assumes the GWL_USERDATA data was set by it and tries to use the data as a this pointer, which then results in a crash.
If I was to do it all again with, what I know now, I would go for a cached hash lookup approach where the window handle is used as the key.
在构造函数中,使用“this”作为 lpParam 参数调用 CreateWindowEx。
然后,在 WM_NCCREATE 上,调用以下代码:
然后,在窗口过程的顶部,您可以执行以下操作:
这允许您执行此操作:
当然,您可以使用相同的技术来调用类似于上面的函数的内容:
。 ..然后可以按预期使用其“this”指针。
In your constructor, call CreateWindowEx with "this" as the lpParam argument.
Then, on WM_NCCREATE, call the following code:
Then, at the top of your window procedure you could do the following:
Which allows you to do this:
Of course, you could use the same technique to call something like your function above:
... which can then use its "this" pointer as expected.
您应该使用
GetWindowLongPtr()
/SetWindowLongPtr()
(或已弃用的GetWindowLong()
/SetWindowLong()
) 。 他们速度很快,并且完全按照您的要求做。 唯一棘手的部分是确定何时调用SetWindowLongPtr()
- 您需要在发送第一个窗口消息(即WM_NCCREATE
)时执行此操作。请参阅本文获取示例代码和更深入的讨论。
线程本地存储是一个坏主意,因为您可能在一个线程中运行多个窗口。
哈希映射也可以,但是为每个窗口消息(并且有很多)计算哈希函数可能会变得昂贵。
我不确定你是如何使用 thunk 的; 你如何传递重击声?
You should use
GetWindowLongPtr()
/SetWindowLongPtr()
(or the deprecatedGetWindowLong()
/SetWindowLong()
). They are fast and do exactly what you want to do. The only tricky part is figuring out when to callSetWindowLongPtr()
- You need to do this when the first window message is sent, which isWM_NCCREATE
.See this article for sample code and a more in-depth discussion.
Thread-local storage is a bad idea, since you may have multiple windows running in one thread.
A hash map would also work, but computing the hash function for every window message (and there are a LOT) can get expensive.
I'm not sure how you mean to use thunks; how are you passing around the thunks?
我使用 SetProp/GetProp 来存储指向窗口本身数据的指针。 我不确定它与你提到的其他项目相比如何。
I've used SetProp/GetProp to store a pointer to data with the window itself. I'm not sure how it stacks up to the other items you mentioned.
您可以使用
GetWindowLongPtr
和SetWindowLongPtr
; 使用 GWLP_USERDATA 将指针附加到窗口。 但是,如果您正在编写自定义控件,我建议使用额外的窗口字节来完成工作。 注册窗口类时,将WNDCLASS::cbWndExtra
设置为数据大小,如下所示,wc.cbWndExtra = sizeof(Ctrl*);
。您可以使用
GetWindowLongPtr
和SetWindowLongPtr
获取和设置该值,并将nIndex
参数设置为0
。 此方法可以保存GWLP_USERDATA
用于其他目的。GetProp
和SetProp
的缺点是,将通过字符串比较来获取/设置属性。You can use
GetWindowLongPtr
andSetWindowLongPtr
; useGWLP_USERDATA
to attach the pointer to the window. However, if you are writing a custom control I would suggest to use extra window bytes to get the job done. While registering the window class set theWNDCLASS::cbWndExtra
to the size of the data like this,wc.cbWndExtra = sizeof(Ctrl*);
.You can get and set the value using
GetWindowLongPtr
andSetWindowLongPtr
withnIndex
parameter set to0
. This method can saveGWLP_USERDATA
for other purposes.The disadvantage with
GetProp
andSetProp
, there will be a string comparison to get/set a property.关于 SetWindowLong() / GetWindowLong() 安全性,根据 Microsoft 的说法:
遗憾的是,直到 10 月 12 日发布安全更新 ,2004 年,Windows 不会强制执行此规则,允许应用程序设置任何其他应用程序的 GWL_USERDATA。 因此,在未打补丁的系统上运行的应用程序很容易受到调用 SetWindowLong() 的攻击。
With regard to SetWindowLong() / GetWindowLong() security, according to Microsoft:
Unfortunately, until the release of a Security Update on October 12, 2004, Windows would not enforce this rule, allowing an application to set any other application's GWL_USERDATA. Therefore, applications running on unpatched systems are vulnerable to attack through calls to SetWindowLong().
我建议在调用
CreateWindow
之前设置一个thread_local
变量,并在WindowProc
中读取它以找出this
变量(我假设您可以控制 WindowProc)。这样,您将在发送到窗口的第一条消息上拥有
this
/HWND
关联。使用此处建议的其他方法,您很可能会错过一些消息:那些在
WM_CREATE
/WM_NCCREATE
/WM_GETMINMAXINFO
之前发送的消息。I recommend setting a
thread_local
variable just before callingCreateWindow
, and reading it in yourWindowProc
to find out thethis
variable (I presume you have control overWindowProc
).This way you'll have the
this
/HWND
association on the very first message sent to you window.With the other approaches suggested here chances are you'll miss on some messages: those sent before
WM_CREATE
/WM_NCCREATE
/WM_GETMINMAXINFO
.ATL的thunk是最高效的。 thunk 执行一次,并将 WINPROC 的回调函数替换为类自己的消息处理成员函数。 随后的消息通过窗口直接调用类成员函数来传递。 没有比这更快的了。
ATL's thunk is the most efficent. the thunk executes once and replaces the callback function for the WINPROC to the classes own message processing member function. subsiquent messages are passed by a direct call to the classes member function by windows. it doesnt get any faster than that.
过去我使用过 CreateWindowEx 的 lpParam 参数:
这里的技巧是使用 HWND 的
static
std::map
到类实例指针。std::map::find
可能比SetWindowLongPtr
方法性能更高。 不过,使用这种方法编写测试代码肯定更容易。顺便说一句,如果您使用的是 win32 对话框,那么您需要使用
DialogBoxParam
函数。In the past I've used the lpParam parameter of
CreateWindowEx
:The trick here is to have a
static
std::map
of HWND to class instance pointers. Its possible that thestd::map::find
might be more performant than theSetWindowLongPtr
method. Its certainly easier to write test code using this method though.Btw if you are using a win32 dialog then you'll need to use the
DialogBoxParam
function.为了防止Zeus编辑器中出现问题,只需在GetMessage函数中指定窗口即可:
NOTE
该窗口必须属于当前线程。
易于阅读的函数文档
In order to prevent the problem that occurred in the Zeus editor, simply specify the window in the GetMessage function:
NOTE
The window must belong to the current thread.
Easy to read Documentation of the function