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

后台应用程序监听键盘活动

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (30投票s)

2005年1月20日

CPOL

4分钟阅读

viewsIcon

207999

downloadIcon

10669

捕获键盘事件的类。

Sample Image - KeyboardListener.gif

引言

此处提供的 KeyboardListener 类允许捕获键盘事件,即使有其他应用程序在前台运行。您的应用程序中可能有多个线程并行监听事件,每个线程以自己的方式处理事件。

如果您只是想为您的应用程序添加热键支持,您也可以查看 CodeProject 文章 “.NET 系统范围热键组件”。但是,如果您对在应用程序中处理键盘事件有其他计划,那么这个 KeyboardListener 类可能就是您想要的。

KeyboardListener 类只能在 Windows XP 上使用,因为它的实现依赖于 Win32 方法 RegisterRawInputDevicesGetRawInputData,而这些方法目前只在 XP 上可用。

使用代码

1. 演示应用程序

让我们从简单的部分开始:在应用程序中使用该类。演示应用程序是一个纯 .NET 应用程序,只包含一个窗体,该窗体又只包含一个文本标签(参见演示项目)。Form1_Load 方法在窗体加载时调用,它只向 KeyboardListener 类导出的静态 s_KeyEventHandler 事件添加一个新的 EventHandler。当然,您可以自由地在应用程序的多个位置添加多个 EventHandler 实例。

private void Form1_Load(object sender, System.EventArgs e)
{
    // Watch for keyboard activity
    KeyboardListener.s_KeyEventHandler += new 
           EventHandler(KeyboardListener_s_KeyEventHandler);
}

传递给 EventHandler 构造函数的 KeyboardListener_s_KeyEventHandler 方法会分析参数,用一些按键信息更新窗体上的文本标签 lblKeyPressed,并根据按键动作改变窗体的背景颜色:当按下按键时,背景变为红色;当释放按键时,背景再次变为绿色。

private void KeyboardListener_s_KeyEventHandler(object sender, EventArgs e)
{
    KeyboardListener.UniversalKeyEventArgs eventArgs = 
                                (KeyboardListener.UniversalKeyEventArgs)e;
    lblKeyPressed.Text = string.Format("Key = {0}  Msg = {1}  Text = {2}", 
                     eventArgs.m_Key, eventArgs.m_Msg, eventArgs.KeyData);

    // 256 : key down    257 : key up
    if(eventArgs.m_Msg == 256)
    {
        this.BackColor = Color.Red;
    }
    else
    {
       this.BackColor = Color.Green;
    }
}

2. KeyboardListener 类

KeyboardListener 类由三个重要部分组成。首先,有前面提到的静态 EventHandler 变量 s_KeyEventHandler。任何对接收键盘事件感兴趣的应用程序线程都可以订阅此事件。

public static event EventHandler s_KeyEventHandler;

其次,是静态 KeyHandler 方法,当发生键盘事件时会调用它。它会遍历 s_KeyEventHandler 中包含的所有 EventHandler 实例,并用一个 UniversalKeyEventArgs 实例调用每个实例,该实例包含键盘事件提供的 **按键** 和 **消息**。遍历所有委托的优点是,通过在循环中添加 try/catch 块,一个 EventHandler 可能会失败,但所有其他 EventHandler 实例仍然会被调用。换句话说,一个订阅者失败不会阻止其他订阅者被调用。

private static void KeyHandler(ushort key, uint msg)
{
    if(s_KeyEventHandler != null)
    {
        Delegate[] delegates = s_KeyEventHandler.GetInvocationList();

        foreach(Delegate del in delegates)
        {
            EventHandler sink = (EventHandler)del;

            try
            {
                // This is a static class, therefore null
                // is passed as the object reference
                sink(null,new UniversalKeyEventArgs(key,msg));
            }

            // You can add some meaningful code to this catch block
            catch{};
        }
    }
}

最后是 ListeningWindow 类。顾名思义,此类的一个实例用于监听。它监听键盘事件的发生。实际上,这个类只有一个实例。这个实例在调用 KeyboardListener 类的静态构造函数时创建。查看下面的 ListeningWindow 构造函数,您会发现它在构造时需要一个委托。这个委托恰好是上面提到的 KeyHandler。因此,在应用程序运行时,每当 ListeningWindow 检测到键盘事件时,它就会调用 KeyHandler 委托,然后该委托会调用所有已安装的 EventHandler

