0

I am trying to write a simple desktop application to capture user input and write it to a text file. I have figured out how to capture the mouse mouse movement and mouse clicks. I am writing this information to a text file. Then I am using the application to recreate the actions that were performed with the input devices. Basically I am trying to write my own macro recorder.

I'm not sure what I am doing wrong when it comes to capturing keyboard input. I am using the following code, but it is not being executed when I type with my keyboard. Therefore, it is not writing the keyboard input to the text file. What am I overlooking?

Private Sub Form1_KeyDown(sender As Object, e As KeyEventArgs) Handles MyBase.KeyDown
    Try
        ' Add key down event to recorded actions
        Dim key As Keys = e.KeyCode
        recordedActions.Add(String.Format("KeyDown,{0}", key))
    Catch ex As Exception
        MessageBox.Show("Error capturing key down: " & ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
    End Try
End Sub

Private Sub Form1_KeyUp(sender As Object, e As KeyEventArgs) Handles MyBase.KeyUp
    Try
        ' Add key up event to recorded actions
        Dim key As Keys = e.KeyCode
        recordedActions.Add(String.Format("KeyUp,{0}", key))
    Catch ex As Exception
        MessageBox.Show("Error capturing key up: " & ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
    End Try
End Sub
2
  • 1
    Keyboard events are raised on the control that has the focus. Which is very unlikely to be the form, that could only happen for a form without controls. Same is true for mouse events, it isn't very clear why that isn't a problem. You could implement IMessageFilter in your main form to get input notifications. Example Commented May 17 at 22:15
  • I have a text box on my form that I am typing in, but it doesn't recognize that I am typing in the control. I will look into implementing IMessageFilter
    – crjunk
    Commented May 17 at 22:23

1 Answer 1

0

You need to install a global keyboardhook if you want to capture keyboardinput outside of your own form.

Imports System.ComponentModel
Imports System.Runtime.InteropServices

Public Class Form1
    Dim kh As New KeyboardHook
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        kh.Hook()
    End Sub
    Private Sub Form1_Closing(sender As Object, e As CancelEventArgs) Handles Me.Closing
        kh.Unhook()
    End Sub
End Class

Public Class KeyboardHook : Implements IDisposable

    Public Delegate Function HookCallBack(nCode As Integer, wParam As IntPtr, lParam As IntPtr) As Integer

    <DllImport("Kernel32.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.StdCall)>
    Public Shared Function GetModuleHandle(ByVal ModuleName As String) As IntPtr : End Function

    <DllImport("User32.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.StdCall)>
    Public Shared Function SetWindowsHookEx(idHook As Integer, HookProc As HookCallBack,
           hInstance As IntPtr, ThreadId As Integer) As IntPtr : End Function

    <DllImport("User32.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.StdCall)>
    Public Shared Function CallNextHookEx(hHook As IntPtr, nCode As Integer,
           wParam As IntPtr, lParam As IntPtr) As Integer : End Function

    <DllImport("User32.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.StdCall)>
    Public Shared Function UnhookWindowsHookEx(hHook As IntPtr) As Boolean : End Function

    <StructLayout(LayoutKind.Sequential)>
    Private Structure KBDLLHOOKSTRUCT
        Public vkCode As UInt32
        Public scanCode As UInt32
        Public flags As KBDLLHOOKSTRUCTFlags
        Public time As UInt32
        Public dwExtraInfo As UIntPtr
    End Structure

    <Flags()>
    Private Enum KBDLLHOOKSTRUCTFlags As UInt32
        LLKHF_EXTENDED = &H1
        LLKHF_INJECTED = &H10
        LLKHF_ALTDOWN = &H20
        LLKHF_UP = &H80
    End Enum

    Public HookHandle As IntPtr = IntPtr.Zero

    Private Const WH_KEYBOARD_LL As Integer = 13

    Private Const HC_ACTION As Integer = 0

    Private Const WM_KEYDOWN = &H100
    Private Const WM_KEYUP = &H101

    Private Const WM_SYSKEYDOWN = &H104
    Private Const WM_SYSKEYUP = &H105

    Private Function KeyProc(
        ByVal nCode As Integer,
        ByVal wParam As IntPtr,
        ByVal lParam As IntPtr) As Integer

        If nCode < 0 OrElse nCode <> HC_ACTION Then Return CallNextHookEx(HookHandle, nCode, wParam, lParam)

        Dim hs As KBDLLHOOKSTRUCT = Marshal.PtrToStructure(Of KBDLLHOOKSTRUCT)(lParam)
        Dim key As Keys = CType(hs.vkCode, Keys)
        Dim scan = hs.scanCode

        Select Case wParam.ToInt32
            Case WM_KEYDOWN
                Debug.Print($"{key} keydown {scan}")
            Case WM_KEYUP
                Debug.Print($"{key} keyup  {scan}")
            Case WM_SYSKEYDOWN 'when alt is down it will use these instead
                Debug.Print($"{key} syskeydown {scan}")
            Case WM_SYSKEYUP
                Debug.Print($"{key} syskeyup {scan}")
        End Select

        Return CallNextHookEx(HookHandle, nCode, wParam, lParam)
    End Function

    Private mhCallBack As HookCallBack = New HookCallBack(AddressOf KeyProc)
    Private disposedValue As Boolean

    Public Sub Hook()
        If HookHandle <> IntPtr.Zero Then Exit Sub
        HookHandle = SetWindowsHookEx(WH_KEYBOARD_LL, mhCallBack,
            GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName), 0)
        If HookHandle = IntPtr.Zero Then Throw New System.Exception("KeyboardHook failed")
    End Sub

    Public Sub Unhook()
        If HookHandle <> IntPtr.Zero Then
            UnhookWindowsHookEx(HookHandle)
            HookHandle = IntPtr.Zero
        End If
    End Sub

    Protected Overridable Sub Dispose(disposing As Boolean)
        If Not disposedValue Then
            If disposing Then
                ' TODO: dispose managed state (managed objects)
            End If

            ' TODO: free unmanaged resources (unmanaged objects) and override finalizer
            Unhook()
            ' TODO: set large fields to null
            disposedValue = True
        End If
    End Sub

    ' TODO: override finalizer only if 'Dispose(disposing As Boolean)' has code to free unmanaged resources
    Protected Overrides Sub Finalize()
        ' Do not change this code. Put cleanup code in 'Dispose(disposing As Boolean)' method
        Dispose(disposing:=False)
        MyBase.Finalize()
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        ' Do not change this code. Put cleanup code in 'Dispose(disposing As Boolean)' method
        Dispose(disposing:=True)
        GC.SuppressFinalize(Me)
    End Sub

End Class

Note that installing a global keyboardhook will be flagged as suspicious by many (if not all) virusscanners.

Read https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-kbdllhookstruct if you need to get more info than just simple up/down.

Not the answer you're looking for? Browse other questions tagged or ask your own question.