четверг, 15 марта 2012 г.

Перехват нажатий клавиш в C#

Иногда, во время написания программ ил игр, требуется перехватить нажатия клавиш на форме приложения. Многим сразу вспомниться событие KeyDown. Казалось бы, что сложного повесить обработчик на событие KeyDown?! Но, если добавить на форму контрол, который перехватывает фокус, то обработчик события начинает вести себя не так, как хотелось бы. Дело в том, что вызывает обработчик активного контрола, например кнопки, и не передается остальным обработчикам. Можно, конечно, всем контролам ставить один обработчик события. Но это не удобно, и могут в последствии появиться подводные камни.. В этой мы решим эту проблему.



В этой статье мы будем использовать ловушки - Hook.
Для начала объявим класс, назовем его KeyHookDecl.

class KeyHookDecl
{
}

И первое, что нам понадобиться, это функции WinApi - SetWindowsHookEx, CallNextHookEx и UnhookWindowsHookEx.
Объявим их и делегат HookProc, из материала msdn, в классе KeyHookDecl:
public delegate int HookProc(int code, int wParam, IntPtr lParam); 
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
public static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public static extern int CallNextHookEx(int idHook, int nCode, int wParam, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
public static extern bool UnhookWindowsHookEx(int hhk);
Только не забываем в подключить  System.Runtime.InteropServices.
Итак, теперь читаем информацию по функции SetWindowsHookEx  в msdn.
Нам понадобиться следующая информация:


WH_KEYBOARD
2
Installs a hook procedure that monitors keystroke messages. 

Если перевести то получается:


WH_KEYBOARD
2
Устанавливает процедуру, которая отслеживает нажатия клавиш.


Это как раз то, что нам нужно, и теперь в классе объявим константу WH_KEYBOARD:
public const int WH_KEYBOARD = 2;
Итак, теперь объявляем основной класс KeyHook:

public class KeyHook
{
}
Теперь первое что нам нужно объявить события KeyDown, KeyUp.

        public event KeyEventHandler KeyDown; 
        public event KeyEventHandler KeyUp;
И не забываем про using System.Windows.Forms, так как именно там объявлены  KeyEventHandler.

Теперь напишем конструктор, объявим функцию HookProc и переменную hhook. Также нам понадобиться функция GetModuleHandle, которую мы импортируем из kernel32.dll.




[DllImport("kernel32.dll")]
public static extern IntPtr GetModuleHandle(string lpModuleName);

int hhook;// хэндл созданой нами ловушки
int HookProc(int code, int wParam, IntPtr lParam)
{
return KeyHookDecl.CallNextHookEx(hhook, code, wParam, lParam);// Вызов следующей ловушки в очереди
}
public KeyHook()
{
 // Создание ловушки
hhook = KeyHookDecl.SetWindowsHookEx(KeyHookDecl.WH_KEYBOARD, new KeyHookDecl.HookProc(HookProc), GetModuleHandle(null), AppDomain.GetCurrentThreadId());
}

Все, наш перехватчик уже функционирует, осталось написать обработчик сообщений, для этого
идем на msdn. На интересует параметр lParam и wParam.







wParam [in]
Type: WPARAM
The virtual-key code of the key that generated the keystroke message.
lParam [in]
Type: LPARAM
The repeat count, scan code, extended-key flag, context code, previous key-state flag, and transition-state flag. For more information about the lParam parameter, see Keystroke Message Flags. The following table describes the bits of this value.
BitsDescription
0-15The repeat count. The value is the number of times the keystroke is repeated as a result of the user's holding down the key.
16-23The scan code. The value depends on the OEM.
24Indicates whether the key is an extended key, such as a function key or a key on the numeric keypad. The value is 1 if the key is an extended key; otherwise, it is 0.
25-28Reserved.
29The context code. The value is 1 if the ALT key is down; otherwise, it is 0.
30The previous key state. The value is 1 if the key is down before the message is sent; it is 0 if the key is up.
31The transition state. The value is 0 if the key is being pressed and 1 if it is being released.




int HookProc(int code, int wParam, IntPtr lParam)
{
BitArray bt = new BitArray(BitConverter.GetBytes((uint)lParam));// для доступа к битам
KeyEventArgs arg = new KeyEventArgs((Keys)wParam);
//Если клавиша нажата - вызвать KeyDown, если отпущена - вызвать KeyUp
if (bt[30]) { if (KeyUp != null) KeyUp(this, arg); } else if (KeyDown != null) KeyDown(this, arg);
KeyHookDecl.CallNextHookEx(hhook, code, wParam, lParam);// Вызов следующей ловушки в очереди
return 1;
}
Теперь напишем деструктор:

    ~KeyHook()
    {
        KeyHookDecl.UnhookWindowsHookEx(hhook);
    }


Вот и все, теперь этим можно пользоваться так:



        private void Form1_Load(object sender, EventArgs e)
        {
            KeyHook kh = new KeyHook();
            kh.KeyDown += new KeyEventHandler(kh_KeyDown);
            kh.KeyUp += new KeyEventHandler(kh_KeyUp);
        }

        void kh_KeyUp(object sender, KeyEventArgs e)
        {
            textBox1.Text += "UP " + e.KeyCode.ToString() + Environment.NewLine;
        }

        void kh_KeyDown(object sender, KeyEventArgs e)
        {
            textBox1.Text += "DOWN " + e.KeyCode.ToString() + Environment.NewLine;
        }

Вот готовый класс


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Reflection;
using System.Collections;

class KeyHookDecl
{
    public const int WH_KEYBOARD = 2;

    public delegate int HookProc(int code, int wParam, IntPtr lParam);
    [DllImport("user32.dll",
     CharSet = CharSet.Auto,
     CallingConvention = CallingConvention.StdCall,
     SetLastError = true)]
     public static extern int SetWindowsHookEx(int idHook,
     HookProc lpfn,
     IntPtr hMod,
     int dwThreadId);
    [DllImport("user32.dll",
     CharSet = CharSet.Auto,
     CallingConvention = CallingConvention.StdCall)]
     public static extern int CallNextHookEx(int idHook,
     int nCode,
     int wParam,
     IntPtr lParam);
    [DllImport("user32.dll",
     CharSet = CharSet.Auto,
     CallingConvention = CallingConvention.StdCall,
     SetLastError = true)]
    public static extern bool UnhookWindowsHookEx(int hhk);
}

public class KeyHook
{
    public event KeyEventHandler KeyDown;
    public event KeyEventHandler KeyUp;

    [DllImport("kernel32.dll")]
    public static extern IntPtr GetModuleHandle(string lpModuleName);

    int hhook;// хэндл созданой нами ловушки
    int HookProc(int code, int wParam, IntPtr lParam)
    {
        // для доступа к битам
        BitArray bt = new BitArray(BitConverter.GetBytes((uint)lParam));
        KeyEventArgs arg = new KeyEventArgs((Keys)wParam);
        //Если клавиша нажата - вызвать KeyDown, если отпущена - вызвать KeyUp
        if (bt[30]) { 
                      if (KeyUp != null) KeyUp(this, arg)
                    } 
                    else 
                    if (KeyDown != null) KeyDown(this, arg);
        // Вызов следующей ловушки в очереди
        KeyHookDecl.CallNextHookEx(0, code, wParam, lParam);
        return 1;
    }
    public KeyHook()
    {
        // Создание ловушки
        hhook = KeyHookDecl.SetWindowsHookEx(KeyHookDecl.WH_KEYBOARD,
        new KeyHookDecl.HookProc(HookProc),
        GetModuleHandle(null),
        AppDomain.GetCurrentThreadId());
    }

    ~KeyHook()
    {
        KeyHookDecl.UnhookWindowsHookEx(hhook);
    }

}






2 комментария:

  1. Спасибо, за статью.
    Самое странное, что вроде должна ловушка срабатывать всегда, а она работает только если в фокусе форма, в случае когда фокусировка переходит на другой объект, ловушка не срабатывает.

    ОтветитьУдалить