【问题标题】:Best method for storing this pointer for use in WndProc存储此指针以在 WndProc 中使用的最佳方法
【发布时间】:2010-09-12 04:19:07
【问题描述】:

我很想知道存储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 的哈希表查找。

【问题讨论】:

    标签: c++ windows winapi oop wndproc


    【解决方案1】:

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

    【讨论】:

    • 我认为 Get/SetProp 绝对是一种干净的方式。它比 Get/SetWindowLong(即 O(1))慢一点,但只要没有为窗口设置“很多”属性,我怀疑这将是一个瓶颈。
    【解决方案2】:

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

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

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

    我不确定您是如何使用 thunk 的;你是怎么绕过这些笨蛋的?

    【讨论】:

    • 就 thunk 而言,您可以为每个窗口(动态生成)设置一个单独的 thunk。您可以将此 thunk 注册为窗口过程,并且此 thunk 已经包含对象指针。我相信这是 ATL/WTL 方法。
    • WM_NCCREATE 不一定是发送的第一条消息,但带有this 信息的 CREATESTRUCT 可以伴随该消息的第一条消息。
    【解决方案3】:

    在您的构造函数中,调用 CreateWindowEx 并将“this”作为 lpParam 参数。

    然后,在 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”指针。

    【讨论】:

    • 为什么需要SetWindowPos()
    • @Timmmm from the man: "某些窗口数据已被缓存,因此您使用 SetWindowLongPtr 所做的更改在调用 SetWindowPos 函数之前不会生效"。
    • 这个答案忽略了所提出的方法以及其他可用方法的缺点。投反对票。
    【解决方案4】:

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

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

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

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

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

    【讨论】:

    • 这可以通过在 WNDCLASSEX 结构中设置额外的窗口数据并在 Get/SetWindowLongPtr 中使用新的偏移量而不是 GWL_USERDATA 来解决。
    • 窗口 消息 没有 GWL_USERDATA。 Windows 有。不要偷看其他窗口的消息,并期望获得您窗口的 this 指针。请注意,您应该同时拥有窗口和窗口 classblogs.msdn.com/oldnewthing/archive/2005/03/03/384285.aspx
    • 问题是在某些情况下,Windows 会向应用程序发送一条消息,其中包含一个不是由应用程序创建的窗口的句柄。但是这种方法假定发送到应用程序的每条消息都将针对应用程序创建的窗口,而事实并非如此。
    • 我受到了这个线程的启发,因此我重新编写了 Zeus 代码以使用哈希查找表进行映射。这种新方法似乎效果很好。
    • @MSalters 不要偷看其他窗口的消息在自己窗口的WndProc 中如何分辨两者之间的区别?
    【解决方案5】:

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

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

    不幸的是,在 2004 年 10 月 12 日发布 Security Update 之前,Windows would not enforce this rule 允许应用程序设置任何其他应用程序的 GWL_USERDATA。因此,在未打补丁的系统上运行的应用程序很容易受到通过调用 SetWindowLong() 的攻击。

    【讨论】:

      【解决方案6】:

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

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

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

      【讨论】:

      • 这通过避免使用 GWL_USERDATA 来解决 Adrian Lopez 提到的问题。
      • 没有详细解释或提供任何示例来演示如何正确使用窗口类来解决这个问题。
      【解决方案7】:

      过去我用过CreateWindowEx的lpParam参数:

      lpParam [in, optional] 类型: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 函数。

      【讨论】:

        【解决方案8】:

        我建议在调用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)
                {
                    // ....
        

        【讨论】:

          【解决方案9】:

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

          【讨论】:

            【解决方案10】:

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

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

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

            Easy to read Documentation of the function

            【讨论】:

              【解决方案11】:

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

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

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

              方法一:cbWndExtra

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

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

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

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

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

              方法二:GWLP_USERDATA

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

              GWLP_USERDATA 的问题在于不清楚,过去也不清楚,究竟这个字段的目的是什么,因此,这个字段的所有者是谁场是。另见this question。普遍的共识是没有共识。 GWLP_USERDATA 很可能是供窗口的用户使用,而不是窗口类的作者。这意味着在 WNDPROC 内部使用它是不正确的,因为 WNDPROC 始终由窗口类作者提供。

              大多数标准窗口控件(例如EDIT)都遵守这一点,并且不会在内部使用GWLP_USERDATA,而是将其留给使用这些控件的窗口。问题是有太多的例子,包括在 MSDN 和 SO 上,它们打破了这个规则并使用GWLP_USERDATA 来实现窗口类。这消除了控件用户将上下文指针与其关联的最简洁和最简单的方法。在最坏的情况下,用户代码不知道GWLP_USERDATA 已被占用,并且可能会覆盖它,这可能会使应用程序崩溃。

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

              方法三:设置属性

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

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

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

              • 您可以使用您自己在GlobalAddAtom 注册的ATOM,而不是字符串。这将提高性能; SetProp 在内部使用 ATOMs 作为属性键,而不是字符串。传递ATOM 会跳过全局原子表中的查找。

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

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

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

              • 全局原子是引用计数的。这意味着计数器可能会在某些时候溢出。为防止溢出,一旦原子的引用计数达到 65536,原子将永远留在原子表中,再多的GlobalDeleteAtom 也无法摆脱它。

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

              方法四:设置窗口子类

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

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

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

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

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

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

              方法5:重击

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

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

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

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

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

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

              方法6:全局查找表

              无需过多解释。在您的应用程序中,实现一个全局表,将HWNDs 存储为键,将上下文数据存储为值。您负责清理表,并在需要时使其足够快。

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

              底线

              如果您是 Window 类作者,这些方法可以工作:

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

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

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

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

              GWLP_USERDATA、SetProp、全局查找表。

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

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

              使用对话框时额外提及

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

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

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2020-07-23
                • 1970-01-01
                • 1970-01-01
                • 2021-12-22
                • 1970-01-01
                • 2021-11-30
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多