Hooks: Capture keys in all Windows applications  

Send By: Radikal (Q3 Team)
Web : http://www.q3.nu
Email: radikal@q3.nu
Date: 16/01/04

Tip accessed 2758 times

 


A lot of people ask me about the possibility that our application Delphi captures the user's keystrokes, although the user doesn't make them our active application being.
Of course... the first thing that we make is to give a turn for the event OnKeyPress of the form, and of course, without obtaining positive results, even putting the property KeyPreview from the form to true...
This happens because our application will only receive messages of the keystrokes when is it who has the focus.
The following step to solve this question is fighting with the keyboard hooks.
A Hook it is not more than a mechanism that will allow us to spy the traffic of messages between Windows and the applications.

To install a hook in our application is something relatively simple, but of course, if we install it in our application, we will only spy the messages that Windows sent to our application, so neither we will have solved the problem.

Then... Which is the solution?. The solution is to install a Hook but at system level, that is to say, a hook that captures all the messages that circulate toward Windows.

Installing a hook at system level has a great added complication that is the fact that the function to the one that calls the hook it must be contained in a DLL, not in our Delphi application.
This condition, will force us, in the first place to build us a DLL, and in second place to to build us some invention to communicate the DLL with our application.

In this trick you have an example of keyboard capture by means of a keyboard Hook to system level.
The example consists of two projects, one for the DLL and another for the example application.

