| 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.
|
|