Capturando eventos del teclado con las API de Windows y C#

Puedes bajarte el código desde este link KeyInterceptor.cs

Introducción

Muchas veces podemos estar interesados en escuchas las teclas que se presionan independientemente de la aplicación activa. Esto es especialmente útil cuando queremos que una aplicación que corre en segundo plano reaccione ante una cierta combinación de teclas. Ejemplo de este tipo de aplicaciones son los capturadores de pantalla. Los key loggers y los bloqueadores (o filtros de teclas) son otros tipos de aplicaciones de esta familia. Ahora vamos a ver como se hacen este tipo de aplicaciones que necesitan hechar mano a las APIs de Windows en .Net, en esta oportunidad, con C#.

Emulando el esquema de manejo de teclado de .Net

El Framework .Net nos permite manejar las teclas mediante dos EventHandlers (delegados):

Lo que haremos será crear una clase KeyboardInterceptor que brinde estos delegados para responder ante los tres eventos KeyDown, KeyPress y KeyUp.

Declarando las funciones del API

Lo primero que hacemos es declarar una clase interna llamada WinApi que contiene todas las definiciones de funciones importadas, estructuras y constantes que necesitaremos para manejar el teclado a bajo nivel. Estas definiciones se encuentran en www.pinvoke.net. También es bueno recomendar aquí el plugin de pinvoke para Visual Studio el cual nos permite buscar e insertar las firmas de estas funciones.

        #region Internal Structures and delegates definition Region
        /// <summary>
        /// The KBDLLHOOKSTRUCT structure contains information about a low-level 
        /// keyboard input event. 
        /// See: http://msdn.microsoft.com/en-us/library/ms644967(VS.85).aspx
        /// </summary>
        internal struct KBDLLHOOKSTRUCT
        {
            public int vkCode;      // Specifies a virtual-key code. The code must  
                                    // be avalue in the range 1 to 254. 
            public int scanCode;    // Specifies a hardware scan code for the key.
            public int flags;       
            public int time;
            public int dwExtraInfo;
        }

        /// <summary>
        /// The LowLevelKeyboardProc hook procedure is an application-defined 
        /// or library-defined callback function used with the SetWindowsHookEx 
        /// function. The system calls this function every time a new keyboard 
        /// input event is about to be posted into a thread input queue.
        /// See: http://msdn.microsoft.com/en-us/library/ms644985(VS.85).aspx
        /// </summary>
        internal delegate IntPtr LowLevelKeyboardProc(
            int nCode, IntPtr wParam, ref KBDLLHOOKSTRUCT lParam);
        #endregion

        #region Internal WinApi class (Win32 api functions)
        /// <summary>
        /// This class define the WinApi signatures.
        /// See: PInvoke web site
        /// </summary>
        [ComVisibleAttribute(false),
         System.Security.SuppressUnmanagedCodeSecurity()]
        internal class WinApi
        {
            #region Constants
            // Constants
            // Windows NT/2000/XP: Installs a hook procedure that monitors low-level 
            // keyboard input events. For more information, see the 
            // LowLevelKeyboardProc hook procedure.
            internal const int WH_KEYBOARD_LL = 13;
            internal const int WM_KEYDOWN = 0x0100;
            internal const int WM_SYSKEYDOWN = 0x104;
            internal const int WM_KEYUP = 0x101;
            internal const int WM_SYSKEYUP = 0x105;

            //Modifier key constants
            internal const int VK_SHIFT = 0x010;
            internal const int VK_CONTROL = 0x011;
            internal const int VK_MENU = 0x012;
            internal const int VK_CAPITAL = 0x014;
            #endregion

            #region DLL Imports Region
            /// <summary>
            /// The SetWindowsHookEx function installs an application-defined hook 
            /// procedure into a hook chain. You would install a hook procedure to 
            /// monitor the system for certain types of events. These events are 
            /// associated either with a specific thread or with all threads in the 
            /// same desktop as the calling thread.
            /// See: http://msdn.microsoft.com/en-us/library/ms644990(VS.85).aspx
            /// </summary>
            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            internal static extern IntPtr SetWindowsHookEx(int idHook,
                LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);

            /// <summary>
            /// The UnhookWindowsHookEx function removes a hook procedure installed 
            /// in a hook chain by the SetWindowsHookEx function. 
            /// See: http://msdn.microsoft.com/en-us/library/ms644993(VS.85).aspx
            /// </summary>
            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            internal static extern bool UnhookWindowsHookEx(IntPtr hhk);

            /// <summary>
            /// The CallNextHookEx function passes the hook information to the next 
            /// hook procedure in the current hook chain. A hook procedure can call 
            /// this function either before or after processing the hook information.
            /// See: http://msdn.microsoft.com/en-us/library/ms644974(VS.85).aspx
            /// </summary>
            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            internal static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
                IntPtr wParam, ref KBDLLHOOKSTRUCT lParam);

            /// <summary>
            /// Retrieves a module handle for the specified module. The module must
            /// have been loaded by the calling process.
            /// See: http://msdn.microsoft.com/en-us/library/ms683199(VS.85).aspx
            /// </summary>
            [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            internal static extern IntPtr GetModuleHandle(string lpModuleName);

            /// <summary>
            /// The GetKeyboardState function copies the status of the 256 virtual 
            /// keys to the specified buffer. 
            /// </summary>
            [DllImport("user32")]
            internal static extern int GetKeyboardState(byte[] pbKeyState);

            /// <summary>
            /// The ToAscii function translates the specified virtual-key code and 
            /// keyboard state to the corresponding character or characters. The 
            /// function translates the code using the input language and physical 
            /// keyboard layout identified by the keyboard layout handle.
            /// </summary>
            [DllImport("user32")]
            internal static extern int ToAscii(int uVirtKey, int uScanCode, 
                byte[] lpbKeyState, byte[] lpwTransKey, int fuState);
            #endregion
        }
        #endregion

