0

With reference to this question, and more specifically to this answer, it seems that dead keys are captured on a keyboad hook only after a MSB manipulation. The author of the answer left a fixed code, but I do not know what the Delphi equivalent is.

My actual code:

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  Windows,
  Messages,
  SysUtils;

var
  hhk: HHOOK;
  Msg: TMsg;

{==================================================================================}

function ReverseBits(b: Byte): Byte;
var
  i: Integer;
begin
  Result := 0;
  for i := 1 to 8 do
  begin
    Result := (Result shl 1) or (b and 1);
    b := b shr 1;
  end;
end;

function GetCharFromVirtualKey(Key: Word): string;
var
  keyboardState: TKeyboardState;
  keyboardLayout: HKL;
  asciiResult: Integer;
begin
  GetKeyboardState(keyboardState);
  keyboardLayout := GetKeyboardLayout(0);
  SetLength(Result, 2);
  asciiResult := ToAsciiEx(Key, ReverseBits(MapVirtualKey(Key, MAPVK_VK_TO_CHAR)), keyboardState, @Result[1], 0, keyboardLayout);
  case asciiResult of
    0:
      Result := '';
    1:
      SetLength(Result, 1);
    2:
      ;
  else
    Result := '';
  end;
end;

{==================================================================================}

function LowLevelKeyboardProc(nCode: Integer; wParam: wParam; lParam: lParam): LRESULT; stdcall;
type
  PKBDLLHOOKSTRUCT = ^TKBDLLHOOKSTRUCT;

  TKBDLLHOOKSTRUCT = record
    vkCode: cardinal;
    scanCode: cardinal;
    flags: cardinal;
    time: cardinal;
    dwExtraInfo: Cardinal;
  end;

  PKeyboardLowLevelHookStruct = ^TKeyboardLowLevelHookStruct;

  TKeyboardLowLevelHookStruct = TKBDLLHOOKSTRUCT;
var
  LKBDLLHOOKSTRUCT: PKeyboardLowLevelHookStruct;
begin
  case nCode of
    HC_ACTION:
      begin
        if wParam = WM_KEYDOWN then
        begin
          LKBDLLHOOKSTRUCT := PKeyboardLowLevelHookStruct(lParam);
          Writeln(GetCharFromVirtualKey(LKBDLLHOOKSTRUCT^.vkCode));
        end;
      end;
  end;
  Result := CallNextHookEx(hhk, nCode, wParam, lParam);
end;

procedure InitHook;
begin
  hhk := SetWindowsHookEx(WH_KEYBOARD_LL, @LowLevelKeyboardProc, HInstance, 0);
  if hhk = 0 then
    RaiseLastOSError;
end;

procedure KillHook;
begin
  if (hhk <> 0) then
    UnhookWindowsHookEx(hhk);
end;

begin
  try
    InitHook;
    while GetMessage(Msg, 0, 0, 0) do
    begin
      TranslateMessage(Msg);
      DispatchMessage(Msg);
    end;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Which output do you expect from your program if it was correct? – fpiette Jun 09 '21 at 15:49
  • @fpiette, Ex: *Shift* + *2* = **@**, *Shift* + *"a"* = **A**, *Caps* = **All a..z uppercase** ... –  Jun 09 '21 at 15:54
  • 4
    @Coringa Why are you reversing the bits of the return value of `MapVirtualKey()`? And why as a `Byte`, when `MapVirtualKey()` returns a `UINT`? The answer you linked to is not reversing any bits at all. It is simply shifting them down so it can check if the high bit is 1. The Delphi equivalent of that answer would be `if ((MapVirtualKey(Key, MAPVK_VK_TO_CHAR) shr 31) and 1) = 0 then ...` or simpler `if (MapVirtualKey(Key, MAPVK_VK_TO_CHAR) and $80000000) = 0 then ...` Or even `Int32(MapVirtualKey(Key, MAPVK_VK_TO_CHAR)) >= 0 then ...` – Remy Lebeau Jun 09 '21 at 16:16
  • @Coringa I think you are misunderstanding of what Dead keys actually are. Dead keys are special keys or key combinations that are not processed immediately but instead combined with next key input event provided that dead key can be combined with next key. If it can be combined one `combined character` is returned. If not two separate characters (one representing the dead key and one representing the second input) are returned one after another. So basically you need to handle dead keys in two consecutive keyboard hook events. – SilverWarior Jun 09 '21 at 18:59
  • @Coringa But based on your comment above it seems that you might be more interested in handling of Shift State which can tell you if keys like Shift, Ctrl or Alt are pressed down or whether `Caps Lock`, `Num Lock`, or `Scroll Lock` are enabled or disabled. these are not refereed as Dead Keys but Modifier Keys. – SilverWarior Jun 09 '21 at 19:03
  • @Remy Lebeau and SilverWarior. thank your comments. All solved here :D. –  Jun 09 '21 at 20:44
  • @Coringa Would you create an answer with the final correct code you have? This would help the community in the future. – fpiette Jun 10 '21 at 05:48

