March 8, 2021
Hot Topics:

Managing Low-Level Keyboard Hooks in VB .NET

  • By Paul Kimmel
  • Send Email »
  • More Articles »

Trapping Key Combinations

Determining if specific key combinations are being pressed requires some tricky gyrations. (Keep in mind that we are working at a pretty low level here.) This code remains pretty much unchanged from the November article. The basic idea is to read the current key press in the KBDLLHOOKSTRUCT.vkCode. If you need to look for specific multi-key combinations, you may need to call GetAsyncKeyState to determine whether additional keys are being held. For example, we call GetAsynckeyState(VK_CONTROL) in listing 2 to see whether the Ctrl key is being held down.

Unhooking the Keyboard

The return value from SetWindowsHookEx is stored. This is the address of the hook we replaced. We don't discard this value because if we want to let some key combinations slip past our hook, we need to use the return value of SetWindwosHookEx to call the old hook. We also use this value to unhook the keyboard, returning the old hook state, when we are finished holding onto the keyboard handler. Call UnhookWindowsHookEx passing the return value from SetWindowsHookEx to restore the original keyboard hook.

The Complete Code Listing

Listing 2 presents the complete revised listing for VB.NET. Most of this code is more of the same kinds of code that we have discussed already, including some additional methods, declare statements, the KDDLLHOOKSTRUCT, and some useful constants. You can copy and paste the code in listing 2 directly into a module to experiment with it. Call HookKeyboard to begin intercepting the three defined key combinations and UnhookKeyboard to restore the old keyboard state.

Listing 2: The complete revised listing for implementing low-level keyboard hooks.

Imports System.Runtime.InteropServices
Imports System.Reflection
Imports System.Drawing
Imports System.Threading

Module Keyboard
  Public Declare Function UnhookWindowsHookEx Lib "user32" _
    (ByVal hHook As Integer) As Integer

  Public Declare Function SetWindowsHookEx Lib "user32" _
    Alias "SetWindowsHookExA" (ByVal idHook As Integer, _
    ByVal lpfn As KeyboardHookDelegate, ByVal hmod As Integer, _
    ByVal dwThreadId As Integer) As Integer

  Private Declare Function GetAsyncKeyState Lib "user32" _
    (ByVal vKey As Integer) As Integer

  Private Declare Function CallNextHookEx Lib "user32" _
    (ByVal hHook As Integer, _
    ByVal nCode As Integer, _
    ByVal wParam As Integer, _
    ByVal lParam As KBDLLHOOKSTRUCT) As Integer

  Public Structure KBDLLHOOKSTRUCT
    Public vkCode As Integer
    Public scanCode As Integer
    Public flags As Integer
    Public time As Integer
    Public dwExtraInfo As Integer
  End Structure

  ' Low-Level Keyboard Constants
  Private Const HC_ACTION As Integer      = 0
  Private Const LLKHF_EXTENDED As Integer = &H1
  Private Const LLKHF_INJECTED As Integer = &H10
  Private Const LLKHF_ALTDOWN As Integer  = &H20
  Private Const LLKHF_UP As Integer       = &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 As Integer = 13&
  Public KeyboardHandle As Integer

  ' Implement this function to block as many
  ' key combinations as you'd like
  Public Function IsHooked( _
    ByRef Hookstruct As KBDLLHOOKSTRUCT) As Boolean

    Debug.WriteLine("Hookstruct.vkCode: " & Hookstruct.vkCode)
    Debug.WriteLine(Hookstruct.vkCode = VK_ESCAPE)
    Debug.WriteLine(Hookstruct.vkCode = VK_TAB)

    If (Hookstruct.vkCode = VK_ESCAPE) And _
      CBool(GetAsyncKeyState(VK_CONTROL) _
      And &H8000) Then

      Call HookedState("Ctrl + Esc blocked")
      Return True
    End If

    If (Hookstruct.vkCode = VK_TAB) And _
      CBool(Hookstruct.flags And _

      Call HookedState("Alt + Tab blockd")
      Return True
    End If

    If (Hookstruct.vkCode = VK_ESCAPE) And _
      CBool(Hookstruct.flags And _
        LLKHF_ALTDOWN) Then

      Call HookedState("Alt + Escape blocked")
      Return True
    End If

    Return False
  End Function

  Private Sub HookedState(ByVal Text As String)
  End Sub

  Public Function KeyboardCallback(ByVal Code As Integer, _
    ByVal wParam As Integer, _
    ByRef lParam As KBDLLHOOKSTRUCT) As Integer

    If (Code = HC_ACTION) Then
      Debug.WriteLine("Calling IsHooked")

      If (IsHooked(lParam)) Then
        Return 1
      End If

    End If

    Return CallNextHookEx(KeyboardHandle, _
      Code, wParam, lParam)

  End Function

  Public Delegate Function KeyboardHookDelegate( _
    ByVal Code As Integer, _
    ByVal wParam As Integer, ByRef lParam As KBDLLHOOKSTRUCT) _
                 As Integer

  <MarshalAs(UnmanagedType.FunctionPtr)> _
  Private callback As KeyboardHookDelegate

  Public Sub HookKeyboard()
    callback = New KeyboardHookDelegate(AddressOf KeyboardCallback)

    KeyboardHandle = SetWindowsHookEx( _
      WH_KEYBOARD_LL, callback, _
      Marshal.GetHINSTANCE( _
      [Assembly].GetExecutingAssembly.GetModules()(0)).ToInt32, 0)

    Call CheckHooked()
  End Sub

  Public Sub CheckHooked()
    If (Hooked()) Then
      Debug.WriteLine("Keyboard hooked")
      Debug.WriteLine("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

End Module

Be aware that mistakes may completely lock up your keyboard and you may need to reboot. To prevent this kind of problem, I use the ThreadPool and a separate thread to release the keyboard after 10 or 15 seconds. This strategy has been invaluable while developing low-level code. You can learn more about multithreading here in past and future articles or by picking up a copy of my book, Visual Basic .NET Unleashed, from Sams.


Run the sample code and you will see that the Windows API is alive and well in .NET. Thankfully, you will need to have very special needs indeed to resort to calling into the Windows API. This is a far cry from VB6, where almost anything useful required interaction with the Windows API.

Disclaimer: The VS IDE hooks the keyboard. You may need to run the sample code outside of the IDE for the keyboard hook API call to succeed.

One of the most important differences between VB6 and VB.NET is the notion of managed code. Code in VB.NET is managed. This means objects can be moved around in memory and garbage collected. Old Windows API methods do not represent managed code. As a result, you may get some quirky behavior when interacting between .NET and the Windows API. If you plan on writing a lot of code that interoperates with the Windows API or COM, I encourage you to pick up a good book on COM Interop and a good advanced book such as my Visual Basic .NET Power Coding from Addison-Wesley that explores these intricate nooks and crannies for you.

About the Author

Paul Kimmel is a freelance writer for Developer.com and CodeGuru.com. Look for his recent book, Visual Basic .NET Power Coding, from Addison-Wesley on Amazon.com. Paul Kimmel is available to help design and build your .NET solutions and can be contacted at pkimmel@softconcepts.com.

# # #

Page 3 of 3

This article was originally published on April 18, 2003

Enterprise Development Update

Don't miss an article. Subscribe to our newsletter below.

Thanks for your registration, follow us on our social networks to keep up-to-date