Introduciendo nuestro Handler en la cadena de handler de Windows

Lo que hay que hacer es indicarle a Windows que cuando se presione una tecla invoque a un método en nuestro código. Esto es lo que estamos haciendo en el constructor de la clase KeyboardInterceptor cunado invocamos a la función WinApi.SetWindowsHookEx() en la que indicamos que cuando se presione una tecla invoque a nuestro método HookCallback.

Luego en este método, en el HookCallback, tratamos cada evento según el comando del que se trate (WM_KEYDOWN, WM_SYSKEYDOWN, WM_KEYUP, WM_SYSKEYUP) y una vez hecho esto pasamos el mensaje al próximo handler en la cadena de handlers subscriptos al evento del teclado a bajo nivel(WH_KEYBOARD_LL) mediante la llamada a la función WinApi.CallNextHookEx(). Para hacer esto, previamente debimos salvar el identificador del handler en la variable hookId.

    /// <summary>
    /// </summary>
    class KeyboardInterceptor : IDisposable 
    {

        #region Private Fields
        private LowLevelKeyboardProc proc;
        private IntPtr hookId = IntPtr.Zero;
        #endregion

        #region Public Constructor
        public KeyboardInterceptor()
        {
            proc = new LowLevelKeyboardProc( HookCallback );
            using (Process curProcess = Process.GetCurrentProcess())
            using (ProcessModule curModule = curProcess.MainModule)
            {
                hookId = WinApi.SetWindowsHookEx(WinApi.WH_KEYBOARD_LL, proc,
                    WinApi.GetModuleHandle(curModule.ModuleName), 0);
            }
        }
        #endregion

        #region The callback method invoked when keys are pressed
        private IntPtr HookCallback(
            int nCode, IntPtr wParam, ref KBDLLHOOKSTRUCT lParam)
        {
            
            int command = (int)wParam;
            bool handled = false;

            if (nCode >= 0)
            {
                if (KeyDown != null && (command == WinApi.WM_KEYDOWN || command == WinApi.WM_SYSKEYDOWN))
                {
                }

                if (KeyPress != null && command == WinApi.WM_KEYDOWN)
                {
                }

                if (KeyUp != null && (command == WinApi.WM_KEYUP || command == WinApi.WM_SYSKEYUP))
                {
                }

            }

            return WinApi.CallNextHookEx(hookId, nCode, wParam, ref lParam);
        }
        #endregion


        #region IDisposable Members
        /// <summary>
        /// Releases the keyboard hook.
        /// </summary>
        public void Dispose()
        {
            WinApi.UnhookWindowsHookEx(hookId);
        }
        #endregion
    }

Posibilitando engancharse y manejar los eventos

Como ya dijimos en la introducción,  implementaremos los tres eventos KeyDown, KeyPress y KeyUp para que una aplicación pueda subscribirse a estos y manejarlos. Lo primero es crear los eventos como sigue:

        #region Public Properties
        public event KeyPressEventHandler KeyPress;
        public event KeyEventHandler KeyUp;
        public event KeyEventHandler KeyDown;
        #endregion

y luego, en el método HookCallback, lanzarlos según corresponda.

