Virtual Developer Workshop: Containerized Development with Docker
Heretofore we had to resort to low-level interrupt handlers for interrupt 0x9 and interrupt 0x16 to intercept keyboard events. (Occasionally I was prone to writing keyboard snoopers—in a much earlier and delinquent incarnation—and recall that these took a bit of effort to do well, and undetected.) Now that we all have to earn a living (and can be held accountable for mischievous and illegal behavior), we might want to write a hook into the keyboard to prevent or discourage a user from re-booting during a critical file operation or something similar. Fortunately we can use a couple of Windows API methods to do this reliably rather than resorting to assembler and interrupt handlers.
In this article I will demonstrate how to intercept keystrokes that are difficult or impossible to catch with the KeyPress and KeyDown events in VB6. The process is a bit involved—although still easier than writing interrupt handlers—but rest assured I will provide plenty of code and a summary recapturing the whole process before we are done. If you are interested in capturing keystroke combinations like Ctrl+Esc or Alt+Tab before Windows processes them then you will learn how to do that in this article. You will also learn a bit about defining and implementing interfaces, if you need to brush up on these skills. The basic process I will be presenting follows:
- Define and declare low-level keyboard API methods
- Define an event handler to respond to low-level Windows keyboard events before Windows handles them
- Trap specific and varied keystroke combinations
- Conceal the complexity of the Windows API methods in a VB6 interface, facilitating reuse of this general keyboard-handling technique
- Safely call other handlers that may exist concurrently with your handler
- Unhooking the keyboard handler when you are finished
Hooking the Keyboard
Most complex tasks in VB6 require some knowledge of the Windows API. This is for the simple reason that VB6 runs on Windows and there is no specific framework capturing this information. The Windows API today is procedural; thus to access capabilities in Windows we have to load a library containing the procedures we want to use and invoke those operations. Fortunately VB6 supports implicit library loading and API invocation by simply declaring the API methods we want to use. (Note: It is worth mentioning that the mechanics of loading and getting API methods are supported by API methods themselves. We will just use the plain old Declare statement though.)
Declaring the SetWindowsHookEx Procedure
To hook the keyboard we will need to declare and invoke SetWindowsHookEx. The declaration can be public in a module or must be private in a class module. The declaration for SetWindowsHookEx follows.
Public 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
Rather than knowing the declaration from scratch I was aware of the method and used the API Viewer (see figure 1) to get the declaration incorporated correctly. (We will use this technique for all of the declarations in this article.)
By examining the API method we can figure out what to call, or we could look in the MSDN help. The first argument is a Long that represents an instruction to the API describing the kind of hook operation to perform. The second argument is actually a function pointer, called a callback. The third argument is the handle to the application instance, and the fourth argument is the application's thread ID. The first thing we will do is define an appropriate callback method.
Figure 1: Use the API Viewer to get Windows API method signatures correct.
Stubbing the Keyboard Callback Method
Unfortunately callback methods must be in modules (not class files) in VB6. Thus we will need to place the callback method for SetWindowsHookEx in a module. The signature of the callback is a function that accepts three Long arguments and returns a Long. (You might have to do a little poking around to discover this information, but I will just provide an empty callback stub for you.)
Public Function KeyboardCallback(ByVal Code As Long, _ ByVal wParam As Long, ByVal lParam As Long) As Long End Function
The signature of the callback method looks suspiciously like a generic Windows message handler. Code helps us decide if the message is for us. The wParam argument contains the actual Windows message constant ad lParam plays the role of pointer to keyboard data. We will only test Code for a single constant value and the lParam is actually a pointer to the keyboard data. We will come back to this in a moment.
Hooking the Keyboard
Now that I have the callback method—albeit an empty method—I can hook the keyboard, which means that my method will be called very early in the message handling process. To hook the keyboard I defined a simple method named HookKeyboard that wraps the more complicated call to SetWindowsHookEx.
Private Const WH_KEYBOARD_LL = 13& Public Sub HookKeyboard() KeyboardHandle = SetWindowsHookEx( _ WH_KEYBOARD_LL, AddressOf KeyboardCallback, _ App.hInstance, 0&) Call CheckHooked End Sub
(All of the code so far is in a single module I named KeyboardHandler.bas.) The constant WH_KEYBOARD_LL defines the kind of hook we want to make: a low-level keyboard hook. There are several other kinds of hooks. For example, you can pass constants that indicate that you want to hook the mouse, a hook for debugging, and one that is suggested as being helpful for computer-based training. The second argument is the AddressOf our KeyboardCallback function. The third argument is the application Windows handle, and we can pass 0 for the thread ID. The value 0 indicates that the hook is associated with all threads on the desktop. By passing 0 we are hooking the keyboard and can effectively trap keys effecting all applications.
With the code we have so far the KeyboardCallback method will be called every time we press a key, whether our application has the focus or not. The next step is to write some code in the callback that evaluates the received keyboard messages.
Implementing the Keyboard Callback Method
Listing 1 contains the implementation I used for the KeyboardCallback function. The code is numbered for convenience, followed by a brief synopsis describing the behavior.
Listing 1: The implementation of my KeybaordCallback procedure.
1: Public Function KeyboardCallback(ByVal Code As Long, _ 2: ByVal wParam As Long, ByVal lParam As Long) As Long 3: 4: Static Hookstruct As KBDLLHOOKSTRUCT 5: 6: If (Code = HC_ACTION) Then 7: ' Copy the keyboard data out of the lParam (which is a pointer) 8: Call CopyMemory(Hookstruct, ByVal lParam, Len(Hookstruct)) 9: 10: If (IsHooked(Hookstruct)) Then 11: KeyboardCallback = 1 12: Exit Function 13: End If 14: 15: End If 16: 17: KeyboardCallback = CallNextHookEx(KeyboardHandle, _ 18: Code, wParam, lParam) 19: 20: End Function
If we check the online help documentation it says that we can handle HC_ACTION, which is defined as constant with a value of 0. If Code is less than 0 then we need to immediately pass the message to CallNextHookEx. Hence, when Code equals HC_ACTION (0) I handle the data on lines 6 through 15; otherwise I pass the message to the next function in the callback chain. (Keep in mind that other programs may have keyboard hooks too.) Let's examine the statement on line 17 and 18 first.
Line 17 returns the value of CallNextHookEx. Notice that I pass it the address of the callback I grabbed when I hooked the keyboard, the code, the wParam, and lParam arguments. wParam contains the constant for the Windows message, and the lParam contains a pointer to the keyboard data itself.
On line 6 if the Code is 0 then I want to handle the message. I could also check the wParam to determine if the message is a WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, or WM_SYSKEYUP. (These are Windows message constants that you can define using the API Viewer.) For our purposes we won't check the wParam. If the Code is 0 then we want to convert the lParam to the type we know it is, a KBDLLHOOKSTRUCT, handle the keyboard input, and return 1 to indicate that the input was handled. I implemented a method IsHooked that afforded me a degree of separation between the murky API stuff and the keys I wanted to block. IsHooked is where you and I can decide what key combinations to manage manually.
Getting the Keyboard Data
Lines 4 and 8 of listing 1 introduced a type and an API method that we haven't talked about yet. I couldn't find KBDLLHOOKSTRUCT in my Windows API Viewer, but CopyMemory was there. (Both the type and the API method are shown in listing 2.)
Listing 2: The KBDLLHOOKSTRUCT type and the CopyMemory method.
Private Declare Sub CopyMemory Lib "kernel32" _ Alias "RtlMoveMemory" _ (pDest As Any, _ pSource As Any, _ ByVal cb As Long) Private Type KBDLLHOOKSTRUCT vkCode As Long scanCode As Long flags As Long time As Long dwExtraInfo As Long End Type
(I'm not sure how anyone without a lot of knowledge about the API was ever supposed to put this together.) As I mentioned lParam is really a pointer, which we don't have in VB6. We use long integers to represent pointers, as a pointer is really just a number representing a location in memory. CopyMemory can copy information from one location in memory to a second location. Hence, we use CopyMemory to get the keyboard data from the address pointed to by lParam into our local static variable. (We are basically just copying the keyboard data here.)
The reason I wrote this code as I have shown you so far is that you could easily reuse this code in any context, over and over without revisiting the low-level API stuff. Simple reuse HookKeyboard, KeyboardCallback, and all you would have to do is re-implement IsHooked.
Chaining Hooked Callbacks
The signature for CallNextHookEx is provided for reference here. It is a good idea when you are hooking low-level operations to keep track of the thing that hooked it before you did. In this way your low-level hook will not foul up someone else's low-level hook. HookKeyboard does that.
Private Declare Function CallNextHookEx Lib "user32" _ (ByVal hHook As Long, _ ByVal nCode As Long, _ ByVal wParam As Long, _ ByVal lParam As Long) As Long
I was unable to find CallNextHookEx in the API Viewer. (I have to admit that since .NET was released I haven't been as diligent about updating my MSDN for Visual Studio 6 as I should be. I am sure you're more diligent and won't have any trouble finding these declarations in your API Viewer.)
IsHooked: Checking for Blocked Key Combinations
|Tip: Event the low-level keyboard hook will not block some key combinations, for instance, Ctrl+Alt+Del. I believe Ctrl+Alt+Del generates an interrupt 0x19, which is not routed as a keyboard message. You probably have to implement an interrupt handler for interrupt 0x19 to catch Ctrl+Alt+Del.|
We now have the low-level API hooking code out of the way, and the data is in the somewhat more manageable KBDLLHOOKSTRUCT. Following my implementation all we have to is define IsHooked to return a Boolean that indicates if we want to handle any particular keyboard combination or not. Listing 3 demonstrates one implementation of IsHooked.
Listing 3: An implementation of my IsHooked method checks for Alt+Tab, Ctrl+Esc, and Alt+Esc.
' Implement this function to block as many key combinations as ' you'd like Public Function IsHooked(ByRef Hookstruct As KBDLLHOOKSTRUCT) _ As Boolean If (KeyboardHook Is Nothing) Then IsHooked = False Exit Function End If ' Checks for Ctrl + Esc If (Hookstruct.vkCode = VK_ESCAPE) And _ CBool(GetAsyncKeyState(VK_CONTROL) _ And &H8000) Then IsHooked = KeyboardHook.BlockControlEscape Call HookedState(IsHooked, "Ctrl + Esc blocked") Exit Function End If ' Checks for Alt + Tab If (Hookstruct.vkCode = VK_TAB) And _ CBool(Hookstruct.flags And _ LLKHF_ALTDOWN) Then IsHooked = KeyboardHook.BlockAltTab Call HookedState(IsHooked, "Alt + Tab blockd") Exit Function End If ' Checks for Alt + Esc If (Hookstruct.vkCode = VK_ESCAPE) And _ CBool(Hookstruct.flags And _ LLKHF_ALTDOWN) Then IsHooked = KeyboardHook.BlockAltEscape Call HookedState(IsHooked, "Alt + Escape blocked") Exit Function End If End Function
We have to pass user defined types ByRef. Even in IsHooked you can quickly determine that figuring out what keyboard combinations were being pressed is murky. Checking virtual keys, getting virtual key states asynchronously, and Anding flag values requires too much special knowledge. We only want to write this code one time.
I defined IsHooked to check for Ctrl+Esc, Alt+Tab, and Alt+Esc. If any of these combinations are pressed then I block them. More beneficially you will probably want to write the code to notify your application if any of these combinations are pressed and dynamically allow your application to block them or not based on some application state. (We will take a look at one way to do this in the last section "Genericizing Keyboard Hooks".) Let's finish our current discussion by implementing the code to unhook the keyboard.
Unhooking the Keyboard
When your application exits you want to release you hook. You need to call UnhookWindowsHookEx. To be symmetric I implemented a simpler method to wrap up the API method. Listing 4 contains the declaration for UnhookWindowsHookEx and the implementation of my wrapper method.
Listing 4: Unhooking the keyboard.
Public Declare Function UnhookWindowsHookEx Lib "user32" _ (ByVal hHook As Long) As Long Public Sub UnhookKeyboard() If (Hooked) Then Call UnhookWindowsHookEx(KeyboardHandle) End If End Sub
Before we wrap up and I show you the complete listing for the sample code I want to explain the meaning of the three statements in listing 2 that look something like this one: IsHooked = KeyboardHook.BlockAltEscape.
Genericizing Keyboard Hooks
I enjoy discovering nuts and bolts information that is highly specialized, but I have to make a distinction between fun and pragmatics. Figuring all of this stuff out (with some help from MSDN) was a lot of fun. However, as a matter of pragmatics it is too convoluted to ever repeat, especially when writing software for hire. As a result I created a very simple interface that uses named methods to represent the hooked values. Any class or Form that implements my interface can receive a notification when a specific keyboard combination is pressed. Using this strategy I will never have to figure out what the relationship between the flags, the vkCode, and the hexadecimal number &H8000 are again. This is fun only to the extent that I am a factoid junky, but there is a dichotomy between trying to be productive and figuring out these details.
Listing 5 contains the interface that I defined to wire up to the low-level keyboard code. Doing so vastly simplified a Form's interaction with the API code.
Listing 5: The interface
' KeyboardHook.cls Public Function BlockControlEscape() As Boolean End Function Public Function BlockAltEscape() As Boolean End Function Public Function BlockAltTab() As Boolean End Function
Finally I defined a public variable KeyboardHook. If I assign an object that implements KeyboardHook to this public variable then the IsHooked method will call the appropriate KeyboardHook method, and that method—completely unaware of the low-level API code—can determine if it want to block that key combination or not. A Form that implements and wires into KeyboardHook is shown in the complete code listing, listing 6.
Listing 6: A single complete source code listing for a Form, class, and module that demonstrates the low-level keyboard hooks and an interface that simplifies the hook behavior.
' Form1.frm Option Explicit Implements KeyboardHook Private Sub Form_Load() Set KeyboardHandler.KeyboardHook = Me HookKeyboard End Sub Private Sub Form_Unload(Cancel As Integer) UnhookKeyboard End Sub Private Function KeyboardHook_BlockAltEscape() As Boolean End Function Private Function KeyboardHook_BlockAltTab() As Boolean End Function Private Function KeyboardHook_BlockControlEscape() As Boolean KeyboardHook_BlockControlEscape = True End Function ' Keyboardhandler.bas - Demonstrates low-level keyboard hooks ' Copyright (c) 2002. All Rights Reserved ' By Paul Kimmel. firstname.lastname@example.org 'http://msdn.microsoft.com/library/default.asp?url= '/library/en-us/winui/WinUI/WindowsUserInterface '/Windowing/Hooks/HookReference/HookFunctions/LowLevelKeyboardProc.aspOption Explicit Public Declare Function UnhookWindowsHookEx Lib "user32" _ (ByVal hHook As Long) As Long Public 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 Private Declare Sub CopyMemory Lib "kernel32" _ Alias "RtlMoveMemory" _ (pDest As Any, _ pSource As Any, _ ByVal cb As Long) Private Declare Function GetAsyncKeyState Lib "user32" _ (ByVal vKey As Long) As Integer Private Declare Function CallNextHookEx Lib "user32" _ (ByVal hHook As Long, _ ByVal nCode As Long, _ ByVal wParam As Long, _ ByVal lParam As Long) As Long Private Type KBDLLHOOKSTRUCT vkCode As Long scanCode As Long flags As Long time As Long dwExtraInfo As Long End Type ' Low-Level Keyboard Constants Private Const HC_ACTION = 0 Private Const LLKHF_EXTENDED = &H1 Private Const LLKHF_INJECTED = &H10 Private Const LLKHF_ALTDOWN = &H20 Private Const LLKHF_UP = &H80 ' Virtual Keys Public Const VK_TAB = &H9 Public Const VK_CONTROL = &H11 Public Const VK_ESCAPE = &H1B Public Const VK_DELETE = &H2E Private Const WH_KEYBOARD_LL = 13& Public KeyboardHandle As Long Public KeyboardHook As KeyboardHook ' Implement this function to block as many key combinations as ' you'd like Public Function IsHooked(ByRef Hookstruct As KBDLLHOOKSTRUCT) _ As Boolean If (KeyboardHook Is Nothing) Then IsHooked = False Exit Function End If If (Hookstruct.vkCode = VK_ESCAPE) And _ CBool(GetAsyncKeyState(VK_CONTROL) _ And &H8000) Then IsHooked = KeyboardHook.BlockControlEscape Call HookedState(IsHooked, "Ctrl + Esc blocked") Exit Function End If If (Hookstruct.vkCode = VK_TAB) And _ CBool(Hookstruct.flags And _ LLKHF_ALTDOWN) Then IsHooked = KeyboardHook.BlockAltTab Call HookedState(IsHooked, "Alt + Tab blocked") Exit Function End If If (Hookstruct.vkCode = VK_ESCAPE) And _ CBool(Hookstruct.flags And _ LLKHF_ALTDOWN) Then IsHooked = KeyboardHook.BlockAltEscape Call HookedState(IsHooked, "Alt + Escape blocked") Exit Function End If End Function Private Sub HookedState(ByVal Hooked As Boolean, _ ByVal Text As String) If (Hooked) Then Debug.Print Text End Sub Public Function KeyboardCallback(ByVal Code As Long, _ ByVal wParam As Long, ByVal lParam As Long) As Long Static Hookstruct As KBDLLHOOKSTRUCT If (Code = HC_ACTION) Then ' Copy the keyboard data out of the lParam (which is a pointer) Call CopyMemory(Hookstruct, ByVal lParam, Len(Hookstruct)) If (IsHooked(Hookstruct)) Then KeyboardCallback = 1 Exit Function End If End If KeyboardCallback = CallNextHookEx(KeyboardHandle, _ Code, wParam, lParam) End Function Public Sub HookKeyboard() KeyboardHandle = SetWindowsHookEx( _ WH_KEYBOARD_LL, AddressOf KeyboardCallback, _ App.hInstance, 0&) Call CheckHooked End Sub Public Sub CheckHooked() If (Hooked) Then Debug.Print "Keyboard hooked" Else Debug.Print "Keyboard hook failed: " & Err.LastDllError End If End Sub Private Function Hooked() Hooked = KeyboardHandle <> 0 End Function Public Sub UnhookKeyboard() If (Hooked) Then Call UnhookWindowsHookEx(KeyboardHandle) End If End Sub ' KeyboardHook.cls Public Function BlockControlEscape() As Boolean End Function Public Function BlockAltEscape() As Boolean End Function Public Function BlockAltTab() As Boolean End Function
It is possible to do just about anything in Visual Basic 6. We just have to lean on the API some of the time, as we do when hooking the keyboard.
To acquire a low-level hook into the keyboard you need several Windows API methods, including: SetWindowsHookEx, UnHookWindowsHookEx, CopyMemory, CallNextHookEx, and GetAsyncKeyState. In addition you will need the KBDLLHOOKSTRUCT structure. The basic idea is to call SetWindowsHookEx passing a callback method that Windows will call when a low-level key operation occurs. If the Code passed to your callback equals 0 then Copy the keyboard data out of the parameter passed to your callback with the CopyMemory method into a local variable of type KBDLLHOOKSTRUCT; otherwise send all of the information to CallNextHookEx. You can examine the value of the KBDLLHOOKSTRUCT in part with the GetAsyncKeyState method, and when you are all finished make sure you call UnHookWindowsHookEx.
Sounds easy. Well, there is quite a bit of cryptic information involved, which suggests that all of this information should be wrapped up as part of a cleaner framework. This will tide you over for now.
About the Author
Paul Kimmel is a freelance writer for Developer.com and CodeGuru.com. Look for his recent book "Advanced C# Programming" from McGraw-Hill/Osborne on Amazon.com. Paul will be a keynote speaker in "Great Debate: .NET or .NOT?" at the Fall 2002 Comdex in Las Vegas, Nevada. Paul Kimmel is available to help design and build your .NET solutions and can be contacted at email@example.com.
# # #