Mar*_*ram 27 c++ windows oop winapi wndproc
我很想知道存储this指针的最佳/常用方法WndProc.我知道几种方法,但据我所知,每种方法都有其自身的缺点.我的问题是:
有哪些不同的方式来生成这种代码:
CWindow::WndProc(UINT msg, WPARAM wParam, LPARAM)
{
this->DoSomething();
}
Run Code Online (Sandbox Code Playgroud)
我可以想到Thunks,HashMaps,Thread Local Storage和Window User Data结构.
每种方法的优点/缺点是什么?
代码示例和建议获得的分数.
这纯粹是出于好奇心.使用MFC之后,我一直想知道它是如何工作的,然后开始考虑ATL等.
编辑:我可以HWND在窗口过程中有效使用的最早的地方是什么?它记录为WM_NCCREATE- 但如果您实际进行实验,那不是第一个发送到窗口的消息.
编辑: ATL使用thunk访问此指针.MFC使用HWNDs 的哈希表查找.
小智 11
在构造函数中,使用"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);
Run Code Online (Sandbox Code Playgroud)
然后,在窗口过程的顶部,您可以执行以下操作:
MyWindowClass *wndptr = (MyWindowClass*) GetWindowLongPtr(hwnd, GWL_USERDATA);
Run Code Online (Sandbox Code Playgroud)
这允许你这样做:
wndptr->DoSomething();
Run Code Online (Sandbox Code Playgroud)
当然,您可以使用相同的技术来调用上面的函数:
wndptr->WndProc(msg, wparam, lparam);
Run Code Online (Sandbox Code Playgroud)
...然后可以按预期使用其"this"指针.
jus*_*sij 11
虽然使用SetWindowLongPtr和GetWindowLongPtr来访问GWL_USERDATA可能听起来不错,但我强烈建议不要使用这种方法.
这正是Zeus编辑所使用的接近,近年来它只引起了痛苦.
我认为发生的事情是第三方窗口消息被发送到Zeus,它们也设置了GWL_USERDATA值.一个应用程序特别是Microsoft工具,它提供了在任何Windows应用程序中输入亚洲字符的替代方法(即某种软件键盘实用程序).
问题是Zeus总是假设GWL_USERDATA数据是由它设置的,并尝试将数据用作this指针,然后导致崩溃.
如果我要再次使用,我现在所知道的,我将采用缓存的哈希查找方法,其中窗口句柄用作键.
你应该使用GetWindowLongPtr()/ SetWindowLongPtr()(或不推荐的GetWindowLong()/ SetWindowLong()).它们速度快,完全符合您的要求.唯一棘手的部分是确定何时调用SetWindowLongPtr()- 您需要在发送第一个窗口消息时执行此操作,即WM_NCCREATE.
有关示例代码和更深入的讨论,请参阅此文章.
线程本地存储是一个坏主意,因为您可能在一个线程中运行多个窗口.
哈希映射也可以工作,但计算每个窗口消息的哈希函数(并且有很多)可能会变得昂贵.
我不确定你是如何使用thunk的; 你怎么绕过thunk?
这个问题在 SO 上有很多重复和几乎重复,但我见过的几乎没有一个答案探索他们选择的解决方案的陷阱。
有几种方法可以将任意数据指针与窗口相关联,并且有两种不同的情况需要考虑。根据情况,可能性是不同的。
我假设问题不是将数据指针放入WNDPROC最初的数据指针,而是如何存储它以供后续调用使用。
当 Windows 创建一个窗口实例时,它会在内部分配一个WND结构体。这个结构体有一定的大小,包含各种与窗口相关的东西,比如它的位置、它的窗口类和它当前的 WNDPROC。在此结构的末尾,Windows 可选择分配一些属于该结构的附加字节。编号在 中指定,在WNDCLASSEX.cbWndExtra中使用RegisterWindowClassEx。
这意味着只有在您是注册窗口类的人时才能使用此方法,即您正在创作窗口类。
应用程序不能直接访问该WND结构。相反,使用GetWindowLong[Ptr]. 非负索引访问结构末尾额外字节内的内存。“0”将访问第一个额外字节。
如果您正在创作窗口类,这是一种干净且快速的方法。大多数 Windows 内部控件似乎都使用这种方法。
不幸的是,这种方法在对话(DialogBox族)中效果不佳。除了提供对话框模板之外,您还将拥有一个对话框窗口类,这会变得维护起来很麻烦(除非您出于其他原因需要这样做)。如果你确实想在对话框中使用它,你必须在对话框模板中指定窗口类名,确保在显示对话框之前注册这个窗口类,并且你需要WNDPROC为对话框实现 a (或使用DefDlgProc)。将所有对额外内存的访问偏移DLGWINDOWEXTRA(包括 的值cbWndExtra)。另请参阅下面的对话框专有的额外方法。
上述WND结构恰好包含一个指针大小的字段,系统不使用该字段。使用GetWindowLongPtr负索引访问它。负索引将访问WND结构内的字段。请注意,根据this,负索引似乎并不代表内存偏移量,而是任意的。
问题GWLP_USERDATA是不清楚,过去也不清楚,这个领域的目的究竟是什么,因此,这个领域的所有者是谁。另请参阅此问题。普遍的共识是没有共识。它很可能GWLP_USERDATA是供窗口用户使用的,而不是供窗口类的作者使用的。这意味着在 WNDPROC 内部使用它是不正确的,因为 WNDPROC 始终由窗口类作者提供。
大多数标准的窗口控件(例如EDIT)都遵循这一点并且不在GWLP_USERDATA内部使用,将其留给使用这些控件的窗口。问题是有太多的例子,包括在 MSDN 和 SO 上,它们打破了这个规则并GWLP_USERDATA用于实现窗口类。这消除了控件用户将上下文指针与其关联的最简洁和最简单的方法。最坏的情况是,用户代码不知道它GWLP_USERDATA被占用,可能会覆盖它,这可能会使应用程序崩溃。
由于有关 所有权的长期争议GWLP_USERDATA,使用它通常是不安全的。如果您正在创作一个窗口类,那么您可能永远都不应该使用它。如果您使用的是窗口,则只有在确定窗口类未使用它时才应该这样做。
该SetProp系列函数实现对属性表的访问。每个窗口都有自己独立的属性。这个表的key是API表面级别的一个字符串,但在内部它实际上是一个ATOM。
SetProp可以被窗口类作者和窗口用户使用,它也有问题,但它们与GWLP_USERDATA. 您必须确保用作属性键的字符串不会发生冲突。winodw 用户可能不一定知道窗口类作者在内部使用了哪些字符串。例如,即使不太可能发生冲突,您也可以通过将 GUID 用作字符串来完全避免它们。在查看全局原子表的内容时很明显,许多程序以这种方式使用 GUID。
SetProp必须小心使用。大多数资源都没有解释这个函数的缺陷。在内部,它使用GlobalAddAtom. 这有几个含义,在使用此函数时需要考虑:
您可以使用ATOM您自己注册的 来代替字符串GlobalAddAtom。这将提高性能;SetProp在内部使用ATOMs 作为属性键,而不是字符串。传递 an 会ATOM跳过全局原子表中的查找。
全局原子表中可能的字符串原子数在系统范围内限制为 16384。使用许多不同的属性名称是一个坏主意,更不用说这些名称是在运行时动态生成的。相反,您可以使用单个属性来存储指向包含您需要的所有数据的结构的指针。
如果您使用的是 GUID,那么对您正在使用的每个窗口使用相同的 GUID 是安全的,即使在不同的软件项目中也是如此,因为每个窗口都有自己的属性。这样,您的所有软件最多只会使用全局原子表中的两个条目(您需要一个 GUID 用于编写的窗口类,一个用于使用的窗口)。事实上,定义两个事实上的标准 GUID 可能是有意义的,每个人都可以将其用于上下文指针。
由于属性使用GlobalAddAtom,您必须确保原子未注册。进程存在时不清除全局原子,会阻塞全局原子表,直到操作系统重新启动。为此,您必须确保RemoveProp调用了它。这通常是一个好地方WM_NCDESTROY。
全局原子是引用计数的。这意味着计数器可能会在某个时刻溢出。为了防止溢出,一旦原子的引用计数达到65536,该原子将永远留在原子表中,再多GlobalDeleteAtom也无法摆脱它。
如果您想使用SetProp. 除此之外,SetProp/GetProp是一种非常干净和防御性的方法。如果开发人员同意为所有窗口使用相同的 2 个原子名称,则可以大大减轻原子泄漏的危险,但这不会发生。
SetWindowSubclass旨在允许覆盖WNDPROC特定窗口的 ,以便您可以在自己的回调中处理一些消息,并将其余消息委托给原始WNDPROC. 例如,这可用于侦听EDIT控件中的特定组合键,而将其余消息留给其原始实现。
的一个方便的副作用SetWindowSubclass是新的,替换WNDPROC实际上不是 a WNDPROC,而是 a SUBCLASSPROC。
SUBCLASSPROC有 2 个附加参数,其中之一是DWORD_PTR dwRefData. 这是任意指针大小的数据。数据来自您,通过最后一个参数调用SetWindowSubclass. 然后将数据传递给替换的每次调用SUBCLASSPROC。如果每个人 WNDPROC都有这个参数就好了!
此方法仅对窗口类作者有帮助。在窗口的初始创建期间(例如WM_CREATE),窗口将自身子类化(例如,它可以使用dwRefDatafrom lParam,或者在适当的情况下将其分配到那里)。通常会进入的其余代码WNDPROC被移动到替换代码中SUBCLASSPROC。
它甚至可以用在对话框自己的WM_INITDIALOG消息中。如果对话框显示为DialogParamW,则最后一个参数可以用作消息dwRefData中的SetWindowSubclass调用WM_INITDIALOG。然后,所有其余的对话逻辑都进入 new SUBCLASSPROC,它将dwRefData为每条消息接收它。请注意,这会稍微改变语义。您现在是在对话框的窗口过程级别而不是对话框过程级别进行编写。
在内部,SetWindowSubclass使用SetProp原子名称为的属性(using )UxSubclassInfo。的每个实例都SetWindowSubclass使用这个名称,因此它实际上已经在任何系统的全局原子表中。它WNDPROC用一个WNDPROC被调用的MasterSubclassProc. 该函数使用UxSubclassInfo属性中的数据来获取dwRefData并调用所有已注册的SUBCLASSPROC函数。这也意味着您可能不应该UxSubclassInfo将任何内容用作您自己的属性名称。
thunk 是可以执行的动态生成的函数。它的目的是调用另一个函数,但附加的参数似乎神奇地无处不在。
这将让您定义一个类似于 的函数WNDPROC,但它有一个附加参数。此参数可以等效于“this”指针。然后,在创建窗口时,您将原始存根WNDPROC替换为 thunk,该 thunkWNDPROC使用附加参数调用真实的伪 - 。
其工作方式是,当创建 thunk 时,它在内存中为加载指令生成机器代码,将额外参数的值加载为常量,然后跳转指令到通常需要一个函数的地址附加参数。然后可以像调用常规WNDPROC.
此方法可供窗口类作者使用,并且速度极快。然而,实现并非微不足道。该AtlThunk系列函数实现了这一点,但有一个怪癖。它不会添加额外的参数。相反,它取代了HWND的参数WNDPROC与数据的任意一块。但是,这不是大问题,因为您的任意数据可能包含HWND窗口的 。
与该SetWindowSubclass方法类似,您将在窗口创建期间使用任意数据指针创建 thunk。然后,用WNDPROCthunk替换窗口。所有真正的工作都在WNDPROCthunk 所针对的新的、伪的中进行。
Thunk 根本不会与全局原子表混淆,也没有字符串唯一性的考虑。然而,就像堆内存中分配的所有其他东西一样,它们必须被释放,之后,可能不再调用 thunk。由于WM_NCDESTROY是窗口收到的最后一条消息,因此可以在此处执行此操作。否则,您必须确保WNDPROC在释放 thunk 时重新安装原始文件。
无需过多解释。在您的应用程序中,实现一个全局表,您将HWNDs存储为键,将上下文数据存储为值。您负责清理表,并在需要时使其足够快。
窗口类作者可以将私有表用于他们的实现,而窗口用户可以使用他们自己的表来存储特定于应用程序的信息。无需担心原子或字符串的唯一性。
如果您是Window Class Author ,则这些方法有效:
cbWndExtra、(GWLP_USERDATA)、SetProp、SetWindowSubclass、Thunk、全局查找表。
Window Class Author 意味着您正在编写WNDPROC函数。例如,您可能正在实现一个自定义图片框控件,它允许用户平移和缩放。您可能需要额外的数据来存储平移/缩放数据(例如作为 2D 转换矩阵),以便您可以WM_PAINT正确地实现您的代码。
建议:避免使用 GWLP_USERDATA,因为用户代码可能依赖它;如果可能,请使用 cbWndExtra。
如果您是Window 用户,这些方法有效:
GWLP_USERDATA、SetProp、全局查找表。
Window User 意味着您正在创建一个或多个窗口并在您自己的应用程序中使用它们。例如,您可能正在动态创建可变数量的按钮,并且每个按钮都与不同的数据相关联,这些数据在被单击时是相关的。
建议:如果 GWLP_USERDATA 是标准的 Windows 控件,或者您确定该控件不在内部使用它,请使用它。
默认情况下,对话框使用cbWndExtra设置为DLGWINDOWEXTRA. 可以为对话框定义您自己的窗口类,您可以在其中分配,例如,DLGWINDOWEXTRA + sizeof(void*)然后访问GetWindowLongPtrW(hDlg, DLGWINDOWEXTRA)。但是在这样做的同时,您会发现自己不得不回答您不喜欢的问题。例如,WNDPROC您使用哪个(您可以使用DefDlgProc),或者您使用哪些类样式(默认对话框恰好使用CS_SAVEBITS | CS_DBLCLKS,但祝您找到权威参考的好运)。
在DLGWINDOEXTRA字节中,对话框碰巧保留了一个指针大小的字段,可以使用GetWindowLongPtrwith index访问该字段DWLP_USER。这是一种额外的GWLP_USERDATA,并且在理论上具有相同的问题。在实践中,我只见过 this 在里面使用DLGPROC,最终被传递给DialogBox[Param]. 毕竟,窗口用户仍然拥有GWLP_USERDATA. 因此,几乎在任何情况下都可以安全地用于窗口类实现。