Dado que el KeyPressEventArg recibe un caracter como parámetro en su constructor, lo que debemos hacer es convertir, o mejor dicho pedirle a windows que partiendo de la virtual key presionada, nos diga que caracter ascii corresponde a la tecla presionada.

    if (KeyDown != null && (command == WinApi.WM_KEYDOWN || command == WinApi.WM_SYSKEYDOWN))
     {
          KeyDown(this, new KeyEventArgs((Keys)lParam.vkCode));
     }
     if (KeyPress != null && command == WinApi.WM_KEYDOWN)
     {
          shift = ((Keys.Shift & Control.ModifierKeys) == Keys.Shift);
          caps = ((Keys.Capital & Control.ModifierKeys) == Keys.Capital);

          byte[] keyState = new byte[256];
          WinApi.GetKeyboardState(keyState);
          byte[] inBuffer = new byte[2];
          if (WinApi.ToAscii(lParam.vkCode, lParam.scanCode, keyState, inBuffer, lParam.flags) == 1)
          {
                char key = (char)inBuffer[0];
                if ((caps ^ shift) && Char.IsLetter(key)) key = Char.ToUpper(key);

                KeyPressEventArgs e = new KeyPressEventArgs(key);
                KeyPress(this, e);
          }
     }
     if (KeyUp != null && (command == WinApi.WM_KEYUP || command == WinApi.WM_SYSKEYUP))
     {
          KeyUp(this, new KeyEventArgs((Keys)lParam.vkCode));
     }

Utilizando esta clase

Primero hay que crear una instancia de la clase que hemos llamado KeyInterceptor. Esto lo hacemos utilizando un bloque using para que cuando se sale de allí se libere el hook gracias a que hemos hecho a nuestra clase Disposable y en justamente en el método Dispose que liberamos el hook. Esto es algo muy importante.

En este caso declaramos la instancia como static y pública para poder referenciarla desde un formulario que llamamos KeyViwerForm.

    static class MainApp
    {
        public static KeyboardInterceptor ki;

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            using (ki = new KeyboardInterceptor())
            {
                Application.Run(new KeyViewerForm());
            }
        }
    }

En el formulario KeyViewerForm, más precisamente en su constructor, nos subscribimos a los tres eventos y codificamos los manejadores.

  #region Key Events Management
  private void Form1_Load(object sender, EventArgs e)
  {
        MainApp.ki.KeyPress += new KeyPressEventHandler(kh_KeyPress);
        MainApp.ki.KeyDown += new KeyEventHandler(ki_KeyDown);
        MainApp.ki.KeyUp += new KeyEventHandler(ki_KeyUp);
  }

  private void kh_KeyPress(object sender, KeyPressEventArgs e)
  {
    // put here code for key press 
  }

  private void ki_KeyDown(object sender, KeyEventArgs e)
  {
    // put here code for key down 
  }

  private void ki_KeyUp(object sender, KeyEventArgs e)
  {
    // put here code for key up 
  }
  #endregion

Bien, eso es todo.


Ir al principio Inicio del artículo

Atículos publicados por Lucas E. Ontivero Autor: Lucas E. Ontivero - 11/10/2008
      Tags:




Comentarios:
Hola gracias por tu excelente artículo. Por favor necesito tu ayuda para enviar pulsaciones de teclas pero a una APLICACION EN PANTALLA COMPLETA(un juego) no a una ventana que esto es fácil pero a una aplicación en pantalla completa simplemente me ignora los timers que envian las pulsaciones, por favor si pudieras ayudarme, gracias
Publicado por: jorge
Hola Jorge, no sé si te endiendo del todo bien por loo que voy a tratar de responder según lo que entiendo:

1.- Si el juego no es tuyo, y es una aplicación Win32 (pantalla completa o no), podes enviarle cualquier mensaje que quieras mediente SendMessage() [http://msdn.microsoft.com/en-us/library/ms644950.aspx]. Mira un poco los resultados de esta busqueda [http://www.google.com.ar/search?hl=es&client=firefox-a&rls=org.mozilla%3Aes-ES%3Aofficial&hs=xjs&q=send+key+sendmessage&btnG=Buscar&meta=]

2.- Si el juego es tuyo (lo estas desarrollando vos), podes usar el código de este artículo.

Suerte.
Moderador Publicado por: Lucas E. Ontivero
Hola soy Jorge muchas gracias por responderme Lucas...voy a investigarlo
Publicado por: jorge
Hola Lucas y Feliz Año…Lo que necesito es enviar pulsaciones de teclas a un juego que funciona en pantalla completa: San Andreas, Need for Speed,etc así por ejemplo podria al pulsar una tecla para San Andreas (al digitar “hesoyam” en el teclado, San Andreas saca un pack de armas) enviar una cadena que genere un truco de juego o talvez si pongo en un timer que se mande la flecha ABAJO cada 10 segundos y estoy en el menú del juego debería bajarse el foco cada 10 segundos!.. porque si yo aplasto la flecha abajo en el teclado eso es lo que hace. Una verdadera simulación de teclas no deberia poder hacer esto? E intentado con SendMessage, keydevent, sendkeys, e investigado hooks pero nada, este problema es parte de un proyecto importante para mi, cualquier orientación muchas gracias.
Publicado por: jorge

Para poder comentar el artículo es necesario que te registres como usuario.
Si aún no eres un usuario registrado puedes hacerlo por medio del siguiente link.

Si ya eres un usuario registrado simplemente autentifícate para poder comentar el artículo.
Para autentificarte como usuario puedes hacerlo por medio del siguiente link.