【问题标题】:Call SetWindowsHookEx with method defined in header file使用头文件中定义的方法调用 SetWindowsHookEx
【发布时间】:2015-10-08 19:46:49
【问题描述】:

我正在尝试向一个类添加一个低级鼠标挂钩。我可以通过将此函数放在我的 CPP 文件中来做到这一点:

LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    //my hook code here
    return CallNextHookEx(0, nCode, wParam, lParam);
} 

然后,我在类构造函数中设置了钩子,如下所示:

HHOOK mousehook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, NULL, 0);

这可以很好地拦截鼠标事件,但是由于回调函数没有在我的类中定义,它无法访问我的任何类变量。

因此,我尝试在头文件中定义回调函数,如下所示:

LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam);

在我的 CPP 文件中是这样的(TMainForm 是类):

LRESULT CALLBACK TMainForm::MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
     //my hook code here
     return CallNextHookEx(0, nCode, wParam, lParam);
}

但是,当我尝试像这样编译时,出现以下错误:

[bcc32 Error] MainFormU.cpp(67): E2034 Cannot convert 'long (__stdcall * (_closure )(int,unsigned int,long))(int,unsigned int,long)' to 'long (__stdcall *)(int,unsigned int,long)'

[bcc32 Error] MainFormU.cpp(67): E2342 Type mismatch in parameter 'lpfn' (wanted 'long (__stdcall *)(int,unsigned int,long)', got 'void')

我到底做错了什么?自从我将它作为我的TMainForm 课程的一部分后,现在的方法有何不同?

【问题讨论】:

  • 您不能将常规成员函数作为回调传递。根据this answer,Visual Studio 确实允许您传递静态成员函数,但它不是合法的 C++。相反,请使用普通函数(如您的第一个示例)并让它调用您的类成员函数。 (我假设有问题的类是单例的?如果不是,你必须有其他方法来识别正确的实例。)
  • 成员函数有一个隐藏的“this”参数,使用回调的 Win32 API 不知道。
  • @HarryJohnston 这不是那个答案所说的。静态成员函数只是一个常规函数,可以在这里有效地使用。非静态成员函数是一个非常不同的野兽。
  • 你可以在钩子函数中创建你的类的静态实例,并使用这个实例来调用类中的方法。

标签: c++ windows winapi hook c++builder


【解决方案1】:

您不能使用非静态类方法作为回调。非静态方法有一个隐藏的this 参数,因此回调的签名与SetWindowsHookEx() 期望的签名不匹配。即使编译器允许(只能通过强制转换来完成),API 也无法解释 this 参数。

如果您想让回调成为该类的成员(以便它可以访问私有字段等),则必须将其声明为 static 以删除 this 参数,但随后您必须在需要时使用表单的全局指针来访问它,例如:

class TMainForm : public TForm
{
private:
    HHOOK hMouseHook;
    static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam);
    void MouseHook(int nCode, WPARAM wParam, LPARAM lParam);
public:
    __fastcall TMainForm(TComponent *Owner);
    __fastcall ~TMainForm();
};
extern TMainForm *MainForm;

__fastcall TMainForm::TMainForm(TComponent *Owner)
    : TForm(Owner)
{
    hMouseHook = SetWindowsHookEx(WH_MOUSE_LL, &MouseHookProc, NULL, 0);
}

__fastcall TMainForm::~TMainForm()
{
    if (hMouseHook)
        UnhookWindowsHookEx(hMouseHook);
}

LRESULT CALLBACK TMainForm::MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    MainForm->MouseHook(nCode, wParam, lParam);
    return CallNextHookEx(0, nCode, wParam, lParam);
}

void TMainForm::MouseHook(int nCode, WPARAM wParam, LPARAM lParam)
{
    // my hook code here
}

话虽如此,您应该考虑使用Raw Input API 而不是SetWindowsHookEx()LowLevelMouseProc documentation 甚至这么说:

注意 调试挂钩无法跟踪此类低级鼠标挂钩。如果应用程序必须使用低级挂钩,它应该在专用线程上运行挂钩,将工作传递给工作线程,然后立即返回。 在应用程序需要使用低级挂钩的大多数情况下,它应该改为监视原始输入。这是因为原始输入可以比低级挂钩更有效地异步监视针对其他线程的鼠标和键盘消息。有关原始输入的更多信息,请参阅Raw Input

使用原始输入,鼠标将WM_INPUT 消息直接发送到您的窗口。

如果你使用VCL,你可以重写虚拟WndProc()方法来处理WM_INPUT消息,不需要静态方法:

class TMainForm : public TForm
{
protected:
    virtual void __fastcall CreateWnd();
    virtual void __fastcall WndProc(TMessage &Message);
};

void __fastcall TMainForm::CreateWnd()
{
    TForm::CreateWnd();

    RAWINPUTDEVICE Device = {0};
    Device.usUsagePage =  0x01;
    Device.usUsage = 0x02;
    Device.dwFlags = RIDEV_INPUTSINK;
    Device.hwndTarget = this->Handle;

    RegisterRawInputDevices(&Device, 1, sizeof(RAWINPUTDEVICE));
}

void __fastcall TMainForm::WndProc(TMessage &Message)
{
    if (Message.Msg == WM_INPUT)
    {
        HRAWINPUT hRawInput = (HRAWINPUT) Message.LParam;
        UINT size = 0;
        if (GetRawInputData(hRawInput, RID_INPUT, NULL, &size, sizeof(RAWINPUTHEADER)) == 0)
        {
            LPBYTE buf = new BYTE[size];

            if (GetRawInputData(hRawInput, RID_INPUT, buf, &size, sizeof(RAWINPUTHEADER)) != 0)
            {
                RAWINPUT *input = (RAWINPUT*) buf;
                // use input->data.mouse or input->data.hid as needed...
            }

            delete[] buf;
        }
    }

    TForm::WndProc(Message);
}

