The Unofficial Newsletter of Delphi Users - by Robert Vivrette




Hooking Into the VCL

by Clinton Johnson - xepol@poboxes.com

This represents an extension of a concept shown by Grahame Marsh earlier in UNDU (see There's Method to My Hook Madness!).  I've extended his code, and created VCL non-visual wrappers for each possible system hook.  I'm not going to go into much detail about what I've done this time, I'm going to let people go through the source for that.

Please remember that hooks can impact system performance greatly, particularily when you create a system hook instead of an application hook.  On that note, I would like to draw your attention to the ACTIVE property.  Just because the component exists, does not mean that it has a hook into the system.  The hook is dynamically created as needed based on the active property, this should make the components a little easier to use and manage.

Enjoy...

unit hooks;

interface

Uses Windows,SysUtils,Classes,Controls;

{$IFNDEF VER100} THIS CODE WAS WRITTEN FOR DELPHI 3, YOU SHOULD NOT USE IT WITH ANY OTHER VERSION{$ENDIF}

//*****************************************************************************
//
// Hooks Unit - Written For Delphi 3 By Clinton R. Johnson, using code written by
//              by Grahame Marsh - gsmarsh@aol.com, in an article for UNDU.
//
// Written April 5, 1998 - The code written by Clinton R. Johnson is donated to
// the public domain.  Code written by Grahame Marsh may have additional
// restrictions.
//
// Not all portions of this code has been tested in all circumstances.  Use at
// your own discression, it comes with no warrenty express or implied.
//
// Grahame Marsh is responsible for the MakeHookInstance and FreeHookInstance
// code (which is based on the MakeObjectInstance code in the Forms.pas unit
// from Borland if you want further examples).  His goal was to simplify the
// creation of Hooks in Delphi.  He acheived his goal, but I figured it could
// be even simpler.  No housekeeping tasks involved, no need to issue the low
// level calls to stay in the chain, etc.  Delphi is after all a visual enviroment,
// so I dislike writing code where I can just drop a component.  The only change
// I've made to his code was to rename the THOOKCALL structure to THOOKMSG, I
// found his name too confusing & kept mixing it up with the call instance itself.
//
// If you investigate this code, the most power is exposed once you get to the
// code level instead of visual implementation, but that's the way it should be
// in my opinion - multiple levels of complexity, this keeps the true novice from
// playing with things before they at least learn how to winow out & use the
// most powerful units.
//
// I've created a basic non-visual component to host the hook, and then created
// descendant objects for each hook.  I've not created any special messages
// structures for each handle, and I've not done any of the low level handing
// recommended.  I've left that up to you, to implement as you see best.
//
//*****************************************************************************

Type
  THookMsg = Packed record
               Code   : integer;
               WParam : WPARAM;
               LParam : LPARAM;
               Result : LResult
             end;

Type
  THook = Class;
  THookMethod = procedure (var HookMsg: THookMsg) of object;
  THookNotify = procedure (Hook : THook; var Hookmsg: THookMsg) of object;

  THook = Class(TComponent)
  Private
    fHook               : hHook;
    fHookProc           : Pointer;
    fOnPreExecute       : THookNotify;
    fOnPostExecute      : THookNotify;
    fActive             : Boolean;
    fLoadedActive       : Boolean;

    fThreadID           : Integer;

    Procedure SetActive(NewState : Boolean);
    Procedure SetThreadID(NewID : INteger);
    Procedure HookProc(Var HookMsg : THookMsg);
  Protected
    Procedure PreExecute(Var HookMsg : THookMsg; Var Handled : Boolean); Virtual;
    Procedure PostExecute(Var HookMsg : THookMsg); Virtual;
    Function AllocateHook : hHook; Virtual; Abstract;
    Procedure Loaded; Override;
  Public
    Constructor Create(Owner : TComponent); Override;
    Destructor Destroy; Override;
    Property ThreadID : Integer Read fThreadID Write SetThreadID Stored False;
    Property Active : Boolean Read fActive Write SetActive;
    Property OnPreExecute : THookNotify  Read fOnPreExecute Write fOnPreExecute;
    Property OnPostExecute : THookNotify  Read fOnPostExecute Write fOnPostExecute;
  Published
  End;

Type
  TCallWndProcHook = Class(THook)
  Private
  Protected
  Public
    Function AllocateHook : hHook; Override;
  Published
    Property Active;
    Property OnPreExecute;
    Property OnPostExecute;
  End;

Type
  TCallWndProcRetHook = Class(THook)
  Private
  Protected
  Public
    Function AllocateHook : hHook; Override;
  Published
    Property Active;
    Property OnPreExecute;
    Property OnPostExecute;
  End;

Type
  TCBTHook = Class(THook)
  Private
  Protected
  Public
    Function AllocateHook : hHook; Override;
  Published
    Property Active;
    Property OnPreExecute;
    Property OnPostExecute;
  End;

Type
  TDebugHook = Class(THook)
  Private
  Protected
  Public
    Function AllocateHook : hHook; Override;
  Published
    Property Active;
    Property OnPreExecute;
    Property OnPostExecute;
  End;

Type
  TGetMessageHook = Class(THook)
  Private
  Protected
  Public
    Function AllocateHook : hHook; Override;
  Published
    Property Active;
    Property OnPreExecute;
    Property OnPostExecute;
  End;

Type
  TJournalPlaybackHook = Class(THook)
  Private
  Protected
  Public
    Function AllocateHook : hHook; Override;
  Published
    Property Active;
    Property OnPreExecute;
    Property OnPostExecute;
  End;

Type
  TJournalRecordHook = Class(THook)
  Private
  Protected
  Public
    Function AllocateHook : hHook; Override;
  Published
    Property Active;
    Property OnPreExecute;
    Property OnPostExecute;
  End;

Type
  TKeyboardHook = Class(THook)
  Private
  Protected
  Public
    Function AllocateHook : hHook; Override;
  Published
    Property Active;
    Property OnPreExecute;
    Property OnPostExecute;
  End;

Type
  TMouseHook = Class(THook)
  Private
  Protected
  Public
    Function AllocateHook : hHook; Override;
  Published
    Property Active;
    Property OnPreExecute;
    Property OnPostExecute;
  End;

Type
  TMsgHook = Class(THook)
  Private
  Protected
  Public
    Function AllocateHook : hHook; Override;
  Published
    Property Active;
    Property OnPreExecute;
    Property OnPostExecute;
  End;

Type
  TShellHook = Class(THook)
  Private
  Protected
  Public
    Function AllocateHook : hHook; Override;
  Published
    Property Active;
    Property OnPreExecute;
    Property OnPostExecute;
  End;

Type
  TSysMsgHook = Class(THook)
  Private
  Protected
  Public
    Function AllocateHook : hHook; Override;
  Published
    Property Active;
    Property OnPreExecute;
    Property OnPostExecute;
  End;

function  MakeHookInstance (Method: THookMethod): pointer;
procedure FreeHookInstance (ObjectInstance: pointer);

Procedure Register;

implementation

const
  InstanceCount = 313;  // set so that sizeof (TInstanceBlock) < PageSize

type
  PObjectInstance = ^TObjectInstance;
  TObjectInstance = packed record
                      Code: Byte;
                      Offset: Integer;
                    case Integer of
                      0: (Next: PObjectInstance);
                      1: (Method: THookMethod);
                    end;
Type
  PInstanceBlock = ^TInstanceBlock;
  TInstanceBlock = packed record
                     Next: PInstanceBlock;
                     Code: array[1..2] of Byte;
                     WndProcPtr: Pointer;
                     Instances: array[0..InstanceCount] of TObjectInstance;
                   end;
var
  InstBlockList : PInstanceBlock  = nil;
  InstFreeList  : PObjectInstance = nil;

function StdHookProc (Code, WParam: WPARAM; LParam: LPARAM): LResult; stdcall; assembler;

asm
  XOR     EAX,EAX
  PUSH    EAX
  PUSH    LParam
  PUSH    WParam
  PUSH    Code
  MOV     EDX,ESP
  MOV     EAX,[ECX].Longint[4]
  CALL    [ECX].Pointer
  ADD     ESP,12
  POP     EAX
end;

{ Allocate a hook method instance }
function CalcJmpOffset(Src, Dest: Pointer): Longint;
begin
  Result := Longint(Dest) - (Longint(Src) + 5);
end;

function MakeHookInstance(Method: THookMethod): Pointer;

const
  BlockCode: array [1..2] of Byte = ($59, $E9);
  PageSize = 4096;

var
  Block: PInstanceBlock;
  Instance: PObjectInstance;

begin
  if InstFreeList = nil then
  begin
    Block := VirtualAlloc (nil, PageSize, MEM_COMMIT,PAGE_EXECUTE_READWRITE);
    Block^.Next := InstBlockList;
    Move(BlockCode, Block^.Code, SizeOf(BlockCode));
    Block^.WndProcPtr := Pointer(CalcJmpOffset(@Block^.Code[2],@StdHookProc));
    Instance := @Block^.Instances;
    repeat
      Instance^.Code := $E8;
      Instance^.Offset := CalcJmpOffset(Instance, @Block^.Code);
      Instance^.Next := InstFreeList;
      InstFreeList := Instance;
      Inc(Longint(Instance), SizeOf(TObjectInstance));
    until Longint(Instance) - Longint(Block) >= SizeOf(TInstanceBlock);
    InstBlockList := Block
  end;
  Result := InstFreeList;
  Instance := InstFreeList;
  InstFreeList := Instance^.Next;
  Instance^.Method := Method
end;

{ Free a hook method instance }
procedure FreeHookInstance (ObjectInstance: Pointer);

Begin
  if ObjectInstance <> nil then
  Begin
    PObjectInstance(ObjectInstance)^.Next := InstFreeList;
    InstFreeList := ObjectInstance
  End
End;

Constructor THook.Create(Owner : TComponent);

Begin
  Inherited Create(Owner);
  fHookProc := MakeHookInstance(HookProc);
  fActive := false;
  fLoadedActive := False;
  fHook := 0;
  ThreadID := GetCurrentThreadID;
End;

Destructor THook.Destroy;

Begin
  Active := False;
  FreeHookInstance(fHookProc);
  Inherited;
End;

Procedure THook.SetActive(NewState : Boolean);

Begin
  If (csLoading in componentState) Then
  Begin
    fLoadedActive := NewState;
  End Else If (fActive<>NewState) Then
  Begin
    fActive := NewState;
    Case (Active And (Not (csDesigning In ComponentState))) Of
      True : Begin
               fHook := AllocateHook;
               If (fHook=0) Then
               Begin
                 fActive := False;
                 Raise Exception.Create(Classname+' CREATION FAILED!');
               End;
             End;
      False : Begin
                If (FHook<>0) Then UnhookWindowsHookEx(fHook);
                fHook := 0;
              End;
    End;
  End;
End;

Procedure THook.SetThreadID(NewID : INteger);

Var
  IsActive              : Boolean;

Begin
  IsActive := fActive;
  Active := False;
  fThreadID := NewID;
  Active := IsActive;
End;

Procedure THook.Loaded;

Begin
  Inherited;
  Active := fLoadedActive;
End;

Procedure THook.HookProc(Var HookMsg : THookMsg);

Var
  Handled               : Boolean;

Begin
  Handled := False;
  PreExecute(HookMsg,Handled);
  If Not Handled Then
  Begin
    with HookMsg do Result := CallNextHookEx (fHook, Code, wParam, lParam);
    PostExecute(HookMsg);
  End;
End;

Procedure THook.PreExecute(Var HookMsg : THookMsg; Var Handled : Boolean);

Begin
  If Assigned(fOnPreExecute) then
  Begin
    fOnPreExecute(Self,HookMsg);
  End;
End;

Procedure THook.PostExecute(Var HookMsg : THookMsg);

Begin
  If Assigned(fOnPostExecute) then
  Begin
    fOnPostExecute(Self,HookMsg);
  End;
End;

Function TCallWndProcHook.AllocateHook : hHook;

Begin
  Result := SetWindowsHookEx (WH_CALLWNDPROC, fHookProc, HInstance, ThreadID);
End;

Function TCallWndProcRetHook.AllocateHook : hHook;

Begin
  Result := SetWindowsHookEx(WH_CALLWNDPROCRET,fHookProc,hInstance,ThreadID);
End;

Function TCBTHook.AllocateHook : hHook;

Begin
  Result := SetWindowsHookEx(WH_CBT,fHookProc,hInstance,ThreadID);
End;

Function TDebugHook.AllocateHook : hHook;

Begin
  Result := SetWindowsHookEx(WH_DEBUG,fHookProc,hInstance,ThreadID);
End;

Function TGetMessageHook.AllocateHook : hHook;

Begin
  Result := SetWindowsHookEx(WH_GETMESSAGE,fHookProc,hInstance,ThreadID);
End;

Function TJournalPlaybackHook.AllocateHook : hHook;

Begin
  Result := SetWindowsHookEx(WH_JOURNALPLAYBACK,fHookProc,hInstance,ThreadID);
End;

Function TJournalRecordHook.AllocateHook : hHook;

Begin
  Result := SetWindowsHookEx(WH_JOURNALRECORD,fHookProc,hInstance,ThreadID);
End;

Function TKeyboardHook.AllocateHook : hHook;

Begin
  Result := SetWindowsHookEx(WH_KEYBOARD,fHookProc,hInstance,ThreadID);
End;

Function TMouseHook.AllocateHook : hHook;

Begin
  Result := SetWindowsHookEx(WH_MOUSE,fHookProc,hInstance,ThreadID);
End;

Function TMsgHook.AllocateHook : hHook;

Begin
  Result := SetWindowsHookEx(WH_MSGFILTER,fHookProc,hInstance,ThreadID);
End;

Function TShellHook.AllocateHook : hHook;

Begin
  Result := SetWindowsHookEx(WH_SHELL,fHookProc,hInstance,ThreadID);
End;
Function TSysMsgHook.AllocateHook : hHook;

Begin
  Result := SetWindowsHookEx(WH_SYSMSGFILTER,fHookProc,hInstance,ThreadID);
End;

Procedure Register;

Begin
  RegisterComponents('Hooks',[TCallWndProcHook,TCallWndProcRetHook,TCBTHook,TDebugHook,TGetMessageHook,
                             TJournalPlaybackHook,TJournalRecordHook,TKeyboardHook,TMouseHook,TMsgHook,
                             TShellHook,TSysMsgHook]);
End;

end.