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

