Page 1 of 1

Dead keys with keyboard hook - how?

Posted: Wed Dec 24, 2025 3:33 am
by BarryG
Hi all. Hope you have a Merry Christmas. :)

I'm using a modified version of the keyboard hook code by Michael (viewtopic.php?p=600684#p600684), and a user has emailed me saying that my app doesn't let him type in his native language anymore, because the "dead keys" of a Portuguese layout aren't being recognized. I'm using the keyboard layout shown in the screenshot below.

So here's a small test snippet where basically I'm using his keyboard language and trying to hold down ^ and then type a to get ä. It's not working, and quite frankly I'm not sure how to fix it. What makes it harder is that my app does auto-completion of typed text, so as you can see in the snippet I'm using the "text$" variable to track what's been typed, so this needs to be taken in consideration as well.

Note: The snippet doesn't take the Shift key into account, but it should (so Shift+A should show "A" and not "a" like it does now).

The end goal is for "text$" to be Hi ä there when I type Hi {shiftdown}^{shiftup}a there.

PS: The snippet works perfectly fine with an English keyboard layout.

Image

Code: Select all

Structure KeyboardState
  b.b[256]
EndStructure

Global kbs.KeyboardState, Hook, text$

Procedure.l myKeyboardHook(nCode, wParam, *p.KBDLLHOOKSTRUCT)
  
  If nCode = #HC_ACTION
    If wParam = #WM_KEYUP Or wParam = #WM_SYSKEYUP
    
      key = *p\vkcode
      
      k$=Space(9)
      ToAscii_(key,MapVirtualKey_(key,#MAPVK_VK_TO_CHAR),@kbs,@k$,0)
      text$ + Trim(k$)
      
      SetGadgetText(1, text$)
      
    EndIf
  EndIf
  
  ProcedureReturn CallNextHookEx_(0, nCode, wParam, *p)
  
EndProcedure

OpenWindow(0,0,0,500,180,"Hook Test",#PB_Window_SystemMenu|#PB_Window_ScreenCentered)
StringGadget(0,10,10,480,130,"")
TextGadget(1,10,150,480,20,"")

SetActiveGadget(0)

SetWindowsHookEx_(#WH_KEYBOARD_LL,@myKeyboardHook(),GetModuleHandle_(0),0)

Repeat
  Select WaitWindowEvent()
    Case #PB_Event_CloseWindow
      quit=1
  EndSelect
Until quit

Re: Dead keys with keyboard hook - how?

Posted: Wed Dec 24, 2025 11:11 am
by breeze4me
There is a way to track character input rather than individual key presses, as shown in the code below. If necessary, use it with a keyboard hook.
There's also a WM_DEADCHAR message, so check it out if needed.
https://learn.microsoft.com/en-us/windo ... m-deadchar

Code: Select all

Structure KeyboardState
  b.b[256]
EndStructure

Global kbs.KeyboardState, Hook, text$

Procedure.l myGetMsgProc(nCode, wParam, *p.MSG)
  
  If nCode = #HC_ACTION
    
    If *p\message = #WM_CHAR
      
      text$ + Chr(*p\wParam)
      
      SetGadgetText(1, text$)
      
    EndIf
  EndIf
  
  ProcedureReturn CallNextHookEx_(0, nCode, wParam, *p)
  
EndProcedure

OpenWindow(0,0,0,500,180,"Hook Test",#PB_Window_SystemMenu|#PB_Window_ScreenCentered)
StringGadget(0,10,10,480,130,"")
TextGadget(1,10,150,480,20,"")

SetActiveGadget(0)

Hook = SetWindowsHookEx_(#WH_GETMESSAGE,@myGetMsgProc(),GetModuleHandle_(0),GetCurrentThreadId_())

Repeat
  Select WaitWindowEvent()
    Case #PB_Event_CloseWindow
      quit=1
  EndSelect
Until quit

UnhookWindowsHookEx_(Hook)


Re: Dead keys with keyboard hook - how?

Posted: Wed Dec 24, 2025 11:01 pm
by BarryG
Thanks for the example, breeze4me. :) Works perfectly like that, but I can't seem to make it work globally outside my app (for example with Notepad so that the typed chars are correct in Notepad and also correctly shown in the TextGadget). I will try again tomorrow, as it's Christmas morning here in Australia (8am). Thanks for the present! :mrgreen:

Re: Dead keys with keyboard hook - how?

Posted: Thu Dec 25, 2025 8:58 am
by breeze4me
BarryG wrote: Wed Dec 24, 2025 11:01 pm (for example with Notepad so that the typed chars are correct in Notepad and also correctly shown in the TextGadget).
Merry Christmas! :mrgreen:

Tested on Windows 10.
If the handles for Notepad are not found, please fix it yourself. :wink:


Hook.dll code:

Code: Select all

EnableExplicit

#APP_HookInput = #WM_APP + 246
#GUID_FileMap$ = "MyAppFM_bd9b51f3-9905-4b7a-a63b-7ea7353e2c42"

Global g_hMod, g_hFileMap, *g_MapView

ProcedureDLL.l myGetMsgProc(nCode, wParam, *p.MSG)
  Protected hWnd
  
  If nCode = #HC_ACTION
    
    If *p\message = #WM_CHAR
      If *g_MapView
        hWnd = PeekI(*g_MapView)
        If hWnd
          SendMessage_(hWnd, #APP_HookInput, *p\hwnd, *p\wParam)
        EndIf
      EndIf
    EndIf
    
  EndIf
  
  ProcedureReturn CallNextHookEx_(0, nCode, wParam, *p)
EndProcedure

ProcedureDLL SetHook(hWndTarget)
  Protected hHook, tid.l
  
  If hWndTarget
    tid = GetWindowThreadProcessId_(hWndTarget, 0)
    If tid
      hHook = SetWindowsHookEx_(#WH_GETMESSAGE, @myGetMsgProc(), g_hMod, tid)
    EndIf
  EndIf
  
  ProcedureReturn hHook
EndProcedure

ProcedureDLL UnHook(hHook)
  ProcedureReturn UnhookWindowsHookEx_(hHook)
EndProcedure

ProcedureDLL AttachProcess(Instance)
  g_hMod = Instance
  g_hFileMap = OpenFileMapping_(#FILE_MAP_READ, 0, #GUID_FileMap$)
  If g_hFileMap
    *g_MapView = MapViewOfFile_(g_hFileMap, #FILE_MAP_READ, 0, 0, 64)
  EndIf
EndProcedure

ProcedureDLL DetachProcess(Instance)
  
  If *g_MapView
    UnmapViewOfFile_(*g_MapView)
  EndIf
  If g_hFileMap
    CloseHandle_(g_hFileMap)
  EndIf
  
EndProcedure
Main code:

Code: Select all

#APP_HookInput = #WM_APP + 246

#GUID_FileMap$ = "MyAppFM_bd9b51f3-9905-4b7a-a63b-7ea7353e2c42"

Prototype ptSetHook(hWndTarget)
Prototype ptUnHook(hHook)

Global SetHook.ptSetHook
Global UnHook.ptUnHook


Global NewList g_hHook()
Global NewMap g_Text$()

Global g_hFileMap, *g_MapView


If OpenLibrary(0, "Hook.dll")
  SetHook = GetFunction(0, "SetHook")
  UnHook = GetFunction(0, "UnHook")
  
  If SetHook And UnHook
    Debug "Open."
  Else
    Debug "Cannot open the hook Dll."
    End
  EndIf
EndIf

Procedure WndProc_Main(hWnd, uMsg, wParam, lParam)
  If uMsg = #APP_HookInput
    
    g_Text$(Str(wParam)) + Chr(lParam)
    Debug "Target hWnd: " + wParam + " " + g_Text$()
    
    ProcedureReturn 1
  EndIf
  
  ProcedureReturn #PB_ProcessPureBasicEvents
EndProcedure


;-----------------------------------------------------------
;- Test
;-----------------------------------------------------------

; Open two Notepad windows.
RunProgram("notepad.exe")
RunProgram("notepad.exe")

Sleep_(1500)

Global NewList g_HwndNotepad()

; Save the handles of two Notepad windows.
Procedure EnumWindowsProc(hWnd, lParam)
  Protected s.s{64}
  
  If GetClassName_(hWnd, @s, 60)
    If s = "Notepad"
      If AddElement(g_HwndNotepad())
        g_HwndNotepad() = hWnd
      EndIf
    EndIf
  EndIf
  
  ProcedureReturn #True
EndProcedure

EnumWindows_(@EnumWindowsProc(), 0)

If ListSize(g_HwndNotepad()) <> 2
  Debug "Notepad instances <> 2. Quit."
  End
EndIf


If OpenWindow(0, 0, 0, 270, 80, "", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  ButtonGadget(1, 10, 20, 120, 30, "Set Hook")
  ButtonGadget(2, 140, 20, 120, 30, "UnHook")
  
  SetWindowCallback(@WndProc_Main(), 0)
  
  
  ; After creating shared memory, store the window handle value that will receive messages.
  g_hFileMap = CreateFileMapping_(#INVALID_HANDLE_VALUE, 0, #PAGE_READWRITE | #SEC_COMMIT, 0, 64, #GUID_FileMap$)
  If g_hFileMap
    *g_MapView = MapViewOfFile_(g_hFileMap, #FILE_MAP_ALL_ACCESS, 0, 0, 64)
    If *g_MapView
      PokeI(*g_MapView, WindowID(0))
    EndIf
  EndIf
  
  Repeat
    Select WaitWindowEvent()
      Case #PB_Event_CloseWindow
        Quit = 1
        
      Case #PB_Event_Gadget
        Select EventGadget()
          Case 1
            
            ForEach g_HwndNotepad()
              hHook = SetHook(g_HwndNotepad())
              If hHook
                If AddElement(g_hHook())
                  g_hHook() = hHook
                EndIf
                Debug "hHook : " + hHook
              EndIf
            Next
            
          Case 2
            ForEach g_hHook()
              If g_hHook()
                Result = UnhookWindowsHookEx_(g_hHook())
                Debug "UnHook: " + g_hHook() + " " + Result
                g_hHook() = 0
              EndIf
            Next
            
            ClearList(g_hHook())
            
        EndSelect
    EndSelect
  Until Quit
  
EndIf

If *g_MapView
  UnmapViewOfFile_(*g_MapView)
EndIf

If g_hFileMap
  CloseHandle_(g_hFileMap)
EndIf

Re: Dead keys with keyboard hook - how?

Posted: Fri Dec 26, 2025 1:41 am
by BarryG
Thanks for trying, but this line doesn't output anything on Win 11:

Code: Select all

Debug "Target hWnd: " + wParam + " " + g_Text$()
Don't know if it's an easy fix? I don't know what to fix anyway. :(

Re: Dead keys with keyboard hook - how?

Posted: Fri Dec 26, 2025 3:52 am
by breeze4me
The issue seems to be that the hook is only installed on a single thread associated with a specific window. The code below installs a global hook without specifying a thread, so it should work.

Hook.dll code:

Code: Select all

EnableExplicit

#APP_HookInput = #WM_APP + 246
#GUID_FileMap$ = "MyAppFM_bd9b51f3-9905-4b7a-a63b-7ea7353e2c42"

Global g_hMod, g_hFileMap, *g_MapView

ProcedureDLL.l myGetMsgProc(nCode, wParam, *p.MSG)
  Protected hWnd
  
  If nCode = #HC_ACTION
    
    If *p\message = #WM_CHAR
      If *g_MapView
        hWnd = PeekI(*g_MapView)
        If hWnd
          SendMessage_(hWnd, #APP_HookInput, *p\hwnd, *p\wParam)
        EndIf
      EndIf
    EndIf
    
  EndIf
  
  ProcedureReturn CallNextHookEx_(0, nCode, wParam, *p)
EndProcedure

ProcedureDLL SetHook()
  ProcedureReturn SetWindowsHookEx_(#WH_GETMESSAGE, @myGetMsgProc(), g_hMod, 0)
EndProcedure

ProcedureDLL UnHook(hHook)
  ProcedureReturn UnhookWindowsHookEx_(hHook)
EndProcedure

ProcedureDLL AttachProcess(Instance)
  g_hMod = Instance
  g_hFileMap = OpenFileMapping_(#FILE_MAP_READ, 0, #GUID_FileMap$)
  If g_hFileMap
    *g_MapView = MapViewOfFile_(g_hFileMap, #FILE_MAP_READ, 0, 0, 64)
  EndIf
EndProcedure

ProcedureDLL DetachProcess(Instance)
  
  If *g_MapView
    UnmapViewOfFile_(*g_MapView)
  EndIf
  If g_hFileMap
    CloseHandle_(g_hFileMap)
  EndIf
  
EndProcedure
Main code:

Code: Select all

#APP_HookInput = #WM_APP + 246
#GUID_FileMap$ = "MyAppFM_bd9b51f3-9905-4b7a-a63b-7ea7353e2c42"

Prototype ptSetHook()
Prototype ptUnHook(hHook)

Global SetHook.ptSetHook
Global UnHook.ptUnHook


Global NewMap g_Text$()

Global g_hFileMap, *g_MapView


If OpenLibrary(0, "Hook.dll")
  SetHook = GetFunction(0, "SetHook")
  UnHook = GetFunction(0, "UnHook")
EndIf

If SetHook And UnHook
  Debug "Open."
Else
  Debug "Cannot open the hook Dll."
  End
EndIf

Procedure WndProc_Main(hWnd, uMsg, wParam, lParam)
  If uMsg = #APP_HookInput
    
    g_Text$(Str(wParam)) + Chr(lParam)
    Debug "Target hWnd: " + wParam + " " + g_Text$()
    
    ProcedureReturn 1
  EndIf
  
  ProcedureReturn #PB_ProcessPureBasicEvents
EndProcedure



RunProgram("notepad.exe")
RunProgram("notepad.exe")

Sleep_(1500)


If OpenWindow(0, 0, 0, 270, 80, "", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  ButtonGadget(1, 10, 20, 120, 30, "Set Hook")
  ButtonGadget(2, 140, 20, 120, 30, "UnHook")
  
  SetWindowCallback(@WndProc_Main(), 0)
  
  
  ; After creating shared memory, store the window handle value that will receive messages.
  g_hFileMap = CreateFileMapping_(#INVALID_HANDLE_VALUE, 0, #PAGE_READWRITE | #SEC_COMMIT, 0, 64, #GUID_FileMap$)
  If g_hFileMap
    *g_MapView = MapViewOfFile_(g_hFileMap, #FILE_MAP_ALL_ACCESS, 0, 0, 64)
    If *g_MapView
      PokeI(*g_MapView, WindowID(0))
    EndIf
  EndIf
  
  Repeat
    Select WaitWindowEvent()
      Case #PB_Event_CloseWindow
        Quit = 1
        
      Case #PB_Event_Gadget
        Select EventGadget()
          Case 1
            If hHook = 0
              hHook = SetHook()
              If hHook
                Debug "hHook : " + hHook
              EndIf
            EndIf
            
          Case 2
            If hHook
              ;Result = UnhookWindowsHookEx_(hHook)
              Result = UnHook(hHook)
              Debug "UnHook: " + hHook + " " + Result
              hHook = 0
            EndIf
            
        EndSelect
    EndSelect
  Until Quit
  
EndIf

If *g_MapView
  UnmapViewOfFile_(*g_MapView)
EndIf

If g_hFileMap
  CloseHandle_(g_hFileMap)
EndIf

Re: Dead keys with keyboard hook - how?

Posted: Fri Dec 26, 2025 8:15 am
by BarryG
Almost there... keys like Esc and Enter aren't detected on some apps that don't recognise them. Is there a way to make them always get detected, no matter which window or app has the focus? BTW, I appreciate your help.

Re: Dead keys with keyboard hook - how?

Posted: Fri Dec 26, 2025 10:05 am
by breeze4me
If necessary, use it with a keyboard hook.
As mentioned in the first answer, you can use a keyboard(low level) hook together.
Where to ignore and where to allow specific key inputs depends on the situation, so decide based on your needs.

Hook.dll code:

Code: Select all

EnableExplicit

#APP_HookInput = #WM_APP + 246
#GUID_FileMap$ = "MyAppFM_bd9b51f3-9905-4b7a-a63b-7ea7353e2c42"

Global g_hMod, g_hFileMap, *g_MapView

ProcedureDLL.l myGetMsgProc(nCode, wParam, *p.MSG)
  Protected hWnd
  
  If nCode = #HC_ACTION
    
    ; Whether to ignore the press of specific keys here is up to you.
    
    ;If *p\message = #WM_CHAR
    If *p\message = #WM_CHAR And (*p\wParam <> #VK_ESCAPE And *p\wParam <> #VK_RETURN)
      If *g_MapView
        hWnd = PeekI(*g_MapView)
        If hWnd
          SendMessage_(hWnd, #APP_HookInput, *p\hwnd, *p\wParam)
        EndIf
      EndIf
    EndIf
    
  EndIf
  
  ProcedureReturn CallNextHookEx_(0, nCode, wParam, *p)
EndProcedure

ProcedureDLL SetHook()
  ProcedureReturn SetWindowsHookEx_(#WH_GETMESSAGE, @myGetMsgProc(), g_hMod, 0)
EndProcedure

ProcedureDLL UnHook(hHook)
  ProcedureReturn UnhookWindowsHookEx_(hHook)
EndProcedure

ProcedureDLL AttachProcess(Instance)
  g_hMod = Instance
  g_hFileMap = OpenFileMapping_(#FILE_MAP_READ, 0, #GUID_FileMap$)
  If g_hFileMap
    *g_MapView = MapViewOfFile_(g_hFileMap, #FILE_MAP_READ, 0, 0, 64)
  EndIf
EndProcedure

ProcedureDLL DetachProcess(Instance)
  
  If *g_MapView
    UnmapViewOfFile_(*g_MapView)
  EndIf
  If g_hFileMap
    CloseHandle_(g_hFileMap)
  EndIf
  
EndProcedure
Main code:

Code: Select all

#APP_HookInput = #WM_APP + 246
#APP_HookKBD = #WM_APP + 247

#GUID_FileMap$ = "MyAppFM_bd9b51f3-9905-4b7a-a63b-7ea7353e2c42"

Prototype ptSetHook()
Prototype ptUnHook(hHook)

Global SetHook.ptSetHook
Global UnHook.ptUnHook


Global NewMap g_Text$()

Global g_hFileMap, *g_MapView


Procedure.l myKeyboardHook(nCode, wParam, *p.KBDLLHOOKSTRUCT)
  
  If nCode = #HC_ACTION
    
    If (wParam = #WM_KEYUP Or wParam = #WM_SYSKEYUP) And (*p\vkcode = #VK_ESCAPE Or *p\vkcode = #VK_RETURN)
      SendMessage_(WindowID(0), #APP_HookKBD, 0, *p\vkcode)
      
      ; Or you can send it using the same event code. Choose as needed.
      ;SendMessage_(WindowID(0), #APP_HookInput, 0, *p\vkcode)
    EndIf
    
  EndIf
  
  ProcedureReturn CallNextHookEx_(0, nCode, wParam, *p)
EndProcedure

If OpenLibrary(0, "Hook.dll")
  SetHook = GetFunction(0, "SetHook")
  UnHook = GetFunction(0, "UnHook")
EndIf

If SetHook And UnHook
  Debug "Open."
Else
  Debug "Cannot open the hook Dll."
  End
EndIf

Procedure WndProc_Main(hWnd, uMsg, wParam, lParam)
  
  If uMsg = #APP_HookKBD
    Debug "Key pressed: " + lParam
  EndIf
  
  If uMsg = #APP_HookInput
    
    g_Text$(Str(wParam)) + Chr(lParam)
    Debug "Target hWnd: " + wParam + " " + g_Text$()
    
    ProcedureReturn 1
  EndIf
  
  ProcedureReturn #PB_ProcessPureBasicEvents
EndProcedure



RunProgram("notepad.exe")
RunProgram("notepad.exe")

Sleep_(1500)


If OpenWindow(0, 0, 0, 270, 80, "", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  ButtonGadget(1, 10, 20, 120, 30, "Set Hook")
  ButtonGadget(2, 140, 20, 120, 30, "UnHook")
  
  SetWindowCallback(@WndProc_Main(), 0)
  
  
  ; After creating shared memory, store the window handle value that will receive messages.
  g_hFileMap = CreateFileMapping_(#INVALID_HANDLE_VALUE, 0, #PAGE_READWRITE | #SEC_COMMIT, 0, 64, #GUID_FileMap$)
  If g_hFileMap
    *g_MapView = MapViewOfFile_(g_hFileMap, #FILE_MAP_ALL_ACCESS, 0, 0, 64)
    If *g_MapView
      PokeI(*g_MapView, WindowID(0))
    EndIf
  EndIf
  
  Repeat
    Select WaitWindowEvent()
      Case #PB_Event_CloseWindow
        Quit = 1
        
      Case #PB_Event_Gadget
        Select EventGadget()
          Case 1
            If hHook = 0
              hHook = SetHook()
              If hHook
                Debug "hHook : " + hHook
              EndIf
            EndIf
            
            If hHookKBD = 0
              hHookKBD = SetWindowsHookEx_(#WH_KEYBOARD_LL, @myKeyboardHook(), GetModuleHandle_(0), 0)
              If hHookKBD
                Debug "hHookKBD : " + hHookKBD
              EndIf
            EndIf
            
          Case 2
            If hHook
              Result = UnHook(hHook)
              ;Result = UnhookWindowsHookEx_(hHook)
              Debug "UnHook(hHook): " + hHook + " " + Result
              hHook = 0
            EndIf
            
            If hHookKBD
              Result = UnhookWindowsHookEx_(hHookKBD)
              Debug "UnHook(hHookKBD): " + hHookKBD + " " + Result
              hHookKBD = 0
            EndIf
            
        EndSelect
    EndSelect
  Until Quit
  
EndIf

If *g_MapView
  UnmapViewOfFile_(*g_MapView)
EndIf

If g_hFileMap
  CloseHandle_(g_hFileMap)
EndIf

Re: Dead keys with keyboard hook - how?

Posted: Fri Dec 26, 2025 10:20 am
by BarryG
Thank you, kind sir. It now fully works as I need. :) Can you PM me, please?

BTW, this GUID can be anything, right? Just something unique to my app?

Code: Select all

#GUID_FileMap$ = "MyAppFM_bd9b51f3-9905-4b7a-a63b-7ea7353e2c42"

Re: Dead keys with keyboard hook - how?

Posted: Fri Dec 26, 2025 10:32 am
by breeze4me
BarryG wrote: Fri Dec 26, 2025 10:20 am BTW, this GUID can be anything, right? Just something unique to my app?

Code: Select all

#GUID_FileMap$ = "MyAppFM_bd9b51f3-9905-4b7a-a63b-7ea7353e2c42"
Yes, if the string is unique worldwide. :mrgreen:

Re: Dead keys with keyboard hook - how?

Posted: Sat Dec 27, 2025 9:41 am
by Rinzwind
FYI: won't catch all keys... ie anywhere in MS word or in start menu. The application itself prevents passing messages on?

Re: Dead keys with keyboard hook - how?

Posted: Sat Dec 27, 2025 11:06 am
by breeze4me
Rinzwind wrote: Sat Dec 27, 2025 9:41 am FYI: won't catch all keys... ie anywhere in MS word or in start menu. The application itself prevents passing messages on?
For applications that are not traditional Win32 applications(such as some modern apps like Universal Windows Platform (UWP) apps), the WM_CHAR message may not be hooked.
Depending on the frameworks used by apps, even low level keyboard hooks may or may not be possible.
Even so, for the Windows Start menu (at least in Windows 10), key input can be monitored using the WH_KEYBOARD_LL hook.

The AI's explanation is more detailed, so take a look.
Why standard WH_KEYBOARD_LL hooks are problematic for UWP

App Container Sandbox: UWP apps run within a secure, isolated app container that limits their access to system-wide functions, including cross-process hooking mechanisms.

DLL Injection Restrictions: Global hooks typically require a DLL to be injected into the address space of other processes. For UWP and Windows Store app processes, hook DLLs are not loaded in-process unless installed by UIAccess processes (like accessibility tools).

API Limitations: Access to certain traditional Win32 APIs may be restricted or behave differently within the UWP environment.