在 Windows Mobile 中使用输入钩子
对 Windows Mobile 的未公开消息钩子函数进行探讨。
![]() | ![]() |
目录
引言
在本文中,我们将探讨 Windows Mobile 中未记录且不受支持的钩子 API。此 API 允许我们的应用程序接收来自系统的输入消息,即使它没有输入焦点。此功能对于记录按键、鼠标移动和宏录制非常有用。这些函数只能用于接收输入消息,不能用于阻止或更改这些消息。
随附的示例应用程序使用 WTL 和 Boost。我发现这些库极大地提高了我的代码质量。如果您不使用它们(您真的应该使用!),本文中介绍的概念仍然适用。
QA 日志钩子 API
本文使用的日志钩子 API 函数在 pwinuser.h 中定义,该文件作为平台构建器的一部分提供。如果您没有平台构建器,可以通过一些快速的 Internet 搜索轻松找到它们的定义。尽管 Microsoft 确实记录了 Windows 的消息钩子函数,但即使使用平台构建器,也没有此移动版钩子 API 的官方文档。
HOOKPROC
- 当检测到用户输入事件时,由钩子机制激活的回调过程。此过程由 GWES.exe 调用,而不是您的应用程序的进程。这意味着我们不能在此函数中设置断点来调试它,也不能使用NKDbgPrintfW
并在输出窗口中查看调试信息。CallNextHookEx
- 允许操作系统处理任何其他应用程序的钩子。这在HOOKPROC
中调用。QASetWindowsJournalHook
- 激活钩子机制。QAUnhookWindowsJournalHook
- 停用钩子机制。当您完成钩子操作时,调用此函数非常重要。如果未正确调用它,您可能需要重新启动您的 Windows Mobile 设备。
HOOKPROC 回调
我们从 HOOKPROC
回调函数的实现开始。
由于 HOOKPROC
回调函数由另一个进程(GWES.exe)调用,因此我们必须使用进程间通信方法将消息数据从该过程传回我们的进程。对于此示例,我们将使用 消息队列。当 HOOKPROC
收到 EVENTMSG
时,我们将打开一个命名的队列并将该消息写入其中。
当我们完成消息处理后,我们调用 CallNextHookEx
以允许其他钩子处理该消息。更改发送给 CallNextHookEx
的消息的内容只会影响发送给其他钩子的信息。我们无法更改或阻止用户正在与之交互的应用程序的输入。
/// global journal handle.
HHOOK journal_;
static LRESULT JournalCallback( int nCode, WPARAM wParam, LPARAM lParam )
{
if( HC_ACTION == nCode )
{
EVENTMSG* msg = reinterpret_cast< PEVENTMSG >( lParam );
if( msg )
{
// We've received a message; put it on the message queue.
CMessageQueue message_queue( JOURNAL_QUEUE_NAME,
0,
sizeof( EVENTMSG ),
FALSE,
MSGQUEUE_NOPRECOMMIT |
MSGQUEUE_ALLOW_BROKEN );
message_queue.WriteMsgQueue( msg, sizeof( EVENTMSG ), 0, 0 );
}
}
// tell the OS to continue processing other hooks.
return ::CallNextHookEx( journal_, nCode, wParam, lParam );
}
消息处理线程
调用时,此线程将侦听发送到日志事件消息队列的消息,并激活回调函数以通知用户类有新事件可供处理。使用此方法,我们从钩子进程中获取消息,并使其安全地可供我们的应用程序使用。
void JournalHook::JournalThread( OnJournalEvent callback )
{
// listen on our interprocess message queue for hooked events
CMessageQueue message_queue( JOURNAL_QUEUE_NAME,
0,
sizeof( EVENTMSG ),
TRUE,
MSGQUEUE_NOPRECOMMIT | MSGQUEUE_ALLOW_BROKEN );
// initialize the journal hook
EVENTMSG evt = { 0 };
journal_ = ::QASetWindowsJournalHook( WH_JOURNALRECORD,
JournalCallback,
&evt );
DWORD bytes_read = 0;
DWORD flags = 0;
HANDLE handles[] = { message_queue, stop_event_ };
// listen for journal events from JournalCallback.
while( ::WaitForMultipleObjects( _countof( handles ),
handles,
FALSE,
INFINITE ) == WAIT_OBJECT_0 )
{
EVENTMSG msg = { 0 };
if( message_queue.ReadMsgQueue( &msg,
sizeof( EVENTMSG ),
&bytes_read,
0,
&flags ) )
{
// We received an event. Package it up and alert the user.
MSG send = { msg.hwnd,
msg.message,
msg.paramH,
msg.paramL,
msg.time,
{ 0, 0 } };
callback( send );
}
else
{
// we failed to read from the message queue. exit.
break;
}
}
// cleanup the journal hook.
::QAUnhookWindowsJournalHook( WH_JOURNALRECORD );
journal_ = NULL;
}
整合所有内容
解决了困难的部分后,我们现在可以展示一个简单的活动对象实现,该实现将允许使用类访问用户输入事件,即使它们定向到另一个应用程序。
/// Active object that alerts its user of any user input events regardless of
/// which process has input focus.
class JournalHook
{
public:
JournalHook( void ) : stop_event_( NULL, FALSE, FALSE, NULL )
{
};
/// prototype for the function called when a journal event occurs
/// @param MSG - windows user input message received
typedef boost::function< void( const MSG& msg ) > OnJournalEvent;
/// start listening for journal events
/// @param OnJournalEvent - activated when an event occurs
void Start( OnJournalEvent callback )
{
stop_event_.ResetEvent();
journal_thread_.reset( new CThread(
boost::bind( &JournalHook::JournalThread, this, callback ) ) );
};
/// stop listening for journal events
void Stop()
{
stop_event_.SetEvent();
journal_thread_->Join();
};
private:
/// journal callback function activated by GWES.exe
static LRESULT JournalCallback( int nCode, WPARAM wParam, LPARAM lParam );
/// thread handle that listens for journal events
boost::shared_ptr< CThread > journal_thread_;
/// This class' thread function that listens for journal event messages
/// @param OnJournalEvent - activated by this function when an event occurs.
void JournalThread( OnJournalEvent callback );
/// event signaled when we should stop listening for events
CEvent stop_event_;
}; // class JournalHook
在应用程序中使用 API
我们的日志钩子活动对象负责使用日志钩子 API 的困难部分。它在应用程序中的用法非常简单
- 启动活动对象并提供一个回调函数,该函数将在日志钩子 API 检测到事件时被激活。
- 在回调函数中,像处理任何其他 Windows 消息一样处理提供的
MSG
。由于我们的示例包含用户界面,我们使用PostMessage
API 返回到 UI 线程,然后再修改任何对话框控件。 - 请记住在退出应用程序时停止活动对象。这确保了钩子 API 被正确清理。
static const UINT UWM_USER_INPUT = ::RegisterWindowMessage( _T( "UWM_USER_INPUT" ) );
/// Our journal hook active object
JournalHook hook_;
/// WM_INITDIALOG message handler
LRESULT CJournalHookDemoDialog::OnInitDialog( UINT /*uMsg*/,
WPARAM /*wParam*/,
LPARAM /*lParam*/,
BOOL& bHandled )
{
// perform dialog initialization...
// start listening for events from the journal hook API
hook_.Start( boost::bind( &CJournalHookDemoDialog::OnHookEvent, this, _1 ) );
return ( bHandled = FALSE );
}
/// JournalHook callback function activated when a user input event occurs.
void CJournalHookDemoDialog::OnHookEvent( const MSG& msg )
{
// We received a user-input notification from the Journal Hook. Get back to
// our UI thread context to update the UI.
// We use PostMessage() rather than the simpler SendMessage() to avoid a
// deadlock if the user calls JournalHook::Stop() while we're processing
// this message.
PostMessage( UWM_USER_INPUT, ( WPARAM )new MSG( msg ) );
}
/// UWM_USER_INPUT message handler
LRESULT CJournalHookDemoDialog::OnUserInput( UINT /*uMsg*/,
WPARAM wParam,
LPARAM /*lParam*/,
BOOL& /*bHandled*/ )
{
std::auto_ptr< MSG > msg( reinterpret_cast< MSG* >( wParam ) );
switch( msg->message )
{
case WM_SYSKEYDOWN:
// the user pressed a physical key
break;
case WM_SYSKEYUP:
// the user released a physical key
break;
case WM_MOUSEMOVE:
// the user moved the stylus
break;
case WM_LBUTTONDOWN:
// the user pressed the stylus to the screen
break;
case WM_LBUTTONUP:
// the user lifted the stylus off the screen
break;
}
// display some information in our UI.
return 0;
}
/// WM_CLOSE message handler
LRESULT CJournalHookDemoDialog::OnClose( UINT /*uMsg*/,
WPARAM /*wParam*/,
LPARAM /*lParam*/,
BOOL& bHandled )
{
hook_.Stop();
EndDialog( IDCANCEL );
return ( bHandled = FALSE );
}
结论
Windows Mobile QA 日志钩子 API 的未记录性质给我们带来了几个实现挑战。我希望本文及其随附的演示代码已经演示了如何克服这些挑战,并使它的基本用法变得清晰。