public ListeningWindow(KeyDelegate keyHandlerFunction)
{
    m_KeyHandler = keyHandlerFunction;

    CreateParams cp = new CreateParams();

    // Fill in the CreateParams details.
    cp.Caption = "Hidden window";
    cp.ClassName = null;
    cp.X = 0x7FFFFFFF;
    cp.Y = 0x7FFFFFFF;
    cp.Height = 0;
    cp.Width = 0;
    cp.Style = WS_CLIPCHILDREN;

    // Create the actual invisible window
    this.CreateHandle(cp);

    // Register for Keyboard notification
    unsafe
    {
        try
        {
            RAWINPUTDEV myRawDevice = new RAWINPUTDEV();
            myRawDevice.usUsagePage = 0x01;
            myRawDevice.usUsage = 0x06;
            myRawDevice.dwFlags = RIDEV_INPUTSINK;
            myRawDevice.hwndTarget = this.Handle.ToPointer();

            if (RegisterRawInputDevices(&myRawDevice, 1, 
                         (uint)sizeof(RAWINPUTDEV)) == false) 
            {
                int err = Marshal.GetLastWin32Error();
                throw new Win32Exception(err, 
                       "ListeningWindow::RegisterRawInputDevices");
            }
        }

        catch {throw;}
    }
}

ListeningWindow 类是 NativeWindow 类的子类,它只包含两个有趣的定义:构造函数和 WndProc 方法。

ListeningWindow 构造函数中,会创建一个不可见的窗口,然后将其 *注册* 以接收键盘输入消息。这些消息由 WndProc 方法分析,该方法重写了 NativeWindow 基类的方法。每当 WndProc 方法收到键盘消息时,它会检查该消息是否与上一条消息不同,如果不同,则调用在 ListeningWindow 构造函数中安装的委托。

protected override void WndProc(ref Message m)
{
    ...
    receivedBytes = 
      (uint)GetRawInputData((RAWINPUTHKEYBOARD*)(m.LParam.ToPointer()), 
      RID_INPUT, lpb, &dwSize, sizeof_RAWINPUTHEADER);
    if ( receivedBytes == dwSize )
    {
        RAWINPUTHKEYBOARD* keybData = (RAWINPUTHKEYBOARD*)lpb;

        // Finally, analyze the data
        if(keybData->header.dwType == RIM_TYPEKEYBOARD)
        {
            if((m_PrevControlKey != keybData->VKey) || 
               (m_PrevMessage != keybData->Message))
            {
                m_PrevControlKey = keybData->VKey;
                m_PrevMessage = keybData->Message;

                // Call the delegate in case data satisfies
                m_KeyHandler(keybData->VKey,keybData->Message);
            }
        }
    }
    ...
}

使用 PInvoke

KeyboardListener 类实现的关键是 **RegisterRawInputDevices** 和 **GetRawInputData** 这两个方法。它们能够捕获和处理后台的键盘事件。由于 .NET 不支持这些调用,因此它们是通过 PInvoke 机制调用的。

[DllImport("User32.dll",CharSet = CharSet.Ansi,SetLastError=true)] 
[return : MarshalAs(UnmanagedType.Bool)] 
internal static extern unsafe bool 
   RegisterRawInputDevices( RAWINPUTDEV* rawInputDevices, 
   uint numDevices, uint size);

[DllImport("User32.dll",CharSet = CharSet.Ansi,SetLastError=true)]
[return : MarshalAs(UnmanagedType.I4)]
internal static extern unsafe int GetRawInputData( void* hRawInput, 
   uint uiCommand, byte* pData, uint* pcbSize, uint cbSizeHeader);

未来构想

  1. UniversalKeyEventArgsEventArgs 类的子类,您可能会问为什么需要它。老实说,没有它也可以。但那时您必须组装正确的 Keys 实例作为参数传递给 EventArgs 构造函数。在本次发布中,我选择了一种简单的方法,这肯定可以在未来的版本中得到改进。
  2. 使用相同的技术,可以创建一个 MouseListener 类。请参阅我的下一篇文章。

历史

  • 2005 年 1 月 20 日,版本 1.0.0,初始发布。
© . All rights reserved.