1 Answers1

1

In answer to last comment of @fpiette, here is my solution:

program Project1;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  Windows,
  Messages,
  SysUtils;

var
  hhk: HHOOK;
  Msg: TMsg;

function IsTextCharForKeyTip(AKey: Word): Boolean;
var
  keyboardLayout: HKL;
  ActiveWindow: HWND;
  ActiveThreadID: DWord;
  ARes: UINT;
begin
  ActiveWindow := GetForegroundWindow;
  ActiveThreadID := GetWindowThreadProcessId(ActiveWindow, nil);
  keyboardLayout := GetKeyboardLayout(ActiveThreadID);
  ARes := MapVirtualKeyEx(AKey, MAPVK_VK_TO_CHAR, keyboardLayout);
  Result := ((ARes and $FFFF0000) = 0) and (Char(ARes) <> ' ') and (CharInSet(Char(ARes), [#32..#255]));
end;

function LowLevelKeyboardProc(nCode: Integer; wParam: wParam; lParam: lParam): LRESULT; stdcall;
type
  PKBDLLHOOKSTRUCT = ^TKBDLLHOOKSTRUCT;

  TKBDLLHOOKSTRUCT = record
    vkCode: cardinal;
    scanCode: cardinal;
    flags: cardinal;
    time: cardinal;
    dwExtraInfo: cardinal;
  end;

  PKeyboardLowLevelHookStruct = ^TKeyboardLowLevelHookStruct;

  TKeyboardLowLevelHookStruct = TKBDLLHOOKSTRUCT;
var
  LKBDLLHOOKSTRUCT: PKeyboardLowLevelHookStruct;
  keyboardState: TKeyboardState;
  keyboardLayout: HKL;
  ScanCode: Integer;
  ActiveWindow: HWND;
  ActiveThreadID: DWord;
  CapsKey, ShiftKey: Boolean;
  KeyString: string;
begin
  Result := CallNextHookEx(hhk, nCode, wParam, lParam);
  case nCode of
    HC_ACTION:
      begin
        CapsKey := False;
        ShiftKey := False;
        if Odd(GetKeyState(VK_CAPITAL)) then
          if GetKeyState(VK_SHIFT) < 0 then
            CapsKey := False
          else
            CapsKey := True
        else if GetKeyState(VK_SHIFT) < 0 then
          ShiftKey := True
        else
          ShiftKey := False;

        if GetKeyState(VK_BACK) < 0 then
          Write('[Backspace]') { #08 }; // #08, overwrites what was deleted on the console :D.

        if GetKeyState(VK_SPACE) < 0 then
          Write(#32);

        if GetKeyState(VK_TAB) < 0 then
          Write(#09);

        case wParam of
          WM_KEYDOWN, WM_SYSKEYDOWN:
            begin
              LKBDLLHOOKSTRUCT := PKeyboardLowLevelHookStruct(lParam);

              if (not IsTextCharForKeyTip(LKBDLLHOOKSTRUCT^.vkCode)) then
                Exit;

              SetLength(KeyString, 2);
              ActiveWindow := GetForegroundWindow;
              ActiveThreadID := GetWindowThreadProcessId(ActiveWindow, nil);
              keyboardLayout := GetKeyboardLayout(ActiveThreadID);
              GetKeyboardState(keyboardState);
              ScanCode := MapVirtualKeyEx(LKBDLLHOOKSTRUCT^.vkCode, MAPVK_VK_TO_CHAR, keyboardLayout);
              ToAsciiEx(LKBDLLHOOKSTRUCT^.vkCode, ScanCode, keyboardState, @KeyString[1], 0, keyboardLayout);

              if CapsKey or ShiftKey then
                Write(UpperCase(KeyString[1]))
              else
                Write(LowerCase(KeyString[1]));
            end;
        end;
      end;
  end;
end;

procedure InitHook;
begin
  hhk := SetWindowsHookEx(WH_KEYBOARD_LL, @LowLevelKeyboardProc, HInstance, 0);
  if hhk = 0 then
    RaiseLastOSError;
end;

procedure KillHook;
begin
  if (hhk <> 0) then
    UnhookWindowsHookEx(hhk);
end;

begin
  try
    InitHook;
    while True do
    begin
      if PeekMessage(Msg, 0, 0, 0, 0) then
      begin
        GetMessage(Msg, 0, 0, 0);
        TranslateMessage(Msg);
        DispatchMessage(Msg);
      end;
    end;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;

end.
  • I note that this still doesn't fulfil what you said you wanted - it won't give you @ for shift key held down with 2 pressed (neither will my keyboard as I'm not in the US!) It might be helpful to other users if you highlighted this as a partial solution to what you originally wanted. – Rob Lambden Jun 10 '21 at 13:45
  • 1
    This doesn't seems to work as I expect according to what you asked. I'm using a [French-Belgian keyboard](https://commons.wikimedia.org/wiki/File:Belgian_keyboard_layout.png). The 3rd key on the top row has symbols "é" (Key alone), "2" (Shift) and "@" (AltGr). Key alone and key+shift works. But Key + AltGr doesn't work. You code doesn't display anything while it should display "@". This is the same with all keys having an AltGr code associated. – fpiette Jun 11 '21 at 07:12
  • @fpiette, i don't know fix it. Some suggesion? –  Jun 11 '21 at 14:43
  • Perhaps: `var ScanCode: Integer; ... ScanCode := MapVirtualKeyEx(LKBDLLHOOKSTRUCT^.vkCode, MAPVK_VK_TO_CHAR, KeyboardLayout); ToAsciiEx(LKBDLLHOOKSTRUCT^.vkCode, ScanCode, ...` ? –  Jun 11 '21 at 15:00
  • @Coringa You should edit your answer to say "...here is my **partial** solution. Remains to make Alt-Gr working." – fpiette Jun 11 '21 at 15:33
  • @fpiette, new code on answer, *Alt-Gr* is working!. Now i think that will work to several keyboard layout. –  Jun 11 '21 at 18:00
  • @Coringa It work now for "@" but not for all Alt-Gr enabled keys. For example, the key having symbols "^" (Key alone), "¨" (shift) and "[" (Alt-Gr) on [French-Belgian keyboard layout](https://commons.wikimedia.org/wiki/File:Belgian_keyboard_layout.png) doesn't work. Probably because both "^" and "¨" on the key are dead keys used to produce "â" or "ê" or "ü" and more... Also note that your code break the accented character for other application as well. Probably the combination of dead key and Alt-Gr is causing the trouble. – fpiette Jun 11 '21 at 19:10
  • @fpiette, *`" note that your code break the accented character for other application as well."`* - i will try fix it. But if someone already have a solution, say me please :D. –  Jun 11 '21 at 19:54
  • @fpiette, *`"note that your code break the accented character for other application as well."`* - Seems that [not have a solution to this](https://forums.codeguru.com/showthread.php?480514-ToUnicodeEx-and-dead-keys-inside-system-wide-hooks). Will work only if remove [`ToAsciiEx`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-toasciiex) call. Then us must choose (between us and user) who see the correct output :-). –  Jun 11 '21 at 21:17