Search code examples
delphifiremonkeyfiremonkey-fm2

firemonkey and global hot key?


I'm looking for a way to cach a global hotkey in firemonkey app (windows only, at least for now). After some frustration and googling this is supposed to work: register hotkey with winapi call

RegisterHotKey(FmxHandleToHWND(form1.Handle), 0 , MOD_CONTROL, $41); 

it returns true.
and then catch the hotkey in a forms' procedure

procedure WMHotKey(var Msg: TWMHotKey); message WM_HOTKEY;

but this one is never called. I've used to do that in vcl apps before so my guess is that firemonkey handles messages in different way. So the question is: How do I catch global hotkeys in firemonkey app?

edit: some example of applying that solution. I've created an unit with little class

unit fire_hotkey;

interface

uses windows, messages,allocatehwnd;

type
  TMsgHandler = procedure (var Msg: TMessage) of object;

  THotClass = class(TObject)
    fMsgHandlerHWND : HWND;
    proc:TMsgHandler;
    constructor Create;
    procedure init;
    destructor Destroy; override;
  end;

implementation

{ hotClass }

constructor THotClass.Create;
begin
  inherited;

end;

destructor THotClass.Destroy;
begin
  ThreadDeallocateHWnd(fMsgHandlerHWND);
  inherited;
end;

procedure THotClass.init;
begin
   fMsgHandlerHWND := ThreadAllocateHWnd(proc,true);
end;

end.

then my main form has a procedure for processing hotkey events:

procedure TformEditor.WMHotKey(var Msg: TMessage);
begin
  if Msg.Msg = WM_HOTKEY then
  begin
    //call lua function or sth
    //...
  end
  else
    Msg.Result := DefWindowProc(hotkeyGrabber.fMsgHandlerHWND, Msg.Msg, Msg.wParam, Msg.lParam);
end;

and there is a global hotkeyGrabber:THotClass; that gets initialized on form create:

  hotkeyGrabber:=THotClass.Create;
  hotkeyGrabber.proc:=WMHotKey;
  hotkeyGrabber.init;

after that you should register hotkeys like in usual vcl app and they will be cought http://www.swissdelphicenter.ch/torry/showcode.php?id=147 hope it makes sense


Solution

  • The FMX framework won't route messages to your form. So, your WMHotKey will never be called because the FMX framework never calls Dispatch. You can see that this is the case by inspecting the WndProc method declared in the implementation section of the FMX.Platform.Win unit.

    The easiest way to solve this problem will be to create your own window by calling CreateWindow. And then implementing a window procedure for that window that will handle the WM_HOTKEY message.

    I've wrapped those low-level API calls like this:

    unit AllocateHWnd;
    
    interface
    
    uses
      System.SysUtils, System.Classes, System.SyncObjs, Winapi.Messages, Winapi.Windows;
    
    function ThreadAllocateHWnd(AMethod: TWndMethod; MessageOnly: Boolean): HWND;
    procedure ThreadDeallocateHWnd(Wnd: HWND);
    
    implementation
    
    const
      GWL_METHODCODE = SizeOf(Pointer)*0;
      GWL_METHODDATA = SizeOf(Pointer)*1;
      ThreadAllocateHWndClassName = 'MyCompanyName_ThreadAllocateHWnd';
    
    var
      ThreadAllocateHWndLock: TCriticalSection;
      ThreadAllocateHWndClassRegistered: Boolean;
    
    function ThreadAllocateHWndProc(Window: HWND; Message: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
    var
      Proc: TMethod;
      Msg: TMessage;
    begin
      Proc.Code := Pointer(GetWindowLongPtr(Window, GWL_METHODCODE));
      Proc.Data := Pointer(GetWindowLongPtr(Window, GWL_METHODDATA));
      if Assigned(TWndMethod(Proc)) then begin
        Msg.Msg := Message;
        Msg.wParam := wParam;
        Msg.lParam := lParam;
        Msg.Result := 0;
        TWndMethod(Proc)(Msg);
        Result := Msg.Result
      end else begin
        Result := DefWindowProc(Window, Message, wParam, lParam);
      end;
    end;
    
    function ThreadAllocateHWnd(AMethod: TWndMethod; MessageOnly: Boolean): HWND;
    
      procedure RegisterThreadAllocateHWndClass;
      var
        WndClass: TWndClass;
      begin
        if ThreadAllocateHWndClassRegistered then begin
          exit;
        end;
        ZeroMemory(@WndClass, SizeOf(WndClass));
        WndClass.lpszClassName := ThreadAllocateHWndClassName;
        WndClass.hInstance := HInstance;
        WndClass.lpfnWndProc := @ThreadAllocateHWndProc;
        WndClass.cbWndExtra := SizeOf(TMethod);
        Winapi.Windows.RegisterClass(WndClass);
        ThreadAllocateHWndClassRegistered := True;
      end;
    
    begin
      ThreadAllocateHWndLock.Acquire;
      Try
        RegisterThreadAllocateHWndClass;
        if MessageOnly then begin
          Result := CreateWindow(ThreadAllocateHWndClassName, '', 0, 0, 0, 0, 0, HWND_MESSAGE, 0, HInstance, nil);
        end else begin
          Result := CreateWindowEx(WS_EX_TOOLWINDOW, ThreadAllocateHWndClassName, '', WS_POPUP, 0, 0, 0, 0, 0, 0, HInstance, nil);
        end;
        Win32Check(Result<>0);
        SetWindowLongPtr(Result, GWL_METHODDATA, NativeInt(TMethod(AMethod).Data));
        SetWindowLongPtr(Result, GWL_METHODCODE, NativeInt(TMethod(AMethod).Code));
      Finally
        ThreadAllocateHWndLock.Release;
      End;
    end;
    
    procedure ThreadDeallocateHWnd(Wnd: HWND);
    begin
      Win32Check(DestroyWindow(Wnd));
    end;
    
    initialization
      ThreadAllocateHWndLock := TCriticalSection.Create;
    
    finalization
      ThreadAllocateHWndLock.Free;
    
    end.
    

    This is a thread-safe version of the VCL AllocateHWnd which is notorious for being unusable outside the main thread.

    What you need to do is create a class with a window procedure, i.e. something that implements a TWndMethod. It can be an instance method or a class method. Then simply call ThreadAllocateHWnd to create the window, and pass that window to RegisterHotKey. When it's time to unwind it all, unregister your hot key, and destroy the window with a call to ThreadDeallocateHWnd.