65.9K
CodeProject 正在变化。 阅读更多。
Home

类函数回调

starIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

1.00/5 (6投票s)

2005年4月28日

5分钟阅读

viewsIcon

90908

演示了如何回调类中非静态的函数。

引言

在这篇文章中,我将向您展示如何回调类中非静态的函数。这将利用函数、指针和一些汇编。据我所知,没有汇编是不可能实现的。但是,本指南不需要您了解任何汇编知识;它将解释其工作原理。本指南无意教授汇编,网络上有很多优秀的汇编教程。不过,这是一种高级技术,即使是初学者也能从中受益。

我们需要什么?一个能够进行汇编的编译器和一个类!在本文中,我还将向您展示我如何使用此技术通过窗口过程转发消息。那么,让我们开始吧!

首先,我们需要一个基类。在本示例中,我使用 MFC 来演示此技术的工作原理。因此,如果您想按照本示例进行操作,请使用 MFC 创建一个对话框窗口。该类是主窗口的对话框类。

class CMyTestDlg
{
  public:
    ...
    CProcEdit m_MyEdit;
  protected:
    LRESULT WindowProc(CWnd* pCtrl, UINT message, WPARAM wParam, LPARAM lParam);
};

也许您注意到了 `WindowProc` 不是虚函数,并且带有一个额外的参数:`pCtrl`。这是因为,在本例中,我打算将其设为一个“全局”的 `WindowProc`。所有控件都将消息转发给它,有点像原始窗口过程(非 MFC)。

既然有了这个,我们就需要创建整个转发函数。怎么做?这很简单。我们只需派生出我们想要转发消息的控件,创建一个新类。让我们也定义一下这个类……

typedef LRESULT (WndProcPtr)(UINT,WPARAM,LPARAM);

class CProcEdit: public CEdit
{
  public:
    struct CallbackPtr
    {
      CWnd* pThisPtr;
      WndProcPtr* pWndProcPtr;
    } pCallbackPtr;
  protected:
     virtual LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam);
};

我们不需要更复杂的类。它继承了 MFC 的 `CEdit` 类(编辑类)的所有内容。无论如何,这个类的目的只是转发其消息。这就是为什么我们重写 `WindowProc`。您可能还在想为什么会有 `struct` 和 **两个** 参数?函数指针只有一个变量。是的……但我们需要两个参数:一个用于函数地址,另一个指向包含该函数的类本身。明白了吗?很好。至于为什么需要第二个……我稍后会再解释。

现在,为了使其能够进行回调,我们必须在初始化(或创建)对话框或类时设置这些变量。所以,我们在主对话框的 `InitDialog` 中这样做。现在,我们开始使用汇编的第一部分。先给您看代码,然后讨论其作用……

BOOL CMyTestDlg::OnInitDialog()
{
  WndProcPtr* pCallback;
  __asm
  {
    mov eax, WindowProc;
    mov pCallback, eax;
  };
  m_MyEdit.pCallbackPtr.pThisPtr = this;
  m_MyEdit.pCallbackPtr.pWndProcPtr = pCallback;
};

就这样。现在让我们看看这段代码的作用……为什么需要汇编?……汇编到底做了什么?让我解释一下……如果您对此有经验,应该知道……

pCallback = WindowProc;

……会导致编译错误。这就是为什么我们必须使用汇编!在那里没有这样的限制。“mov”操作码将数据从内存移出或移入内存。所以,首先,我们将 `WindowProc` 函数的偏移量移入 `eax` 寄存器。然后,我们将 `eax` 寄存器的内容移入我们的 `pCallback` 变量。请注意,由于汇编语言的限制,我们不能直接将内容移入变量。之后,`pCallback` 变量就包含了我们函数的地址(或偏移量)。然后,我们将编辑类的两个变量设置为指向我们的对话框类和函数。好了,跟上了吗?希望您跟上了!

既然我们已经初始化了类,就必须确保编辑类转发我们的消息。这可能有点棘手……总之,这是代码

LRESULT CProcEdit::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
  if (pWndProc) 
  {
    __asm
    { 
      // Pass arguments (right to left)
      mov eax, [lParam];
      push eax;
      mov eax, [wParam];
      push eax;
      mov eax, [message];
      push eax;
      mov eax, this;
      push eax;

      // Fill ecx with the class placement
      mov eax, this;
      add eax, pThisPtr;
      mov ecx, [eax];

     // Call function
     mov eax, pWndProc;
     add eax, this;
     mov eax, [eax];
     call eax;
    }
  }
  return CEdit::WindowProc(message, wParam, lParam);
}

呼,这确实是一堆汇编代码。它做了什么?为什么我们需要汇编来调用函数指针?嗯,这不像您想的那么简单……首先,让我告诉您为什么普通调用会失败。您知道,在类中有一个“this”指针。这个“this”指针存储在 `ecx` 寄存器中。`ecx` 寄存器必须指向类所在的偏移量。那么,如果我们正常调用函数,会发生什么?`ecx` 中的偏移量是**当前**类的偏移量——换句话说,是 `CProcEdit` 类的偏移量!当这种情况发生时,我们调用的函数将无法正确访问其成员,甚至可能导致访问冲突。为什么会这样?函数就在类里面,所以……为什么不能直接调用它们并使其工作?

好问题……不幸的是,事情并非如此……您是否曾在 DLL 中导出过类的一部分函数?这是可以工作的。但是当您调用它们时,“this”指针是 `NULL`!为什么?那是因为函数**不**存储在类中!看看这个类……

class CAnotherClass
{
  int arg1;
  int arg2;
  void myfunc() { }
};

对这个类执行 `sizeof` 将返回 8!因为有两个 `int` 参数。函数**不**被计算在内!所以……当调用类中的函数时,函数必须知道类存在的位置才能访问其数据。这就是为什么有一个“this”指针,它存储在 `ecx` 寄存器中。

这就是为什么我们需要汇编。我们需要确保我们正确地填充了 `ecx` 寄存器。因此,我们还获取了指向类本身的指针。好了,那么代码做了什么?让我们看看……

首先,我们将参数推入堆栈。要将某物推入堆栈,我们必须先将其放入寄存器。所有参数都是从右到左传递的,所以是这样。其次,我们必须用类地址(或偏移量)填充 `ecx`!第三,我们调用函数。所有这些是如何工作的,我将不再详细解释……如果您想知道,那就去找个汇编课程吧。

最后,我们调用基类的 `windowproc` 函数,以便它能够进一步处理消息。这就是我们需要做的所有事情!不过,关于此示例,我有一个注意事项。在 `CMyTestDlg` 的被调用 `windowproc` 中,**不要调用基类的窗口过程函数!** 为什么?因为消息不是为该类设计的。如果您将它们传递下去,您可能会在窗体上看到奇怪的绘制和其他奇怪的事情。相反,返回 0 或其他值;这无关紧要,因为我们不返回此窗口过程返回的值。

……这就是全部了!现在您可能可以理解为什么类函数回调是不可能的。如果还不明白……我猜我可以告诉您。很简单。编译器**不知道函数属于哪个类**。

编程愉快!

© . All rights reserved.