Title: Keyboard wide system hook
Question: Capturing keys in all Windows
Answer:
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:
liWe make DLL with two functions that we will export, one to install the hook and another for ununstall it.
liThere 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.
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_READ,False,'ElReceptor');
{Si no existe, no enviamos nada a la aplicacion receptora}
{If dont, send nothing to receiver application}
if FicheroM0 then
begin
PReceptor:=MapViewOfFile(FicheroM,FILE_MAP_READ,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.
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
liMake a new empty application
liPut a TMemo (Memo1) in the form
liChange 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 HandleDLL0 then
FreeLibrary(HandleDLL);
{Cerramos la vista del fichero y el fichero}
{Close the memfile and the View}
if FicheroM0 then
begin
UnmapViewOfFile(PReceptor);
CloseHandle(FicheroM);
end;
end;
end.
liRecord 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');
liOnce 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');
liNow, 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);
liOnce 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;
liWell, 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.
liLet 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 FicheroM0 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 HandleDLL0 then
FreeLibrary(HandleDLL);
And the file:
{Cerramos la vista del fichero y el fichero}
{Close the memfile and the View}
if FicheroM0 then
begin
UnmapViewOfFile(PReceptor);
CloseHandle(FicheroM);
end;
end;