Page 1 of 1

keyboard hook gives events without ExamineKeyboard()

Posted: Thu Oct 16, 2014 11:11 pm
by BasicallyPure
If you have ever wanted to detect all keyboard events without having a screen and
using ExamineKeyboard() then using a hook might be what you are looking for.

I know an easy way is by using the #WM_CHAR event in the event loop but we are
often reminded not to use non-PureBasic events in our event loops.

I decided to take a dive into the deep water and look for another solution.
I investigated the use of subclassing but results were not satisfactory.
The use of a hook seemed to be the best bet so after much research I have
produced the following bit of code.

Note: this is for windows only.

You will see that any keypress is detected and a custom event is fired.
Any gadgets that use keyboard input will still behave normally.
The state of shift, ctrl, and alt can be determined as well with each event.
The virtual key code is provided but sorry, not the ascii code.
If you want ascii code you will have to convert it yourself.

Notice in the example that the return key can be detected and by
using GetActiveGadget() you can easily have your code respond
as desired. This is useful for gadgets that use string input such as
the StringGadget, SpinGadget, or EditorGadget or others.

Code: Select all

; Keyboard Hook example
; screen and ExamineKeyboard() is not required
; a custom event is generated when any key is pressed
; virtual key code can be obtained along with shift, ctrl, alt states
; by BasicallyPure
; date: 10/13/2014
; OS: windows
; compiler: PB 5.30
; forum topic: http://www.purebasic.fr/english/viewtopic.php?f=12&t=60775

EnableExplicit

Define vKeyCode, scanCode, shiftState, altKeyState, ctrlKeyState, Quit = #False
Define hHookProc ; <--- must have this for hook procedure

#KeyBoard_Event = #PB_Event_FirstCustomValue ; hook generates this event on any key press

#WinMain = 1

