33

我很想知道存储this指针以供在WndProc. 我知道几种方法,但据我所知,每种方法都有自己的缺点。我的问题是:

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

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

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

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

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

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

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

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

4

11 回答 11

14

在您的构造函数中,使用“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”指针。

于 2008-09-22T21:52:02.087 回答
14

虽然使用SetWindowLongPtrGetWindowLongPtr访问GWL_USERDATA听起来不错,但我强烈建议不要使用这种方法。

这正是Zeus编辑器使用的方法,近年来它只引起了痛苦。

我认为发生的情况是第三方 Windows 消息被发送到Zeus也设置了GWL_USERDATA值。特别是一个应用程序是 Microsoft 工具,它提供了一种在任何 Windows 应用程序中输入亚洲字符的替代方法(即某种软件键盘实用程序)。

问题是Zeus总是假设GWL_USERDATA数据是由它设置的,并试图将数据用作this 指针,这会导致崩溃。

如果我要再次使用我现在所知道的一切来做这一切,我会采用缓存哈希查找方法,其中窗口句柄用作键。

于 2008-09-23T00:29:07.277 回答
11

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

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

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

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

我假设问题不是将数据指针放入WNDPROC最初的位置,而是如何存储它以供后续调用。

方法一:cbWndExtra

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

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

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

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

不幸的是,这种方法在对话(DialogBox家庭)中表现不佳。除了提供对话框模板之外,您还有一个对话框窗口类,维护起来会变得很麻烦(除非您出于其他原因需要这样做)。如果您确实想将它与对话框一起使用,则必须在对话框模板中指定窗口类名称,确保在显示对话框之前已注册此窗口类,并且您需要为WNDPROC对话框实现一个(或使用DefDlgProc)。偏移对额外内存的所有访问DLGWINDOWEXTRA(包括cbWndExtra-- 这是因为在内部,对话框已经需要一些额外的内存才能正常运行)。另请参阅下面的对话框独有的额外方法。

方法二:GWLP_USERDATA

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

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

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

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

方法三:SetProp

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

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

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

  • ATOM您可以使用注册新字符串时获得的 来代替字符串GlobalAddAtom。ATOM 只是一个整数。这将提高性能;SetProp内部总是使用ATOMs 作为属性键,而不是字符串。传递一个ATOM跳过搜索全局原子表中的字符串。

  • 全局原子表中可能的字符串原子数在系统范围内限制为 16384。使用许多不同的属性名称是一个坏主意,更不用说这些名称是在运行时动态生成的。相反,您可以使用单个属性来存储指向包含您需要的所有数据的结构的指针。

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

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

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

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

方法四:SetWindowSubclass

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

一个方便的副作用SetWindowSubclass的,替换WNDPROC实际上不是一个WNDPROC,而是一个SUBCLASSPROC

SUBCLASSPROC有 2 个附加参数,其中之一是DWORD_PTR dwRefData. 这是任意指针大小的数据。数据来自您,通过最后一个参数调用SetWindowSubclass. 然后将数据传递给替换的每次调用SUBCLASSPROC。如果每个 WNDPROC人都有这个参数!

此方法仅对窗口类作者有所帮助。(1)在窗口的初始创建期间(例如WM_CREATE),窗口子类化自身(例如,它可以使用dwRefDatafrom lParam,或者如果合适的话,就在那儿分配它)。通常会进入的其余代码WNDPROC被移到替换处SUBCLASSPROC

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

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

方法 5:重击

thunk 是可以执行的动态生成的函数。它的目的是调用另一个函数,但附加的参数似乎不知从何而来。

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

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

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

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

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

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

方法六:全局查找表

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

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

底线

如果您是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,或者您确定该控件不会在内部使用它。

使用对话框时额外提及

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

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

于 2021-01-24T22:19:26.453 回答
9

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

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

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

我不确定您是如何使用 thunk 的;你是如何绕过重击的?

于 2008-09-22T21:42:59.737 回答
6

我已经使用 SetProp/GetProp 来存储指向带有窗口本身的数据的指针。我不确定它如何与您提到的其他项目叠加。

于 2008-09-22T21:41:53.967 回答
4

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

您可以使用 和 参数设置为 来获取和GetWindowLongPtr设置值。这种方法可以保存用于其他目的。SetWindowLongPtrnIndex0GWLP_USERDATA

和的缺点是GetPropSetProp会有一个字符串比较来获取/设置一个属性。

于 2011-09-12T10:13:21.267 回答
3

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

如果 hWnd 参数指定的窗口与调用线程不属于同一进程,则 SetWindowLong 函数将失败。

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

于 2008-09-24T06:22:37.437 回答
2

我建议thread_local在调用之前设置一个变量CreateWindow,然后在你的文件中读取它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)
        {
            // ....
于 2015-10-02T21:51:46.763 回答
0

在过去,我使用了 lpParam 参数CreateWindowEx

lpParam [in,可选] 类型:LPVOID

指向要通过 WM_CREATE 消息的 lParam 参数指向的 CREATESTRUCT 结构(lpCreateParams 成员)传递给窗口的值的指针。此消息在返回之前由该函数发送到创建的窗口。如果应用程序调用 CreateWindow 来创建 MDI 客户端窗口,lpParam 应该指向 CLIENTCREATESTRUCT 结构。如果一个 MDI 客户窗口调用 CreateWindow 来创建一个 MDI 子窗口,lpParam 应该指向一个 MDICREATESTRUCT 结构。如果不需要其他数据,lpParam 可能为 NULL。

这里的技巧是对static std::map类实例指针有一个 HWND。它可能std::map::findSetWindowLongPtr方法更有效。不过,使用这种方法编写测试代码当然更容易。

顺便说一句,如果您使用的是 win32 对话框,那么您将需要使用该DialogBoxParam功能。

于 2014-05-29T22:54:41.457 回答
0

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

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

注意 窗口必须属于当前线程。

易于阅读的功能文档

于 2020-08-16T07:53:47.307 回答
0

ATL 的 thunk 是最有效的。thunk 执行一次并将 WINPROC 的回调函数替换为类自己的消息处理成员函数。后续消息通过 windows 直接调用类成员函数来传递。没有比这更快的了。

于 2018-11-18T07:03:26.947 回答