delphikeyboard-shortcutsdelphi-10.1-berlintaction

How to avoid repeating execution of TAction.Shortcut?


In Delphi 10.1.2, inside a TActionList I have created a TAction with these properties and assigned a shortcut Ctrl+F12 to it:

enter image description here

At runtime, when I keep the shortcut keys Ctrl+F12 pressed, the action is executed repeatedly (with speed depending on the system keyboard repeating speed).

So how can I make the action execute only ONCE (until these keys are lifted up), even if the user keeps the keys pressed down or if the user's system has a high keyboard repeat speed setting?


Solution

  • You can retrieve system keyboard settings by using SystemParametersInfo. SPI_GETKEYBOARDDELAY gives the repeat delay; the time between the first and second generated events. SPI_GETKEYBOARDSPEED gives keyboard repeat rate; the duration between events after the initial delay. The documentation has approximate values of the slowest and fastest settings which may help you decide on an acting point.

    Suppose you decided to act on. Since shortcuts expose no direct relation to the event that generated them, they have no property or anything that could reveal information about if it is an initial, delayed, or repeated execution.

    Then, one option is to disable a shortcut's execution as soon as it is entered and re-enable after the appropriate key has been released. You have to set KeyPreview of the form to true to implement this solution as any of the controls on the form might be the one with the focus when a shortcut is generated.

    A less cumbersome solution I would find is to prevent generation of the shortcut when it's not generated by an initial key-down. You have to watch for key down events this time.

    One possible implementation can be to install a local keyboard hook.

    var
      KeybHook: HHOOK;
    
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      KeybHook := SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, 0, GetCurrentThreadId);
    end;
    
    procedure TForm1.FormDestroy(Sender: TObject);
    begin
      UnhookWindowsHookEx(KeybHook);
    end;
    

    It's probably tempting to test for the repeat count of the interested key in the callback, however, as mentioned in the documentation for WM_KEYDOWN, the repeat count is not cumulative. What that practically means is that the OS does not supply this information. The previous key state information is provided though. That would be bit 30 of the "lParam" of the callback. You can prevent any keyboard message when your shortcut keys are down, and the primary shortcut has been previously down, from reaching the window procedure of the focused control.

    function KeyboardProc(code: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT;
        stdcall;
    begin
      if code > 0 then begin
        if (wParam = vk_f12) and (GetKeyState(VK_CONTROL) < 0) and
            ((lParam and $40000000) > 0) then begin
          Result := 1;
          Exit;
        end;
      end;
      Result := CallNextHookEx(KeybHook, code, wParam, lParam);
    end;
    


    Lastly, don't disregard the probability that if a user's system has been set up with a fast keyboard repeat, it's the user's choice rather than not.