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

新手上路,请多包涵

我很想知道存储 this 指针以用于 WndProc 的最佳/常用方法。我知道几种方法,但据我所知,每种方法都有自己的缺点。我的问题是:

产生这种代码有哪些不同的方法:

 CWindow::WndProc(UINT msg, WPARAM wParam, LPARAM)
{
  this->DoSomething();
}

我可以想到 Thunks、HashMaps、Thread Local Storage 和 Window User Data 结构。

这些方法的优缺点是什么?

代码示例和建议获得的积分。

这纯粹是出于好奇。使用 MFC 后,我一直想知道它是如何工作的,然后开始考虑 ATL 等。

编辑: 我可以在窗口 proc 中有效使用 HWND 的最早位置是什么?它被记录为 WM_NCCREATE - 但如果你真的进行实验,那 不是 发送到窗口的第一条消息。

编辑: ATL 使用 thunk 来访问 this 指针。 MFC 使用 HWND s 的哈希表查找。

原文由 Mark Ingram 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 588
1 个回答

这个问题在 SO 上有很多重复和几乎重复,但我所看到的几乎没有一个答案探索了他们选择的解决方案的缺陷。

将任意数据指针与窗口关联的方法有多种,有两种不同的情况需要考虑。根据情况,可能性是不同的。

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

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

我假设问题 不是 最初将数据指针放入 WNDPROC (这只是通过 CREATESTRUCT lpParam 参数中的--- CreateWindow[ExW] ),而是如何存储它以供后续调用。

方法一:cbWndExtra

当 Windows 创建一个窗口的实例时,它会在内部分配一个 WND 结构。这个结构有一定的大小,包含各种与窗口相关的东西,比如它的位置、它的窗口类和它当前的 WNDPROC。在该结构的末尾,Windows 可选择分配一些属于该结构的附加字节。数字在 — 中指定,在 WNDCLASSEX.cbWndExtra RegisterWindowClassEx 使用。

这意味着只有当您是注册窗口类的人时才能使用此方法,即您正在 _编写窗口类_。

应用程序不能直接访问 WND 结构。相反,使用 GetWindowLong[Ptr] 。非负索引访问结构末尾额外字节内的内存。 “0”将访问第一个额外的字节。

如果您正在创作窗口类,这是一种干净、快速的方法。大多数 Windows 内部控件似乎都使用这种方法。

不幸的是,这种方法在对话框( DialogBox 系列)中表现不佳。除了提供对话框模板之外,您还有一个对话框窗口类,维护起来会变得很麻烦(除非您出于其他原因需要这样做)。如果您确实想将它与对话框一起使用,则必须在对话框模板中指定窗口类名称,确保在显示对话框之前已注册此窗口类,并且您需要为对话框实现 WNDPROC (或使用 DefDlgProc )。此外,所有对话框已经在 cbWndExtra 中保留了一定数量的字节,以便对话框管理器正常运行。所需的额外字节数是 DLGWINDOWEXTRA 常量。这意味着 您的 内容需要位于对话框已经保留的额外字节 _之后_。通过 DLGWINDOWEXTRA (包括您在窗口类中指定的 cbWndExtra 的值)偏移对额外内存的所有访问。

另请参阅下面的对话框独有的额外方法。

方法二:GWLP_USERDATA

前面提到的 WND 结构恰好包含一个指针大小的字段,系统不使用该字段。使用带有负索引的 GetWindowLongPtr 访问它(即 GWLP_USERDATA )。负索引将访问 WND 结构中的字段。请注意,根据 this ,负索引似乎并不代表内存偏移,而是任意的。

GWLP_USERDATA 的问题是不清楚,过去也不清楚这个字段的 确切 用途是什么,因此这个字段的所有者是谁。另请参阅 此问题普遍的共识是没有共识。 很可能 GWLP_USERDATA 是 _供窗口用户使用的_,而不是 _窗口类的作者_。这意味着在 WNDPROC 内部使用它是完全错误的,因为 WNDPROC 总是由窗口类作者提供。

我个人确信,这是提出 GWLP_USERDATA 的工程师的意图,因为如果这是真的,那么整个 API 是可靠的、可扩展的和面向未来的。但如果它不是真的,那么 API 就不是那些,而且它与 cbWndExtra 是多余的。

我知道的所有标准 Windows 控件(例如 BUTTONEDIT 等)都遵守这一点,不要在内部使用 GWLP_USERDATA ,让它免费 使用 这些控件的窗口。问题是有太多的例子,包括在 MSDN 和 SO 上,它们打破了这个规则并使用 GWLP_USERDATA 来实现窗口类。这有效地消除了控制 用户 将上下文指针与其关联的最干净和最简单的方法,仅仅是因为太多人在做“错误”(根据我对“错误”的定义)。在最坏的情况下,用户代码不知道 GWLP_USERDATA 被占用,并且可能会覆盖它,这可能会使应用程序崩溃。

