personal info | online resume | project details | vb downloads | contact
Page Links
 Article Index
 
 
Get Hooked Up
 
 
Download a copy of the code that accompanies this article.
 
What is a Hook?
A few weeks ago, we talked about subclassing. Subclassing allowed you as a Visual Basic programmer to intercept messages that were being sent to your application. If you were interested in a particular message, you could act based on that message and then allow it to continue to be processed normally by Windows. Subclassing allows you to act on some messages that the VB
runtime doesn't normally allow you to see. After reading that, hopefully you were opened up to a new world of capabilities you thought previously unreachable by a VB application, and in a sense, you would have been right.  In some cases, even subclassing may not be enough. Sometimes, you may need to intercept events before they even reach your application. In some cases, you may want to tap into events that subclassing doesn't catch or that don't even involve your application.  To do this, you need to tap into the world of Windows Hooks.

MSDN defines a Windows hook as a mechanism by which a function can intercept events (messages, mouse actions, keystrokes) before they reach an application. The function can act on events and, in some cases, modify or discard them. Functions that receive events are called filter functions and are classified according to the type of event they intercept. For example, a filter function might want to receive all keyboard or mouse events. For Windows to call a filter function, the filter function must be installed, that is, attached, to a Windows hook (for example, to a keyboard hook).  Attaching one or more filter functions to a hook is known as setting a hook.  If a hook has more than one filter function attached, Windows maintains a chain of filter functions. The most recently installed function is at the beginning of the chain, and the least recently installed function is at the end.

When a hook has one or more filter functions attached and an event occurs that triggers the hook, Windows calls the first filter function in the filter function chain. This action is known as calling the hook. For example, if a filter function is attached to the CBT hook and an event that triggers the hook occurs (for example, a window is about to be created), Windows calls the CBT hook by calling the first function in the filter function chain.

To maintain and access filter functions, applications use the SetWindowsHookEx and the UnhookWindowsHookEx functions. To identify each hook, there is a predefined constant beginning with WH_. To get the values for these constants, as well as the declarations of the  SetWindowsHookEx and
UnhookWindowsHookEx functions, check out MSDN on line at msdn.microsoft.com.
Hook Uses

Hooks provide powerful capabilities for Windows-based applications. These applications can use hooks to:

  • Process or modify all messages meant for all the dialog boxes, message boxes, scroll bars, or menus for an application (WH_MSGFILTER).

  • Process or modify all messages meant for all the dialog boxes, message boxes, scroll bars, or menus for the system (WH_SYSMSGFILTER).

  • Process or modify all messages (of any type) for the system whenever a GetMessage or a PeekMessage function is called (WH_GETMESSAGE).

  • Process or modify all messages (of any type) whenever a SendMessage function is called (WH_CALLWNDPROC).

  • Record or play back keyboard and mouse events (WH_JOURNALRECORD, WH_JOURNALPLAYBACK).

  • Process, modify, or remove keyboard events (WH_KEYBOARD).

  • Process, modify, or discard mouse events (WH_MOUSE).

  • Respond to certain system actions, making it possible to develop computer-based training (CBT) for applications (WH_CBT).

  • Prevent another filter from being called (WH_DEBUG).
Down to Business

Enough "theory" I suppose. What can you actually use a hook for? Well, one example I think of is to solve a problem that hounded me on the first real VB I wrote. I was trying to validate the contents of a TextBox. When the user tabbed off the TextBox, I would validate the contents using the _LostFocus event. If I found a problem, I would show the user the problem I had via a MsgBox, and return focus to the offensive TextBox using SetFocus.  If you've done anything like this, you know the problem with using this approach. Returning focus to the bad TextBox using SetFocus would also cause the _LostFocus event to fire for the control the user tabbed to in the first place. If this happened to be another control that contained validation code in it's _LostFocus event, this event would also fire. If
the contents of neither controls were valid, you could find yourself in a constant loop of MsgBox statements and the user would find themselves killing your application. In this situation, I would find myself saying "boy I wish there was a _LosingFocus event in addition to a _LostFocus event". You smell a hook?

To utilize a hook, the first thing you have to do is install it. To install a hook, you use the SetWindowsHookEx function. Here's it declaration:

Declare Function SetWindowsHookEx _
   Lib "user32" _
   Alias "SetWindowsHookExA" _
   (ByVal idHook As Long, _
   ByVal lpFn As Long, _
   ByVal hmod As Long, _
   ByVal dwThreadId As Long) As Long

The parameters for SetWindowsHookEx are:

idHook - the type of hook you want to install (more below)
   lpFn - the address of a filter function to call
   hmod - the instance handle of the module containing the filter
