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

时间:2008-09-22 21:34:50

标签: c++ windows winapi oop wndproc

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

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

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

我可以想到Thunks,HashMaps,Thread Local Storage和Window User Data struct。

每种方法的优点/缺点是什么?

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

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

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

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

11 个答案:

答案 0 :(得分:12)

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

答案 1 :(得分:10)

使用 SetWindowLongPtr GetWindowLongPtr 访问 GWL_USERDATA 可能听起来不错,我强烈建议不< / strong>使用这种方法。

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

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

问题是Zeus始终假设 GWL_USERDATA 数据由它设置并尝试将数据用作此指针,然后导致崩溃。

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

答案 2 :(得分:7)

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

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

哈希映射也可以工作,但计算每个窗口消息的哈希函数(并且有一个 LOT )会变得很昂贵。

我不确定你是如何使用thunk的;你是怎么绕过thunk的?

答案 3 :(得分:6)

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

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

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

方法一:cbWndExtra

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

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

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

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

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

方法 2:GWLP_USERDATA

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

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

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

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

方法 3:SetProp

SetProp 系列函数实现对属性表的访问。每个窗口都有自己独立的属性。这个表的key是API表面的一个字符串,但内部其实是一个ATOM。

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

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

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

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

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

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

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

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

方法四:SetWindowSubclass

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

EDIT 的一个方便的副作用是 new 替换 SetWindowSubclass 实际上不是 WNDPROC,而是 WNDPROC。< /p>

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

这个方法只对窗口类作者有帮助。在窗口的初始创建期间(例如 WNDPROC),窗口对自身进行子类化(例如,它可以使用 WM_CREATE 中的 dwRefData,或者在适当的情况下将其分配到那里)。通常会出现在 lParam 中的其余代码被移至替换 WNDPROC

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

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

方法 5:Thunk

thunk 是一个动态生成的可以执行的函数。它的目的是调用另一个函数,但附加的参数似乎神奇地无处不在。

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

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

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

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

Thunks 根本不会干扰全局原子表,也没有字符串唯一性的考虑。但是,就像堆内存中分配的所有其他内容一样,它们必须被释放,之后可能不再调用 thunk。由于 WNDPROC 是窗口收到的最后一条消息,因此可以在此处执行此操作。否则,您必须确保在释放 thunk 时重新安装原始 WM_NCDESTROY

方法六:全局查找表

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

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

底线

如果您是窗口类作者,这些方法有效:

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

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

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

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

GWLP_USERDATA、SetProp、全局查找表。

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

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

使用对话框时额外提及

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

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

答案 4 :(得分:5)

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

答案 5 :(得分:3)

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

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

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

答案 6 :(得分:2)

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

  

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

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

答案 7 :(得分:0)

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

  

lpParam [in,optional]类型:LPVOID

     

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

这里的技巧是为类实例指针提供static std::map个HWND。 std::map::find可能比SetWindowLongPtr方法更具性能。使用这种方法编写测试代码肯定更容易。

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

答案 8 :(得分:0)

我建议在致电thread_local之前设置一个CreateWindow变量,然后在WindowProc中查看该变量以找出this变量(我认为您可以控制WindowProc {1}})。

这样,您在发送给您的第一条消息窗口中就会有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 :(得分:0)

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

答案 10 :(得分:0)

为了防止在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