keyboard hook gives events without ExamineKeyboard()

Share your advanced PureBasic knowledge/code with the community.
User avatar
BasicallyPure
Enthusiast
Enthusiast
Posts: 539
Joined: Thu Mar 24, 2011 12:40 am
Location: Iowa, USA

keyboard hook gives events without ExamineKeyboard()

Post 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
Last edited by BasicallyPure on Sat Aug 01, 2015 3:49 am, edited 1 time in total.
BasicallyPure
Until you know everything you know nothing, all you have is what you believe.
User avatar
Thunder93
Addict
Addict
Posts: 1788
Joined: Tue Mar 21, 2006 12:31 am
Location: Canada

Re: keyboard hook gives events without ExamineKeyboard()

Post by Thunder93 »

Thanks for sharing. :)
ʽʽSuccess is almost totally dependent upon drive and persistence. The extra energy required to make another effort or try another approach is the secret of winning.ʾʾ --Dennis Waitley
User avatar
BasicallyPure
Enthusiast
Enthusiast
Posts: 539
Joined: Thu Mar 24, 2011 12:40 am
Location: Iowa, USA

Re: keyboard hook gives events without ExamineKeyboard()

Post by BasicallyPure »

You're welcome. :)
BasicallyPure
Until you know everything you know nothing, all you have is what you believe.
User avatar
minimy
Enthusiast
Enthusiast
Posts: 556
Joined: Mon Jul 08, 2013 8:43 pm
Location: off world

Re: keyboard hook gives events without ExamineKeyboard()

Post by minimy »

Very good code!! Thanks a lot BasicallyPure!! :D
If translation=Error: reply="Sorry, Im Spanish": Endif
juror
Enthusiast
Enthusiast
Posts: 228
Joined: Mon Jul 09, 2007 4:47 pm
Location: Courthouse

Re: keyboard hook gives events without ExamineKeyboard()

Post by juror »

Nice 8)

Thanks for sharing.
Thade
Enthusiast
Enthusiast
Posts: 266
Joined: Sun Aug 03, 2003 12:06 am
Location: Austria

Re: keyboard hook gives events without ExamineKeyboard()

Post by Thade »

What a coincidence. Exactly what I was looking for to make two apps better ...

Thank you very much
--------------
Yes, its an Irish Wolfhound.
Height: 107 cm; Weight: 88 kg
PB
PureBasic Expert
PureBasic Expert
Posts: 7581
Joined: Fri Apr 25, 2003 5:24 pm

Re: keyboard hook gives events without ExamineKeyboard()

Post 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.
I compile using 5.31 (x86) on Win 7 Ultimate (64-bit).
"PureBasic won't be object oriented, period" - Fred.
User avatar
JHPJHP
Addict
Addict
Posts: 2252
Joined: Sat Oct 09, 2010 3:47 am

keyboard hook gives events without ExamineKeyboard()

Post 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
Last edited by JHPJHP on Mon Oct 20, 2014 9:37 pm, edited 1 time in total.

If you're not investing in yourself, you're falling behind.

My PureBasic StuffFREE STUFF, Scripts & Programs.
My PureBasic Forum ➤ Questions, Requests & Comments.
PB
PureBasic Expert
PureBasic Expert
Posts: 7581
Joined: Fri Apr 25, 2003 5:24 pm

Re: keyboard hook gives events without ExamineKeyboard()

Post by PB »

Thanks JHPJHP, that's a great starting point to build from. :)
I compile using 5.31 (x86) on Win 7 Ultimate (64-bit).
"PureBasic won't be object oriented, period" - Fred.
IdeasVacuum
Always Here
Always Here
Posts: 6426
Joined: Fri Oct 23, 2009 2:33 am
Location: Wales, UK
Contact:

Re: keyboard hook gives events without ExamineKeyboard()

Post 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?
IdeasVacuum
If it sounds simple, you have not grasped the complexity.
User avatar
JHPJHP
Addict
Addict
Posts: 2252
Joined: Sat Oct 09, 2010 3:47 am

Re: keyboard hook gives events without ExamineKeyboard()

Post 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

If you're not investing in yourself, you're falling behind.

My PureBasic StuffFREE STUFF, Scripts & Programs.
My PureBasic Forum ➤ Questions, Requests & Comments.
IdeasVacuum
Always Here
Always Here
Posts: 6426
Joined: Fri Oct 23, 2009 2:33 am
Location: Wales, UK
Contact:

Re: keyboard hook gives events without ExamineKeyboard()

Post by IdeasVacuum »

Thanks JHPJHP. I have used GetKeyState() before, just didn't think of it!
IdeasVacuum
If it sounds simple, you have not grasped the complexity.
Post Reply