How can I capture keyboard status when my application doesn't have the focus?
Asked Answered
F

2

9

My girlfriend's new laptop doesn't have indicator LEDs for NumLock and CapsLock, so I wrote a small program which show their status on screen:

procedure TForm1.Timer1Timer(Sender: TObject);  
var  
  KeyState: TKeyboardState;  
begin  
  GetKeyboardState(KeyState);  
  if KeyState[VK_NUMLOCK] = 0 then  
    PanelNumLock.Color := clSilver  
  else  
    PanelNumLock.Color := clLime;  
  if KeyState[VK_CAPITAL] = 0 then  
    PanelCapsLock.Color := clSilver  
  else  
    PanelCapsLock.Color := clLime;  
end;

enter image description here

This works as long as my program has the focus, but when the focus goes to anther program statuses are no longer updated. (However, just moving the mouse over the form, no clicking, is sufficient for updating.)

How can I let the program update when another application has the focus?

Fandango answered 22/12, 2015 at 7:1 Comment(1)
Your last paragraph already suggests one possible solution: use GetCursorPos and SendMessage(..., WM_MOUSEMOVE, ...) to force the message loop to update the current keyboard status in your application if it's in the background.Deprived
K
9

You can simply use GetKeyState in your Timer.

if GetKeyState(VK_NUMLOCK) = 1 then
  PanelNumLock.Color := clLime
else
  PanelNumLock.Color := clSilver;

if GetKeyState(VK_CAPITAL) = 1 then
  PanelCapsLock.Color := clLime
else
  PanelCapsLock.Color := clSilver;

This works even when your application doesn't have the focus. Tested on XP.

Kalinda answered 23/12, 2015 at 9:25 Comment(2)
Works under Win 7 as well. Thanks a bunch. (I hear very bad things about Win 10, but I can heartily recommend upgrading from XP to Win 7. It will have support up to 2020.)Fandango
Just for the record both solutions works under Windows 10Groundmass
G
6

You should use a Low Level Keyboard Hook, because then you can get notified for every keystroke even if you application does not have focus.

I've create a small example for you

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, Vcl.ExtCtrls;

const
  WM_UpdateScreen = WM_USER + 1;

type
  TForm1 = class(TForm)
    PanelCapsLock: TPanel;
    PanelNumlock: TPanel;
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  private
    FHook: hHook;
    KeyState: TKeyboardState;
  public
    procedure UpdateScreen(var message: TMessage); message WM_UpdateScreen;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

type
  pKBDLLHOOKSTRUCT = ^KBDLLHOOKSTRUCT;

  KBDLLHOOKSTRUCT = packed record
    vkCode: DWORD;
    scanCodem: DWORD;
    flags: DWORD;
    time: DWORD;
    dwExtraInfo: ULONG_PTR;
  end;

var
  hkHook: hHook;

function LowLevelKeyboardProc(code: Integer; WParam: WParam; LParam: LParam): LRESULT stdcall;
const
  LLKHF_UP = $0080;
var
  Hook: pKBDLLHOOKSTRUCT;
  bControlKeyDown: Boolean;
begin
  try
    Hook := pKBDLLHOOKSTRUCT(LParam);
    case code of
      HC_ACTION:
        begin
          if (Hook^.flags and LLKHF_UP) <> 0 then
            if Hook.vkCode in [VK_NUMLOCK, VK_CAPITAL] then
              PostMessage(Form1.Handle, WM_UpdateScreen, Hook.vkCode, 0);
        end;
    end;
  finally
    Result := CallNextHookEx(hkHook, code, WParam, LParam);
  end;
end;

procedure HookIt;
begin
  hkHook := SetWindowsHookEx(WH_KEYBOARD_LL, @LowLevelKeyboardProc, hInstance, 0);
end;

procedure UnHookIt;
begin
  UnHookWindowsHookEx(hkHook);
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  UnHookIt;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  GetKeyboardState(KeyState);
  PostMessage(Handle, WM_UpdateScreen, VK_CAPITAL, 1);
  PostMessage(Handle, WM_UpdateScreen, VK_NUMLOCK, 1);
  HookIt;
end;

procedure TForm1.UpdateScreen(var message: TMessage);
begin
  if message.LParam = 0 then
    if KeyState[message.WParam] = 0 then
      KeyState[message.WParam] := 1
    else
      KeyState[message.WParam] := 0;

  if KeyState[VK_NUMLOCK] = 0 then
    PanelNumlock.Color := clSilver
  else
    PanelNumlock.Color := clLime;
  if KeyState[VK_CAPITAL] = 0 then
    PanelCapsLock.Color := clSilver
  else
    PanelCapsLock.Color := clLime;

end;

end.

Basically on formCreate I hook the Keyboard, and tells my program in which function I need my notification. In my example I called it LowLevelKeyboardProc

Then you just need to test fot which key is pressed and if it is one of CapsLock of Num lock then nofify the form.

Groundmass answered 22/12, 2015 at 8:13 Comment(5)
This does not work as expected. Your hook posts WM_UpdateScreen message. Which then calls GetKeyboardState. if the form is not focused the state will not be reflected (back to the OPs initial problem). tested on XP.Kalinda
It did on my computerGroundmass
I cant see how it did. The timer did the same thing. the problem is GetKeyboardState on a non focused window.Kalinda
@Kalinda you are absolutly right! I've just tested my code again. And now I've updated the example.Groundmass
Works just fine now. +1 for a nice WH_KEYBOARD_LL example.Kalinda

© 2022 - 2024 — McMap. All rights reserved.