function (0 in our case)
dwThreadId - the thread ID for which the hook is to be installed (0 for a
system-wide hook)

To set up our CBT hook, we would call SetWindowsHookEx as follows:
m_lCBTHook = SetWindowsHookEx(WH_CBT, AddressOf CBTProc, 0,
   App.ThreadID)

 m_lCBTHook is a Long variable representing a handle to the installed hook.
We will use this handle later when we uninstall our hook using
UnhookWindowsHookEx.

WH_CBT is one of the hook types you can use in a call to SetWindowsHookEx.
The different hook types, along with their purposes, as shown above. While
we are not talking about developing a computer-based training application,
we need to be notified whenever the system is about to change the focus from
one TextBox to another. A CBT hook allows us to see this action before it
takes place. WH_CBT is defined as follows:
Private Const WH_CBT = 5

Once our hook is set, Windows will begin calling the function we specified
in the lpfn parameter of SetWindowsHookEx. In our example above, this was
the CBTProc function. The parameters passed to CBTProc are:

lngCode - the CBT hook code
WP - the handle to the window gaining the keyboard focus
LP - the handle to the window losing the keyboard focus

This function is as follows:
Private Function CBTProc(ByVal lngCode As Long, ByVal WP As Long, ByVal LP
As Long) As Long
Dim bCancel As Boolean
Static bInCBTProc As Boolean

  On Error Resume Next

  ' prevent multiple entries into this function
  If bInCBTProc Then
  CBTProc = CallNextHookEx(m_lCBTHook, lngCode, WP, LP)

   Exit Function
  End If

  bInCBTProc = True

  ' if lngCode < 0 then we must pass the message
  ' to the next hook procedure in the chain and
  ' return it's value and exit the function.
  If lngCode < 0 Then
   CBTProc = CallNextHookEx(m_lCBTHook, lngCode, WP, LP)

   bInCBTProc = False

   Exit Function
  End If

  ' if the CBT hook code was HCBT_SETFOCUS, the focus is about
  ' to change and we need to let the user know
  If lngCode = HCBT_SETFOCUS Then
   ' call the routine which raises the LosingFocus event
  CallLosingFocus LP, WP, bCancel

   ' interpret the ByRef parameter values
   If Not bCancel Then
    ' if the focus change was left as is, allow processing
    ' of the message to continue on along the
    ' hook chain
  CBTProc = CallNextHookEx(m_lCBTHook, lngCode, WP, LP)
   Else
    ' the focus change was cancelled in the LosingFocus
    ' event so kill it here
    CBTProc = 1
  End If

   clear the recursion flag
   bInCBTProc = False

   'all done!
   Exit Function
  End If

  ' if the hook code wasn't a HCBT_SETFOCUS, continue on
  ' down the hook chain here
  CBTProc = CallNextHookEx(m_lCBTHook, lngCode, WP, LP)

  ' clear the recursion flag
  bInCBTProc = False
End Function

In this function, we interpret whether or not we're interested in the CBT
hook code that was generated. For our example, we're looking for a change
in the input focus (HCBT_SETFOCUS, defined as Private Const HCBT_SETFOCUS =
9). If true, we call a routine which gets a reference to our CFocusHook
class and asks it to raise our LosingFocus event:
Private Sub CallLosingFocus(ByVal CurrentWindow As Long, ByVal NextWindow As
Long, ByRef Cancel As Boolean)
Dim EventObject As CFocusEvents
Dim lAddress As Variant

  ' go through each hooked object
  For Each lAddress In g_nHookedObjects
   ' ensure the pointer still points to something valid
   If Not IsBadCodePtr(lAddress) Then
    ' get a reference to the class
    Set EventObject = GetObjectFromAddress(lAddress)

    ' get the class to raise the LosingFocus event
    EventObject.RaiseLosingFocus CurrentWindow, _
NextWindow, _
Cancel
   End If
  Next

  Set EventObject = Nothing
End Sub

IsBadCodePtr verifies that the memory address contained in lAddress still
points to a valid memory location. It is defined as follows:

Declare Function IsBadCodePtr _
    Lib "kernel32" _
    (ByVal lpFn As Long) As Long

GetObjectFromAddress uses the memory location of the CFocusHook class to get
and return a reference to it:

Public Function GetObjectFromAddress(ByVal lAddress As Long) As CFocusHook
Dim cfh As CFocusHook

  If Not lAddress = 0 Then
   If Not IsBadCodePtr(lAddress) Then
    CopyMemory cfh, lAddress, 4
    Set GetObjectFromAddress = cfh
    CopyMemory cfh, 0&, 4
   End If
  End If