由于关于 GWLP_USERDATA 所有权的长期争议,使用它通常并不安全。如果您正在 创作 一个窗口类,那么您可能永远都不应该使用它。如果你正在 使用 一个窗口,你应该只在你确定它没有被窗口类使用时才这样做。

方法三:SetProp

SetProp 系列函数实现对属性表的访问。每个窗口都有自己独立的属性。该表的键是 API 表面级别的字符串,但在内部它实际上是一个 ATOM。

SetProp 可以被窗口类 作者 和窗口 _用户使用_,它也有问题,但它们与 GWLP_USERDATA 不同。您必须确保用作属性键的字符串不会发生冲突。窗口用户可能不一定知道窗口类作者在内部使用什么字符串。即使不太可能发生冲突,您也可以通过使用 GUID 作为字符串来完全避免它们,例如。从全局 ATOM 表的内容可以看出,许多程序都以这种方式使用 GUID。

SetProp 必须小心使用。大多数资源都没有解释这个函数的缺陷。在内部,它使用 GlobalAddAtom 。这有几个含义,在使用此功能时需要考虑:

  • 当调用 SetProp (或任何其他使用全局 ATOM 表的 API)时,您可以使用 ATOM 来代替字符串,这是您在注册新字符串时获得的 GlobalAddAtom 。 ATOM 只是一个整数,它引用 ATOM 表中的一个条目。这将提高性能; SetProp 内部始终使用 ATOM s 作为属性键,从不使用字符串。传递字符串会导致 SetProp 和类似的函数首先在内部搜索 ATOM 表以查找匹配项。传递 ATOM 直接跳过在全局原子表中搜索字符串。

  • 全局原子表中可能的字符串原子数在系统范围内限制为 16384。这是因为原子是 16 位的 uint,范围从 0xC0000xFFFF (低于 0xC000 的所有值都是指向固定字符串的伪原子(使用,但你不能保证没有其他人在使用它们))。使用许多不同的属性名称是一个坏主意,更不用说这些名称是在运行时动态生成的。相反,您可以使用单个属性来存储指向包含您需要的所有数据的结构的指针。

  • 如果您使用的是 GUID,则可以安全地对正在使用的 每个 窗口使用相同的 GUID,即使跨不同的软件项目也是如此,因为每个窗口都有自己的属性。这样,您的所有软件最多只会用完全局原子表中的 两个 条目(您最多需要一个 GUID 作为窗口类作者,最多需要一个 GUID 作为窗口类用户)。事实上,定义两个事实上的标准 GUID 可能是有意义的,每个人都可以将其用于他们的上下文指针(实际上不会发生)。

  • 因为属性使用 GlobalAddAtom ,所以您必须确保原子未注册。进程存在时全局原子不会被清理,并且会阻塞全局原子表,直到操作系统重新启动。为此,您必须确保调用了 RemoveProp 。这样做的好地方通常是 WM_NCDESTROY

  • 全局原子是引用计数的。这意味着计数器可能会在某些时候溢出。为防止溢出,一旦原子的引用计数达到 65536,原子将永远留在原子表中,再多的 GlobalDeleteAtom 都无法摆脱它。在这种情况下,必须重新启动操作系统以释放原子表。

如果要使用 SetProp ,请避免使用许多不同的原子名称。除此之外, SetProp / GetProp 是一种非常干净和防御性的方法。如果开发人员同意对所有窗口使用相同的 2 个 atom 名称,则可以大大减轻 atom 泄漏的危险,但这不会发生。

方法四:SetWindowSubclass

SetWindowSubclass 旨在允许覆盖特定窗口的 WNDPROC ,以便您可以在自己的回调中处理一些消息,并将其余消息委托给原始窗口 WNDPROC 。例如,这可用于侦听 EDIT 控件中的特定键组合,同时将其余消息留给其原始实现。

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

SUBCLASSPROC 有 2 个附加参数,其中一个是 DWORD_PTR dwRefData 。这是任意指针大小的数据。数据来自您,通过最后一个参数 SetWindowSubclass 。然后将数据传递给替换的 每次调用 SUBCLASSPROC 。如果 每个 WNDPROC 都有这个参数,那么我们就不会陷入这种可怕的境地!

