存储此指针以便在 WndProc 中使用的最佳方法

发布于 2024-07-06 01:57:57 字数 640 浏览 12 评论 0原文

我有兴趣知道存储 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 HWNDs.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

兲鉂ぱ嘚淚 2024-07-13 01:57:57

这个问题在 SO 上有很多重复和几乎重复的问题,但我见过的几乎没有一个答案探讨了他们选择的解决方案的陷阱。

有多种方法可以将任意数据指针与窗口关联起来,并且需要考虑两种不同的情况。 根据具体情况,可能性也不同。

情况 1 是当您编写窗口类时。 这意味着您正在实现WNDPROC,并且您的意图是让其他人在他们的应用程序中使用您的窗口类。 您通常不知道谁将使用您的窗口类以及用途。

情况 2 是当您使用自己的应用程序中已存在的窗口类时。 一般来说,您无权访问窗口类源代码,也无法对其进行修改。

我假设问题不是最初将数据指针放入 WNDPROC 中(这只是通过 CREATESTRUCTCreateWindow[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 控件(例如 BUTTONEDIT 等)都遵循这一点,并且不在内部使用 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。 这是因为原子是范围从 0xC0000xFFFF 的 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每次调用。 如果只有 every WNDPROC 有这个参数,那么我们就不会处于这种可怕的情况了!

此方法仅对窗口类作者有帮助。(1) 在窗口初始创建期间(例如 WM_CREATE),窗口会对其自身进行子类化(它可以为< code>dwRefData 就在那里,如果合适的话)。 释放可能在 WM_NCDESTROY 中效果最好。 通常放入 WNDPROC 的其余代码被移至替换的 SUBCLASSPROC 中。

它甚至可以用在对话框自己的 WM_INITDIALOG 消息中。 如果对话框使用 DialogParamW 显示,则最后一个参数可用作 WM_INITDIALOGSetWindowSubclass 调用中的 dwRefData > 消息。 然后,所有其余的对话逻辑都进入新的 SUBCLASSPROC 中,它将为每条消息接收此 dwRefData。 请注意,这会稍微改变语义。 您现在是在对话框的窗口 过程级别编写,而不是在对话框过程级别。

在内部,SetWindowSubclass 使用一个原子名称为 UxSubclassInfo 的属性(使用 SetProp)。 SetWindowSubclass 的每个实例都使用此名称,因此它几乎已经存在于任何系统上的全局原子表中。 它用名为 MasterSubclassProcWNDPROC 替换了窗口的原始 WNDPROC。 该函数使用 UxSubclassInfo 属性中的数据来获取 dwRefData 并调用所有注册的 SUBCLASSPROC 函数。 这也意味着您可能不应该使用 UxSubclassInfo 作为您自己的任何属性名称。

方法5:Thunk

thunk 是一个小函数,其机器代码在运行时在内存中动态生成。 它的目的是调用另一个函数,但带有似乎神奇地突然出现的附加参数。

这可以让您定义一个类似于 WNDPROC 的函数,但它有一个附加参数。 该参数可以等效于“this”指针。 然后,在创建窗口时,您将原始存根 WNDPROC 替换为一个 thunk,该 thunk 通过附加参数调用真正的伪 WNDPROC

其工作原理是,当创建 thunk 时,它会在内存中生成加载指令的机器代码,将额外参数的值加载为常量,然后跳转到地址的跳转指令通常需要附加参数的函数。 然后可以像常规 WNDPROC 一样调用 thunk 本身。

该方法可供窗口类作者使用,并且速度非常快。 然而,实施并非易事。 AtlThunk 系列函数实现了这一点,但有一个怪癖。 它不添加额外参数。 相反,它用您的任意数据(指针大小)替换 WNDPROCHWND 参数。 然而,这并不是一个大问题,因为您的任意数据可能是指向包含窗口的 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 the CREATESTRUCT with the lpParam parameter in CreateWindow[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 in WNDCLASSEX.cbWndExtra, which is used in RegisterWindowClassEx.

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, use GetWindowLong[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 a WNDPROC for the dialog (or use DefDlgProc). Furthermore, all dialogs already reserve a certain amount of bytes in cbWndExtra for the dialog manager to function properly. The number of extra bytes needed is the DLGWINDOWEXTRA 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 by DLGWINDOWEXTRA (including the value of cbWndExtra 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 using GetWindowLongPtr with a negative index (namely, GWLP_USERDATA). A negative index will access fields inside the WND 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 that GWLP_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 with cbWndExtra.

All standard windows controls that I am aware of (e.g. BUTTON, EDIT, etc.) adhere to this and do not use GWLP_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 use GWLP_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 that GWLP_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 from GWLP_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 uses GlobalAddAtom. 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 an ATOM, which you get when you register a new string GlobalAddAtom. An ATOM is just an integer which refers to one entry in the ATOM table. This will improve performance; SetProp internally always uses ATOMs as property keys, never strings. Passing a string causes SetProp and similar functions to internally search the ATOM table for a match first. Passing an ATOM 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 to 0xFFFF (all values below 0xC000 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 that RemoveProp is called. A good place for this is usually WM_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 the WNDPROC of a specific window, so that you can handle some messages in your own callback, and delegate the rest of the messages to the original WNDPROC. For example, this can be used to listen for specific key combinations in an EDIT control, while leaving the rest of the messages to its original implementation.

A convenient side effect of SetWindowSubclass is that the new, replacement WNDPROC is not actually a WNDPROC, but a SUBCLASSPROC.

SUBCLASSPROC has 2 additional parameters, one of them is DWORD_PTR dwRefData. This is arbitrary pointer-sized data. The data comes from you, through the last parameter to SetWindowSubclass. The data is then passed to every invocation of the replacement SUBCLASSPROC. If only every WNDPROC 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 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.

It can even be used in a dialog's own WM_INITDIALOG message. If the dialog is shown with DialogParamW, the last parameter can be used as dwRefData in a SetWindowSubclass call in the WM_INITDIALOG message. Then, all the rest of the dialog logic goes in the new SUBCLASSPROC, which will receive this dwRefData 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 (using SetProp) whose atom name is UxSubclassInfo. Every instance of SetWindowSubclass uses this name, so it will already be in the global atom table on practically any system. It replaces the window's original WNDPROC with a WNDPROC called MasterSubclassProc. That function uses the data in the UxSubclassInfo property to get the dwRefData and call all registered SUBCLASSPROC functions. This also implies that you should probably not use UxSubclassInfo 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 stub WNDPROC 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 the HWND parameter of WNDPROC 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 the HWND 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's WNDPROC 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 original WNDPROC 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 HWNDs 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 your WM_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 to DLGWINDOWEXTRA. It is possible to define your own window class for a dialog, where you allocate, say, DLGWINDOWEXTRA + sizeof(void*), and then access GetWindowLongPtrW(hDlg, DLGWINDOWEXTRA). But while doing so you will find yourself having to answer questions you won't like. For example, which WNDPROC do you use (answer: you can use DefDlgProc), or which class styles do you use (the default dialogs happen to use CS_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 using GetWindowLongPtr with index DWLP_USER. This is kind of an additional GWLP_USERDATA, and, in theory, has the same problems. In practice I have only ever seen this used inside the DLGPROC which ends up being passed to DialogBox[Param]. After all, the window user still has GWLP_USERDATA. So it is probably safe to use for the window class implementation in practically every situation.

靑春怀旧 2024-07-13 01:57:57

虽然使用 SetWindowLongPtrGetWindowLongPtr 访问 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.

无边思念无边月 2024-07-13 01:57:57

在构造函数中,使用“this”作为 lpParam 参数调用 CreateWindowEx

然后,在 WM_NCCREATE 上,调用以下代码:

SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR) ((CREATESTRUCT*)lParam)->lpCreateParams);
SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER);

然后,在窗口过程的顶部,您可以执行以下操作:

MyWindowClass *wndptr = (MyWindowClass*) GetWindowLongPtr(hwnd, GWL_USERDATA);

这允许您执行此操作:

wndptr->DoSomething();

当然,您可以使用相同的技术来调用类似于上面的函数的内容:

wndptr->WndProc(msg, wparam, lparam);

。 ..然后可以按预期使用其“this”指针。

In your constructor, call CreateWindowEx with "this" as the lpParam argument.

Then, on WM_NCCREATE, call the following code:

SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR) ((CREATESTRUCT*)lParam)->lpCreateParams);
SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER);

Then, at the top of your window procedure you could do the following:

MyWindowClass *wndptr = (MyWindowClass*) GetWindowLongPtr(hwnd, GWL_USERDATA);

Which allows you to do this:

wndptr->DoSomething();

Of course, you could use the same technique to call something like your function above:

wndptr->WndProc(msg, wparam, lparam);

... which can then use its "this" pointer as expected.

白云悠悠 2024-07-13 01:57:57

您应该使用 GetWindowLongPtr()/SetWindowLongPtr() (或已弃用的 GetWindowLong()/SetWindowLong()) 。 他们速度很快,并且完全按照您的要求做。 唯一棘手的部分是确定何时调用 SetWindowLongPtr() - 您需要在发送第一个窗口消息(即 WM_NCCREATE)时执行此操作。
请参阅本文获取示例代码和更深入的讨论。

线程本地存储是一个坏主意,因为您可能在一个线程中运行多个窗口。

哈希映射也可以,但是为每个窗口消息(并且有很多)计算哈希函数可能会变得昂贵。

我不确定你是如何使用 thunk 的; 你如何传递重击声?

You should use GetWindowLongPtr()/SetWindowLongPtr() (or the deprecated GetWindowLong()/SetWindowLong()). They are fast and do exactly what you want to do. The only tricky part is figuring out when to call SetWindowLongPtr() - You need to do this when the first window message is sent, which is WM_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?

零度℉ 2024-07-13 01:57:57

我使用 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.

是伱的 2024-07-13 01:57:57

您可以使用GetWindowLongPtrSetWindowLongPtr; 使用 GWLP_USERDATA 将指针附加到窗口。 但是,如果您正在编写自定义控件,我建议使用额外的窗口字节来完成工作。 注册窗口类时,将 WNDCLASS::cbWndExtra 设置为数据大小,如下所示,wc.cbWndExtra = sizeof(Ctrl*);

您可以使用 GetWindowLongPtrSetWindowLongPtr 获取和设置该值,并将 nIndex 参数设置为 0。 此方法可以保存GWLP_USERDATA用于其他目的。

GetPropSetProp 的缺点是,将通过字符串比较来获取/设置属性。

You can use GetWindowLongPtr and SetWindowLongPtr; use GWLP_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 the WNDCLASS::cbWndExtra to the size of the data like this, wc.cbWndExtra = sizeof(Ctrl*);.

You can get and set the value using GetWindowLongPtr and SetWindowLongPtr with nIndex parameter set to 0. This method can save GWLP_USERDATA for other purposes.

The disadvantage with GetProp and SetProp, there will be a string comparison to get/set a property.

山人契 2024-07-13 01:57:57

关于 SetWindowLong() / GetWindowLong() 安全性,根据 Microsoft 的说法:

如果出现以下情况,SetWindowLong 函数将失败
hWnd指定的窗口
参数不属于同一个
进程作为调用线程。

遗憾的是,直到 10 月 12 日发布安全更新 ,2004 年,Windows 不会强制执行此规则,允许应用程序设置任何其他应用程序的 GWL_USERDATA。 因此,在未打补丁的系统上运行的应用程序很容易受到调用 SetWindowLong() 的攻击。

With regard to SetWindowLong() / GetWindowLong() security, according to Microsoft:

The SetWindowLong function fails if
the window specified by the hWnd
parameter does not belong to the same
process as the calling thread.

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().

兔小萌 2024-07-13 01:57:57

我建议在调用 CreateWindow 之前设置一个 thread_local 变量,并在 WindowProc 中读取它以找出 this变量(我假设您可以控制 WindowProc)。

这样,您将在发送到窗口的第一条消息上拥有 this/HWND 关联。

使用此处建议的其他方法,您很可能会错过一些消息:那些在 WM_CREATE / WM_NCCREATE / WM_GETMINMAXINFO 之前发送的消息。

class Window
{
    // ...
    static thread_local Window* _windowBeingCreated;
    static thread_local std::unordered_map<HWND, Window*> _hwndMap;
    // ...
    HWND _hwnd;
    // ...
    // all error checking omitted
    // ...
    void Create (HWND parentHWnd, UINT nID, HINSTANCE hinstance)
    {
        // ...
        _windowBeingCreated = this;
        ::CreateWindow (YourWndClassName, L"", WS_CHILD | WS_VISIBLE, x, y, w, h, parentHWnd, (HMENU) nID, hinstance, NULL);
    }

    static LRESULT CALLBACK Window::WindowProcStatic (HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
    {
        Window* _this;
        if (_windowBeingCreated != nullptr)
        {
            _hwndMap[hwnd] = _windowBeingCreated;
            _windowBeingCreated->_hwnd = hwnd;
            _this = _windowBeingCreated;
            windowBeingCreated = NULL;
        }
        else
        {
            auto existing = _hwndMap.find (hwnd);
            _this = existing->second;
        }

        return _this->WindowProc (msg, wparam, lparam);
    }

    LRESULT Window::WindowProc (UINT msg, WPARAM wparam, LPARAM lparam)
    {
        switch (msg)
        {
            // ....

I recommend setting a thread_local variable just before calling CreateWindow, and reading it in your WindowProc to find out the this variable (I presume you have control over WindowProc).

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.

class Window
{
    // ...
    static thread_local Window* _windowBeingCreated;
    static thread_local std::unordered_map<HWND, Window*> _hwndMap;
    // ...
    HWND _hwnd;
    // ...
    // all error checking omitted
    // ...
    void Create (HWND parentHWnd, UINT nID, HINSTANCE hinstance)
    {
        // ...
        _windowBeingCreated = this;
        ::CreateWindow (YourWndClassName, L"", WS_CHILD | WS_VISIBLE, x, y, w, h, parentHWnd, (HMENU) nID, hinstance, NULL);
    }

    static LRESULT CALLBACK Window::WindowProcStatic (HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
    {
        Window* _this;
        if (_windowBeingCreated != nullptr)
        {
            _hwndMap[hwnd] = _windowBeingCreated;
            _windowBeingCreated->_hwnd = hwnd;
            _this = _windowBeingCreated;
            windowBeingCreated = NULL;
        }
        else
        {
            auto existing = _hwndMap.find (hwnd);
            _this = existing->second;
        }

        return _this->WindowProc (msg, wparam, lparam);
    }

    LRESULT Window::WindowProc (UINT msg, WPARAM wparam, LPARAM lparam)
    {
        switch (msg)
        {
            // ....
み零 2024-07-13 01:57:57

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.

决绝 2024-07-13 01:57:57

过去我使用过 CreateWindowEx 的 lpParam 参数:

lpParam [输入,可选] 类型:LPVOID

指向要通过 CREATESTRUCT 传递给窗口的值的指针
lParam 参数指向的结构体(lpCreateParams 成员)
WM_CREATE 消息。 该消息通过以下方式发送到创建的窗口
这个函数在返回之前。 如果应用程序调用CreateWindow
要创建 MDI 客户端窗口,lpParam 应指向
CLIENTCREATESTRUCT 结构。 如果 MDI 客户端窗口调用
CreateWindow创建一个MDI子窗口,lpParam应该指向一个
MDICREATESTRUCT 结构。 如果没有附加数据,lpParam 可能为 NULL
是需要的。

这里的技巧是使用 HWND 的 static std::map 到类实例指针。 std::map::find 可能比 SetWindowLongPtr 方法性能更高。 不过,使用这种方法编写测试代码肯定更容易。

顺便说一句,如果您使用的是 win32 对话框,那么您需要使用 DialogBoxParam 函数。

In the past I've used the lpParam parameter of CreateWindowEx:

lpParam [in, optional] Type: LPVOID

Pointer to a value to be passed to the window through the CREATESTRUCT
structure (lpCreateParams member) pointed to by the lParam param of
the WM_CREATE message. This message is sent to the created window by
this function before it returns. If an application calls CreateWindow
to create a MDI client window, lpParam should point to a
CLIENTCREATESTRUCT structure. If an MDI client window calls
CreateWindow to create an MDI child window, lpParam should point to a
MDICREATESTRUCT structure. lpParam may be NULL if no additional data
is needed.

The trick here is to have a static std::map of HWND to class instance pointers. Its possible that the std::map::find might be more performant than the SetWindowLongPtr 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.

驱逐舰岛风号 2024-07-13 01:57:57

为了防止Zeus编辑器中出现问题,只需在GetMessage函数中指定窗口即可:

BOOL GetMessage(
LPMSG lpMsg,
HWND  hWnd, /*A handle to the window whose messages are to be retrieved.*/
UINT  wMsgFilterMin,
UINT  wMsgFilterMax
);

NOTE
该窗口必须属于当前线程。

易于阅读的函数文档

In order to prevent the problem that occurred in the Zeus editor, simply specify the window in the GetMessage function:

BOOL GetMessage(
LPMSG lpMsg,
HWND  hWnd, /*A handle to the window whose messages are to be retrieved.*/
UINT  wMsgFilterMin,
UINT  wMsgFilterMax
);

NOTE
The window must belong to the current thread.

Easy to read Documentation of the function

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文