End Function

Then, back in the CallLosingFocus routine, the reference returned is used to
call into the CFocusHook class and raise the LosingFocus event. The entire
CFocusHook class is as follows:
Option Explicit

Event LosingFocus(ByVal CurrentWindow As Long, ByVal NextWindow As Long,
Cancel As Boolean)

Private Sub Class_Initialize()
  ' add the address of this object to a collection of object
  g_nHookedObjects.Add ObjPtr(Me), CStr(ObjPtr(Me))

  ' install the CBT hook
  InstallCBTHook
End Sub

Private Sub Class_Terminate()
  ' remove this object from the collection of objects
  g_nHookedObjects.Remove CStr(ObjPtr(Me))

  ' uninstall the CBT hook
  RemoveCBTHook
End Sub

' raise the LosingFocus event, allowing the consumer
' to cancel
Friend Sub RaiseLosingFocus(ByVal CurrentWindow As Long, ByVal NextWindow As
Long, Cancel As Boolean)
  RaiseEvent LosingFocus(CurrentWindow, NextWindow, Cancel)
End Sub

As you can see by its declaration, the LosingFocus event has three parameters. The first is the handle of the window that's about to lose the focus. The second is the handle to the window that's about to gain the
focus. The third is a ByRef parameter which will allow the user to cancel the focus change if they choose to.

Try It Out
Let's take a look at an example of using the CFocusHook class to tie all
these ideas together. First, create a form with three TextBox controls
(Text1, Text2, and Text3) and a WithEvents instance of the CFocusHook class
defined as follows:
Private WithEvents m_oFH As CFocusHook

In the Form_Load event, bring the CFocusHook object to life:

  Set m_oFH = New CFocusHook

In the Form_Unload event, remember to kill the reference to the object:

  Set m_oFH = Nothing

Then, code the LosingFocus event:

Private Sub m_oFH_LosingFocus(ByVal CurrentWindow As Long, ByVal NextWindow
As Long, Cancel As Boolean)
  Select Case True
   Case CurrentWindow = Text2.hWnd
    Cancel = (MsgBox("Keep the focus on Text2?", vbQuestion + vbYesNo,
Me.Caption) = vbYes)
   Case Else
    ' validate other things here
  End Select
End Sub
As you can see here, we can use a Select Case statement to determine which control it is that's about to lose the focus and act accordingly. In the code above, we're deciding whether or not to allow the focus to change based
on the user's response to a MsgBox. The button they select is compared with vbYes and put straight into the Cancel parameter of the LosingFocus event.  To add to the demo, add some code in the LostFocus event for Text2:
Private Sub Text2_LostFocus()
  MsgBox "Text2 just lost focus. The active control is now " &
Screen.ActiveControl.Name, vbInformation, Me.Caption
End Sub

With this code, we'll be able to see when the normal VB LostFocus event fires in relation to our new LosingFocus event. If the user allows the focus to change by selecting Yes in the MsgBox of the LosingFocus event, the
Text2_LostFocus event will fire. If they select No, the Text2_LostFocus event will never fire because back in our CBTProc, we didn't allow Windows to continue through the hook chain:
    ' call the routine which raises the LosingFocus event
    CallLosingFocus LP, WP, bCancel

    ' interpret the ByRef parameter values
    If Not bCancel Then
     ' if the focus change was left as is, allow processing
     ' of the message to continue on along the
     ' hook chain
     CBTProc = CallNextHookEx(m_lCBTHook, lngCode, WP, LP)
    Else
     ' the focus change was cancelled in the LosingFocus
     ' event so kill it here
    CBTProc = 1
    End If
If CurrentWindow = Text2.hWnd in the LosingFocus event, we can validate the contents of Text2 and not truly not allow the user to continue moving the focus off of the Text2 TextBox. That also includes clicking on another
TextBox with the mouse.
Wrap Up
This article focused on one small thing you can do with hooks. There are
many other types of hooks and many specific items to you look for with each
one. Hooks allow you to obtain the highest level of control possible over
your application. When used very carefully, you can also monitor events
occurring in other applications as well.

To obtain a list of hooks and their purpose, search MSDN for "Win32 hooks"
or any of the specific hook types. For some additional code and
information, check out the vbAccelerator Hook Library at
http://www.vbaccelerator.com/ (in the Libraries section). This page
includes additional information on hooks, as well as sample code for other
types of hooks.
 
This article has been republished with permission from EZ Programming Weekly. To subscribe, send an email to cdnelson9@hotmail.com
personal info | online resume | project details | vb downloads | contact
Copyright © 2001 by Earl Damron