此方法仅对窗口类作者有所帮助。 (1)在窗口的初始创建过程中(例如 WM_CREATE ),窗口子类化自身(如果合适的话,它可以为 dwRefData 分配内存)。释放可能最好在 WM_NCDESTROY 中。通常会进入 WNDPROC 的其余代码被移动到替换 SUBCLASSPROC 代替。

它甚至可以用在对话框自己的 WM_INITDIALOG 消息中。 If the dialog is shown with DialogParamW , the last parameter can be used as dwRefData in a SetWindowSubclass call in the WM_INITDIALOG message.然后,所有其余的对话逻辑都进入新的 SUBCLASSPROC ,它将接收每个消息的 dwRefData 。请注意,这会稍微改变语义。您现在正在编写对话框的 窗口 过程级别,而不是对话框过程。

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

方法 5:重击

thunk 是一个小函数,其机器代码在运行时在内存中动态生成。它的目的是调用另一个函数,但附加的参数似乎不知从何而来。

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

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

此方法可供窗口类作者使用,而且速度极快。但是,实施并非易事。 AtlThunk 系列函数实现了这一点,但有一个怪癖。它不添加 额外的 参数。相反,它用您的任意数据(指针大小) 替换--- 的 WNDPROC HWND 参数。但是,这不是一个大问题,因为您的任意数据可能是指向包含 HWND 的结构的指针。

SetWindowSubclass 方法类似,您可以在窗口创建期间使用任意数据指针创建 thunk。然后,用 thunk 替换窗口的 WNDPROC 。所有真正的工作都在新的伪 WNDPROC 中进行,这是 thunk 的目标。

Thunks 根本不会弄乱全局原子表,也没有字符串唯一性考虑。但是,就像在堆内存中分配的所有其他内容一样,它们必须被释放,之后可能不再调用 thunk。因为 WM_NCDESTROY 是窗口收到的最后一条消息,所以这里就是这样做的地方。否则,释放thunk时必须确保重新安装原版 WNDPROC

请注意,这种将“this”指针偷运到回调函数中的方法实际上在许多生态系统中无处不在,包括 C# 与本机 C 函数的互操作。

方法六:全局查找表

无需长篇大论。在您的应用程序中,实现一个全局表,在其中存储 HWND s 作为键和上下文数据作为值。你有责任清理桌子,如果需要,让它足够快。

窗口类作者可以使用私有表来实现他们的实现,窗口用户可以使用他们自己的表来存储特定于应用程序的信息。无需担心原子或字符串的唯一性。

底线

如果您是 _Window 类作者_,则这些方法有效:

cbWndExtra, (GWLP_USERDATA), SetProp, SetWindowSubclass, Thunk, 全局查找表。

Window Class Author 表示您正在编写 WNDPROC 函数。例如,您可能正在实现一个自定义图片框控件,它允许用户平移和缩放。您可能需要额外的数据来存储平移/缩放数据(例如,作为 2D 转换矩阵),以便您可以正确实现 WM_PAINT 代码。

建议:避免使用 GWLP_USERDATA,因为用户代码可能依赖它;如果可能,请使用 cbWndExtra。

如果您是 Window User ,则这些方法有效:

GWLP_USERDATA,SetProp,全局查找表。

窗口用户意味着您正在创建一个或多个窗口并在您自己的应用程序中使用它们。例如,您可能正在动态创建可变数量的按钮,并且每个按钮都与被单击时相关的不同数据相关联。

建议:如果它是标准的 Windows 控件,则使用 GWLP_USERDATA,或者您确定该控件不会在内部使用它。否则, SetProp

使用对话框时额外提及

默认情况下,对话框使用将 cbWndExtra 设置为 DLGWINDOWEXTRA 的窗口类。可以为对话框定义自己的窗口类,在其中分配 DLGWINDOWEXTRA + sizeof(void*) ,然后访问 GetWindowLongPtrW(hDlg, DLGWINDOWEXTRA) 。但在这样做的同时,你会发现自己不得不回答你不喜欢的问题。例如,您使用哪个 WNDPROC (答案:您可以使用 DefDlgProc ),或者您使用哪种类样式(默认对话框恰好使用 CS_SAVEBITS | CS_DBLCLKS ,但祝你好运找到权威参考)。

DLGWINDOEXTRA 字节中,对话框碰巧保留了一个指针大小的字段,可以使用 GetWindowLongPtr 和索引 DWLP_USER 访问该字段。这是一种额外的 GWLP_USERDATA ,理论上也有同样的问题。在实践中,我只见过在 DLGPROC 中使用它,最终被传递给 DialogBox[Param] 。毕竟 窗口用户 还有 GWLP_USERDATA 。因此,几乎在所有情况下都可以安全地使用 _窗口类实现_。

原文由 dialer 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题