You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在macOS上用Delphi FireMonkey实现全局键盘按键替换?

Delphi FireMonkey macOS全局组合按键替换实现示例

前置准备

  • 权限配置:在项目的Info.plist中添加NSInputMonitoringUsageDescription键,值填写应用需要监控输入的原因(例如"捕获组合按键以实现快捷文本输入");运行应用后,前往macOS系统设置→隐私与安全性→输入监控,勾选你的应用。
  • 依赖单元:需引用Macapi.CoreGraphics、Macapi.CoreFoundation、Macapi.ObjCRuntime、FMX.Mac.Menu、FMX.Platform.Mac等单元。

完整实现代码

unit MainForm;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
  Macapi.CoreGraphics, Macapi.CoreFoundation, Macapi.ObjCRuntime,
  FMX.Mac.Menu, FMX.Platform.Mac;

type
  TMainForm = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    FEventTap: CFMachPortRef;
    FRunLoopSource: CFRunLoopSourceRef;
    FPressedKeys: array[0..255] of Boolean;
    function EventTapCallback(proxy: CGEventTapProxy; type_: CGEventType;
      event: CGEventRef; refcon: Pointer): CGEventRef; static;
    procedure SimulateKeyPress(const AChar: Char);
    procedure SimulateTextInput(const AText: string);
    procedure SetupStatusBarMenu;
  public
  end;

var
  MainForm: TMainForm;

implementation

{$R *.fmx}

uses
  Macapi.Foundation, Macapi.AppKit;

procedure TMainForm.FormCreate(Sender: TObject);
var
  EventMask: CGEventMask;
begin
  FillChar(FPressedKeys, SizeOf(FPressedKeys), False);

  // 创建全局事件监听:捕获按键按下/抬起事件
  EventMask := CGEventMaskBit(kCGEventKeyDown) or CGEventMaskBit(kCGEventKeyUp);
  FEventTap := CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, 0,
    EventMask, @EventTapCallback, Self);

  if FEventTap <> nil then
  begin
    FRunLoopSource := CFMachPortCreateRunLoopSource(kCFAllocatorDefault, FEventTap, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), FRunLoopSource, kCFRunLoopCommonModes);
    CGEventTapEnable(FEventTap, True);
  end
  else
    ShowMessage('无法创建事件监听,请检查输入监控权限');

  // 配置状态栏菜单,让应用后台运行
  SetupStatusBarMenu;
  Visible := False; // 隐藏主窗口
end;

procedure TMainForm.FormDestroy(Sender: TObject);
begin
  if FRunLoopSource <> nil then
  begin
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), FRunLoopSource, kCFRunLoopCommonModes);
    CFRelease(FRunLoopSource);
  end;
  if FEventTap <> nil then
  begin
    CGEventTapEnable(FEventTap, False);
    CFRelease(FEventTap);
  end;
end;

function TMainForm.EventTapCallback(proxy: CGEventTapProxy; type_: CGEventType;
  event: CGEventRef; refcon: Pointer): CGEventRef;
var
  KeyCode: CGKeyCode;
begin
  Result := event;
  KeyCode := CGEventGetIntegerValueField(event, kCGKeyboardEventKeyCode);

  case type_ of
    kCGEventKeyDown:
      begin
        FPressedKeys[KeyCode] := True;

        // 检测组合键:P(35) + E(14)同时按下
        if FPressedKeys[35] and FPressedKeys[14] then
        begin
          Result := nil; // 取消原按键事件,不让目标应用接收P/E
          SimulateTextInput('people'); // 模拟输入目标文本
          // 重置按键状态,避免重复触发
          FPressedKeys[35] := False;
          FPressedKeys[14] := False;
        end;
      end;
    kCGEventKeyUp:
      begin
        FPressedKeys[KeyCode] := False;
      end;
  end;
end;

procedure TMainForm.SimulateKeyPress(const AChar: Char);
var
  EventDown, EventUp: CGEventRef;
  KeyCode: CGKeyCode;
  Modifiers: CGEventFlags;