如果您使用 FireMonkey,则没有 WndProc() 方法来处理窗口消息(FireMonkey 根本不会将窗口消息发送给用户代码)。但是,您可以对 FireMonkey 在内部创建的窗口进行子类化,这样您仍然可以收到WM_INPUT 消息。需要静态方法,但不必依赖全局指针,可以将 Form 对象作为子类的参数传递:

class TMainForm : public TForm
{
private:
    static LRESULT CALLBACK SubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR  uIdSubclass, DWORD_PTR dwRefData);
protected:
    virtual void __fastcall CreateHandle();
};

void __fastcall TMainForm::CreateHandle()
{
    TForm::CreateHandle();

    HWND hWnd = Platform::Win::WindowHandleToPlatform(this->Handle)->Wnd;

    SetWindowSubclass(hWnd, &SubclassProc, 1, (DWORD_PTR)this);

    RAWINPUTDEVICE Device = {0};
    Device.usUsagePage =  0x01;
    Device.usUsage = 0x02;
    Device.dwFlags = RIDEV_INPUTSINK;
    Device.hwndTarget = hWnd;

    RegisterRawInputDevices(&Device, 1, sizeof(RAWINPUTDEVICE));
}

LRESULT CALLBACK TMainForm::SubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR  uIdSubclass, DWORD_PTR dwRefData)
{
    TMainForm *pThis = (TMainForm*) dwRefData;

    switch (uMsg)
    {
        case WM_INPUT:
        {
            // ...
            break;
        }

        case WM_NCDESTROY:
        {
            RemoveWindowSubclass(hWnd, &SubclassProc, uIdSubclass);
            break;
        }
    }

    return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}

【讨论】:

  • +1。您是否碰巧参考了在 Win32 回调中使用静态成员函数是否安全?我对 C++ 标准的阅读表明这被正式认为是未定义的行为(因为根据 5.2.2,指向具有 C++ 链接的函数的指针作为指向具有 C 链接的函数的指针无效)但我猜Visual Studio 是否支持将其作为标准的扩展?
  • 我认为链接在这里不适用。如果您在同一个项目中有一个.c 源文件和一个.cpp 源文件,其中.c 文件被编译为C,.cpp 文件被编译为C++,并且.c 文件正在尝试要调用.cpp 文件中的函数,反之亦然,则将应用链接。但在跨 DLL 边界传递函数指针时不会。在这种情况下,SetWindowsHookEx() 只关心给它一个指向与HOOKPROC 类型定义的签名匹配的函数的指针。无论是用 C 还是 C++ 实现,它的链接都不会影响它的签名。
  • 根据标准,链接是签名的一部分。 7.5 第 1 段说“具有不同语言链接的两个函数类型是不同的类型,即使它们在其他方面相同。”和 5.2.2 第 1 段说“通过函数类型具有与被调用函数定义的函数类型的语言链接不同的语言链接的表达式调用函数是未定义的”。但我不想在这里狡辩,我同意它在 Visual Studio 中有效——我只是希望你有参考来证明它。 :-)
  • 不是来自 C++ 标准的明确参考,但我确实发现了这一点(确实参考了标准):static specifier"静态成员函数的地址可以存储在常规pointer to function ...”,这是所有 Win32 API 关心的。
  • 我认为没有冲突;它当然是指向函数的常规指针,但这并不意味着如果函数签名不匹配,您可以安全地调用它。但实际上,问题是静态成员函数是否保证使用正确的 ABI 实现,我希望 CALLBACK 限定符确保 - 或至少,编译器应该生成错误如果它无法这样做。所以,考虑到所有因素,我现在确信它是安全的,我会闭嘴。 :-)
【解决方案2】:

我遇到了同样的问题,我发现对于我的特殊情况,最好的方法是创建一个指向我的类的静态指针数组。然后在静态钩子方法中,我只是遍历我的类指针并调用它们的钩子函数。

kb_hook.h

typedef KBDLLHOOKSTRUCT khookstruct;
typedef LRESULT lresult;
typedef void (*h_func)(uint64_t key_message, khookstruct* kbdhook);
typedef std::vector<kb_hook*> h_array;

class kb_hook
{
public:
    kb_hook();
    virtual ~kb_hook();

    h_func hook_func;

private:
    static h_array hook_array;

    static lresult static_hook(int code, uint64_t key_message, khookstruct* kbdhook);
};

kb_hook.cpp

kb_hook::kb_hook() : hook_func(NULL)
{
    this->hook_array.push_back(this);
}

lresult kb_hook::static_hook(int code, uint64_t key_message, khookstruct* kbdhook)
{
    if(code == HC_ACTION)
    {
        for(auto it=kb_hook::hook_array.begin();it!=kb_hook::hook_array.end();it++)
        {
            if((*it)->hook_func) std::thread((*it)->hook_func, key_message, kbdhook).detach();
        }
    }

    return CallNextHookEx(NULL, code, key_message, reinterpret_cast<LPARAM>(kbdhook));
}

我知道这是一个老问题,但我只想投入两分钱。我希望这对某人有所帮助。

【讨论】:

    猜你喜欢
    • 2012-05-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多