Question

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

Was it helpful?

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top