begin
  KeyCode := CGKeyCodeForChar(AChar, Modifiers);
  if KeyCode = $FFFF then Exit;

  // 发送按键按下事件
  EventDown := CGEventCreateKeyboardEvent(nil, KeyCode, True);
  if Modifiers <> 0 then CGEventSetFlags(EventDown, Modifiers);
  CGEventPost(kCGHIDEventTap, EventDown);
  CFRelease(EventDown);

  // 发送按键抬起事件
  EventUp := CGEventCreateKeyboardEvent(nil, KeyCode, False);
  if Modifiers <> 0 then CGEventSetFlags(EventUp, Modifiers);
  CGEventPost(kCGHIDEventTap, EventUp);
  CFRelease(EventUp);
end;

procedure TMainForm.SimulateTextInput(const AText: string);
var
  I: Integer;
begin
  for I := 1 to Length(AText) do
    SimulateKeyPress(AText[I]);
end;

procedure TMainForm.SetupStatusBarMenu;
var
  StatusItem: NSStatusItem;
  Menu: NSMenu;
  MenuItem: NSMenuItem;
begin
  StatusItem := TNSStatusItem.Wrap(TNSStatusBar.Wrap(TNSStatusBar.OCClass.systemStatusBar).statusItemWithLength(NSVariableStatusItemLength));
  StatusItem.setTitle(NSString.Wrap(NSString.OCClass.stringWithUTF8String('KeyReplacer')));

  Menu := TNSMenu.Wrap(TNSMenu.OCClass.alloc.initWithTitle(NSString.Wrap(NSString.OCClass.stringWithUTF8String('MainMenu'))));

  // 添加退出菜单项
  MenuItem := TNSMenuItem.Wrap(TNSMenuItem.OCClass.alloc.initWithTitle(
    NSString.Wrap(NSString.OCClass.stringWithUTF8String('退出')),
    nil,
    NSString.Wrap(NSString.OCClass.stringWithUTF8String('quit'))));
  MenuItem.setAction(sel_registerName('terminate:'));
  Menu.addItem(MenuItem);
  MenuItem.release;

  StatusItem.setMenu(Menu);
  Menu.release;
end;

// 辅助函数:将字符转换为对应macOS键码和修饰键
function CGKeyCodeForChar(const AChar: Char; out Modifiers: CGEventFlags): CGKeyCode;
var
  Layout: const PKCKeyboardLayout;
  DeadKeyState: UInt32;
  MaxLength: Size;
  CharBuffer: array[0..3] of Char;
  KeyCode: CGKeyCode;
begin
  Result := $FFFF;
  Modifiers := 0;
  DeadKeyState := 0;
  Layout := TMacOSAllServices.GetCurrentKeyboardLayout;

  for KeyCode := 0 to 255 do
  begin
    MaxLength := SizeOf(CharBuffer);
    if KCGetKeyCodeMap(Layout, KeyCode, 0, @DeadKeyState, @CharBuffer[0], MaxLength) > 0 then
    begin
      if CharBuffer[0] = AChar then
      begin
        Result := KeyCode;
        Exit;
      end;
    end;
    // 检查带Shift修饰的字符
    if KCGetKeyCodeMap(Layout, KeyCode, kCGEventFlagMaskShift, @DeadKeyState, @CharBuffer[0], MaxLength) > 0 then
    begin
      if CharBuffer[0] = AChar then
      begin
        Result := KeyCode;
        Modifiers := kCGEventFlagMaskShift;
        Exit;
      end;
    end;
  end;
end;

end.

关键说明

  1. 事件拦截逻辑:EventTapCallback是全局按键事件的核心处理函数,通过维护FPressedKeys数组记录当前按下的按键,检测到目标组合键时返回nil取消原事件,避免目标应用接收原始按键。
  2. 模拟文本输入:SimulateTextInput通过逐个字符模拟按键操作,实现文本输入的效果;CGKeyCodeForChar辅助函数负责将普通字符转换为macOS对应的键码和修饰键(比如Shift)。
  3. 状态栏运行:SetupStatusBarMenu创建系统状态栏图标和菜单,让应用无需主窗口即可在后台保持活跃。
  4. 键码获取:macOS键码与Windows不同,可在测试时通过打印CGEventGetIntegerValueField(event, kCGKeyboardEventKeyCode)的值,获取目标按键的对应键码。

内容的提问来源于stack exchange,提问作者Tom

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 20:13:26