The operation is the following one:
  • We make DLL with two functions that we will export, one to install the hook and another for ununstall it.
  • There is a third function that is the one that will execute the hook once installed (CallBack). In her, that will make it is to send the data of the message captured to our application.

    The DLL should know in all moment the handle of the receiver application, so we will make him to read it of a memory mapped file that we will create from the own application.
    You have an example of use of memory mapped files in the trick:

    [381] - Share data between two Delphi applications

    We will send the data from the DLL to the application through user's message. You have other tricks in where this technique is also used, for example:

    [162] - Prevent double instance of your application

    Well, let's go with the example:


    DLL that installs the Hook:


  • Make the skeleton of a DLL (File - New - DLL)
  • Change the code of the project for this another:


     library Project1;
    
     {
     Demo de Hook de teclado a nivel de sistema, Radikal.
     Como lo que queremos es capturar las teclas pulsadas en cualquier parte
     de Windows, necesitamos instalar la funcion CallBack a la que llamará
     el Hook en una DLL, que es ésta misma.
     }
    
     uses Windows,
       Messages;
    
     const
      CM_MANDA_TECLA = WM_USER + $1000;
    
     var
      HookDeTeclado     : HHook;
      FicheroM    : THandle;
      PReceptor   : ^Integer;
    
     function CallBackDelHook( Code    : Integer;
                               wParam  : WPARAM;
                               lParam  : LPARAM
                               )       : LRESULT; stdcall;
    
     {Esta es la funcion CallBack a la cual llamará el hook.}
     {This is the CallBack function called by he Hook}
     begin
      {Si una tecla fue pulsada o liberada}
      {if a key was pressed/released}
      if code=HC_ACTION then
      begin
       {Miramos si existe el fichero}
       {if the mapfile exists}
       FicheroM:=OpenFileMapping(FILE_MAP_WRITE,False,'ElReceptor');
       {Si no existe, no enviamos nada a la aplicacion receptora}
       {If dont, send nothing to receiver application}
       if FicheroM<>0 then
       begin
         PReceptor:=MapViewOfFile(FicheroM,FILE_MAP_WRITE,0,0,0);
         PostMessage(PReceptor^,CM_MANDA_TECLA,wParam,lParam);
         UnmapViewOfFile(PReceptor);
         CloseHandle(FicheroM);
       end;
      end;
      {Llamamos al siguiente hook de teclado de la cadena}
      {call to next hook of the chain}
      Result := CallNextHookEx(HookDeTeclado, Code, wParam, lParam)
     end;
    
     procedure HookOn; stdcall;
     {Procedure que instala el hook}
     {procedure for install the hook}
     begin
       HookDeTeclado:=SetWindowsHookEx(WH_KEYBOARD, @CallBackDelHook, HInstance , 0);
     end;
    
     procedure HookOff;  stdcall;
     begin
     {procedure para desinstalar el hook}
     {procedure to uninstall the hook}
       UnhookWindowsHookEx(HookDeTeclado);
     end;
    
     exports
     {Exportamos las procedures...}
     {Export the procedures}
      HookOn,
      HookOff;
    
     begin
     end.
    



    If you has pasted the code into the unit, don't forget assign the form's event OnCreate and OnDestroy in the Object Inspector

    Now record the project with the name: ' HookTeclado.dpr' and compile it (Project - Build All), and you will have generated the DLL of the project.


    Receiver application


  • Make a new empty application
  • Put a TMemo (Memo1) in the form
  • Change the form's unit code by this other:

     unit Unit1;
    
     interface
    
     uses
       Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
       StdCtrls;
    
     const
       NombreDLL       = 'HookTeclado.dll';
       CM_MANDA_TECLA  = WM_USER + $1000;
    
    
     type
       THookTeclado=procedure; stdcall;
    
     type
       TForm1 = class(TForm)
         Label1: TLabel;
         Memo1: TMemo;
         Button1: TButton;
         procedure FormCreate(Sender: TObject);
         procedure FormDestroy(Sender: TObject);
       private
         { Private declarations }
         FicheroM       : THandle;
         PReceptor      : ^Integer;
         HandleDLL      : THandle;
         HookOn,
         HookOff        : THookTeclado;
    
         procedure LlegaDelHook(var message: TMessage); message  CM_MANDA_TECLA;
       public
         { Public declarations }
       end;
    
     var
       Form1: TForm1;
    
     implementation
    
     {$R *.DFM}
    
     procedure TForm1.FormCreate(Sender: TObject);
     begin
       {No queremos que el Memo maneje el teclado...}
       {We dont want that the memo read the keyboard...}
       Memo1.ReadOnly:=TRUE;
    
       HandleDLL:=LoadLibrary( PChar(ExtractFilePath(Application.Exename)+
                                     NombreDLL ) );
       if HandleDLL = 0 then raise Exception.Create('No se pudo cargar la DLL');
    
       @HookOn :=GetProcAddress(HandleDLL, 'HookOn');
       @HookOff:=GetProcAddress(HandleDLL, 'HookOff');
    
       IF not assigned(HookOn) or
          not assigned(HookOff)  then
          raise Exception.Create('No se encontraron las funciones en la DLL'+#13+
                                 'Cannot find the required DLL functions');
    
       {Creamos el fichero de memoria}
       FicheroM:=CreateFileMapping( $FFFFFFFF,
                                   nil,
                                   PAGE_READWRITE,
                                   0,
                                   SizeOf(Integer),
                                   'ElReceptor');
    
        {Si no se creó el fichero, error}
        if FicheroM=0 then
          raise Exception.Create( 'Error al crear el fichero'+
                                  '/Error while create file');
    
        {Direccionamos nuestra estructura al fichero de memoria}
        PReceptor:=MapViewOfFile(FicheroM,FILE_MAP_WRITE,0,0,0);
    
        {Escribimos datos en el fichero de memoria}
        PReceptor^:=Handle;
        HookOn;
     end;
    
     procedure TForm1.LlegaDelHook(var message: TMessage);
     var
        NombreTecla : array[0..100] of char;
        Accion      : string;
     begin
       {Traducimos de Virtual key Code a TEXTO}
       {Virtual key code to Key Name}
       GetKeyNameText(Message.LParam,@NombreTecla,100);
    
       {Miramos si la tecla fué pulsada, soltada o repetida}
       {Look if the key was pressed, released o re-pressed}
       if ((Message.lParam shr 31) and 1)=1
           then Accion:='Soltada' {Released}
       else
       if ((Message.lParam shr 30) and 1)=1
           then Accion:='Repetida' {repressed}
           else Accion:='Pulsada'; {pressed}
    
       Memo1.Lines.Append( Accion+
                           ' tecla: '+
                           String(NombreTecla) );
     end;
    
     procedure TForm1.FormDestroy(Sender: TObject);
     begin
      {Desactivamos el Hook}
      {Uninstall the Hook}
      if Assigned(HookOff) then HookOff;
    
      {Liberamos la DLL}
      {Free the DLL}
      if HandleDLL<>0 then
       FreeLibrary(HandleDLL);
    
      {Cerramos la vista del fichero y el fichero}
      {Close the memfile and the View}
      if FicheroM<>0 then
      begin
        UnmapViewOfFile(PReceptor);
        CloseHandle(FicheroM);
      end;
    
     end;
    
     end.
    



  • Record the project in the same directory of the project of the DLL and compile the application.

    If you have followed the steps until here, you will have in the directory of the two projects a DLL (HookTeclado.DLL) and the executable of the receiver application.
    Execute it, and you will see in the Memo1 all the keys pressed in Windows.

    If you only wanted an example that works, it is not necessary that you continue reading. If you want to know a little more than like the invention works... so... here you have it, step to step:

    We go starting from the event OnCreate of the application:

    First, we put the Memo1 to readonly. Imagine for what reason, or better, it proves to not putting it, to see that it happens...:)


     procedure TForm1.FormCreate(Sender: TObject);
     begin
       {No queremos que el Memo maneje el teclado...}
       {We dont want that the memo read the keyboard...}
       Memo1.ReadOnly:=TRUE;
    


    Now we load the DLL that we will suppose that it will be in the same directory that our executable one. If there was some problem when loading it, we generate an exception, in such a way that the following code would not be executed.

       HandleDLL:=LoadLibrary( PChar(ExtractFilePath(Application.Exename)+
                                     NombreDLL ) );
       if HandleDLL = 0 then raise Exception.Create('No se pudo cargar la DLL');
    


  • Once loaded the DLL, we look for the two functions that they should be in it. If they are not... we generate an exception.

       @HookOn :=GetProcAddress(HandleDLL, 'HookOn');
       @HookOff:=GetProcAddress(HandleDLL, 'HookOff');
    
       IF not assigned(HookOn) or
          not assigned(HookOff)  then
          raise Exception.Create('No se encontraron las funciones en la DLL'+#13+
                                 'Cannot find the required DLL functions');
    


  • Now, we make a memory mapped file, which will use to keep the handle of our form, the DLL will taste this way like who must send him the message with the key that has been pressed just reading this file.

       {Creamos el fichero de memoria}
       FicheroM:=CreateFileMapping( $FFFFFFFF,
                                   nil,
                                   PAGE_READWRITE,
                                   0,
                                   SizeOf(Integer),
                                   'ElReceptor');
    
        {Si no se creó el fichero, error}
        if FicheroM=0 then
          raise Exception.Create( 'Error al crear el fichero'+
                                  '/Error while create file');
    
        {Direccionamos nuestra estructura al fichero de memoria}
        PReceptor:=MapViewOfFile(FicheroM,FILE_MAP_WRITE,0,0,0);
    


  • Once we have the memory mapped file, and a view pointing to it, we record the handle of the form in it, and we activate the Hook, calling to the procedure HookOn of the DLL:

        {Escribimos datos en el fichero de memoria}
        PReceptor^:=Handle;
        HookOn;
     end;
    


  • Well, now see that it happens in our DLL when calling to the function HookOn:

     procedure HookOn; stdcall;
     {Procedure que instala el hook}
     {procedure for install the hook}
     begin
       HookDeTeclado:=SetWindowsHookEx(WH_KEYBOARD, @CallBackDelHook, HInstance , 0);
     end;
    



    As you see, there is not more than a call to SetWindowsHookEx, to install a hook at system level (0 in the last parameter) that will execute the function CallBackDelHook with each message that it captures.

  • Let us see that it makes the function CallBackDelHook when it is executed by the hook:

    First, it checks that the function has been called by a new keyboard event, by means of the if code=HC_ACTION.

     function CallBackDelHook( Code    : Integer;
                               wParam  : WPARAM;
                               lParam  : LPARAM
                               )       : LRESULT; stdcall;
    
     {Esta es la funcion CallBack a la cual llamará el hook.}
     {This is the CallBack function called by he Hook}
     begin
      {Si una tecla fue pulsada o liberada}
      {if a key was pressed/released}
      if code=HC_ACTION then
      begin
    


    If it is this way, that is to say that is a new keyboard event that it is necessary to assist... the first thing that we should make is so to look for the handle from the application to which should send the message with the data of the pressed/released key, which have kept by heart in a file from the application, we try to open the file, and to read this handle, and if everything goes well, we send the message by means of a PostMessage:

       {Miramos si existe el fichero}
       {if the mapfile exists}
       FicheroM:=OpenFileMapping(FILE_MAP_READ,False,'ElReceptor');
       {Si no existe, no enviamos nada a la aplicacion receptora}
       {If dont, send nothing to receiver application}
       if FicheroM<>0 then
       begin
         PReceptor:=MapViewOfFile(FicheroM,FILE_MAP_READ,0,0,0);
         PostMessage(PReceptor^,CM_MANDA_TECLA,wParam,lParam);
    


    once sent the message, we free the file:

         UnmapViewOfFile(PReceptor);
         CloseHandle(FicheroM);
       end;
      end;
    


    later, should call to next hook:

      {Llamamos al siguiente hook de teclado de la cadena}
      {call to next hook of the chain}
      Result := CallNextHookEx(HookDeTeclado, Code, wParam, lParam)
     end;
    


    Well, do we have installed a hook that captures the keyboard events and does it forward them to our application... which the following step is?, of course... to make something to receive it...
    We will have to capture user's message that we have been defined:

     const
      CM_MANDA_TECLA = WM_USER + $1000;
    


    that which we will get adding this line in the private part of the form:

     procedure LlegaDelHook(var message: TMessage); message  CM_MANDA_TECLA;
    


    and of course, the corresponding procedure in the implementation part:

     procedure TForm1.LlegaDelHook(var message: TMessage);
     var
        NombreTecla : array[0..100] of char;
        Accion      : string;
     begin
       {Traducimos de Virtual key Code a TEXTO}
       {Virtual key code to Key Name}
       GetKeyNameText(Message.LParam,@NombreTecla,100);
    
       {Miramos si la tecla fué pulsada, soltada o repetida}
       {Look if the key was pressed, released o re-pressed}
       if ((Message.lParam shr 31) and 1)=1
           then Accion:='Soltada' {Released}
       else
       if ((Message.lParam shr 30) and 1)=1
           then Accion:='Repetida' {repressed}
           else Accion:='Pulsada'; {pressed}
    
       Memo1.Lines.Append( Accion+
                           ' tecla: '+
                           String(NombreTecla) );
     end;
    



    In this example, I simply translate the data of the pressed/released key, translating it to its key name and adding it to the TMemo.
    If you want more information on the parameters than the function will receive, revise the help file Win32.hlp looking for the topic ' KeyboardProc'.
    There you will see the meaning of the parameters wParam and lParam that you will receive in the function.

    For I finish, we have left to undo this whole invention when we leave the application. The OnDestroy event of the application:

    First, uninstall the hook, calling to the HookOff function of the DLL. Care, is to use the if Assigned, because if there has been some problem when loading the DLL in the OnCreate... now we would try to execute something that was not initialized.


     procedure TForm1.FormDestroy(Sender: TObject);
     begin
      {Desactivamos el Hook}
      {Uninstall the Hook}
      if Assigned(HookOff) then HookOff;
    



    Now, free the DLL:


      {Liberamos la DLL}
      {Free the DLL}
      if HandleDLL<>0 then
       FreeLibrary(HandleDLL);
    



    And the file:


      {Cerramos la vista del fichero y el fichero}
      {Close the memfile and the View}
      if FicheroM<>0 then
      begin
        UnmapViewOfFile(PReceptor);
        CloseHandle(FicheroM);
      end;
     end;
    




    Updated at 06/06/2003 (Assign the events)