; here is the hook callback procedure
Procedure MyKeyboardHook(nCode, wParam, lParam)
   ; posts a custom event (#KeyBoard_Event) when a keyboard key is pressed
   ; information about the key press can be obtained with the following:
   ; EventData() returns wParam (the virtual key code).
   ; EventType() returns #True if shift key was down when key was pressed.
   ; EventGadget() returns lParam (repeat count (bits 0-15), scan code (bits 16-23),
   ;      extended-key flag (bit 24), reserved (bits 25-28), context code (bit 29),
   ;      previous key-state flag (bit 30), And transition-state flag (bit 31).
   ; see windows 'KeyboardProc callback function' for more detailed information.
   ; http://msdn.microsoft.com/en-us/library/windows/desktop/ms644984(v=vs.85).aspx
   
   Shared hHookProc
   Static shift, ctrl, alt
   Protected vShift, vlParam
   
   If Not (nCode < 0)
      If Not lParam & $80000000 ; key down
         If wParam = #VK_CONTROL : ctrl  + 1    : EndIf
         If wParam = #VK_SHIFT   : shift + 1    : EndIf
         If wParam = #VK_MENU    : alt + 1      : EndIf
         
         Select wParam
            Case  #VK_A To #VK_Z
               vShift = Bool(shift > 0) ! (GetKeyState_(#VK_CAPITAL) & 1)
            Default
               vShift = Bool(shift > 0)
         EndSelect
         
         ; use bit 25 of vlParam for 'CTRL' key state
         vlParam = (lParam & $FDFFFFFF) | (Bool(ctrl > 0) << 25)
         
         Select wParam
            Case #VK_SHIFT
               If shift < 2 ; prevent auto repeat with shift
                  PostEvent(#KeyBoard_Event,#WinMain,vlParam,vShift,wParam)
               EndIf
            Case #VK_CONTROL
               If ctrl < 2 ; prevent auto repeat with ctrl
                  PostEvent(#KeyBoard_Event,#WinMain,vlParam,vShift,wParam)
               EndIf
            Case #VK_MENU
               If alt < 2 ; prevent auto repeat with alt
                  PostEvent(#KeyBoard_Event,#WinMain,vlParam,vShift,wParam)
               EndIf
            Default
               PostEvent(#KeyBoard_Event,#WinMain,vlParam,vShift,wParam)
         EndSelect
         
      Else ; key up
         Select wParam
            Case #VK_SHIFT   : shift = 0
            Case #VK_CONTROL : ctrl  = 0
            Case #VK_MENU    : alt   = 0
         EndSelect
      EndIf
   EndIf

   ProcedureReturn CallNextHookEx_(hHookProc,nCode,wParam,lParam)
EndProcedure


If OpenWindow(#WinMain,0,0,400,200,"keyboard hook",#PB_Window_ScreenCentered | #PB_Window_SystemMenu)
   
   ; create the hook
   hHookProc = SetWindowsHookEx_(#WH_KEYBOARD,@MyKeyboardHook(),#Null,GetCurrentThreadId_())
   
   StringGadget(0,10,10,100,25,"")
   StringGadget(1,10,45,100,25,"")
   SpinGadget(2,10,90,100,25,0,100,#PB_Spin_Numeric)
   ButtonGadget(3,150,10,50,25,"clear")
   EditorGadget(4,210,10,150,150)
   
   Repeat
      Select WaitWindowEvent()
         Case #PB_Event_CloseWindow
            Quit = #True
            
         Case #PB_Event_Gadget
            Select EventGadget()
               Case 0 : 
               Case 1 : 
               Case 2 : 
               Case 3 : SetGadgetText(4,"") : SetActiveGadget(4)
               Case 4 : 
            EndSelect
            
         Case #KeyBoard_Event ; with this custom event
            ; EventData() gives virtual key code
            ; EventGadget() bits 16-23 gives scan code
            ; EventType() gives shift state
            ; EventGadget() bit 29 gives alt key state
            ; EventGadget() bit 25 gives ctrl key state
            
            vKeyCode     =  EventData()                      ; obtain vKey code
            scanCode     = (EventGadget() & $FF0000) >> 16   ; obtain scan code
            shiftState   =  EventType()                      ; obtain shift state
            altKeyState  = (EventGadget() & $20000000) >> 29 ; obtain alt key state
            ctrlKeyState = (EventGadget() & $2000000) >> 25  ; obtain ctrl key state
            
            Debug "virtual key code = " + vKeyCode +
                  "   |   scan code = " + scanCode +
                  "   |   shift = "     + shiftState +
                  "   |   Alt = "       + altKeyState +
                  "   |   ctrl = "      + ctrlKeyState
            
            ; perform an action if 'return' key was pressed while
            ; the gadget of your choice had focus
            If vKeyCode = #VK_RETURN
               Select GetActiveGadget()
                  Case 0 : SetActiveGadget(1)
                  Case 1 : SetActiveGadget(0)
                  Case 2 : SetGadgetText(2,Str(GetGadgetState(2))) ; spin gadget
               EndSelect
            EndIf
            
      EndSelect
   Until Quit = #True
   
   ; remove hook
   UnhookWindowsHookEx_(hHookProc)
EndIf
Edit: 7/31/2015
After taking another look at this I have decided it may be simpler to just use global variables to retrieve the keyboard data.
Here is the modified code.

Code: Select all

; Keyboard Hook example
; screen and ExamineKeyboard() are not required
; a custom event is generated when any key is pressed
; virtual key code can be obtained along with shift, ctrl, alt states
; by BasicallyPure
; date: 7/30/2015
; OS: windows
; compiler: PB 5.31

EnableExplicit

Define Quit = #False

Global hHookProc, vKeyCode, altKeyState, ctrlKeyState, shiftState, scanCode

#KeyBoard_Event = #PB_Event_FirstCustomValue ; hook generates this event on any key press

#WinMain = 1

; here is the hook callback procedure
Procedure KeyboardHook(nCode, wParam, lParam)
   ; posts this custom event: '#KeyBoard_Event' :when a keyboard key is pressed
   ; information about the key press can be obtained with the following:
   ; vKeyCode     : the virtual key code '#VK_nnn'
   ; scanCode     : the keyboard scan code
   ; shiftState   : 1 = shifted, 0 = unshifted
   ; ctrlKeyState : 1 = pressed, 0 = not pressed
   ; altKeyState  : 1 = pressed, 0 = not pressed
   ; see windows 'KeyboardProc callback function' for more detailed information.
   ; http://msdn.microsoft.com/en-us/library/windows/desktop/ms644984(v=vs.85).aspx
   
   Static shift, ctrl, alt
   
   If Not (nCode < 0)
      If Not lParam & $80000000 ; key down
         If wParam = #VK_CONTROL : ctrl  + 1  : EndIf
         If wParam = #VK_SHIFT   : shift + 1  : EndIf
         If wParam = #VK_MENU    : alt   + 1  : EndIf
         
         shiftState = Bool(shift > 0)
         
         If wParam >= #VK_A And wParam <= #VK_Z ; for CapsLock
            shiftState ! (GetKeyState_(#VK_CAPITAL) & 1)
         EndIf
         
         vKeyCode     = wParam
         scanCode     = (lParam & $FF0000) >> 16
         altKeyState  = Bool(alt > 0)
         ctrlKeyState = Bool(ctrl > 0)
         
         Select wParam
            Case #VK_SHIFT
               If shift < 2 ; prevent auto repeat with shift
                  PostEvent(#KeyBoard_Event)
               EndIf
            Case #VK_CONTROL
               If ctrl < 2 ; prevent auto repeat with ctrl
                  PostEvent(#KeyBoard_Event)
               EndIf
            Case #VK_MENU
               If alt < 2 ; prevent auto repeat with alt
                  PostEvent(#KeyBoard_Event)
               EndIf
            Default
               PostEvent(#KeyBoard_Event)
         EndSelect
         
      Else ; key up
         Select wParam
            Case #VK_SHIFT   : shift = 0
            Case #VK_CONTROL : ctrl  = 0
            Case #VK_MENU    : alt   = 0
         EndSelect
      EndIf
   EndIf

   ProcedureReturn CallNextHookEx_(hHookProc,nCode,wParam,lParam)
EndProcedure


If OpenWindow(#WinMain,0,0,400,200,"keyboard hook",#PB_Window_ScreenCentered | #PB_Window_SystemMenu)
   
   ; create the hook
   hHookProc = SetWindowsHookEx_(#WH_KEYBOARD,@KeyboardHook(),#Null,GetCurrentThreadId_())
   
   TextGadget(#PB_Any,10,5,100,20,"string_1",#PB_String_ReadOnly)
   StringGadget(0,10,25,100,25,"")
   TextGadget(#PB_Any,10,60,100,20,"string_2",#PB_String_ReadOnly)
   StringGadget(1,10,80,100,25,"")
   ButtonGadget(2,10,140,100,30,"clear")
   TextGadget(#PB_Any,210,5,100,20,"editor",#PB_String_ReadOnly)
   EditorGadget(3,210,25,150,150)
   SetActiveGadget(0)
   
   Repeat
      Select WaitWindowEvent()
         Case #PB_Event_CloseWindow
            Quit = #True
            
         Case #PB_Event_Gadget
            Select EventGadget()
               Case 0 : 
               Case 1 : 
               Case 2 ;clear
                  SetGadgetText(0,"")
                  SetGadgetText(1,"")
                  SetGadgetText(3,"") : SetActiveGadget(0)
               Case 3 : 
            EndSelect
            
         Case #KeyBoard_Event ; custom event
            
            Debug "virtual key code = " + vKeyCode +
                  "   |   scan code = " + scanCode +
                  "   |   shift = "     + shiftState +
                  "   |   Alt = "       + altKeyState +
                  "   |   ctrl = "      + ctrlKeyState
            
            ; perform an action if 'return' key was pressed while
            ; the gadget of your choice had focus
            If vKeyCode = #VK_RETURN
               Select GetActiveGadget()
                  Case 0 : SetActiveGadget(1)
                  Case 1 : SetActiveGadget(3)
                  Case 3 : If shiftState : SetActiveGadget(0) : EndIf
               EndSelect
            ElseIf vKeyCode = #VK_TAB
               If GetActiveGadget() = 2 :SetActiveGadget(3) : EndIf
            EndIf
            
      EndSelect
   Until Quit = #True
   
   ; remove hook
   UnhookWindowsHookEx_(hHookProc)
EndIf
BP

Re: keyboard hook gives events without ExamineKeyboard()

Posted: Fri Oct 17, 2014 1:36 am
by Thunder93
Thanks for sharing. :)

Re: keyboard hook gives events without ExamineKeyboard()

Posted: Fri Oct 17, 2014 2:16 am
by BasicallyPure
You're welcome. :)

Re: keyboard hook gives events without ExamineKeyboard()

Posted: Sat Oct 18, 2014 3:41 pm
by minimy
Very good code!! Thanks a lot BasicallyPure!! :D

Re: keyboard hook gives events without ExamineKeyboard()

Posted: Sat Oct 18, 2014 3:57 pm
by juror
Nice 8)

Thanks for sharing.

Re: keyboard hook gives events without ExamineKeyboard()

Posted: Sun Oct 19, 2014 1:20 am
by Thade
What a coincidence. Exactly what I was looking for to make two apps better ...

Thank you very much

Re: keyboard hook gives events without ExamineKeyboard()

Posted: Sun Oct 19, 2014 2:15 am
by PB
Very nice, but I'm looking for a way to do this when the
app itself no longer has the focus (for recording macros).
Do you know how to modify it to do so? Thanks.

keyboard hook gives events without ExamineKeyboard()

Posted: Sun Oct 19, 2014 4:17 am
by JHPJHP
Hi PB,

Similar to BasicallyPure's fine example, without the Gadget filters.
- http://www.purebasic.fr/english/viewtop ... 26#p418026

Global Hook:
- windows only
- captures A - Z
- ESC to exit

Code: Select all

Global HHOOK

Procedure SendKey(KeySwap.s)
  SwapData.INPUT
  SwapData\type = #INPUT_KEYBOARD
  SwapData\ki\wVk = PeekB(@KeySwap)
  SwapData\ki\wScan = 0
  SwapData\ki\dwFlags = 0
  SwapData\ki\time = 0
  SwapData\ki\dwExtraInfo = 0
  SendInput_(1, SwapData, SizeOf(INPUT))
EndProcedure

Procedure.s GetKeyName(KeyCode.i)
  KeyName.s = Space(#MAX_PATH)
  GetKeyNameText_(MapVirtualKey_(KeyCode, #MAPVK_VK_TO_VSC) * $10000, @KeyName, #MAX_PATH)
  ProcedureReturn KeyName
EndProcedure

Procedure KeyboardHook(nCode, wParam, *kc.KBDLLHOOKSTRUCT)
  If nCode = #HC_ACTION
    KeyCode = *kc\vkCode

    Select KeyCode
      Case 27
        UnhookWindowsHookEx_(HHOOK)
        End
      Case 65 To 90
        Debug GetKeyName(KeyCode)
      Default
        SendKey(#NULL$)
        ProcedureReturn 1
    EndSelect
  EndIf
  ProcedureReturn CallNextHookEx_(#Null, nCode, wParam, *kc)
EndProcedure

If OpenWindow(0, 0, 0, 0, 0, "Global Keyboard Hook", #PB_Window_Invisible)
  HHOOK = SetWindowsHookEx_(#WH_KEYBOARD_LL, @KeyboardHook(), #Null, 0)
  Repeat : Until WaitWindowEvent() = #PB_Event_CloseWindow
EndIf

Re: keyboard hook gives events without ExamineKeyboard()

Posted: Sun Oct 19, 2014 8:11 am
by PB
Thanks JHPJHP, that's a great starting point to build from. :)

Re: keyboard hook gives events without ExamineKeyboard()

Posted: Sun Oct 16, 2016 9:14 pm
by IdeasVacuum
If I understand the code snippets correctly, they are detecting keyboard events triggered by apps?

What I would like to do is determine whether or not Caps Lock is on, independent of keyboard events. For example, when entering your password to run Windows, if Caps Lock is on, a warning message to that effect is displayed. At that point, you probably haven't touched the keyboard. So how is that detection done?

Re: keyboard hook gives events without ExamineKeyboard()

Posted: Sun Oct 16, 2016 10:23 pm
by JHPJHP
Hi IdeasVacuum,

A more comprehensive example can be found at the following link; recently updated.
- Services, Stuff, and Shellhook
-- Stuff\OtherStuff\GlobalKeyboardHook.pb

Code: Select all

CapsLock = GetKeyState_(#VK_CAPITAL)
NumLock = GetKeyState_(#VK_NUMLOCK)
ScrollLock = GetKeyState_(#VK_SCROLL)

If CapsLock : Debug "Caps Lock:" + #TAB$ + "ON" : Else : Debug "Caps Lock:" + #TAB$ + "OFF" : EndIf
If NumLock : Debug "Num Lock:" + #TAB$ + "ON" : Else : Debug "Num Lock:" + #TAB$ + "OFF" : EndIf
If ScrollLock : Debug "Scroll Lock:" + #TAB$ + "ON" : Else : Debug "Scroll Lock:" + #TAB$ + "OFF" : EndIf

Re: keyboard hook gives events without ExamineKeyboard()

Posted: Mon Oct 17, 2016 12:32 pm
by IdeasVacuum
Thanks JHPJHP. I have used GetKeyState() before, just didn't think of it!